diff --git a/docs/components/dialog.md b/docs/components/dialog.md
index ec000f4e3..44a726aa8 100644
--- a/docs/components/dialog.md
+++ b/docs/components/dialog.md
@@ -222,11 +222,11 @@ const App = () => {
### Customizing Initial Focus
-By default, the dialog's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element within the dialog. To set focus on a different element, listen for and cancel the `sl-initial-focus` event.
+By default, the dialog's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the dialog. If you want a different element to have focus, add the `autofocus` attribute to it as shown below.
```html preview
-
+
Close
@@ -240,31 +240,20 @@ By default, the dialog's panel will gain focus when opened. This allows a subseq
openButton.addEventListener('click', () => dialog.show());
closeButton.addEventListener('click', () => dialog.hide());
-
- dialog.addEventListener('sl-initial-focus', event => {
- event.preventDefault();
- input.focus({ preventScroll: true });
- });
```
```jsx react
-import { useRef, useState } from 'react';
+import { useState } from 'react';
import { SlButton, SlDialog, SlInput } from '@shoelace-style/shoelace/dist/react';
const App = () => {
- const input = useRef(null);
const [open, setOpen] = useState(false);
- function handleInitialFocus(event) {
- event.preventDefault();
- input.current.focus();
- }
-
return (
<>
- setOpen(false)}>
-
+ setOpen(false)}>
+
setOpen(false)}>
Close
@@ -276,6 +265,6 @@ const App = () => {
};
```
-?> Alternatively, you can add the `autofocus` attribute to any form control to customize initial focus without using JavaScript.
+?> You can further customize initial focus behavior by canceling the `sl-initial-focus` event and setting focus yourself inside the event handler.
[component-metadata:sl-dialog]
diff --git a/docs/components/drawer.md b/docs/components/drawer.md
index 9b9a62fcb..e215982d4 100644
--- a/docs/components/drawer.md
+++ b/docs/components/drawer.md
@@ -410,11 +410,11 @@ const App = () => {
### Customizing Initial Focus
-By default, the drawer's panel will gain focus when opened. This allows the first tab press to focus on the first tabbable element within the drawer. To set focus on a different element, listen for and cancel the `sl-initial-focus` event.
+By default, the drawer's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the drawer. If you want a different element to have focus, add the `autofocus` attribute to it as shown below.
```html preview
-
+
Close
@@ -428,31 +428,20 @@ By default, the drawer's panel will gain focus when opened. This allows the firs
openButton.addEventListener('click', () => drawer.show());
closeButton.addEventListener('click', () => drawer.hide());
-
- drawer.addEventListener('sl-initial-focus', event => {
- event.preventDefault();
- input.focus({ preventScroll: true });
- });
```
```jsx react
-import { useRef, useState } from 'react';
+import { useState } from 'react';
import { SlButton, SlDrawer, SlInput } from '@shoelace-style/shoelace/dist/react';
const App = () => {
- const input = useRef(null);
const [open, setOpen] = useState(false);
- function handleInitialFocus(event) {
- event.preventDefault();
- input.current.focus();
- }
-
return (
<>
- setOpen(false)}>
-
+ setOpen(false)}>
+
setOpen(false)}>
Close
@@ -464,6 +453,5 @@ const App = () => {
};
```
-?> Alternatively, you can add the `autofocus` attribute to any form control to customize initial focus without using JavaScript.
-
+?> You can further customize initial focus behavior by canceling the `sl-initial-focus` event and setting focus yourself inside the event handler.
[component-metadata:sl-drawer]
diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md
index bfb43a767..ee0fe2073 100644
--- a/docs/resources/changelog.md
+++ b/docs/resources/changelog.md
@@ -8,6 +8,8 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
## Next
+- Improved `autofocus` behavior in Safari for `` and `` [#693](https://github.com/shoelace-style/shoelace/issues/693)
+- Removed feature detection for `focus({ preventScroll })` since it no longer works in Safari
- Removed path aliasing and third-party dependencies that it required
## 2.0.0-beta.70
diff --git a/src/components/dialog/dialog.ts b/src/components/dialog/dialog.ts
index 439f5749c..112a23746 100644
--- a/src/components/dialog/dialog.ts
+++ b/src/components/dialog/dialog.ts
@@ -7,15 +7,12 @@ import { emit, waitForEvent } from '../../internal/event';
import Modal from '../../internal/modal';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { HasSlotController } from '../../internal/slot';
-import { isPreventScrollSupported } from '../../internal/support';
import { watch } from '../../internal/watch';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import { LocalizeController } from '../../utilities/localize';
import '../icon-button/icon-button';
import styles from './dialog.styles';
-const hasPreventScroll = isPreventScrollSupported();
-
/**
* @since 2.0
* @status stable
@@ -30,8 +27,8 @@ const hasPreventScroll = isPreventScrollSupported();
* @event sl-after-show - Emitted after the dialog opens and all animations are complete.
* @event sl-hide - Emitted when the dialog closes.
* @event sl-after-hide - Emitted after the dialog closes and all animations are complete.
- * @event sl-initial-focus - 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 sl-initial-focus - Emitted when the dialog opens and is ready to receive focus. Calling
+ * `event.preventDefault()` will prevent focusing and allow you to set it on a different element, such as an input.
* @event {{ source: 'close-button' | 'keyboard' | 'overlay' }} sl-request-close - Emitted when the user attempts to
* close the dialog by clicking the close button, clicking the overlay, or pressing escape. Calling
* `event.preventDefault()` will keep the dialog open. Avoid using this unless closing the dialog will result in
@@ -139,17 +136,6 @@ export default class SlDialog extends LitElement {
this.hide();
}
- // Sets focus on the first child element with autofocus, falling back to the panel if one isn't found
- private setInitialFocus() {
- const target = this.querySelector('[autofocus]');
-
- if (target) {
- (target as HTMLElement).focus({ preventScroll: true });
- } else {
- this.panel.focus({ preventScroll: true });
- }
- }
-
handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.stopPropagation();
@@ -167,18 +153,38 @@ export default class SlDialog extends LitElement {
lockBodyScrolling(this);
+ // When the dialog is shown, Safari will attempt to set focus on whatever element has autofocus. This can cause
+ // the dialogs's animation to jitter (if it starts offscreen), so we'll temporarily remove the attribute, call
+ // `focus({ preventScroll: true })` ourselves, and add the attribute back afterwards.
+ //
+ // Related: https://github.com/shoelace-style/shoelace/issues/693
+ //
+ const autoFocusTarget = this.querySelector('[autofocus]');
+ if (autoFocusTarget) {
+ autoFocusTarget.removeAttribute('autofocus');
+ }
+
await Promise.all([stopAnimations(this.dialog), stopAnimations(this.overlay)]);
this.dialog.hidden = false;
- // Browsers that support el.focus({ preventScroll }) can set initial focus immediately
- if (hasPreventScroll) {
- requestAnimationFrame(() => {
- const slInitialFocus = emit(this, 'sl-initial-focus', { cancelable: true });
- if (!slInitialFocus.defaultPrevented) {
- this.setInitialFocus();
+ // Set initial focus
+ requestAnimationFrame(() => {
+ const slInitialFocus = emit(this, 'sl-initial-focus', { cancelable: true });
+
+ if (!slInitialFocus.defaultPrevented) {
+ // Set focus to the autofocus target and restore the attribute
+ if (autoFocusTarget) {
+ (autoFocusTarget as HTMLInputElement).focus({ preventScroll: true });
+ } else {
+ this.panel.focus({ preventScroll: true });
}
- });
- }
+ }
+
+ // Restore the autofocus attribute
+ if (autoFocusTarget) {
+ autoFocusTarget.setAttribute('autofocus', '');
+ }
+ });
const panelAnimation = getAnimation(this, 'dialog.show');
const overlayAnimation = getAnimation(this, 'dialog.overlay.show');
@@ -187,17 +193,6 @@ export default class SlDialog extends LitElement {
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
]);
- // Browsers that don't support el.focus({ preventScroll }) have to wait for the animation to finish before initial
- // focus to prevent scrolling issues. See: https://caniuse.com/mdn-api_htmlelement_focus_preventscroll_option
- if (!hasPreventScroll) {
- requestAnimationFrame(() => {
- const slInitialFocus = emit(this, 'sl-initial-focus', { cancelable: true });
- if (!slInitialFocus.defaultPrevented) {
- this.setInitialFocus();
- }
- });
- }
-
emit(this, 'sl-after-show');
} else {
// Hide
diff --git a/src/components/drawer/drawer.ts b/src/components/drawer/drawer.ts
index 24825172f..8405eee2e 100644
--- a/src/components/drawer/drawer.ts
+++ b/src/components/drawer/drawer.ts
@@ -8,15 +8,12 @@ import Modal from '../../internal/modal';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { HasSlotController } from '../../internal/slot';
import { uppercaseFirstLetter } from '../../internal/string';
-import { isPreventScrollSupported } from '../../internal/support';
import { watch } from '../../internal/watch';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import { LocalizeController } from '../../utilities/localize';
import '../icon-button/icon-button';
import styles from './drawer.styles';
-const hasPreventScroll = isPreventScrollSupported();
-
/**
* @since 2.0
* @status stable
@@ -31,8 +28,8 @@ const hasPreventScroll = isPreventScrollSupported();
* @event sl-after-show - Emitted after the drawer opens and all animations are complete.
* @event sl-hide - Emitted when the drawer closes.
* @event sl-after-hide - Emitted after the drawer closes and all animations are complete.
- * @event sl-initial-focus - 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 sl-initial-focus - Emitted when the drawer opens and is ready to receive focus. Calling
+ * `event.preventDefault()` will prevent focusing and allow you to set it on a different element, such as an input.
* @event {{ source: 'close-button' | 'keyboard' | 'overlay' }} sl-request-close - Emitted when the user attempts to
* close the drawer by clicking the close button, clicking the overlay, or pressing escape. Calling
* `event.preventDefault()` will keep the drawer open. Avoid using this unless closing the drawer will result in
@@ -156,17 +153,6 @@ export default class SlDrawer extends LitElement {
this.hide();
}
- // Sets focus on the first child element with autofocus, falling back to the panel if one isn't found
- private setInitialFocus() {
- const target = this.querySelector('[autofocus]');
-
- if (target) {
- (target as HTMLElement).focus({ preventScroll: true });
- } else {
- this.panel.focus({ preventScroll: true });
- }
- }
-
handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.stopPropagation();
@@ -187,18 +173,38 @@ export default class SlDrawer extends LitElement {
lockBodyScrolling(this);
}
+ // When the drawer is shown, Safari will attempt to set focus on whatever element has autofocus. This causes the
+ // drawer's animation to jitter, so we'll temporarily remove the attribute, call `focus({ preventScroll: true })`
+ // ourselves, and add the attribute back afterwards.
+ //
+ // Related: https://github.com/shoelace-style/shoelace/issues/693
+ //
+ const autoFocusTarget = this.querySelector('[autofocus]');
+ if (autoFocusTarget) {
+ autoFocusTarget.removeAttribute('autofocus');
+ }
+
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
this.drawer.hidden = false;
- // Browsers that support el.focus({ preventScroll }) can set initial focus immediately
- if (hasPreventScroll) {
- requestAnimationFrame(() => {
- const slInitialFocus = emit(this, 'sl-initial-focus', { cancelable: true });
- if (!slInitialFocus.defaultPrevented) {
- this.setInitialFocus();
+ // Set initial focus
+ requestAnimationFrame(() => {
+ const slInitialFocus = emit(this, 'sl-initial-focus', { cancelable: true });
+
+ if (!slInitialFocus.defaultPrevented) {
+ // Set focus to the autofocus target and restore the attribute
+ if (autoFocusTarget) {
+ (autoFocusTarget as HTMLInputElement).focus({ preventScroll: true });
+ } else {
+ this.panel.focus({ preventScroll: true });
}
- });
- }
+ }
+
+ // Restore the autofocus attribute
+ if (autoFocusTarget) {
+ autoFocusTarget.setAttribute('autofocus', '');
+ }
+ });
const panelAnimation = getAnimation(this, `drawer.show${uppercaseFirstLetter(this.placement)}`);
const overlayAnimation = getAnimation(this, 'drawer.overlay.show');
@@ -207,17 +213,6 @@ export default class SlDrawer extends LitElement {
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
]);
- // Browsers that don't support el.focus({ preventScroll }) have to wait for the animation to finish before initial
- // focus to prevent scrolling issues. See: https://caniuse.com/mdn-api_htmlelement_focus_preventscroll_option
- if (!hasPreventScroll) {
- requestAnimationFrame(() => {
- const slInitialFocus = emit(this, 'sl-initial-focus', { cancelable: true });
- if (!slInitialFocus.defaultPrevented) {
- this.setInitialFocus();
- }
- });
- }
-
emit(this, 'sl-after-show');
} else {
// Hide
diff --git a/src/internal/support.ts b/src/internal/support.ts
deleted file mode 100644
index 79c16a6e0..000000000
--- a/src/internal/support.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// Determines if the browser supports focus({ preventScroll })
-//
-export function isPreventScrollSupported() {
- let supported = false;
-
- document.createElement('div').focus({
- get preventScroll() {
- supported = true;
- return false;
- }
- });
-
- return supported;
-}