-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(dialog): toggle focus-trap on DOM connect and disconnect (#11429)
**Related Issue:** #10731 ## Summary Fixes an issue where a `focus-trap` instance might be left active after the dialog is removed. ### Relevant changes * Adds `useFocusTrap` controller * Extracts and exports function to create default `focus-trap` options
- Loading branch information
Showing
3 changed files
with
131 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
packages/calcite-components/src/controllers/useFocusTrap.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { makeGenericController } from "@arcgis/components-controllers"; | ||
import { createFocusTrap, FocusTrap, Options as FocusTrapOptions } from "focus-trap"; | ||
import { LitElement } from "@arcgis/lumina"; | ||
import { createFocusTrapOptions } from "../utils/focusTrapComponent"; | ||
|
||
export interface UseFocusTrap { | ||
/** | ||
* Activates the focus trap. | ||
* | ||
* @see https://github.com/focus-trap/focus-trap#trapactivate | ||
*/ | ||
activate: (options?: Parameters<FocusTrap["activate"]>[0]) => void; | ||
|
||
/** | ||
* Deactivates the focus trap. | ||
* | ||
* @see https://github.com/focus-trap/focus-trap#trapdeactivate | ||
*/ | ||
deactivate: (options?: Parameters<FocusTrap["deactivate"]>[0]) => void; | ||
|
||
/** | ||
* By default, the host element will be used as the focus-trap element, but if the focus-trap element needs to be a different element, use this method prior to activating to set the focus-trap element. | ||
*/ | ||
overrideFocusTrapEl: (el: HTMLElement) => void; | ||
|
||
/** | ||
* Updates focusable elements within the trap. | ||
* | ||
* @see https://github.com/focus-trap/focus-trap#trapupdatecontainerelements | ||
*/ | ||
updateContainerElements: () => void; | ||
} | ||
|
||
interface UseFocusTrapOptions<T extends LitElement = LitElement> { | ||
/** | ||
* The name of the prop that will trigger the focus trap to activate. | ||
*/ | ||
triggerProp: keyof T; | ||
|
||
/** | ||
* Options to pass to the focus-trap library. | ||
*/ | ||
focusTrapOptions?: FocusTrapOptions; | ||
} | ||
|
||
/** | ||
* A controller for managing focus traps. | ||
* | ||
* Note: traps will be deactivated automatically when the component is disconnected. | ||
* | ||
* @param options | ||
*/ | ||
export const useFocusTrap = <T extends LitElement>( | ||
options: UseFocusTrapOptions<T>, | ||
): ReturnType<typeof makeGenericController<UseFocusTrap, T>> => { | ||
return makeGenericController<UseFocusTrap, T>((component, controller) => { | ||
let focusTrap: FocusTrap; | ||
let focusTrapEl: HTMLElement; | ||
const { focusTrapOptions } = options; | ||
|
||
controller.onConnected(() => { | ||
if (component[options.triggerProp] && focusTrap) { | ||
focusTrap.activate(); | ||
} | ||
}); | ||
controller.onDisconnected(() => focusTrap?.deactivate()); | ||
|
||
return { | ||
activate: (options?: Parameters<FocusTrap["activate"]>[0]) => { | ||
const targetEl = focusTrapEl || component.el; | ||
|
||
if (!targetEl.isConnected) { | ||
return; | ||
} | ||
|
||
if (!focusTrap) { | ||
focusTrap = createFocusTrap(targetEl, createFocusTrapOptions(targetEl, focusTrapOptions)); | ||
} | ||
|
||
focusTrap.activate(options); | ||
}, | ||
deactivate: (options?: Parameters<FocusTrap["deactivate"]>[0]) => focusTrap?.deactivate(options), | ||
overrideFocusTrapEl: (el: HTMLElement) => { | ||
if (focusTrap) { | ||
throw new Error("Focus trap already created"); | ||
} | ||
|
||
focusTrapEl = el; | ||
}, | ||
updateContainerElements: () => { | ||
const targetEl = focusTrapEl || component.el; | ||
return focusTrap?.updateContainerElements(targetEl); | ||
}, | ||
}; | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters