kopia lustrzana https://github.com/shoelace-style/shoelace
fixes #693
rodzic
3fa41ea8d9
commit
eb18d759f1
|
@ -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
|
||||
<sl-dialog label="Dialog" class="dialog-focus">
|
||||
<sl-input placeholder="I will have focus when the dialog is opened"></sl-input>
|
||||
<sl-input autofocus placeholder="I will have focus when the dialog is opened"></sl-input>
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-dialog>
|
||||
|
||||
|
@ -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 });
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```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 (
|
||||
<>
|
||||
<SlDialog label="Dialog" open={open} onSlInitialFocus={handleInitialFocus} onSlAfterHide={() => setOpen(false)}>
|
||||
<SlInput ref={input} placeholder="I will have focus when the dialog is opened" />
|
||||
<SlDialog label="Dialog" open={open} onSlAfterHide={() => setOpen(false)}>
|
||||
<SlInput autofocus placeholder="I will have focus when the dialog is opened" />
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
<sl-drawer label="Drawer" class="drawer-focus">
|
||||
<sl-input placeholder="I will have focus when the drawer is opened"></sl-input>
|
||||
<sl-input autofocus placeholder="I will have focus when the drawer is opened"></sl-input>
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-drawer>
|
||||
|
||||
|
@ -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 });
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```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 (
|
||||
<>
|
||||
<SlDrawer label="Drawer" open={open} onSlInitialFocus={handleInitialFocus} onSlAfterHide={() => setOpen(false)}>
|
||||
<SlInput ref={input} placeholder="I will have focus when the drawer is opened" />
|
||||
<SlDrawer label="Drawer" open={open} onSlAfterHide={() => setOpen(false)}>
|
||||
<SlInput autofocus placeholder="I will have focus when the drawer is opened" />
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
|
@ -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]
|
||||
|
|
|
@ -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 `<sl-dialog>` and `<sl-drawer>` [#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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
Ładowanie…
Reference in New Issue