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();
- }
}
}