diff --git a/docs/components/dialog.md b/docs/components/dialog.md index f33db284d..e57588ed4 100644 --- a/docs/components/dialog.md +++ b/docs/components/dialog.md @@ -108,4 +108,34 @@ By default, dialogs are closed when the user clicks or taps on the overlay. To p ``` +### Customizing Initial Focus + +By default, the dialog's panel will gain focus when opened. To set focus on a different element, listen for the `sl-initial-focus` event. + +```html preview + + + Close + + +Open Dialog + + +``` + [component-metadata:sl-dialog] diff --git a/docs/components/drawer.md b/docs/components/drawer.md index 59069bd93..83a3f49f8 100644 --- a/docs/components/drawer.md +++ b/docs/components/drawer.md @@ -204,4 +204,36 @@ By default, drawers are closed when the user clicks or taps on the overlay. To p ``` +### Customizing Initial Focus + +By default, the drawer's panel will gain focus when opened. To set focus on a different element, listen for the `sl-initial-focus` event. + +```html preview + + + Close + + +Open Drawer + + +``` + [component-metadata:sl-drawer] diff --git a/docs/getting-started/changelog.md b/docs/getting-started/changelog.md index a44c4a75f..64ff48d81 100644 --- a/docs/getting-started/changelog.md +++ b/docs/getting-started/changelog.md @@ -13,6 +13,8 @@ _During the beta period, these restrictions may be relaxed in the event of a mis - Reworked animations into a separate module ([`@shoelace-style/animations`](https://github.com/shoelace-style/animations)) so it's more maintainable and animations are sync with the latest version of animate.css - Animation and easing names are now camelcase (e.g. `easeInOut` instead of `ease-in-out`) - Added initial E2E tests [#169](https://github.com/shoelace-style/shoelace/pull/169) +- Added the `FocusOptions` argument to all components that have a `setFocus()` method +- Added `sl-initial-focus` event to `sl-dialog` and `sl-drawer` so focus can be customized to a specific element - Fixed a bug where `sl-hide` would be emitted twice when closing an alert with `hide()` - Fixed a bug in `sl-color-picker` where the toggle button was smaller than the preview button in Safari - Fixed a bug in `sl-tab-group` where activating a nested tab group didn't work properly [#299](https://github.com/shoelace-style/shoelace/issues/299) diff --git a/package.json b/package.json index a0f795e44..e46c8bb88 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "serve": "node dev-server.js", "start": "concurrently --kill-others \"npm run dev\" \"npm run serve\"", "test.watch": "stencil test --spec --e2e --watchAll", - "test": "stencil test --spec --e2e", + "test": "stencil test --spec --e2e --coverage", "version": "npm run build" }, "devDependencies": { diff --git a/src/components.d.ts b/src/components.d.ts index c1875022e..28ce51a94 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -183,7 +183,7 @@ export namespace Components { /** * Sets focus on the button. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; /** * The button's size. */ @@ -253,7 +253,7 @@ export namespace Components { /** * Sets focus on the checkbox. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; /** * The checkbox's value attribute. */ @@ -751,7 +751,7 @@ export namespace Components { /** * Sets focus on the input. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; /** * Replaces a range of text with a new string. */ @@ -809,7 +809,7 @@ export namespace Components { /** * Sets focus on the button. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; /** * A unique value to store in the menu item. This can be used as a way to identify menu items when selected. */ @@ -873,7 +873,7 @@ export namespace Components { /** * Sets focus on the radio. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; /** * The radio's value attribute. */ @@ -911,7 +911,7 @@ export namespace Components { /** * Sets focus on the input. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; /** * The input's step attribute. */ @@ -957,7 +957,7 @@ export namespace Components { /** * Sets focus on the rating. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; /** * The current rating. */ @@ -1103,7 +1103,7 @@ export namespace Components { /** * Sets focus on the switch. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; /** * The switch's value attribute. */ @@ -1133,7 +1133,7 @@ export namespace Components { /** * Sets focus to the tab. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; } interface SlTabGroup { /** @@ -1265,7 +1265,7 @@ export namespace Components { /** * Sets focus on the textarea. */ - "setFocus": () => Promise; + "setFocus": (options?: FocusOptions) => Promise; /** * Replaces a range of text with a new string. */ @@ -2031,6 +2031,10 @@ declare namespace LocalJSX { * Emitted when the dialog closes. Calling `event.preventDefault()` will prevent it from being closed. */ "onSl-hide"?: (event: CustomEvent) => void; + /** + * Emitted when the dialog opens and the panel gains focus. Calling `event.preventDefault()` will prevent focus and allow you to set it on a different element in the dialog, such as an input or button. + */ + "onSl-initial-focus"?: (event: CustomEvent) => void; /** * Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the dialog from closing. */ @@ -2069,6 +2073,10 @@ declare namespace LocalJSX { * Emitted when the drawer closes. Calling `event.preventDefault()` will prevent it from being closed. */ "onSl-hide"?: (event: CustomEvent) => void; + /** + * Emitted when the drawer opens and the panel gains focus. Calling `event.preventDefault()` will prevent focus and allow you to set it on a different element in the drawer, such as an input or button. + */ + "onSl-initial-focus"?: (event: CustomEvent) => void; /** * Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the drawer from closing. */ diff --git a/src/components/dialog/dialog.tsx b/src/components/dialog/dialog.tsx index dea0aff5a..d0a735cc0 100644 --- a/src/components/dialog/dialog.tsx +++ b/src/components/dialog/dialog.tsx @@ -73,6 +73,12 @@ export class Dialog { /** Emitted after the dialog closes and all transitions are complete. */ @Event({ eventName: 'sl-after-hide' }) slAfterHide: EventEmitter; + /** + * Emitted when the dialog opens and the panel gains focus. Calling `event.preventDefault()` will prevent focus and + * allow you to set it on a different element in the dialog, such as an input or button. + */ + @Event({ eventName: 'sl-initial-focus' }) slInitialFocus: EventEmitter; + /** Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the dialog from closing. */ @Event({ eventName: 'sl-overlay-dismiss' }) slOverlayDismiss: EventEmitter; @@ -120,6 +126,16 @@ export class Dialog { this.modal.activate(); lockBodyScrolling(this.host); + + if (this.open) { + // Wait for the next frame before setting initial focus so the dialog is technically visible + requestAnimationFrame(() => { + const slInitialFocus = this.slInitialFocus.emit(); + if (!slInitialFocus.defaultPrevented) { + this.panel.focus({ preventScroll: true }); + } + }); + } } /** Hides the dialog */ @@ -173,10 +189,6 @@ export class Dialog { this.willShow = false; this.willHide = false; this.open ? this.slAfterShow.emit() : this.slAfterHide.emit(); - - if (this.open) { - this.panel.focus(); - } } } diff --git a/src/components/drawer/drawer.tsx b/src/components/drawer/drawer.tsx index 5067901ea..acb912a14 100644 --- a/src/components/drawer/drawer.tsx +++ b/src/components/drawer/drawer.tsx @@ -10,7 +10,7 @@ let id = 0; * @status stable * * @slot - The drawer's content. - * @slot label - The dialog's label. Alternatively, you can use the label prop. + * @slot label - The drawer's label. Alternatively, you can use the label prop. * @slot footer - The drawer's footer, usually one or more buttons representing various options. * * @part base - The component's base wrapper. @@ -81,6 +81,12 @@ export class Drawer { /** Emitted after the drawer closes and all transitions are complete. */ @Event({ eventName: 'sl-after-hide' }) slAfterHide: EventEmitter; + /** + * Emitted when the drawer opens and the panel gains focus. Calling `event.preventDefault()` will prevent focus and + * allow you to set it on a different element in the drawer, such as an input or button. + */ + @Event({ eventName: 'sl-initial-focus' }) slInitialFocus: EventEmitter; + /** Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the drawer from closing. */ @Event({ eventName: 'sl-overlay-dismiss' }) slOverlayDismiss: EventEmitter; @@ -131,6 +137,16 @@ export class Drawer { this.modal.activate(); lockBodyScrolling(this.host); } + + if (this.open) { + // Wait for the next frame before setting initial focus so the drawer is technically visible + requestAnimationFrame(() => { + const slInitialFocus = this.slInitialFocus.emit(); + if (!slInitialFocus.defaultPrevented) { + this.panel.focus({ preventScroll: true }); + } + }); + } } /** Hides the drawer */ @@ -184,10 +200,6 @@ export class Drawer { this.willShow = false; this.willHide = false; this.open ? this.slAfterShow.emit() : this.slAfterHide.emit(); - - if (this.open) { - this.panel.focus(); - } } }