@lumx/react 2.2.17 → 2.2.19
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/esm/_internal/SideNavigationItem.js +8 -4
- package/esm/_internal/SideNavigationItem.js.map +1 -1
- package/esm/_internal/Tooltip2.js +10 -12
- package/esm/_internal/Tooltip2.js.map +1 -1
- package/esm/_internal/UserBlock.js +9 -2
- package/esm/_internal/UserBlock.js.map +1 -1
- package/esm/_internal/useFocusTrap.js +22 -13
- package/esm/_internal/useFocusTrap.js.map +1 -1
- package/esm/_internal/user-block.js +1 -0
- package/esm/_internal/user-block.js.map +1 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/dialog/Dialog.stories.tsx +4 -1
- package/src/components/dialog/__snapshots__/Dialog.test.tsx.snap +85 -77
- package/src/components/side-navigation/SideNavigation.stories.tsx +26 -0
- package/src/components/side-navigation/SideNavigationItem.test.tsx +19 -2
- package/src/components/side-navigation/SideNavigationItem.tsx +10 -2
- package/src/components/side-navigation/__snapshots__/SideNavigationItem.test.tsx.snap +1 -1
- package/src/components/tooltip/Tooltip.tsx +2 -5
- package/src/components/tooltip/useTooltipOpen.tsx +7 -4
- package/src/components/user-block/UserBlock.stories.tsx +4 -4
- package/src/components/user-block/UserBlock.tsx +9 -3
- package/src/components/user-block/__snapshots__/UserBlock.test.tsx.snap +51 -8
- package/src/hooks/useBooleanState.tsx +4 -10
- package/src/hooks/useFocusTrap.ts +2 -28
- package/src/stories/generated/Dialog/Demos.stories.tsx +1 -0
- package/src/utils/focus/getFirstAndLastFocusable.test.ts +128 -0
- package/src/utils/focus/getFirstAndLastFocusable.ts +27 -0
- package/types.d.ts +6 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { getFirstAndLastFocusable } from '@lumx/react/utils/focus/getFirstAndLastFocusable';
|
|
2
|
+
|
|
3
|
+
function htmlToElement(html: string): any {
|
|
4
|
+
const template = document.createElement('template');
|
|
5
|
+
template.innerHTML = html.trim();
|
|
6
|
+
return template.content.firstChild;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe(getFirstAndLastFocusable.name, () => {
|
|
10
|
+
it('should get empty', () => {
|
|
11
|
+
const element = htmlToElement(`<div></div>`);
|
|
12
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
13
|
+
expect(focusable).toEqual({});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should get single item', () => {
|
|
17
|
+
const element = htmlToElement(`<div><button /></div>`);
|
|
18
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
19
|
+
expect(focusable.last).toBe(focusable.first);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should get first and last', () => {
|
|
23
|
+
const element = htmlToElement(`
|
|
24
|
+
<div>
|
|
25
|
+
<div>Non focusable div</div>
|
|
26
|
+
<button>Simple button</button>
|
|
27
|
+
<div>Non focusable div</div>
|
|
28
|
+
<input />
|
|
29
|
+
<div>Non focusable div</div>
|
|
30
|
+
</div>
|
|
31
|
+
`);
|
|
32
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
33
|
+
expect(focusable.first).toMatchInlineSnapshot(`
|
|
34
|
+
<button>
|
|
35
|
+
Simple button
|
|
36
|
+
</button>
|
|
37
|
+
`);
|
|
38
|
+
expect(focusable.first).toMatchInlineSnapshot(`
|
|
39
|
+
<button>
|
|
40
|
+
Simple button
|
|
41
|
+
</button>
|
|
42
|
+
`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('match focusable elements', () => {
|
|
46
|
+
it('should match input element', () => {
|
|
47
|
+
const element = htmlToElement(`<div><input /></div>`);
|
|
48
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
49
|
+
expect(focusable.first).toMatchInlineSnapshot(`<input />`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should match link element', () => {
|
|
53
|
+
const element = htmlToElement(`<div><a href="#" /></div>`);
|
|
54
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
55
|
+
expect(focusable.first).toMatchInlineSnapshot(`
|
|
56
|
+
<a
|
|
57
|
+
href="#"
|
|
58
|
+
/>
|
|
59
|
+
`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should match textarea element', () => {
|
|
63
|
+
const element = htmlToElement(`<div><textarea /></div>`);
|
|
64
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
65
|
+
expect(focusable.first).toMatchInlineSnapshot(`
|
|
66
|
+
<textarea>
|
|
67
|
+
</div>
|
|
68
|
+
</textarea>
|
|
69
|
+
`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should match element with tabindex', () => {
|
|
73
|
+
const element = htmlToElement(`<div><span tabindex="0" /></div>`);
|
|
74
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
75
|
+
expect(focusable.first).toMatchInlineSnapshot(`
|
|
76
|
+
<span
|
|
77
|
+
tabindex="0"
|
|
78
|
+
/>
|
|
79
|
+
`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should keep disabled=false', () => {
|
|
83
|
+
const element = htmlToElement(`<div><button disabled="false" /><button /></div>`);
|
|
84
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
85
|
+
expect(focusable.first).toMatchInlineSnapshot(`
|
|
86
|
+
<button
|
|
87
|
+
disabled="false"
|
|
88
|
+
/>
|
|
89
|
+
`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should keep aria-disabled=false', () => {
|
|
93
|
+
const element = htmlToElement(`<div><button aria-disabled="false" /><button /></div>`);
|
|
94
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
95
|
+
expect(focusable.first).toMatchInlineSnapshot(`
|
|
96
|
+
<button
|
|
97
|
+
aria-disabled="false"
|
|
98
|
+
/>
|
|
99
|
+
`);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('skip disabled elements', () => {
|
|
104
|
+
it('should skip disabled', () => {
|
|
105
|
+
const element = htmlToElement(`<div><button disabled /><button /></div>`);
|
|
106
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
107
|
+
expect(focusable.first).toMatchInlineSnapshot(`<button />`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should skip aria-disabled', () => {
|
|
111
|
+
const element = htmlToElement(`<div><button aria-disabled /><button /></div>`);
|
|
112
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
113
|
+
expect(focusable.first).toMatchInlineSnapshot(`<button />`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should skip tabindex=-1', () => {
|
|
117
|
+
const element = htmlToElement(`<div><button tabindex="-1" /><button /></div>`);
|
|
118
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
119
|
+
expect(focusable.first).toMatchInlineSnapshot(`<button />`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should skip input type hidden', () => {
|
|
123
|
+
const element = htmlToElement(`<div><input type="hidden" /><button /></div>`);
|
|
124
|
+
const focusable = getFirstAndLastFocusable(element);
|
|
125
|
+
expect(focusable.first).toMatchInlineSnapshot(`<button />`);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** CSS selector listing all tabbable elements. */
|
|
2
|
+
const TABBABLE_ELEMENTS_SELECTOR = `a[href], button, textarea, input:not([type="hidden"]), [tabindex]`;
|
|
3
|
+
|
|
4
|
+
/** CSS selector matching element that are disabled (should not receive focus). */
|
|
5
|
+
const DISABLED_SELECTOR = `[tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])`;
|
|
6
|
+
|
|
7
|
+
const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECTOR);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get first and last elements focusable in an element.
|
|
11
|
+
*
|
|
12
|
+
* @param parentElement The element in which to search focusable elements.
|
|
13
|
+
* @return first and last focusable elements
|
|
14
|
+
*/
|
|
15
|
+
export function getFirstAndLastFocusable(parentElement: HTMLElement) {
|
|
16
|
+
const focusableElements = Array.from(parentElement.querySelectorAll<HTMLElement>(TABBABLE_ELEMENTS_SELECTOR));
|
|
17
|
+
|
|
18
|
+
// First non disabled element.
|
|
19
|
+
const first = focusableElements.find(isNotDisabled);
|
|
20
|
+
// Last non disabled element.
|
|
21
|
+
const last = focusableElements.reverse().find(isNotDisabled);
|
|
22
|
+
|
|
23
|
+
if (last && first) {
|
|
24
|
+
return { first, last };
|
|
25
|
+
}
|
|
26
|
+
return {};
|
|
27
|
+
}
|
package/types.d.ts
CHANGED
|
@@ -1957,6 +1957,11 @@ export interface SideNavigationItemProps extends GenericProps {
|
|
|
1957
1957
|
linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
|
|
1958
1958
|
/** Props to pass to the toggle button (minus those already set by the SideNavigationItem props). */
|
|
1959
1959
|
toggleButtonProps: Pick<IconButtonProps, "label"> & Omit<IconButtonProps, "label" | "onClick" | "icon" | "emphasis" | "color" | "size">;
|
|
1960
|
+
/**
|
|
1961
|
+
* Choose how the children are hidden when closed
|
|
1962
|
+
* ('hide' keeps the children in DOM but hide them, 'unmount' remove the children from the DOM).
|
|
1963
|
+
*/
|
|
1964
|
+
closeMode?: "hide" | "unmount";
|
|
1960
1965
|
/** On action button click callback. */
|
|
1961
1966
|
onActionClick?(evt: React.MouseEvent): void;
|
|
1962
1967
|
/** On click callback. */
|
|
@@ -2641,7 +2646,7 @@ export declare type UserBlockSize = Extract<Size, "s" | "m" | "l">;
|
|
|
2641
2646
|
*/
|
|
2642
2647
|
export interface UserBlockProps extends GenericProps {
|
|
2643
2648
|
/** Props to pass to the avatar. */
|
|
2644
|
-
avatarProps?: AvatarProps
|
|
2649
|
+
avatarProps?: Omit<AvatarProps, "alt">;
|
|
2645
2650
|
/** Additional fields used to describe the user. */
|
|
2646
2651
|
fields?: string[];
|
|
2647
2652
|
/** Props to pass to the link wrapping the avatar thumbnail. */
|