reshaped 2.10.13 → 2.10.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bundle.css +1 -1
  3. package/bundle.js +15 -15
  4. package/components/Actionable/Actionable.js +0 -1
  5. package/components/Autocomplete/Autocomplete.js +7 -2
  6. package/components/Badge/Badge.module.css +1 -1
  7. package/components/Badge/Badge.types.d.ts +1 -1
  8. package/components/Button/Button.module.css +1 -1
  9. package/components/Calendar/CalendarMonth.js +1 -1
  10. package/components/Card/Card.d.ts +1 -1
  11. package/components/Card/Card.module.css +1 -1
  12. package/components/Card/tests/Card.stories.d.ts +1 -1
  13. package/components/Carousel/Carousel.module.css +1 -1
  14. package/components/FormControl/FormControl.context.d.ts +0 -2
  15. package/components/Link/Link.module.css +1 -1
  16. package/components/Loader/Loader.module.css +1 -1
  17. package/components/Modal/Modal.module.css +1 -1
  18. package/components/Overlay/Overlay.js +5 -16
  19. package/components/Pagination/Pagination.module.css +1 -0
  20. package/components/Pagination/PaginationControlled.js +3 -2
  21. package/components/Reshaped/Reshaped.css +1 -1
  22. package/components/Scrim/Scrim.module.css +1 -1
  23. package/components/ScrollArea/ScrollArea.module.css +1 -1
  24. package/components/Skeleton/Skeleton.module.css +1 -1
  25. package/components/Stepper/Stepper.module.css +1 -1
  26. package/components/Switch/Switch.module.css +1 -1
  27. package/components/Table/Table.js +1 -1
  28. package/components/Table/Table.module.css +1 -1
  29. package/components/Table/tests/Table.stories.js +16 -0
  30. package/components/Tabs/Tabs.module.css +1 -1
  31. package/components/Tabs/TabsItem.js +1 -1
  32. package/components/Tabs/TabsList.js +9 -9
  33. package/components/Tabs/tests/Tabs.stories.js +1 -1
  34. package/components/TextField/TextField.module.css +1 -1
  35. package/components/TextField/tests/TextField.stories.js +1 -1
  36. package/components/Toast/Toast.module.css +1 -1
  37. package/components/Toast/ToastContainer.js +6 -6
  38. package/components/Toast/ToastRegion.js +1 -1
  39. package/components/View/View.module.css +1 -1
  40. package/components/_private/Flyout/Flyout.module.css +1 -1
  41. package/components/_private/Flyout/Flyout.types.d.ts +1 -1
  42. package/components/_private/Flyout/FlyoutControlled.js +8 -14
  43. package/config/postcss.d.ts +1 -0
  44. package/config/postcss.js +1 -1
  45. package/hooks/_private/useSingletonKeyboardMode.js +3 -3
  46. package/hooks/useHotkeys.d.ts +6 -6
  47. package/package.json +31 -31
  48. package/styles/padding/padding.module.css +1 -1
  49. package/utilities/a11y/TrapFocus.d.ts +41 -0
  50. package/utilities/a11y/TrapFocus.js +127 -0
  51. package/utilities/a11y/TrapScreenReader.d.ts +15 -0
  52. package/utilities/a11y/TrapScreenReader.js +39 -0
  53. package/utilities/a11y/focus.d.ts +24 -0
  54. package/utilities/a11y/focus.js +90 -0
  55. package/utilities/a11y/keyboardMode.d.ts +3 -0
  56. package/utilities/a11y/keyboardMode.js +10 -0
  57. package/utilities/a11y/types.d.ts +18 -0
  58. package/utilities/a11y/types.js +1 -0
  59. package/utilities/helpers.d.ts +1 -1
  60. package/constants/attributes.d.ts +0 -2
  61. package/constants/attributes.js +0 -2
  62. package/utilities/a11y.d.ts +0 -36
  63. package/utilities/a11y.js +0 -220
@@ -0,0 +1,127 @@
1
+ import Chain from "../Chain.js";
2
+ import * as keys from "../../constants/keys.js";
3
+ import TrapScreenReader from "./TrapScreenReader.js";
4
+ import { getActiveElement, getFocusableElements, focusElement, getFocusData } from "./focus.js";
5
+ import { checkKeyboardMode } from "./keyboardMode.js";
6
+ class TrapFocus {
7
+ constructor(root) {
8
+ this.trigger = null;
9
+ this.options = {};
10
+ this.mutationObserver = null;
11
+ /**
12
+ * Handle keyboard navigation while focus is trapped
13
+ */
14
+ this.handleKeyDown = (event) => {
15
+ if (TrapFocus.chain.tailId !== this.chainId)
16
+ return;
17
+ const { mode, onNavigateOutside, pseudoFocus, includeTrigger } = this.options;
18
+ let navigationMode = "tabs";
19
+ if (mode === "action-menu" || mode === "selection-menu")
20
+ navigationMode = "arrows";
21
+ const key = event.key;
22
+ const isTab = key === keys.TAB;
23
+ const isNextTab = isTab && !event.shiftKey;
24
+ const isBackTab = isTab && event.shiftKey;
25
+ const isUp = navigationMode === "arrows" && key === keys.UP;
26
+ const isDown = navigationMode === "arrows" && key === keys.DOWN;
27
+ const isPrev = (isBackTab && navigationMode === "tabs") || isUp;
28
+ const isNext = (isNextTab && navigationMode === "tabs") || isDown;
29
+ const isFocusedOnTrigger = getActiveElement() === this.trigger;
30
+ const focusData = getFocusData({
31
+ root: this.root,
32
+ target: isPrev ? "prev" : "next",
33
+ options: {
34
+ additionalElement: includeTrigger ? this.trigger : undefined,
35
+ circular: mode !== "action-menu",
36
+ },
37
+ });
38
+ // Release the trap when tab is used in navigation modes that support arrows
39
+ const hasNavigatedOutside = (isTab && navigationMode === "arrows") ||
40
+ (mode === "content-menu" && isTab && focusData.overflow);
41
+ if (hasNavigatedOutside) {
42
+ // Prevent shift + tab event to avoid focus moving after the trap release
43
+ if (isBackTab && !isFocusedOnTrigger)
44
+ event.preventDefault();
45
+ this.release();
46
+ onNavigateOutside === null || onNavigateOutside === void 0 ? void 0 : onNavigateOutside();
47
+ return;
48
+ }
49
+ if (!isPrev && !isNext)
50
+ return;
51
+ event.preventDefault();
52
+ if (!focusData.el)
53
+ return;
54
+ focusElement(focusData.el, { pseudoFocus });
55
+ };
56
+ this.addListeners = () => document.addEventListener("keydown", this.handleKeyDown);
57
+ this.removeListeners = () => document.removeEventListener("keydown", this.handleKeyDown);
58
+ /**
59
+ * Trap the focus, add observer and keyboard event listeners
60
+ * and create a chain item
61
+ */
62
+ this.trap = (options = {}) => {
63
+ const { mode = "dialog", includeTrigger } = options;
64
+ const trigger = getActiveElement();
65
+ const focusable = getFocusableElements(this.root, {
66
+ additionalElement: includeTrigger ? trigger : undefined,
67
+ });
68
+ const pseudoFocus = mode === "selection-menu";
69
+ this.options = Object.assign(Object.assign({}, options), { pseudoFocus });
70
+ this.trigger = trigger;
71
+ this.mutationObserver = new MutationObserver(() => {
72
+ const currentActiveElement = getActiveElement();
73
+ // Focus stayed inside the wrapper, no need to refocus
74
+ if (this.root.contains(currentActiveElement))
75
+ return;
76
+ const focusable = getFocusableElements(this.root, {
77
+ additionalElement: includeTrigger ? trigger : undefined,
78
+ });
79
+ if (!focusable.length)
80
+ return;
81
+ focusElement(focusable[0], { pseudoFocus });
82
+ });
83
+ this.removeListeners();
84
+ if (mode === "dialog")
85
+ this.screenReaderTrap.trap();
86
+ this.mutationObserver.observe(this.root, { childList: true, subtree: true });
87
+ if (!focusable.length)
88
+ return;
89
+ this.addListeners();
90
+ // Don't add back to the chain if we're traversing back
91
+ const tailItem = TrapFocus.chain.tailId && TrapFocus.chain.get(TrapFocus.chain.tailId);
92
+ if (!tailItem || this.root !== tailItem.data.root) {
93
+ this.chainId = TrapFocus.chain.add(this);
94
+ focusElement(focusable[0], { pseudoFocus });
95
+ }
96
+ this.trapped = true;
97
+ };
98
+ /**
99
+ * Disabled the trap focus for the element,
100
+ * cleanup all observers/handlers and trap for the previous element in the chain
101
+ */
102
+ this.release = (releaseOptions = {}) => {
103
+ var _a;
104
+ const { withoutFocusReturn } = releaseOptions;
105
+ if (!this.trapped || !this.chainId)
106
+ return;
107
+ this.trapped = false;
108
+ if (this.trigger) {
109
+ const preventScroll = withoutFocusReturn || !checkKeyboardMode();
110
+ this.trigger.focus({ preventScroll });
111
+ }
112
+ TrapFocus.chain.removePreviousTill(this.chainId, (item) => document.body.contains(item.data.trigger));
113
+ (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
114
+ this.removeListeners();
115
+ this.screenReaderTrap.release();
116
+ const previousItem = TrapFocus.chain.tailId && TrapFocus.chain.get(TrapFocus.chain.tailId);
117
+ if (previousItem) {
118
+ const trapInstance = new TrapFocus(previousItem.data.root);
119
+ trapInstance.trap(previousItem.data.options);
120
+ }
121
+ };
122
+ this.root = root;
123
+ this.screenReaderTrap = new TrapScreenReader(root);
124
+ }
125
+ }
126
+ TrapFocus.chain = new Chain();
127
+ export default TrapFocus;
@@ -0,0 +1,15 @@
1
+ declare class TrapScreenReader {
2
+ root: HTMLElement;
3
+ /**
4
+ * Elements ignored by screen reader when trap is active
5
+ */
6
+ private hiddenElements;
7
+ constructor(root: HTMLElement);
8
+ /**
9
+ * Apply aria-hidden to all elements except the passed
10
+ */
11
+ hideSiblingsFromScreenReader: (el: HTMLElement) => void;
12
+ release: () => void;
13
+ trap: () => void;
14
+ }
15
+ export default TrapScreenReader;
@@ -0,0 +1,39 @@
1
+ class TrapScreenReader {
2
+ constructor(root) {
3
+ /**
4
+ * Elements ignored by screen reader when trap is active
5
+ */
6
+ this.hiddenElements = [];
7
+ /**
8
+ * Apply aria-hidden to all elements except the passed
9
+ */
10
+ this.hideSiblingsFromScreenReader = (el) => {
11
+ let sibling = el.parentNode && el.parentNode.firstChild;
12
+ while (sibling) {
13
+ const notCurrent = sibling !== el;
14
+ const isValid = sibling.nodeType === 1 && !sibling.hasAttribute("aria-hidden");
15
+ if (notCurrent && isValid) {
16
+ sibling.setAttribute("aria-hidden", "true");
17
+ this.hiddenElements.push(sibling);
18
+ }
19
+ sibling = sibling.nextSibling;
20
+ }
21
+ };
22
+ this.release = () => {
23
+ this.hiddenElements.forEach((el) => {
24
+ el.removeAttribute("aria-hidden");
25
+ });
26
+ this.hiddenElements = [];
27
+ };
28
+ this.trap = () => {
29
+ let currentEl = this.root;
30
+ this.release();
31
+ while (currentEl !== document.body) {
32
+ this.hideSiblingsFromScreenReader(currentEl);
33
+ currentEl = currentEl.parentElement;
34
+ }
35
+ };
36
+ this.root = root;
37
+ }
38
+ }
39
+ export default TrapScreenReader;
@@ -0,0 +1,24 @@
1
+ import type { FocusableElement } from "./types";
2
+ export declare const focusableSelector = "a,button,input:not([type=\"hidden\"]),textarea,select,details,[tabindex]:not([tabindex=\"-1\"])";
3
+ export declare const getActiveElement: () => HTMLButtonElement;
4
+ export declare const focusElement: (el: FocusableElement, options?: {
5
+ pseudoFocus?: boolean;
6
+ }) => void;
7
+ export declare const getFocusableElements: (rootEl: HTMLElement, options?: {
8
+ additionalElement?: FocusableElement | null;
9
+ }) => FocusableElement[];
10
+ export declare const getFocusData: (args: {
11
+ root: HTMLElement;
12
+ target: "next" | "prev" | "first" | "last";
13
+ options?: {
14
+ circular?: boolean;
15
+ additionalElement?: FocusableElement | null;
16
+ };
17
+ }) => {
18
+ overflow: boolean;
19
+ el: FocusableElement;
20
+ };
21
+ export declare const focusNextElement: (root: HTMLElement) => void;
22
+ export declare const focusPreviousElement: (root: HTMLElement) => void;
23
+ export declare const focusFirstElement: (root: HTMLElement) => void;
24
+ export declare const focusLastElement: (root: HTMLElement) => void;
@@ -0,0 +1,90 @@
1
+ const pseudoFocusAttribute = "data-rs-focus";
2
+ export const focusableSelector = 'a,button,input:not([type="hidden"]),textarea,select,details,[tabindex]:not([tabindex="-1"])';
3
+ export const getActiveElement = () => {
4
+ const pseudoFocusedEl = document.querySelector(`[${pseudoFocusAttribute}]`);
5
+ return (pseudoFocusedEl || document.activeElement);
6
+ };
7
+ export const focusElement = (el, options) => {
8
+ var _a;
9
+ (_a = document.querySelector(`[${pseudoFocusAttribute}]`)) === null || _a === void 0 ? void 0 : _a.removeAttribute(pseudoFocusAttribute);
10
+ if (options === null || options === void 0 ? void 0 : options.pseudoFocus) {
11
+ el.setAttribute(pseudoFocusAttribute, "true");
12
+ }
13
+ else {
14
+ el.focus();
15
+ }
16
+ };
17
+ export const getFocusableElements = (rootEl, options) => {
18
+ const focusableElements = Array.from(rootEl.querySelectorAll(focusableSelector));
19
+ const filteredElements = focusableElements.filter((el) => {
20
+ if (el.hasAttribute("disabled"))
21
+ return false;
22
+ if (el.clientHeight === 0)
23
+ return false;
24
+ if (el.type === "radio") {
25
+ let sameNameRadioEls;
26
+ if (el.form) {
27
+ const formInputs = el.form.elements.namedItem(el.name);
28
+ // Synthetic error handling for narrowing down types
29
+ // Radio element can't find itself in the form, so we don't need to include it in the array
30
+ if (!formInputs)
31
+ return false;
32
+ const multipleElementsFound = "length" in formInputs;
33
+ if (!multipleElementsFound) {
34
+ // Single element found is always an input radio since we're inside the condition
35
+ sameNameRadioEls = [formInputs];
36
+ }
37
+ else {
38
+ sameNameRadioEls = Array.from(formInputs).filter((el) => "type" in el && el.type === "radio");
39
+ }
40
+ }
41
+ else {
42
+ sameNameRadioEls = Array.from(rootEl.querySelectorAll(`[type="radio"][name="${el.name}"]`));
43
+ }
44
+ if (sameNameRadioEls === null || sameNameRadioEls === void 0 ? void 0 : sameNameRadioEls.length) {
45
+ const checkedEl = Array.from(sameNameRadioEls).find((el) => el.checked);
46
+ if (checkedEl && el !== checkedEl)
47
+ return false;
48
+ if (!checkedEl && el !== sameNameRadioEls[0])
49
+ return false;
50
+ }
51
+ }
52
+ return true;
53
+ });
54
+ if ((options === null || options === void 0 ? void 0 : options.additionalElement) && filteredElements.length) {
55
+ filteredElements.unshift(options.additionalElement);
56
+ }
57
+ return filteredElements;
58
+ };
59
+ export const getFocusData = (args) => {
60
+ const { root, target, options } = args;
61
+ const focusable = getFocusableElements(root, { additionalElement: options === null || options === void 0 ? void 0 : options.additionalElement });
62
+ const focusableLimit = focusable.length - 1;
63
+ const currentElement = getActiveElement();
64
+ const currentIndex = focusable.indexOf(currentElement);
65
+ const positions = {
66
+ next: currentIndex + 1,
67
+ prev: currentIndex - 1,
68
+ first: 0,
69
+ last: focusableLimit,
70
+ };
71
+ let nextIndex = positions[target];
72
+ const isOverflow = nextIndex > focusableLimit || nextIndex < 0;
73
+ if (isOverflow) {
74
+ if (options === null || options === void 0 ? void 0 : options.circular) {
75
+ nextIndex = target === "prev" ? positions.last : positions.first;
76
+ }
77
+ else {
78
+ nextIndex = target === "prev" ? positions.first : positions.last;
79
+ }
80
+ }
81
+ return { overflow: isOverflow, el: focusable[nextIndex] };
82
+ };
83
+ const focusTargetElement = (root, target) => {
84
+ const data = getFocusData({ root, target });
85
+ focusElement(data.el);
86
+ };
87
+ export const focusNextElement = (root) => focusTargetElement(root, "next");
88
+ export const focusPreviousElement = (root) => focusTargetElement(root, "prev");
89
+ export const focusFirstElement = (root) => focusTargetElement(root, "first");
90
+ export const focusLastElement = (root) => focusTargetElement(root, "last");
@@ -0,0 +1,3 @@
1
+ export declare const enableKeyboardMode: () => void;
2
+ export declare const disableKeyboardMode: () => void;
3
+ export declare const checkKeyboardMode: () => boolean;
@@ -0,0 +1,10 @@
1
+ const keyboardModeAttribute = "data-rs-keyboard";
2
+ export const enableKeyboardMode = () => {
3
+ document.documentElement.setAttribute(keyboardModeAttribute, "true");
4
+ };
5
+ export const disableKeyboardMode = () => {
6
+ document.documentElement.removeAttribute(keyboardModeAttribute);
7
+ };
8
+ export const checkKeyboardMode = () => {
9
+ return document.documentElement.hasAttribute(keyboardModeAttribute);
10
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * dialog: Completely trap the focus inside for tab navigation until content is closed
3
+ * example: Modal
4
+ *
5
+ * action-menu: Trap the arrow navigation, while tab moves the focus to
6
+ * the next element on the page after the trigger
7
+ * example: Dropdown Menu
8
+ *
9
+ * content-menu: Include dropdown content into the tab navigation flow and move the focus to
10
+ * the next element on the page after the trigger after navigation through the trapped content
11
+ * example: Header navigation dropdowns
12
+ *
13
+ * selection-menu: Keep the focus on the trigger and enable arrow key navigation with pseudo focusing with data-attributes
14
+ * without moving the focus away from the trigger
15
+ * example: Autocomplete
16
+ */
17
+ export type TrapMode = "dialog" | "action-menu" | "content-menu" | "selection-menu";
18
+ export type FocusableElement = HTMLButtonElement | HTMLInputElement;
@@ -0,0 +1 @@
1
+ export {};
@@ -10,7 +10,7 @@ export declare const classNames: (...args: G.ClassName[]) => string;
10
10
  export declare const responsiveClassNames: <V extends G.Responsive<Value>>(s: Record<string, string>, className: ClassNameResolver, value: V, options?: {
11
11
  excludeValueFromClassName?: boolean;
12
12
  }) => string[];
13
- export declare const responsiveVariables: <V extends Value = Value>(variableName: G.CSSVariable, value?: G.Responsive<V> | undefined) => Record<`--${string}`, V>;
13
+ export declare const responsiveVariables: <V extends Value = Value>(variableName: G.CSSVariable, value?: G.Responsive<V>) => Record<G.CSSVariable, V>;
14
14
  export declare const responsivePropDependency: <Result, T>(prop: G.Responsive<T>, resolver: (value: T, key: G.Viewport) => Result) => Result;
15
15
  export declare const resolveViewportValue: <T>(viewport: G.Viewport, value: G.Responsive<T>) => T | undefined;
16
16
  export {};
@@ -1,2 +0,0 @@
1
- export declare const keyboardModeAttribute = "data-rs-keyboard";
2
- export declare const pseudoFocusAttribute = "data-rs-focus";
@@ -1,2 +0,0 @@
1
- export const keyboardModeAttribute = "data-rs-keyboard";
2
- export const pseudoFocusAttribute = "data-rs-focus";
@@ -1,36 +0,0 @@
1
- /**
2
- * dialog: Completely trap the focus inside for tab navigation until content is closed
3
- * example: Modal
4
- *
5
- * action-menu: Trap the arrow navigation, while tab moves the focus to
6
- * the next element on the page after the trigger
7
- * example: Dropdown Menu
8
- *
9
- * content-menu: Include dropdown content into the tab navigation flow and move the focus to
10
- * the next element on the page after the trigger after navigation through the trapped content
11
- * example: Header navigation dropdowns
12
- *
13
- * selection-menu: Keep the focus on the trigger and enable arrow key navigation with pseudo focusing with data-attributes
14
- * without moving the focus away from the trigger
15
- * example: Autocomplete
16
- */
17
- export type TrapMode = "dialog" | "action-menu" | "content-menu" | "selection-menu";
18
- type ReleaseOptions = {
19
- withoutFocusReturn?: boolean;
20
- };
21
- type Release = (options?: ReleaseOptions) => void;
22
- type TrapOptions = {
23
- onNavigateOutside?: () => void;
24
- includeTrigger?: boolean;
25
- mode?: TrapMode;
26
- };
27
- type Trap = Release | null;
28
- export declare const isKeyboardMode: () => boolean;
29
- export declare const focusableSelector = "a,button,input:not([type=\"hidden\"]),textarea,select,details,[tabindex]:not([tabindex=\"-1\"])";
30
- export declare const getActiveElement: () => HTMLButtonElement;
31
- export declare const focusNextElement: (root: HTMLElement) => void;
32
- export declare const focusPreviousElement: (root: HTMLElement) => void;
33
- export declare const focusFirstElement: (root: HTMLElement) => void;
34
- export declare const focusLastElement: (root: HTMLElement) => void;
35
- export declare const trapFocus: (root: HTMLElement, options?: TrapOptions) => Trap;
36
- export {};
package/utilities/a11y.js DELETED
@@ -1,220 +0,0 @@
1
- import { TAB, UP, DOWN } from "../constants/keys.js";
2
- import { keyboardModeAttribute, pseudoFocusAttribute } from "../constants/attributes.js";
3
- import Chain from "./Chain.js";
4
- export const isKeyboardMode = () => document.documentElement.hasAttribute(keyboardModeAttribute);
5
- export const focusableSelector = 'a,button,input:not([type="hidden"]),textarea,select,details,[tabindex]:not([tabindex="-1"])';
6
- export const getActiveElement = () => {
7
- const pseudoFocusedEl = document.querySelector(`[${pseudoFocusAttribute}]`);
8
- return (pseudoFocusedEl || document.activeElement);
9
- };
10
- const getFocusableElements = (rootEl, extraElement) => {
11
- const focusableElements = Array.from(rootEl.querySelectorAll(focusableSelector));
12
- const filteredElements = focusableElements.filter((el) => {
13
- if (el.hasAttribute("disabled"))
14
- return false;
15
- if (el.clientHeight === 0)
16
- return false;
17
- if (el.type === "radio") {
18
- let sameNameRadioEls;
19
- if (el.form) {
20
- const formInputs = el.form.elements.namedItem(el.name);
21
- // Synthetic error handling for narrowing down types
22
- // Radio element can't find itself in the form, so we don't need to include it in the array
23
- if (!formInputs)
24
- return false;
25
- const multipleElementsFound = "length" in formInputs;
26
- if (!multipleElementsFound) {
27
- // Single element found is always an input radio since we're inside the condition
28
- sameNameRadioEls = [formInputs];
29
- }
30
- else {
31
- sameNameRadioEls = Array.from(formInputs).filter((el) => "type" in el && el.type === "radio");
32
- }
33
- }
34
- else {
35
- sameNameRadioEls = Array.from(rootEl.querySelectorAll(`[type="radio"][name="${el.name}"]`));
36
- }
37
- if (sameNameRadioEls === null || sameNameRadioEls === void 0 ? void 0 : sameNameRadioEls.length) {
38
- const checkedEl = Array.from(sameNameRadioEls).find((el) => el.checked);
39
- if (checkedEl && el !== checkedEl)
40
- return false;
41
- if (!checkedEl && el !== sameNameRadioEls[0])
42
- return false;
43
- }
44
- }
45
- return true;
46
- });
47
- if (extraElement && filteredElements.length)
48
- filteredElements.unshift(extraElement);
49
- return filteredElements;
50
- };
51
- const getFocusData = (args) => {
52
- const { root, extraElement, target, mode } = args;
53
- const focusable = getFocusableElements(root, extraElement);
54
- const focusableLimit = focusable.length - 1;
55
- const currentElement = getActiveElement();
56
- const currentIndex = focusable.indexOf(currentElement);
57
- const positions = {
58
- next: currentIndex + 1,
59
- prev: currentIndex - 1,
60
- first: 0,
61
- last: focusableLimit,
62
- };
63
- let nextIndex = positions[target];
64
- const isOverflow = nextIndex > focusableLimit || nextIndex < 0;
65
- if (isOverflow) {
66
- // Disable circular navigation for action menus
67
- if (mode === "action-menu") {
68
- nextIndex = target === "prev" ? positions.first : positions.last;
69
- }
70
- else {
71
- nextIndex = target === "prev" ? positions.last : positions.first;
72
- }
73
- }
74
- return { overflow: isOverflow, el: focusable[nextIndex] };
75
- };
76
- const focusElement = (el, mode) => {
77
- var _a;
78
- (_a = document.querySelector(`[${pseudoFocusAttribute}]`)) === null || _a === void 0 ? void 0 : _a.removeAttribute(pseudoFocusAttribute);
79
- if (mode === "selection-menu") {
80
- el.setAttribute(pseudoFocusAttribute, "true");
81
- }
82
- else {
83
- el.focus();
84
- }
85
- };
86
- const focusTargetElement = (root, target) => {
87
- const data = getFocusData({ root, target });
88
- focusElement(data.el);
89
- };
90
- export const focusNextElement = (root) => focusTargetElement(root, "next");
91
- export const focusPreviousElement = (root) => focusTargetElement(root, "prev");
92
- export const focusFirstElement = (root) => focusTargetElement(root, "first");
93
- export const focusLastElement = (root) => focusTargetElement(root, "last");
94
- const trapScreenReader = (() => {
95
- let affectedElements = [];
96
- const applyHiddenToSiblings = (el) => {
97
- let sibling = el.parentNode && el.parentNode.firstChild;
98
- while (sibling) {
99
- const notCurrent = sibling !== el;
100
- const isValid = sibling.nodeType === 1 && !sibling.hasAttribute("aria-hidden");
101
- if (notCurrent && isValid) {
102
- sibling.setAttribute("aria-hidden", "true");
103
- affectedElements.push(sibling);
104
- }
105
- sibling = sibling.nextSibling;
106
- }
107
- };
108
- const release = () => {
109
- affectedElements.forEach((el) => {
110
- el.removeAttribute("aria-hidden");
111
- });
112
- affectedElements = [];
113
- };
114
- return (el) => {
115
- let currentEl = el;
116
- if (affectedElements.length)
117
- release();
118
- while (currentEl !== document.body) {
119
- applyHiddenToSiblings(currentEl);
120
- currentEl = currentEl.parentElement;
121
- }
122
- return { release };
123
- };
124
- })();
125
- export const trapFocus = (() => {
126
- let resetListeners = null;
127
- let srTrap = null;
128
- const chain = new Chain();
129
- return (root, options = {}) => {
130
- const { mode = "dialog", onNavigateOutside, includeTrigger } = options;
131
- const triggerElement = getActiveElement();
132
- const isDialog = mode === "dialog";
133
- const isContentMenu = mode === "content-menu";
134
- const isSelectionMenu = mode === "selection-menu";
135
- const isTabMode = isDialog || isContentMenu;
136
- const isArrowsMode = mode === "action-menu" || isSelectionMenu;
137
- const focusable = getFocusableElements(root, includeTrigger ? triggerElement : undefined);
138
- let chainId;
139
- // Re-focus on the first element if content has changed
140
- const observer = new MutationObserver(() => {
141
- // Focus stayed inside the wrapper, no need to refocus
142
- if (root.contains(document.activeElement))
143
- return;
144
- const focusable = getFocusableElements(root, includeTrigger ? triggerElement : undefined);
145
- if (!focusable.length)
146
- return;
147
- focusElement(focusable[0], mode);
148
- });
149
- const release = (releaseOptions = {}) => {
150
- const { withoutFocusReturn } = releaseOptions;
151
- chain.removePreviousTill(chainId, (item) => document.body.contains(item.data.trigger));
152
- observer.disconnect();
153
- if (triggerElement) {
154
- triggerElement.focus({ preventScroll: withoutFocusReturn || !isKeyboardMode() });
155
- }
156
- if (resetListeners) {
157
- resetListeners();
158
- if (srTrap)
159
- srTrap.release();
160
- resetListeners = null;
161
- srTrap = null;
162
- }
163
- const previousItem = chain.tailId && chain.get(chain.tailId);
164
- if (previousItem) {
165
- trapFocus(previousItem.data.root, previousItem.data.options);
166
- }
167
- };
168
- const handleKeyDown = (event) => {
169
- const key = event.key;
170
- const isTab = key === TAB;
171
- const isNextTab = isTab && !event.shiftKey;
172
- const isBackTab = isTab && event.shiftKey;
173
- const isUp = isArrowsMode && key === UP;
174
- const isDown = isArrowsMode && key === DOWN;
175
- const isPrev = (isBackTab && isTabMode) || isUp;
176
- const isNext = (isNextTab && isTabMode) || isDown;
177
- const isFocusedOnTrigger = getActiveElement() === triggerElement;
178
- const focusData = getFocusData({
179
- root,
180
- target: isPrev ? "prev" : "next",
181
- extraElement: includeTrigger ? triggerElement : undefined,
182
- mode,
183
- });
184
- // Release the trap when tab is used in navigation modes that support arrows
185
- const hasNavigatedOutside = (isTab && isArrowsMode) || (isContentMenu && isTab && focusData.overflow);
186
- if (hasNavigatedOutside) {
187
- // Prevent shift + tab event to avoid focus moving after the trap release
188
- if (isBackTab && !isFocusedOnTrigger)
189
- event.preventDefault();
190
- release();
191
- onNavigateOutside === null || onNavigateOutside === void 0 ? void 0 : onNavigateOutside();
192
- return;
193
- }
194
- // We return after the last condition because Tab can be used for releasing in arrows mode
195
- if (!isPrev && !isNext)
196
- return;
197
- event.preventDefault();
198
- if (!focusData.el)
199
- return;
200
- focusElement(focusData.el, mode);
201
- };
202
- // Reset event listeners if focus is trapped elsewhere
203
- if (resetListeners)
204
- resetListeners();
205
- if (isDialog)
206
- srTrap = trapScreenReader(root);
207
- observer.observe(root, { childList: true, subtree: true });
208
- if (!focusable.length)
209
- return null;
210
- document.addEventListener("keydown", handleKeyDown);
211
- resetListeners = () => document.removeEventListener("keydown", handleKeyDown);
212
- // Don't add back to the chain if we're traversing back
213
- const tailItem = chain.tailId && chain.get(chain.tailId);
214
- if (!tailItem || root !== tailItem.data.root) {
215
- chainId = chain.add({ root, trigger: triggerElement, options });
216
- focusElement(focusable[0], mode);
217
- }
218
- return release;
219
- };
220
- })();