@stack-spot/portal-components 2.7.0 → 2.8.1
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.
- package/CHANGELOG.md +14 -0
- package/dist/hooks/keyboard.d.ts +35 -3
- package/dist/hooks/keyboard.d.ts.map +1 -1
- package/dist/hooks/keyboard.js +33 -8
- package/dist/hooks/keyboard.js.map +1 -1
- package/dist/utils/accessibility.d.ts +4 -2
- package/dist/utils/accessibility.d.ts.map +1 -1
- package/dist/utils/accessibility.js +12 -20
- package/dist/utils/accessibility.js.map +1 -1
- package/package.json +1 -1
- package/src/hooks/keyboard.tsx +57 -11
- package/src/utils/accessibility.ts +15 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.8.1](https://github.com/stack-spot/portal-commons/compare/portal-components@v2.8.0...portal-components@v2.8.1) (2024-10-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* fixes utility function focusFirstChild(element) ([#463](https://github.com/stack-spot/portal-commons/issues/463)) ([b221eb0](https://github.com/stack-spot/portal-commons/commit/b221eb063f59786e2f32c4324d211975fc72d894))
|
|
9
|
+
|
|
10
|
+
## [2.8.0](https://github.com/stack-spot/portal-commons/compare/portal-components@v2.7.0...portal-components@v2.8.0) (2024-10-25)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* adds new features to the keyboard hook ([#459](https://github.com/stack-spot/portal-commons/issues/459)) ([39b669c](https://github.com/stack-spot/portal-commons/commit/39b669c4ea4b551b399be50678399e14607ce167))
|
|
16
|
+
|
|
3
17
|
## [2.7.0](https://github.com/stack-spot/portal-commons/compare/portal-components@v2.6.1...portal-components@v2.7.0) (2024-10-18)
|
|
4
18
|
|
|
5
19
|
|
package/dist/hooks/keyboard.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
interface
|
|
2
|
+
interface Options<T extends HTMLElement = HTMLDivElement> {
|
|
3
3
|
/**
|
|
4
4
|
* A query selector that returns every html element that must be navigable through the keyboard.
|
|
5
5
|
*/
|
|
@@ -11,8 +11,29 @@ interface Props {
|
|
|
11
11
|
/**
|
|
12
12
|
* Function to call when TAB is pressed at the last item in the list of items returned by the query selector. Will be the same as
|
|
13
13
|
* onPressEscape if not specified.
|
|
14
|
+
*
|
|
15
|
+
* Attention: has no effect if `disableTabBehavior` is true.
|
|
14
16
|
*/
|
|
15
17
|
onPressLastTab?: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Pass this function if you want any behavior when the user presses the arrow left.
|
|
20
|
+
*/
|
|
21
|
+
onPressArrowLeft?: () => void;
|
|
22
|
+
/**
|
|
23
|
+
* Pass this function if you want any behavior when the user presses the arrow right.
|
|
24
|
+
*/
|
|
25
|
+
onPressArrowRight?: () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Disables any alteration to the tab key.
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
disableTabBehavior?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* If you already have a ref to the element you want to attach the events to, you can pass it in this prop.
|
|
33
|
+
*
|
|
34
|
+
* If you pass a ref. The events won't be attached to the document, instead, they will be attached to the element referred by the ref.
|
|
35
|
+
*/
|
|
36
|
+
ref?: React.RefObject<T>;
|
|
16
37
|
}
|
|
17
38
|
/**
|
|
18
39
|
* Creates listeners for controlling a Menu UI through the keyboard.
|
|
@@ -20,11 +41,22 @@ interface Props {
|
|
|
20
41
|
* - Arrow up: previous element in the iterator returned by the query selectors. Last element, if the current element is the first.
|
|
21
42
|
* - Tab: same as Arrow down, but has a different behavior if the element is the last (see onPressLastTab).
|
|
22
43
|
* - Esc: determined by onPressEscape.
|
|
23
|
-
* @param props {@link
|
|
44
|
+
* @param props {@link Options}.
|
|
24
45
|
* @returns an object with the element controlled by the keyboard (useRef); a function to attach the keyboard events and a function to
|
|
25
46
|
* detach the keyboard events.
|
|
26
47
|
*/
|
|
27
|
-
export declare function useKeyboardControls<T extends HTMLElement = HTMLDivElement>(
|
|
48
|
+
export declare function useKeyboardControls<T extends HTMLElement = HTMLDivElement>(
|
|
49
|
+
/**
|
|
50
|
+
* Options for the keyboard controls.
|
|
51
|
+
*/
|
|
52
|
+
{ querySelectors, onPressEscape, onPressLastTab, onPressArrowLeft, onPressArrowRight, disableTabBehavior, ref, }: Options<T>,
|
|
53
|
+
/**
|
|
54
|
+
* Calls `attachKeyboardListeners` (mount) and `detachKeyboardListeners` (unmount) whenever the deps passed as parameter changes.
|
|
55
|
+
*
|
|
56
|
+
* If deps are undefined, this component doesn't automatically add these listeners and you have to use the functions returned in the
|
|
57
|
+
* result.
|
|
58
|
+
*/
|
|
59
|
+
deps?: any[]): {
|
|
28
60
|
keyboardControlledElement: import("react").RefObject<T>;
|
|
29
61
|
attachKeyboardListeners: () => void;
|
|
30
62
|
detachKeyboardListeners: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../../src/hooks/keyboard.tsx"],"names":[],"mappings":";AAEA,UAAU,
|
|
1
|
+
{"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../../src/hooks/keyboard.tsx"],"names":[],"mappings":";AAEA,UAAU,OAAO,CAAC,CAAC,SAAS,WAAW,GAAG,cAAc;IACtD;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,GAAG,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,WAAW,GAAG,cAAc;AACxE;;GAEG;AACH,EACE,cAAc,EAAE,aAAa,EAAE,cAA8B,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,GAAG,GAC5H,EAAE,OAAO,CAAC,CAAC,CAAC;AACb;;;;;GAKG;AACH,IAAI,CAAC,EAAE,GAAG,EAAE;;;;EAiEb"}
|
package/dist/hooks/keyboard.js
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
|
-
import { useCallback, useRef } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* Creates listeners for controlling a Menu UI through the keyboard.
|
|
4
4
|
* - Arrow down: next element in the iterator returned by the query selectors. First element, if the current element is the last.
|
|
5
5
|
* - Arrow up: previous element in the iterator returned by the query selectors. Last element, if the current element is the first.
|
|
6
6
|
* - Tab: same as Arrow down, but has a different behavior if the element is the last (see onPressLastTab).
|
|
7
7
|
* - Esc: determined by onPressEscape.
|
|
8
|
-
* @param props {@link
|
|
8
|
+
* @param props {@link Options}.
|
|
9
9
|
* @returns an object with the element controlled by the keyboard (useRef); a function to attach the keyboard events and a function to
|
|
10
10
|
* detach the keyboard events.
|
|
11
11
|
*/
|
|
12
|
-
export function useKeyboardControls(
|
|
13
|
-
|
|
12
|
+
export function useKeyboardControls(
|
|
13
|
+
/**
|
|
14
|
+
* Options for the keyboard controls.
|
|
15
|
+
*/
|
|
16
|
+
{ querySelectors, onPressEscape, onPressLastTab = onPressEscape, onPressArrowLeft, onPressArrowRight, disableTabBehavior, ref, },
|
|
17
|
+
/**
|
|
18
|
+
* Calls `attachKeyboardListeners` (mount) and `detachKeyboardListeners` (unmount) whenever the deps passed as parameter changes.
|
|
19
|
+
*
|
|
20
|
+
* If deps are undefined, this component doesn't automatically add these listeners and you have to use the functions returned in the
|
|
21
|
+
* result.
|
|
22
|
+
*/
|
|
23
|
+
deps) {
|
|
24
|
+
const localRef = useRef(null);
|
|
25
|
+
const keyboardControlledElement = ref ?? localRef;
|
|
14
26
|
const listeners = useRef({});
|
|
15
27
|
listeners.current = { onPressEscape, onPressLastTab };
|
|
16
28
|
const keyboardControls = useCallback((event) => {
|
|
29
|
+
const eventKey = event.key;
|
|
17
30
|
const target = event?.target;
|
|
18
31
|
function getSelectableAnchors() {
|
|
19
32
|
return keyboardControlledElement.current?.querySelectorAll(querySelectors) ?? [];
|
|
20
33
|
}
|
|
21
|
-
function handleArrows(key =
|
|
34
|
+
function handleArrows(key = eventKey) {
|
|
22
35
|
const anchors = getSelectableAnchors();
|
|
23
36
|
let i = 0;
|
|
24
37
|
while (i < anchors.length && document.activeElement !== anchors[i])
|
|
@@ -36,6 +49,8 @@ export function useKeyboardControls({ querySelectors, onPressEscape, onPressLast
|
|
|
36
49
|
target?.click();
|
|
37
50
|
},
|
|
38
51
|
Tab: () => {
|
|
52
|
+
if (disableTabBehavior)
|
|
53
|
+
return;
|
|
39
54
|
const anchors = getSelectableAnchors();
|
|
40
55
|
if (document.activeElement === anchors[anchors.length - 1])
|
|
41
56
|
listeners.current.onPressLastTab?.();
|
|
@@ -45,15 +60,25 @@ export function useKeyboardControls({ querySelectors, onPressEscape, onPressLast
|
|
|
45
60
|
},
|
|
46
61
|
ArrowUp: handleArrows,
|
|
47
62
|
ArrowDown: handleArrows,
|
|
63
|
+
ArrowLeft: onPressArrowLeft,
|
|
64
|
+
ArrowRight: onPressArrowRight,
|
|
48
65
|
};
|
|
49
|
-
handlers[
|
|
66
|
+
handlers[eventKey]?.();
|
|
50
67
|
}, []);
|
|
51
68
|
const attachKeyboardListeners = useCallback(() => {
|
|
52
|
-
document
|
|
69
|
+
const element = ref?.current ?? document;
|
|
70
|
+
element.addEventListener('keydown', keyboardControls);
|
|
53
71
|
}, []);
|
|
54
72
|
const detachKeyboardListeners = useCallback(() => {
|
|
55
|
-
document
|
|
73
|
+
const element = ref?.current ?? document;
|
|
74
|
+
element.removeEventListener('keydown', keyboardControls);
|
|
56
75
|
}, []);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!deps)
|
|
78
|
+
return;
|
|
79
|
+
attachKeyboardListeners();
|
|
80
|
+
return detachKeyboardListeners;
|
|
81
|
+
}, deps);
|
|
57
82
|
return { keyboardControlledElement, attachKeyboardListeners, detachKeyboardListeners };
|
|
58
83
|
}
|
|
59
84
|
//# sourceMappingURL=keyboard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyboard.js","sourceRoot":"","sources":["../../src/hooks/keyboard.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"keyboard.js","sourceRoot":"","sources":["../../src/hooks/keyboard.tsx"],"names":[],"mappings":"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"}
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
* @param current the reference element to focus the next. If not provided, will be the currently active element.
|
|
10
10
|
*/
|
|
11
11
|
export declare function focusNextIgnoringChildren(current?: HTMLElement | null): void;
|
|
12
|
-
type TagPriority = 'a' | 'button' | 'input' | 'textarea' | 'select' | 'other';
|
|
13
|
-
type TagPriorityElement = TagPriority | TagPriority[];
|
|
12
|
+
export type TagPriority = 'a' | 'button' | 'input' | 'textarea' | 'select' | 'other';
|
|
13
|
+
export type TagPriorityElement = TagPriority | TagPriority[];
|
|
14
14
|
interface FocusOptions {
|
|
15
15
|
/**
|
|
16
16
|
* Instead of focusing the first element overall, focus the first according to this list of priorities.
|
|
17
|
+
*
|
|
18
|
+
* 'other' means elements that are normally not focusable, but have positive tabIndex values.
|
|
17
19
|
*/
|
|
18
20
|
priority?: TagPriorityElement[];
|
|
19
21
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessibility.d.ts","sourceRoot":"","sources":["../../src/utils/accessibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,QAWrE;AAED,
|
|
1
|
+
{"version":3,"file":"accessibility.d.ts","sourceRoot":"","sources":["../../src/utils/accessibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,QAWrE;AAED,MAAM,MAAM,WAAW,GAAG,GAAG,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;AACpF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,WAAW,EAAE,CAAA;AAE5D,UAAU,YAAY;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAChC;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,EAAE,EAAE,QAAa,EAAE,MAAM,EAAE,GAAE,YAAiB,QAkB/H;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,WAYnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,QAO9D"}
|
|
@@ -51,32 +51,24 @@ const selectors = {
|
|
|
51
51
|
* @param options optional.
|
|
52
52
|
*/
|
|
53
53
|
export function focusFirstChild(element, { priority = [], ignore } = {}) {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const allFocusableTags = ['a', 'button', 'input', 'other', 'select', 'textarea'];
|
|
55
|
+
const focusableList = [
|
|
56
|
+
element?.querySelectorAll(allFocusableTags.map(t => selectors[t]).join(', ')),
|
|
57
|
+
];
|
|
56
58
|
for (const p of priority) {
|
|
57
59
|
const tags = Array.isArray(p) ? p : [p];
|
|
58
|
-
const querySelectors = tags.map(t =>
|
|
59
|
-
|
|
60
|
-
return selectors[t];
|
|
61
|
-
});
|
|
62
|
-
focusable = element?.querySelectorAll(querySelectors.join(', '));
|
|
63
|
-
if (focusable)
|
|
64
|
-
break;
|
|
60
|
+
const querySelectors = tags.map(t => selectors[t]);
|
|
61
|
+
focusableList.unshift(element?.querySelectorAll(querySelectors.join(', ')));
|
|
65
62
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const styles = window.getComputedStyle(f);
|
|
73
|
-
if (styles.display != 'none' && styles.visibility != 'hidden') {
|
|
74
|
-
elementToFocus = f;
|
|
75
|
-
break;
|
|
63
|
+
for (const focusable of focusableList ?? []) {
|
|
64
|
+
for (const f of focusable ?? []) {
|
|
65
|
+
if (!ignore || !f.matches(ignore)) {
|
|
66
|
+
const styles = window.getComputedStyle(f);
|
|
67
|
+
if (styles.display != 'none' && styles.visibility != 'hidden')
|
|
68
|
+
return f.focus();
|
|
76
69
|
}
|
|
77
70
|
}
|
|
78
71
|
}
|
|
79
|
-
elementToFocus?.focus?.();
|
|
80
72
|
}
|
|
81
73
|
/**
|
|
82
74
|
* Checks if an element can receive focus.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../../src/utils/accessibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAA4B;IACpE,OAAO,GAAG,OAAO,IAAI,QAAQ,CAAC,aAA4B,CAAA;IAC1D,OAAO,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC;QAC9C,OAAO,GAAG,OAAO,EAAE,aAAa,CAAA;IAClC,CAAC;IACD,OAAO,GAAG,OAAO,EAAE,kBAAiC,CAAA;IACpD,OAAO,OAAO,IAAI,OAAO,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;QACvC,OAAO,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAgB,CAAA;IACtG,CAAC;IACD,IAAI,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAA;;QAC1B,eAAe,CAAC,QAAQ,CAAC,CAAA;AAChC,CAAC;
|
|
1
|
+
{"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../../src/utils/accessibility.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/hooks/keyboard.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useCallback, useRef } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
|
-
interface
|
|
3
|
+
interface Options<T extends HTMLElement = HTMLDivElement> {
|
|
4
4
|
/**
|
|
5
5
|
* A query selector that returns every html element that must be navigable through the keyboard.
|
|
6
6
|
*/
|
|
@@ -12,8 +12,29 @@ interface Props {
|
|
|
12
12
|
/**
|
|
13
13
|
* Function to call when TAB is pressed at the last item in the list of items returned by the query selector. Will be the same as
|
|
14
14
|
* onPressEscape if not specified.
|
|
15
|
+
*
|
|
16
|
+
* Attention: has no effect if `disableTabBehavior` is true.
|
|
15
17
|
*/
|
|
16
18
|
onPressLastTab?: () => void,
|
|
19
|
+
/**
|
|
20
|
+
* Pass this function if you want any behavior when the user presses the arrow left.
|
|
21
|
+
*/
|
|
22
|
+
onPressArrowLeft?: () => void,
|
|
23
|
+
/**
|
|
24
|
+
* Pass this function if you want any behavior when the user presses the arrow right.
|
|
25
|
+
*/
|
|
26
|
+
onPressArrowRight?: () => void,
|
|
27
|
+
/**
|
|
28
|
+
* Disables any alteration to the tab key.
|
|
29
|
+
* @default false
|
|
30
|
+
*/
|
|
31
|
+
disableTabBehavior?: boolean,
|
|
32
|
+
/**
|
|
33
|
+
* If you already have a ref to the element you want to attach the events to, you can pass it in this prop.
|
|
34
|
+
*
|
|
35
|
+
* If you pass a ref. The events won't be attached to the document, instead, they will be attached to the element referred by the ref.
|
|
36
|
+
*/
|
|
37
|
+
ref?: React.RefObject<T>,
|
|
17
38
|
}
|
|
18
39
|
|
|
19
40
|
/**
|
|
@@ -22,25 +43,39 @@ interface Props {
|
|
|
22
43
|
* - Arrow up: previous element in the iterator returned by the query selectors. Last element, if the current element is the first.
|
|
23
44
|
* - Tab: same as Arrow down, but has a different behavior if the element is the last (see onPressLastTab).
|
|
24
45
|
* - Esc: determined by onPressEscape.
|
|
25
|
-
* @param props {@link
|
|
46
|
+
* @param props {@link Options}.
|
|
26
47
|
* @returns an object with the element controlled by the keyboard (useRef); a function to attach the keyboard events and a function to
|
|
27
48
|
* detach the keyboard events.
|
|
28
49
|
*/
|
|
29
50
|
export function useKeyboardControls<T extends HTMLElement = HTMLDivElement>(
|
|
30
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Options for the keyboard controls.
|
|
53
|
+
*/
|
|
54
|
+
{
|
|
55
|
+
querySelectors, onPressEscape, onPressLastTab = onPressEscape, onPressArrowLeft, onPressArrowRight, disableTabBehavior, ref,
|
|
56
|
+
}: Options<T>,
|
|
57
|
+
/**
|
|
58
|
+
* Calls `attachKeyboardListeners` (mount) and `detachKeyboardListeners` (unmount) whenever the deps passed as parameter changes.
|
|
59
|
+
*
|
|
60
|
+
* If deps are undefined, this component doesn't automatically add these listeners and you have to use the functions returned in the
|
|
61
|
+
* result.
|
|
62
|
+
*/
|
|
63
|
+
deps?: any[],
|
|
31
64
|
) {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
65
|
+
const localRef = useRef<T>(null)
|
|
66
|
+
const keyboardControlledElement = ref ?? localRef
|
|
67
|
+
const listeners = useRef<Pick<Options, 'onPressEscape' | 'onPressLastTab'>>({})
|
|
34
68
|
listeners.current = { onPressEscape, onPressLastTab }
|
|
35
69
|
|
|
36
|
-
const keyboardControls = useCallback((event:
|
|
70
|
+
const keyboardControls = useCallback((event: Event) => {
|
|
71
|
+
const eventKey = (event as KeyboardEvent).key
|
|
37
72
|
const target = event?.target as HTMLElement | null
|
|
38
73
|
|
|
39
74
|
function getSelectableAnchors() {
|
|
40
75
|
return keyboardControlledElement.current?.querySelectorAll(querySelectors) ?? []
|
|
41
76
|
}
|
|
42
77
|
|
|
43
|
-
function handleArrows(key =
|
|
78
|
+
function handleArrows(key = eventKey) {
|
|
44
79
|
const anchors = getSelectableAnchors()
|
|
45
80
|
let i = 0
|
|
46
81
|
while (i < anchors.length && document.activeElement !== anchors[i]) i++
|
|
@@ -58,6 +93,7 @@ export function useKeyboardControls<T extends HTMLElement = HTMLDivElement>(
|
|
|
58
93
|
target?.click()
|
|
59
94
|
},
|
|
60
95
|
Tab: () => {
|
|
96
|
+
if (disableTabBehavior) return
|
|
61
97
|
const anchors = getSelectableAnchors()
|
|
62
98
|
if (document.activeElement === anchors[anchors.length - 1]) listeners.current.onPressLastTab?.()
|
|
63
99
|
else handleArrows('ArrowDown')
|
|
@@ -65,18 +101,28 @@ export function useKeyboardControls<T extends HTMLElement = HTMLDivElement>(
|
|
|
65
101
|
},
|
|
66
102
|
ArrowUp: handleArrows,
|
|
67
103
|
ArrowDown: handleArrows,
|
|
104
|
+
ArrowLeft: onPressArrowLeft,
|
|
105
|
+
ArrowRight: onPressArrowRight,
|
|
68
106
|
}
|
|
69
107
|
|
|
70
|
-
handlers[
|
|
108
|
+
handlers[eventKey]?.()
|
|
71
109
|
}, [])
|
|
72
110
|
|
|
73
111
|
const attachKeyboardListeners = useCallback(() => {
|
|
74
|
-
document
|
|
112
|
+
const element = ref?.current ?? document
|
|
113
|
+
element.addEventListener('keydown', keyboardControls)
|
|
75
114
|
}, [])
|
|
76
115
|
|
|
77
116
|
const detachKeyboardListeners = useCallback(() => {
|
|
78
|
-
document
|
|
117
|
+
const element = ref?.current ?? document
|
|
118
|
+
element.removeEventListener('keydown', keyboardControls)
|
|
79
119
|
}, [])
|
|
80
120
|
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!deps) return
|
|
123
|
+
attachKeyboardListeners()
|
|
124
|
+
return detachKeyboardListeners
|
|
125
|
+
}, deps)
|
|
126
|
+
|
|
81
127
|
return { keyboardControlledElement, attachKeyboardListeners, detachKeyboardListeners }
|
|
82
128
|
}
|
|
@@ -21,12 +21,14 @@ export function focusNextIgnoringChildren(current?: HTMLElement | null) {
|
|
|
21
21
|
else focusFirstChild(document)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
type TagPriority = 'a' | 'button' | 'input' | 'textarea' | 'select' | 'other'
|
|
25
|
-
type TagPriorityElement = TagPriority | TagPriority[]
|
|
24
|
+
export type TagPriority = 'a' | 'button' | 'input' | 'textarea' | 'select' | 'other'
|
|
25
|
+
export type TagPriorityElement = TagPriority | TagPriority[]
|
|
26
26
|
|
|
27
27
|
interface FocusOptions {
|
|
28
28
|
/**
|
|
29
29
|
* Instead of focusing the first element overall, focus the first according to this list of priorities.
|
|
30
|
+
*
|
|
31
|
+
* 'other' means elements that are normally not focusable, but have positive tabIndex values.
|
|
30
32
|
*/
|
|
31
33
|
priority?: TagPriorityElement[],
|
|
32
34
|
/**
|
|
@@ -65,31 +67,23 @@ const selectors: Record<TagPriority, string> = {
|
|
|
65
67
|
* @param options optional.
|
|
66
68
|
*/
|
|
67
69
|
export function focusFirstChild(element: HTMLElement | Document | null | undefined, { priority = [], ignore }: FocusOptions = {}) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
const allFocusableTags: TagPriority[] = ['a', 'button', 'input', 'other', 'select', 'textarea']
|
|
71
|
+
const focusableList: (NodeListOf<HTMLElement> | undefined)[] = [
|
|
72
|
+
element?.querySelectorAll(allFocusableTags.map(t => selectors[t]).join(', ')),
|
|
73
|
+
]
|
|
70
74
|
for (const p of priority) {
|
|
71
75
|
const tags = Array.isArray(p) ? p : [p]
|
|
72
|
-
const querySelectors = tags.map(t =>
|
|
73
|
-
|
|
74
|
-
return selectors[t]
|
|
75
|
-
})
|
|
76
|
-
focusable = element?.querySelectorAll(querySelectors.join(', '))
|
|
77
|
-
if (focusable) break
|
|
76
|
+
const querySelectors = tags.map(t => selectors[t])
|
|
77
|
+
focusableList.unshift(element?.querySelectorAll(querySelectors.join(', ')))
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (!ignore || !f.matches(ignore)) {
|
|
85
|
-
const styles = window.getComputedStyle(f)
|
|
86
|
-
if (styles.display != 'none' && styles.visibility != 'hidden') {
|
|
87
|
-
elementToFocus = f
|
|
88
|
-
break
|
|
79
|
+
for (const focusable of focusableList ?? []) {
|
|
80
|
+
for (const f of focusable ?? []) {
|
|
81
|
+
if (!ignore || !f.matches(ignore)) {
|
|
82
|
+
const styles = window.getComputedStyle(f)
|
|
83
|
+
if (styles.display != 'none' && styles.visibility != 'hidden') return f.focus()
|
|
89
84
|
}
|
|
90
85
|
}
|
|
91
86
|
}
|
|
92
|
-
elementToFocus?.focus?.()
|
|
93
87
|
}
|
|
94
88
|
|
|
95
89
|
/**
|