kopia lustrzana https://github.com/shoelace-style/shoelace
add support for external modals; fixes #1571
rodzic
2ed5a4ff97
commit
4b448fa52d
|
@ -14,6 +14,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||||
|
|
||||||
## Next
|
## 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 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]
|
- 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]
|
- 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.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.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.
|
* @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 {
|
export default class SlDialog extends ShoelaceElement {
|
||||||
static styles: CSSResultGroup = styles;
|
static styles: CSSResultGroup = styles;
|
||||||
|
@ -69,8 +73,8 @@ export default class SlDialog extends ShoelaceElement {
|
||||||
|
|
||||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||||
private readonly localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
private modal = new Modal(this);
|
|
||||||
private originalTrigger: HTMLElement | null;
|
private originalTrigger: HTMLElement | null;
|
||||||
|
public modal = new Modal(this);
|
||||||
|
|
||||||
@query('.dialog') dialog: HTMLElement;
|
@query('.dialog') dialog: HTMLElement;
|
||||||
@query('.dialog__panel') panel: 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.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.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.
|
* @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 {
|
export default class SlDrawer extends ShoelaceElement {
|
||||||
static styles: CSSResultGroup = styles;
|
static styles: CSSResultGroup = styles;
|
||||||
|
@ -75,8 +79,8 @@ export default class SlDrawer extends ShoelaceElement {
|
||||||
|
|
||||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||||
private readonly localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
private modal = new Modal(this);
|
|
||||||
private originalTrigger: HTMLElement | null;
|
private originalTrigger: HTMLElement | null;
|
||||||
|
public modal = new Modal(this);
|
||||||
|
|
||||||
@query('.drawer') drawer: HTMLElement;
|
@query('.drawer') drawer: HTMLElement;
|
||||||
@query('.drawer__panel') panel: HTMLElement;
|
@query('.drawer__panel') panel: HTMLElement;
|
||||||
|
|
|
@ -5,6 +5,7 @@ let activeModals: HTMLElement[] = [];
|
||||||
|
|
||||||
export default class Modal {
|
export default class Modal {
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
|
isExternalActivated: boolean;
|
||||||
tabDirection: 'forward' | 'backward' = 'forward';
|
tabDirection: 'forward' | 'backward' = 'forward';
|
||||||
currentFocus: HTMLElement | null;
|
currentFocus: HTMLElement | null;
|
||||||
|
|
||||||
|
@ -12,6 +13,7 @@ export default class Modal {
|
||||||
this.element = element;
|
this.element = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Activates focus trapping. */
|
||||||
activate() {
|
activate() {
|
||||||
activeModals.push(this.element);
|
activeModals.push(this.element);
|
||||||
document.addEventListener('focusin', this.handleFocusIn);
|
document.addEventListener('focusin', this.handleFocusIn);
|
||||||
|
@ -19,6 +21,7 @@ export default class Modal {
|
||||||
document.addEventListener('keyup', this.handleKeyUp);
|
document.addEventListener('keyup', this.handleKeyUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deactivates focus trapping. */
|
||||||
deactivate() {
|
deactivate() {
|
||||||
activeModals = activeModals.filter(modal => modal !== this.element);
|
activeModals = activeModals.filter(modal => modal !== this.element);
|
||||||
this.currentFocus = null;
|
this.currentFocus = null;
|
||||||
|
@ -27,13 +30,24 @@ export default class Modal {
|
||||||
document.removeEventListener('keyup', this.handleKeyUp);
|
document.removeEventListener('keyup', this.handleKeyUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Determines if this modal element is currently active or not. */
|
||||||
isActive() {
|
isActive() {
|
||||||
// The "active" modal is always the most recent one shown
|
// The "active" modal is always the most recent one shown
|
||||||
return activeModals[activeModals.length - 1] === this.element;
|
return activeModals[activeModals.length - 1] === this.element;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkFocus() {
|
/** Activates external modal behavior and temporarily disables focus trapping. */
|
||||||
if (this.isActive()) {
|
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);
|
const tabbableElements = getTabbableElements(this.element);
|
||||||
if (!this.element.matches(':focus-within')) {
|
if (!this.element.matches(':focus-within')) {
|
||||||
const start = tabbableElements[0];
|
const start = tabbableElements[0];
|
||||||
|
@ -56,11 +70,9 @@ export default class Modal {
|
||||||
return getTabbableElements(this.element).findIndex(el => el === this.currentFocus);
|
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
|
||||||
* Checks if the `startElement` is already focused. This is important if the modal already
|
// to the first tab key.
|
||||||
* has an existing focus prior to the first tab key.
|
private startElementAlreadyFocused(startElement: HTMLElement) {
|
||||||
*/
|
|
||||||
startElementAlreadyFocused(startElement: HTMLElement) {
|
|
||||||
for (const activeElement of activeElements()) {
|
for (const activeElement of activeElements()) {
|
||||||
if (startElement === activeElement) {
|
if (startElement === activeElement) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -70,8 +82,8 @@ export default class Modal {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown = (event: KeyboardEvent) => {
|
private handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key !== 'Tab') return;
|
if (event.key !== 'Tab' || this.isExternalActivated) return;
|
||||||
|
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
this.tabDirection = 'backward';
|
this.tabDirection = 'backward';
|
||||||
|
|
Ładowanie…
Reference in New Issue