Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dialog): Add Foundation#setInitialFocusEl API. #4719

Merged
merged 8 commits into from
May 16, 2019
Merged
13 changes: 10 additions & 3 deletions packages/mdc-dialog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ avoiding the need to tab through to the appropriate button to confirm the choice

To indicate that a button represents the default action, add the `mdc-dialog__button--default` modifier class.
For example:

```html
...
<footer class="mdc-dialog__actions">
Expand Down Expand Up @@ -336,6 +335,12 @@ Mixin | Description

> *NOTE*: The `max-width` and `max-height` mixins only apply their maximum when the viewport is large enough to accommodate the specified value when accounting for the specified margin on either side. When the viewport is smaller, the dialog is sized such that the given margin is retained around the edges.

## Other Customizations
joyzhong marked this conversation as resolved.
Show resolved Hide resolved
CSS Class | Description
--- | ---
`mdc-dialog__button--default` | Optional. Add to a button to indicate that it is the default action button (see Default Action Button section above).
`mdc-dialog__element--focus` | Optional. Add to an element to indicate that it is the element to initially focus on after the dialog has opened. This is the element that should be passed in to `Adapter#setInitialFocusEl`.

## `MDCDialog` Properties and Methods

Property | Value Type | Description
Expand Down Expand Up @@ -373,8 +378,8 @@ Method Signature | Description
`hasClass(className: string) => boolean` | Returns whether the given class exists on the root element.
`addBodyClass(className: string) => void` | Adds a class to the `<body>`.
`removeBodyClass(className: string) => void` | Removes a class from the `<body>`.
`eventTargetMatches(target: EventTarget \| null, selector: string) => void` | Returns `true` if the target element matches the given CSS selector, otherwise `false`.
`trapFocus() => void` | Sets up the DOM such that keyboard navigation is restricted to focusable elements within the dialog surface (see [Handling Focus Trapping](#handling-focus-trapping) below for more details).
`eventTargetMatches(target: EventTarget | null, selector: string) => void` | Returns `true` if the target element matches the given CSS selector, otherwise `false`.
`trapFocus(initialFocusEl?: HTMLElement) => void` | Sets up the DOM such that keyboard navigation is restricted to focusable elements within the dialog surface (see [Handling Focus Trapping](#handling-focus-trapping) below for more details). If `initialFocusEl` is set, also moves focus to that element.
`releaseFocus() => void` | Removes any effects of focus trapping on the dialog surface (see [Handling Focus Trapping](#handling-focus-trapping) below for more details).
`isContentScrollable() => boolean` | Returns `true` if `mdc-dialog__content` can be scrolled by the user, otherwise `false`.
`areButtonsStacked() => boolean` | Returns `true` if `mdc-dialog__action` buttons (`mdc-dialog__button`) are stacked vertically, otherwise `false` if they are side-by-side.
Expand All @@ -400,6 +405,8 @@ Method Signature | Description
`setScrimClickAction(action: string)` | Sets the action reflected when the scrim is clicked. Setting to `''` disables closing the dialog via scrim click.
`getAutoStackButtons() => boolean` | Returns whether stacked/unstacked action button layout is automatically handled during layout logic.
`setAutoStackButtons(autoStack: boolean) => void` | Sets whether stacked/unstacked action button layout is automatically handled during layout logic.
`getInitialFocusEl() => HTMLElement|null` | Gets the element to focus on after dialog has finished opening.
`setInitialFocusEl(el: HTMLElement|null)` | Sets the element to focus on after dialog has finished opening. Passed as an argument to `Adapter#trapFocus`.
joyzhong marked this conversation as resolved.
Show resolved Hide resolved
`handleClick(event: MouseEvent)` | Handles `click` events on or within the dialog's root element.
`handleKeydown(event: KeyboardEvent)` | Handles `keydown` events on or within the dialog's root element.
`handleDocumentKeydown(event: Event)` | Handles `keydown` events on or within the document while the dialog is open.
Expand Down
2 changes: 1 addition & 1 deletion packages/mdc-dialog/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface MDCDialogAdapter {
areButtonsStacked(): boolean;
getActionFromEvent(evt: Event): string | null;

trapFocus(): void;
trapFocus(focusElement?: HTMLElement): void;
releaseFocus(): void;
clickDefaultButton(): void;
reverseButtons(): void;
Expand Down
1 change: 1 addition & 0 deletions packages/mdc-dialog/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const strings = {
CONTENT_SELECTOR: '.mdc-dialog__content',
DEFAULT_BUTTON_SELECTOR: '.mdc-dialog__button--default',
DESTROY_ACTION: 'destroy',
INITIAL_FOCUS_EL_SELECTOR: '.mdc-dialog__element--focus',
joyzhong marked this conversation as resolved.
Show resolved Hide resolved
OPENED_EVENT: 'MDCDialog:opened',
OPENING_EVENT: 'MDCDialog:opening',
SCRIM_SELECTOR: '.mdc-dialog__scrim',
Expand Down
12 changes: 11 additions & 1 deletion packages/mdc-dialog/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
private scrimClickAction_ = strings.CLOSE_ACTION;
private autoStackButtons_ = true;
private areButtonsStacked_ = false;
// Element to focus on after dialog has finished opening.
private initialFocusEl_: HTMLElement|null = null;
joyzhong marked this conversation as resolved.
Show resolved Hide resolved
joyzhong marked this conversation as resolved.
Show resolved Hide resolved

constructor(adapter?: Partial<MDCDialogAdapter>) {
super({...MDCDialogFoundation.defaultAdapter, ...adapter});
Expand Down Expand Up @@ -109,7 +111,7 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {

this.animationTimer_ = setTimeout(() => {
this.handleAnimationTimerEnd_();
this.adapter_.trapFocus();
this.adapter_.trapFocus(this.initialFocusEl_ || undefined);
joyzhong marked this conversation as resolved.
Show resolved Hide resolved
this.adapter_.notifyOpened();
}, numbers.DIALOG_ANIMATION_OPEN_TIME_MS);
});
Expand Down Expand Up @@ -166,6 +168,14 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
this.autoStackButtons_ = autoStack;
}

getInitialFocusEl(): HTMLElement|null {
return this.initialFocusEl_;
}

setInitialFocusEl(el: HTMLElement|null) {
this.initialFocusEl_ = el;
}

layout() {
if (this.layoutFrame_) {
cancelAnimationFrame(this.layoutFrame_);
Expand Down
25 changes: 24 additions & 1 deletion test/unit/mdc-dialog/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ test('#open activates focus trapping on the dialog surface', () => {
clock.tick(100);
clock.tick(numbers.DIALOG_ANIMATION_OPEN_TIME_MS);

td.verify(mockAdapter.trapFocus());
td.verify(mockAdapter.trapFocus(undefined));
});

test('#close deactivates focus trapping on the dialog surface', () => {
Expand Down Expand Up @@ -514,3 +514,26 @@ test('#getScrimClickAction reflects setting of #setScrimClickAction', () => {
foundation.setScrimClickAction(action);
assert.strictEqual(foundation.getScrimClickAction(), action);
});

test('#getInitialFocusEl reflects setting of #setInitialFocusEl', () => {
const {foundation} = setupTest();
const el = document.createElement('button');
foundation.setInitialFocusEl(el);
assert.strictEqual(foundation.getInitialFocusEl(), el);
});

test('#setInitialFocusEl element is passed in as an arg to Adapter#trapFocus', () => {
const {foundation, mockAdapter} = setupTest();
const clock = installClock();

const button = document.createElement('button');
foundation.setInitialFocusEl(button);
foundation.open();

// Wait for application of opening class and setting of additional timeout prior to full open animation timeout
clock.runToFrame();
clock.tick(100);
clock.tick(numbers.DIALOG_ANIMATION_OPEN_TIME_MS);

td.verify(mockAdapter.trapFocus(button));
});