Skip to content

Commit

Permalink
fix(dialog): updates for delivering dialog content accessibly
Browse files Browse the repository at this point in the history
  • Loading branch information
Westbrook committed Feb 18, 2022
1 parent 89f5128 commit f0ed33c
Show file tree
Hide file tree
Showing 16 changed files with 359 additions and 109 deletions.
2 changes: 2 additions & 0 deletions packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"exports": {
".": "./src/index.js",
"./src/*": "./src/*",
"./condition-attribute-with-id": "./src/condition-attribute-with-id.js",
"./condition-attribute-with-id.js": "./src/condition-attribute-with-id.js",
"./decorators": "./src/decorators.js",
"./decorators.js": "./src/decorators.js",
"./directives": "./src/directives.js",
Expand Down
46 changes: 46 additions & 0 deletions packages/base/src/condition-attribute-with-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

export function conditionAttributeWithoutId(
el: HTMLElement,
attribute: string,
ids: string[]
): void {
const ariaDescribedby = el.getAttribute(attribute);
let descriptors = ariaDescribedby ? ariaDescribedby.split(/\s+/) : [];
descriptors = descriptors.filter(
(descriptor) => !ids.find((id) => descriptor === id)
);
if (descriptors.length) {
el.setAttribute(attribute, descriptors.join(' '));
} else {
el.removeAttribute(attribute);
}
}

export function conditionAttributeWithId(
el: HTMLElement,
attribute: string,
id: string | string[]
): () => void {
const ids = Array.isArray(id) ? id : [id];
const ariaDescribedby = el.getAttribute(attribute);
const descriptors = ariaDescribedby ? ariaDescribedby.split(/\s+/) : [];
const hadIds = ids.every((id) => descriptors.indexOf(id) > -1);
if (hadIds)
return (): void => {
return;
};
descriptors.push(...ids);
el.setAttribute(attribute, descriptors.join(' '));
return () => conditionAttributeWithoutId(el, attribute, ids);
}
11 changes: 0 additions & 11 deletions packages/dialog/dialog-wrapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,6 @@ import { DialogWrapper } from '@spectrum-web-components/dialog';
function handleEvent({type}) {
spAlert(this, `<sp-dialog-wrapper> '${type}' event handled.`);
dialogWrapper.open = false;
dialogWrapper.dispatchEvent(
new Event('close', {
bubbles: true,
})
);
dialogWrapper.removeEventListener('confirm', handleEvent);
dialogWrapper.removeEventListener('secondary', handleEvent);
dialogWrapper.removeEventListener('cancel', handleEvent);
Expand Down Expand Up @@ -107,15 +102,9 @@ import { DialogWrapper } from '@spectrum-web-components/dialog';
onClick="
const overlayTrigger = this.parentElement;
const dialogWrapper = overlayTrigger.clickContent;
dialogWrapper.open = true;
function handleEvent({type}) {
spAlert(this, `<sp-dialog-wrapper> '${type}' event handled.`);
dialogWrapper.open = false;
dialogWrapper.dispatchEvent(
new Event('close', {
bubbles: true,
})
);
dialogWrapper.removeEventListener('confirm', handleEvent);
dialogWrapper.removeEventListener('secondary', handleEvent);
dialogWrapper.removeEventListener('cancel', handleEvent);
Expand Down
114 changes: 91 additions & 23 deletions packages/dialog/src/Dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
query,
} from '@spectrum-web-components/base/src/decorators.js';
import { ifDefined } from '@spectrum-web-components/base/src/directives.js';
import { conditionAttributeWithId } from '@spectrum-web-components/base/src/condition-attribute-with-id.js';

import '@spectrum-web-components/divider/sp-divider.js';
import '@spectrum-web-components/action-button/sp-action-button.js';
Expand All @@ -33,9 +34,27 @@ import {
FocusVisiblePolyfillMixin,
ObserveSlotPresence,
} from '@spectrum-web-components/shared';
import { firstFocusableIn } from '@spectrum-web-components/shared/src/first-focusable-in.js';

import styles from './dialog.css.js';
import type { ActionButton } from '@spectrum-web-components/action-button';

function gatherAppliedIdsFromSlottedChildren(
slot: HTMLSlotElement,
idBase: string
): string[] {
const assignedElements = slot.assignedElements();
const ids: string[] = [];
assignedElements.forEach((el, i) => {
if (el.id) {
ids.push(el.id);
} else {
const id = idBase + `-${i}`;
el.id = id;
ids.push(id);
}
});
return ids;
}

/**
* @element sp-dialog
Expand All @@ -58,6 +77,9 @@ export class Dialog extends FocusVisiblePolyfillMixin(
return [styles, crossStyles];
}

@query('.close-button')
closeButton?: ActionButton;

@query('.content')
private contentElement!: HTMLDivElement;

Expand Down Expand Up @@ -88,30 +110,12 @@ export class Dialog extends FocusVisiblePolyfillMixin(
@property({ type: String, reflect: true })
public size?: 's' | 'm' | 'l';

public focus(): void {
if (this.shadowRoot) {
const firstFocusable = firstFocusableIn(this.shadowRoot);
if (firstFocusable) {
if (firstFocusable.updateComplete) {
firstFocusable.updateComplete.then(() =>
firstFocusable.focus()
);
/* c8 ignore next 3 */
} else {
firstFocusable.focus();
}
this.removeAttribute('tabindex');
}
/* c8 ignore next 3 */
} else {
super.focus();
}
}

public close(): void {
this.dispatchEvent(
new Event('close', {
bubbles: true,
composed: true,
cancelable: true,
})
);
}
Expand All @@ -123,6 +127,7 @@ export class Dialog extends FocusVisiblePolyfillMixin(
<slot
name="heading"
class=${ifDefined(this.hasHero ? this.hasHero : undefined)}
@slotchange=${this.onHeadingSlotchange}
></slot>
${this.error
? html`
Expand Down Expand Up @@ -194,12 +199,75 @@ export class Dialog extends FocusVisiblePolyfillMixin(
return super.shouldUpdate(changes);
}

protected onContentSlotChange(): void {
this.shouldManageTabOrderForScrolling();
protected firstUpdated(changes: PropertyValues): void {
super.firstUpdated(changes);
this.setAttribute('role', 'dialog');
}

static instanceCount = 0;
private labelledbyId = `sp-dialog-label-${Dialog.instanceCount++}`;
private conditionLabelledby?: () => void;
private conditionDescribedby?: () => void;

private onHeadingSlotchange({
target,
}: Event & { target: HTMLSlotElement }): void {
if (this.conditionLabelledby) {
this.conditionLabelledby();
delete this.conditionLabelledby;
}
const ids = gatherAppliedIdsFromSlottedChildren(
target,
this.labelledbyId
);
if (ids.length) {
this.conditionLabelledby = conditionAttributeWithId(
this,
'aria-labelledby',
ids
);
}
}

private describedbyId = `sp-dialog-description-${Dialog.instanceCount++}`;

protected onContentSlotChange({
target,
}: Event & { target: HTMLSlotElement }): void {
if (this.conditionDescribedby) {
this.conditionDescribedby();
delete this.conditionDescribedby;
}
const ids = gatherAppliedIdsFromSlottedChildren(
target,
this.describedbyId
);
if (ids.length && ids.length < 4) {
this.conditionDescribedby = conditionAttributeWithId(
this,
'aria-describedby',
ids
);
} else if (!ids.length) {
const idProvided = !!this.id;
if (!idProvided) this.id = this.describedbyId;
const conditionDescribedby = conditionAttributeWithId(
this,
'aria-describedby',
this.id
);
this.conditionDescribedby = () => {
conditionDescribedby();
if (!idProvided) {
this.removeAttribute('id');
}
};
}
}

public connectedCallback(): void {
super.connectedCallback();
this.tabIndex = 0;
window.addEventListener(
'resize',
this.shouldManageTabOrderForScrolling
Expand Down
34 changes: 28 additions & 6 deletions packages/dialog/src/DialogWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class DialogWrapper extends FocusVisiblePolyfillMixin(SpectrumElement) {

public focus(): void {
if (this.shadowRoot) {
const firstFocusable = firstFocusableIn(this.shadowRoot);
const firstFocusable = firstFocusableIn(this.dialog);
if (firstFocusable) {
if (firstFocusable.updateComplete) {
firstFocusable.updateComplete.then(() =>
Expand All @@ -121,6 +121,12 @@ export class DialogWrapper extends FocusVisiblePolyfillMixin(SpectrumElement) {
}
}

public overlayWillCloseCallback(): boolean {
if (!this.open) return false;
this.close();
return true;
}

private dismiss(): void {
if (!this.dismissable) {
return;
Expand Down Expand Up @@ -152,8 +158,16 @@ export class DialogWrapper extends FocusVisiblePolyfillMixin(SpectrumElement) {
);
}

protected handleClose(event: Event): void {
event.stopPropagation();
this.close();
}

public close(): void {
this.open = false;
}

private dispatchClosed(): void {
this.dispatchEvent(
new Event('close', {
bubbles: true,
Expand All @@ -163,13 +177,17 @@ export class DialogWrapper extends FocusVisiblePolyfillMixin(SpectrumElement) {

protected handleUnderlayTransitionend(): void {
if (!this.open) {
this.dispatchClosed();
this.resolveTransitionPromise();
}
}

protected handleModalTransitionend(): void {
if (this.open || !this.underlay) {
this.resolveTransitionPromise();
if (!this.open) {
this.dispatchClosed();
}
}
}

Expand Down Expand Up @@ -203,7 +221,7 @@ export class DialogWrapper extends FocusVisiblePolyfillMixin(SpectrumElement) {
?error=${this.error}
mode=${ifDefined(this.mode ? this.mode : undefined)}
size=${ifDefined(this.size ? this.size : undefined)}
@close=${this.close}
@close=${this.handleClose}
>
${this.hero
? html`
Expand Down Expand Up @@ -271,10 +289,14 @@ export class DialogWrapper extends FocusVisiblePolyfillMixin(SpectrumElement) {
}

protected updated(changes: PropertyValues<this>): void {
if (changes.has('open') && this.open) {
this.dialog.updateComplete.then(() => {
this.dialog.shouldManageTabOrderForScrolling();
});
if (changes.has('open')) {
if (this.open) {
this.dialog.updateComplete.then(() => {
this.dialog.shouldManageTabOrderForScrolling();
});
} else {
this.tabIndex = 0;
}
}
}

Expand Down
Loading

0 comments on commit f0ed33c

Please sign in to comment.