kopia lustrzana https://github.com/shoelace-style/shoelace
add support for external modals; fixes #1571
rodzic
2ed5a4ff97
commit
4b448fa52d
docs/pages/resources
src
components
dialog
drawer
internal
|
@ -14,6 +14,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
|||
|
||||
## Next
|
||||
|
||||
- Added the `modal` property to `<sl-dialog>` and `<sl-drawer>` to support third-party modals [#1571]
|
||||
- Fixed a bug in the autoloader causing it to register non-Shoelace elements [#1563]
|
||||
- Fixed a bug in `<sl-switch>` that resulted in improper spacing between the label and the required asterisk [#1540]
|
||||
- Removed error when a missing popup anchor is provided [#1548]
|
||||
|
|
|
@ -60,6 +60,10 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @animation dialog.denyClose - The animation to use when a request to close the dialog is denied.
|
||||
* @animation dialog.overlay.show - The animation to use when showing the dialog's overlay.
|
||||
* @animation dialog.overlay.hide - The animation to use when hiding the dialog's overlay.
|
||||
*
|
||||
* @property modal - Exposes the internal modal utility that controls focus trapping. To temporarily disable focus
|
||||
* trapping and allow third-party modals spawned from an active Shoelace modal, call `modal.activateExternal()` when
|
||||
* the third-party modal opens. Upon closing, call `modal.deactivateExternal()` to restore Shoelace's focus trapping.
|
||||
*/
|
||||
export default class SlDialog extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
@ -69,8 +73,8 @@ export default class SlDialog extends ShoelaceElement {
|
|||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private modal = new Modal(this);
|
||||
private originalTrigger: HTMLElement | null;
|
||||
public modal = new Modal(this);
|
||||
|
||||
@query('.dialog') dialog: HTMLElement;
|
||||
@query('.dialog__panel') panel: HTMLElement;
|
||||
|
|
|
@ -68,6 +68,10 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @animation drawer.denyClose - The animation to use when a request to close the drawer is denied.
|
||||
* @animation drawer.overlay.show - The animation to use when showing the drawer's overlay.
|
||||
* @animation drawer.overlay.hide - The animation to use when hiding the drawer's overlay.
|
||||
*
|
||||
* @property modal - Exposes the internal modal utility that controls focus trapping. To temporarily disable focus
|
||||
* trapping and allow third-party modals spawned from an active Shoelace modal, call `modal.activateExternal()` when
|
||||
* the third-party modal opens. Upon closing, call `modal.deactivateExternal()` to restore Shoelace's focus trapping.
|
||||
*/
|
||||
export default class SlDrawer extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
@ -75,8 +79,8 @@ export default class SlDrawer extends ShoelaceElement {
|
|||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private modal = new Modal(this);
|
||||
private originalTrigger: HTMLElement | null;
|
||||
public modal = new Modal(this);
|
||||
|
||||
@query('.drawer') drawer: HTMLElement;
|
||||
@query('.drawer__panel') panel: HTMLElement;
|
||||
|
|
|
@ -5,6 +5,7 @@ let activeModals: HTMLElement[] = [];
|
|||
|
||||
export default class Modal {
|
||||
element: HTMLElement;
|
||||
isExternalActivated: boolean;
|
||||
tabDirection: 'forward' | 'backward' = 'forward';
|
||||
currentFocus: HTMLElement | null;
|
||||
|
||||
|
@ -12,6 +13,7 @@ export default class Modal {
|
|||
this.element = element;
|
||||
}
|
||||
|
||||
/** Activates focus trapping. */
|
||||
activate() {
|
||||
activeModals.push(this.element);
|
||||
document.addEventListener('focusin', this.handleFocusIn);
|
||||
|
@ -19,6 +21,7 @@ export default class Modal {
|
|||
document.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
/** Deactivates focus trapping. */
|
||||
deactivate() {
|
||||
activeModals = activeModals.filter(modal => modal !== this.element);
|
||||
this.currentFocus = null;
|
||||
|
@ -27,13 +30,24 @@ export default class Modal {
|
|||
document.removeEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
/** Determines if this modal element is currently active or not. */
|
||||
isActive() {
|
||||
// The "active" modal is always the most recent one shown
|
||||
return activeModals[activeModals.length - 1] === this.element;
|
||||
}
|
||||
|
||||
checkFocus() {
|
||||
if (this.isActive()) {
|
||||
/** Activates external modal behavior and temporarily disables focus trapping. */
|
||||
activateExternal() {
|
||||
this.isExternalActivated = true;
|
||||
}
|
||||
|
||||
/** Deactivates external modal behavior and re-enables focus trapping. */
|
||||
deactivateExternal() {
|
||||
this.isExternalActivated = false;
|
||||
}
|
||||
|
||||
private checkFocus() {
|
||||
if (this.isActive() && !this.isExternalActivated) {
|
||||
const tabbableElements = getTabbableElements(this.element);
|
||||
if (!this.element.matches(':focus-within')) {
|
||||
const start = tabbableElements[0];
|
||||
|
@ -56,11 +70,9 @@ export default class Modal {
|
|||
return getTabbableElements(this.element).findIndex(el => el === this.currentFocus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the `startElement` is already focused. This is important if the modal already
|
||||
* has an existing focus prior to the first tab key.
|
||||
*/
|
||||
startElementAlreadyFocused(startElement: HTMLElement) {
|
||||
// Checks if the `startElement` is already focused. This is important if the modal already has an existing focus prior
|
||||
// to the first tab key.
|
||||
private startElementAlreadyFocused(startElement: HTMLElement) {
|
||||
for (const activeElement of activeElements()) {
|
||||
if (startElement === activeElement) {
|
||||
return true;
|
||||
|
@ -70,8 +82,8 @@ export default class Modal {
|
|||
return false;
|
||||
}
|
||||
|
||||
handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Tab') return;
|
||||
private handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Tab' || this.isExternalActivated) return;
|
||||
|
||||
if (event.shiftKey) {
|
||||
this.tabDirection = 'backward';
|
||||
|
|
Ładowanie…
Reference in New Issue