-
Notifications
You must be signed in to change notification settings - Fork 79
feat(dialog, modal, popover, input-date-picker, input-time-picker, sheet): support stacked component sequential closing with escape #9231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
b9b684d
281619e
2ad2c8e
c95e3aa
c54cab1
e3823ea
1187a3a
fadbb0d
0622da1
00e576b
55fbb07
0af108e
47cf21d
6a1ad45
8ca1dc5
d2324b2
5861f82
95af390
69dbd5a
ba39a7f
3e4b38e
85f4ef2
7418a25
a9dd5c0
58622a4
0376e14
f11fb0f
31764b2
a7cebb6
c993e02
327aa38
785fed5
84f881e
84c20d8
9db785e
06570cd
c3e5da4
ef5ad2a
16564f1
f7dd170
a493275
6d3ed51
d8d0412
9f18ccc
1e67bf8
3e94a4d
16c7f1a
5d5b90b
20cde10
249081a
829d93b
d8acece
76a4cf7
766ec1e
f720d10
6bf5a5f
7f110b6
0b582b8
7afdb64
d8f34c5
87f4f56
b645dea
353ec39
79ddd20
337342b
212f6bc
0f8c4e2
fd53ff4
ee5d656
7e3dda8
06e68ff
cfe719d
b7f7411
b75baee
fc85f74
5097b0b
2c40889
5117688
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,12 @@ export interface FocusTrapComponent { | |
*/ | ||
el: HTMLElement; | ||
|
||
/** When `true`, disables the default close on escape behavior. */ | ||
driskull marked this conversation as resolved.
Show resolved
Hide resolved
|
||
escapeDisabled?: boolean; | ||
|
||
/** When `true`, disables the closing of the component when clicked outside. */ | ||
outsideCloseDisabled?: boolean; | ||
|
||
/** | ||
* When `true`, prevents focus trapping. | ||
*/ | ||
|
@@ -27,6 +33,11 @@ export interface FocusTrapComponent { | |
* This should be implemented for components that allow user content and/or have conditionally-rendered focusable elements within the trap. | ||
*/ | ||
updateFocusTrapElements?: () => Promise<void>; | ||
|
||
/** | ||
* Method that gets called when the focus trap is deactivated. | ||
*/ | ||
onFocusTrapDeactivate?(): void; | ||
driskull marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
export type FocusTrap = _FocusTrap; | ||
|
@@ -58,9 +69,10 @@ export function connectFocusTrap(component: FocusTrapComponent, options?: Connec | |
} | ||
|
||
const focusTrapOptions: FocusTrapOptions = { | ||
clickOutsideDeactivates: true, | ||
Elijbet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
escapeDeactivates: false, | ||
clickOutsideDeactivates: !component.outsideCloseDisabled ?? true, | ||
escapeDeactivates: !component.escapeDisabled ?? true, | ||
Elijbet marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FIXME: I noticed one issue with us relying on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm that's not good. It seems to have been that way from the beginning (first release) We could ask and see why its that way and if there is any chance of having a focusTrap option to not use capture events. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess instead of using escapeDeactivates, we would have to handle closing the appropriate focus trap element in the trapStack Would have to look at our trapStack and call trap.deactivate() https://github.com/focus-trap/focus-trap?tab=readme-ov-file#trapdeactivate This means we would set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would need to setup an event listener on the document to listen for the escape key and close the top most focusTrap in the focusTrapStack. We could probably have a method in the To setup the event listener, we'll need to track how many active focus traps there are by having some trap count in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's an example of what I'm thinking: diff --git forkSrcPrefix/packages/calcite-components/src/utils/focusTrapComponent.ts forkDstPrefix/packages/calcite-components/src/utils/focusTrapComponent.ts
index 877c3dc04f3329dd1355a30e8af3d888b3e96cf4..e88af0d7cdf6c530382888b3f8a02c5537bc6dd7 100644
--- forkSrcPrefix/packages/calcite-components/src/utils/focusTrapComponent.ts
+++ forkDstPrefix/packages/calcite-components/src/utils/focusTrapComponent.ts
@@ -2,6 +2,9 @@ import { createFocusTrap, FocusTrap as _FocusTrap, Options as FocusTrapOptions }
import { FocusableElement, focusElement, tabbableOptions } from "./dom";
import { focusTrapStack } from "./config";
+// i'm not sure if this needs to be a map of all documents. Would the keydown event bubble up out of shadow dom elements? If so, then the map isn't necessary.
+const focusTrapDocumentMap = new Map<Document, number>();
+
/**
* Defines interface for components with a focus trap. Focusable content is required for components implementing focus trapping with this interface.
*/
@@ -51,6 +54,7 @@ interface ConnectFocusTrapOptions {
*/
export function connectFocusTrap(component: FocusTrapComponent, options?: ConnectFocusTrapOptions): void {
const { el } = component;
+ const { ownerDocument } = el;
const focusTrapNode = options?.focusTrapEl || el;
if (!focusTrapNode) {
@@ -68,12 +72,28 @@ export function connectFocusTrap(component: FocusTrapComponent, options?: Connec
...options?.focusTrapOptions,
// the following options are not overridable
- document: el.ownerDocument,
+ document: ownerDocument,
tabbableOptions,
trapStack: focusTrapStack,
};
component.focusTrap = createFocusTrap(focusTrapNode, focusTrapOptions);
+ const newCount = (focusTrapDocumentMap.get(ownerDocument) || 0) + 1;
+ if (newCount === 1) {
+ addEscapeListener(ownerDocument);
+ }
+ focusTrapDocumentMap.set(ownerDocument, newCount);
+}
+
+// would need to call this on disconnect of all focus trap components.
+export function disconnectFocusTrap(component: FocusTrapComponent): void {
+ const { el } = component;
+ const { ownerDocument } = el;
+ const newCount = (focusTrapDocumentMap.get(ownerDocument) || 1) - 1;
+ if (newCount === 0) {
+ removeEscapeListener(ownerDocument);
+ }
+ focusTrapDocumentMap.set(ownerDocument, newCount);
}
/**
@@ -119,3 +139,17 @@ export function deactivateFocusTrap(
export function updateFocusTrapElements(component: FocusTrapComponent): void {
component.focusTrap?.updateContainerElements(component.el);
}
+
+function escapeHandler(event: KeyboardEvent): void {
+ if (event.key === "Escape") {
+ focusTrapStack[focusTrapStack.length - 1]?.deactivate(); // deactivate the last active focus trap. This might be the 0 index. I'm not sure.
+ }
+}
+
+function addEscapeListener(document: Document): void {
+ document.addEventListener("keydown", escapeHandler);
+}
+
+function removeEscapeListener(document: Document): void {
+ document.removeEventListener("keydown", escapeHandler);
+} This would let us handle the escape key and close the last active trap. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Example: #10197 |
||
fallbackFocus: focusTrapNode, | ||
onDeactivate: () => component.onFocusTrapDeactivate(), | ||
setReturnFocus: (el) => { | ||
focusElement(el as FocusableElement); | ||
return false; | ||
|
Uh oh!
There was an error while loading. Please reload this page.