@blockle/blocks-react 3.2.0 → 3.3.0
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/dist/components/display/Divider/divider.css.js +3 -3
- package/dist/components/form/Label/Label.d.ts +1 -1
- package/dist/components/form/Label/Label.d.ts.map +1 -1
- package/dist/components/form/Select/select.css.js +1 -1
- package/dist/components/navigation/Tabs/Tab.d.ts +21 -0
- package/dist/components/navigation/Tabs/Tab.d.ts.map +1 -0
- package/dist/components/navigation/Tabs/Tab.js +138 -0
- package/dist/components/navigation/Tabs/TabIndicator.d.ts +6 -0
- package/dist/components/navigation/Tabs/TabIndicator.d.ts.map +1 -0
- package/dist/components/navigation/Tabs/TabIndicator.js +19 -0
- package/dist/components/navigation/Tabs/TabList.d.ts +10 -0
- package/dist/components/navigation/Tabs/TabList.d.ts.map +1 -0
- package/dist/components/navigation/Tabs/TabList.js +101 -0
- package/dist/components/navigation/Tabs/TabListContext.d.ts +13 -0
- package/dist/components/navigation/Tabs/TabListContext.d.ts.map +1 -0
- package/dist/components/navigation/Tabs/TabListContext.js +6 -0
- package/dist/components/navigation/Tabs/TabNav.d.ts +15 -0
- package/dist/components/navigation/Tabs/TabNav.d.ts.map +1 -0
- package/dist/components/navigation/Tabs/TabNav.js +44 -0
- package/dist/components/navigation/Tabs/TabPanel.d.ts +14 -0
- package/dist/components/navigation/Tabs/TabPanel.d.ts.map +1 -0
- package/dist/components/navigation/Tabs/TabPanel.js +31 -0
- package/dist/components/navigation/Tabs/Tabs.d.ts +13 -0
- package/dist/components/navigation/Tabs/Tabs.d.ts.map +1 -0
- package/dist/components/navigation/Tabs/Tabs.js +33 -0
- package/dist/components/navigation/Tabs/TabsContext.d.ts +13 -0
- package/dist/components/navigation/Tabs/TabsContext.d.ts.map +1 -0
- package/dist/components/navigation/Tabs/TabsContext.js +27 -0
- package/dist/components/navigation/Tabs/tabs.css.d.ts +7 -0
- package/dist/components/navigation/Tabs/tabs.css.d.ts.map +1 -0
- package/dist/components/navigation/Tabs/tabs.css.js +122 -0
- package/dist/components/overlay/Dialog/Dialog.d.ts +1 -1
- package/dist/components/overlay/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/overlay/Dialog/Dialog.js +9 -7
- package/dist/components/overlay/Drawer/Drawer.d.ts +1 -1
- package/dist/components/overlay/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/overlay/Drawer/Drawer.js +9 -7
- package/dist/components/overlay/Menu/Menu.d.ts +52 -0
- package/dist/components/overlay/Menu/Menu.d.ts.map +1 -0
- package/dist/components/overlay/Menu/Menu.js +273 -0
- package/dist/components/overlay/Menu/menu-utils.d.ts +33 -0
- package/dist/components/overlay/Menu/menu-utils.d.ts.map +1 -0
- package/dist/components/overlay/Menu/menu-utils.js +54 -0
- package/dist/components/overlay/Popover/Popover.d.ts +9 -0
- package/dist/components/overlay/Popover/Popover.d.ts.map +1 -1
- package/dist/components/overlay/Popover/Popover.js +70 -31
- package/dist/components/overlay/Popover/popover-utils.d.ts +32 -1
- package/dist/components/overlay/Popover/popover-utils.d.ts.map +1 -1
- package/dist/components/overlay/Popover/popover-utils.js +77 -22
- package/dist/components/overlay/modalOverlayStack.d.ts +8 -0
- package/dist/components/overlay/modalOverlayStack.d.ts.map +1 -0
- package/dist/components/overlay/modalOverlayStack.js +37 -0
- package/dist/components/overlay/useModalOverlay.d.ts +6 -14
- package/dist/components/overlay/useModalOverlay.d.ts.map +1 -1
- package/dist/components/overlay/useModalOverlay.js +8 -31
- package/dist/components/typography/textOverflow.css.d.ts.map +1 -1
- package/dist/hooks/useModalDialog/useModalDialog.d.ts +5 -0
- package/dist/hooks/useModalDialog/useModalDialog.d.ts.map +1 -1
- package/dist/hooks/useModalDialog/useModalDialog.js +3 -2
- package/dist/hooks/usePreventBodyScroll/usePreventBodyScroll.d.ts +5 -0
- package/dist/hooks/usePreventBodyScroll/usePreventBodyScroll.d.ts.map +1 -1
- package/dist/hooks/usePreventBodyScroll/usePreventBodyScroll.js +31 -16
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/testUtils/browser/contrast.d.ts +6 -2
- package/dist/testUtils/browser/contrast.d.ts.map +1 -1
- package/dist/testUtils/browser/emulateNoAnchorPositioning.d.ts +8 -0
- package/dist/testUtils/browser/emulateNoAnchorPositioning.d.ts.map +1 -0
- package/dist/testUtils/browser/render.d.ts +6 -2
- package/dist/testUtils/browser/render.d.ts.map +1 -1
- package/dist/testUtils/browser/screenshot.d.ts +2 -0
- package/dist/testUtils/browser/screenshot.d.ts.map +1 -0
- package/dist/testUtils/testTheme.css.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { setFileScope, endFileScope } from "@vanilla-extract/css/fileScope";
|
|
2
|
+
import { layers, tabIndicatorVars } from "@blockle/blocks-core";
|
|
3
|
+
import { style, globalStyle } from "@vanilla-extract/css";
|
|
4
|
+
setFileScope("src/components/navigation/Tabs/tabs.css.ts", "@blockle/blocks-react");
|
|
5
|
+
const selectedTrigger = '[aria-selected="true"], [aria-current="page"]';
|
|
6
|
+
const selected = '> :is([aria-selected="true"], [aria-current="page"])';
|
|
7
|
+
const anchorName = "--blockle-tab";
|
|
8
|
+
const anchorPositioning = "(anchor-name: --a) and (anchor-scope: --a)";
|
|
9
|
+
const anchorCss = (properties) => properties;
|
|
10
|
+
const list = style({
|
|
11
|
+
"@layer": {
|
|
12
|
+
[layers.molecule]: {
|
|
13
|
+
// The Tab indicator is positioned against this box
|
|
14
|
+
position: "relative",
|
|
15
|
+
display: "flex",
|
|
16
|
+
// One row that scrolls sideways, never wraps. The Tab indicator is a
|
|
17
|
+
// child, so it scrolls with the tabs.
|
|
18
|
+
overflowX: "auto",
|
|
19
|
+
"@supports": {
|
|
20
|
+
// Keeps another tab list's selected tab from anchoring this list's
|
|
21
|
+
// Tab indicator
|
|
22
|
+
[anchorPositioning]: anchorCss({ anchorScope: anchorName })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const navList = style({
|
|
28
|
+
"@layer": {
|
|
29
|
+
[layers.molecule]: {
|
|
30
|
+
display: "flex",
|
|
31
|
+
flexShrink: 0,
|
|
32
|
+
listStyle: "none",
|
|
33
|
+
margin: 0,
|
|
34
|
+
padding: 0
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const indicator = style({
|
|
39
|
+
"@layer": {
|
|
40
|
+
[layers.molecule]: {
|
|
41
|
+
display: "none",
|
|
42
|
+
"@supports": {
|
|
43
|
+
[anchorPositioning]: anchorCss({
|
|
44
|
+
display: "block",
|
|
45
|
+
position: "absolute",
|
|
46
|
+
positionAnchor: anchorName,
|
|
47
|
+
left: `anchor(${anchorName} left)`,
|
|
48
|
+
width: `anchor-size(${anchorName} width)`,
|
|
49
|
+
bottom: 0,
|
|
50
|
+
height: tabIndicatorVars.thickness,
|
|
51
|
+
backgroundColor: tabIndicatorVars.color,
|
|
52
|
+
pointerEvents: "none"
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
// Moves only for users who haven't asked for less motion
|
|
56
|
+
"@media": {
|
|
57
|
+
"(prefers-reduced-motion: no-preference)": {
|
|
58
|
+
transitionProperty: "left, width",
|
|
59
|
+
transitionDuration: tabIndicatorVars.duration,
|
|
60
|
+
transitionTimingFunction: tabIndicatorVars.easing
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
selectors: {
|
|
64
|
+
// Nothing selected, nothing to point at
|
|
65
|
+
[`${list}:not(:has(${selectedTrigger})) > &`]: {
|
|
66
|
+
display: "none"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const tab = style({
|
|
73
|
+
"@layer": {
|
|
74
|
+
[layers.molecule]: {
|
|
75
|
+
position: "relative",
|
|
76
|
+
flexShrink: 0,
|
|
77
|
+
selectors: {
|
|
78
|
+
// Static bar under the selected tab. Without anchor positioning it is
|
|
79
|
+
// the only mark; the moving Tab indicator replaces it everywhere else.
|
|
80
|
+
[`&:has(${selected})::after`]: {
|
|
81
|
+
content: '""',
|
|
82
|
+
position: "absolute",
|
|
83
|
+
insetInline: 0,
|
|
84
|
+
insetBlockEnd: 0,
|
|
85
|
+
blockSize: tabIndicatorVars.thickness,
|
|
86
|
+
backgroundColor: tabIndicatorVars.color
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"@supports": {
|
|
90
|
+
[anchorPositioning]: {
|
|
91
|
+
selectors: {
|
|
92
|
+
[`&:has(${selected})`]: anchorCss({ anchorName }),
|
|
93
|
+
[`&:has(${selected})::after`]: { content: "none" }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
const insetRing = { outlineOffset: "-2px !important" };
|
|
101
|
+
const trigger = style({
|
|
102
|
+
"@layer": {
|
|
103
|
+
[layers.molecule]: {
|
|
104
|
+
":focus-visible": insetRing
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const slot = style({});
|
|
109
|
+
globalStyle(`${slot} :is(button, a, [tabindex]):focus-visible`, {
|
|
110
|
+
"@layer": {
|
|
111
|
+
[layers.molecule]: insetRing
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
endFileScope();
|
|
115
|
+
export {
|
|
116
|
+
indicator,
|
|
117
|
+
list,
|
|
118
|
+
navList,
|
|
119
|
+
slot,
|
|
120
|
+
tab,
|
|
121
|
+
trigger
|
|
122
|
+
};
|
|
@@ -9,7 +9,7 @@ export type DialogProps = {
|
|
|
9
9
|
className?: string;
|
|
10
10
|
size?: DialogTheme['variants']['size'];
|
|
11
11
|
'aria-label'?: string;
|
|
12
|
-
} & HTMLElementProps<HTMLDialogElement>;
|
|
12
|
+
} & Omit<HTMLElementProps<HTMLDialogElement>, 'size'>;
|
|
13
13
|
export declare const Dialog: React.FC<DialogProps>;
|
|
14
14
|
export {};
|
|
15
15
|
//# sourceMappingURL=Dialog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/overlay/Dialog/Dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,eAAe,EAGpB,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAK9B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAInE,KAAK,WAAW,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;AAE7C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACrD,kFAAkF;IAClF,QAAQ,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,gBAAgB,CAAC,iBAAiB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/overlay/Dialog/Dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,eAAe,EAGpB,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAK9B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAInE,KAAK,WAAW,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;AAE7C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACrD,kFAAkF;IAClF,QAAQ,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,IAAI,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;AAEtD,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAgDxC,CAAC"}
|
|
@@ -4,7 +4,7 @@ import { classnames, composeRefs } from "@blockle/blocks-core";
|
|
|
4
4
|
import { useRef } from "react";
|
|
5
5
|
import { useComponentStyles } from "../../../hooks/useComponentStyles/useComponentStyles.js";
|
|
6
6
|
import { useModalDialog } from "../../../hooks/useModalDialog/useModalDialog.js";
|
|
7
|
-
import { useModalOverlay
|
|
7
|
+
import { useModalOverlay } from "../useModalOverlay.js";
|
|
8
8
|
import { dialog } from "./dialog.css.js";
|
|
9
9
|
const Dialog = ({
|
|
10
10
|
children,
|
|
@@ -23,23 +23,25 @@ const Dialog = ({
|
|
|
23
23
|
dialog: true,
|
|
24
24
|
variants: { size }
|
|
25
25
|
});
|
|
26
|
-
const
|
|
26
|
+
const closeRequestProps = useModalOverlay(dialogRef, {
|
|
27
27
|
open,
|
|
28
28
|
onRequestClose,
|
|
29
29
|
onKeyDown,
|
|
30
30
|
onKeyDownCapture,
|
|
31
31
|
onCancel
|
|
32
32
|
});
|
|
33
|
-
const { visible, onAnimationEnd } = useModalDialog(
|
|
33
|
+
const { visible, onAnimationEnd, openAttribute } = useModalDialog(
|
|
34
|
+
dialogRef,
|
|
35
|
+
open
|
|
36
|
+
);
|
|
34
37
|
if (!visible) {
|
|
35
38
|
return null;
|
|
36
39
|
}
|
|
37
|
-
|
|
38
|
-
return /* @__PURE__ */ jsx(DialogContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
40
|
+
return /* @__PURE__ */ jsx(
|
|
39
41
|
"dialog",
|
|
40
42
|
{
|
|
41
43
|
ref: composeRefs(ref, dialogRef),
|
|
42
|
-
open:
|
|
44
|
+
open: openAttribute,
|
|
43
45
|
className: classnames(dialog, dialogClassName, className),
|
|
44
46
|
onAnimationEnd,
|
|
45
47
|
onTransitionEnd: onAnimationEnd,
|
|
@@ -47,7 +49,7 @@ const Dialog = ({
|
|
|
47
49
|
...closeRequestProps,
|
|
48
50
|
children
|
|
49
51
|
}
|
|
50
|
-
)
|
|
52
|
+
);
|
|
51
53
|
};
|
|
52
54
|
export {
|
|
53
55
|
Dialog
|
|
@@ -10,7 +10,7 @@ export type DrawerProps = {
|
|
|
10
10
|
className?: string;
|
|
11
11
|
placement?: keyof typeof styles.placement;
|
|
12
12
|
size?: DrawerTheme['variants']['size'];
|
|
13
|
-
} & HTMLElementProps<HTMLDialogElement>;
|
|
13
|
+
} & Omit<HTMLElementProps<HTMLDialogElement>, 'size'>;
|
|
14
14
|
export declare const Drawer: React.FC<DrawerProps>;
|
|
15
15
|
export {};
|
|
16
16
|
//# sourceMappingURL=Drawer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../../src/components/overlay/Drawer/Drawer.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,eAAe,EAGpB,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAK9B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAE1C,KAAK,WAAW,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;AAE7C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACrD,kFAAkF;IAClF,QAAQ,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,OAAO,MAAM,CAAC,SAAS,CAAC;IAC1C,IAAI,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;CACxC,GAAG,gBAAgB,CAAC,iBAAiB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../../src/components/overlay/Drawer/Drawer.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,eAAe,EAGpB,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAK9B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAE1C,KAAK,WAAW,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;AAE7C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACrD,kFAAkF;IAClF,QAAQ,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,OAAO,MAAM,CAAC,SAAS,CAAC;IAC1C,IAAI,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;CACxC,GAAG,IAAI,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;AAEtD,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAuDxC,CAAC"}
|
|
@@ -4,7 +4,7 @@ import { classnames, composeRefs } from "@blockle/blocks-core";
|
|
|
4
4
|
import { useRef } from "react";
|
|
5
5
|
import { useComponentStyles } from "../../../hooks/useComponentStyles/useComponentStyles.js";
|
|
6
6
|
import { useModalDialog } from "../../../hooks/useModalDialog/useModalDialog.js";
|
|
7
|
-
import { useModalOverlay
|
|
7
|
+
import { useModalOverlay } from "../useModalOverlay.js";
|
|
8
8
|
import { drawer, placement } from "./drawer.css.js";
|
|
9
9
|
const Drawer = ({
|
|
10
10
|
open,
|
|
@@ -24,23 +24,25 @@ const Drawer = ({
|
|
|
24
24
|
root: true,
|
|
25
25
|
variants: { placement: placement$1, size }
|
|
26
26
|
});
|
|
27
|
-
const
|
|
28
|
-
const { contextValue, closeRequestProps } = useModalOverlay(dialogRef, {
|
|
27
|
+
const closeRequestProps = useModalOverlay(dialogRef, {
|
|
29
28
|
open,
|
|
30
29
|
onRequestClose,
|
|
31
30
|
onKeyDown,
|
|
32
31
|
onKeyDownCapture,
|
|
33
32
|
onCancel
|
|
34
33
|
});
|
|
35
|
-
const { visible, onAnimationEnd } = useModalDialog(
|
|
34
|
+
const { visible, onAnimationEnd, openAttribute } = useModalDialog(
|
|
35
|
+
dialogRef,
|
|
36
|
+
open
|
|
37
|
+
);
|
|
36
38
|
if (!visible) {
|
|
37
39
|
return null;
|
|
38
40
|
}
|
|
39
|
-
return /* @__PURE__ */ jsx(
|
|
41
|
+
return /* @__PURE__ */ jsx(
|
|
40
42
|
"dialog",
|
|
41
43
|
{
|
|
42
44
|
ref: composeRefs(ref, dialogRef),
|
|
43
|
-
open:
|
|
45
|
+
open: openAttribute,
|
|
44
46
|
className: classnames(
|
|
45
47
|
drawer,
|
|
46
48
|
placement[placement$1],
|
|
@@ -53,7 +55,7 @@ const Drawer = ({
|
|
|
53
55
|
...closeRequestProps,
|
|
54
56
|
children
|
|
55
57
|
}
|
|
56
|
-
)
|
|
58
|
+
);
|
|
57
59
|
};
|
|
58
60
|
export {
|
|
59
61
|
Drawer
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { HTMLElementProps } from '@blockle/blocks-core';
|
|
2
|
+
import { PopoverProps } from '../Popover/Popover.js';
|
|
3
|
+
type ReactElement = React.ReactElement<any, string | React.JSXElementConstructor<any>>;
|
|
4
|
+
export type MenuActionItem = {
|
|
5
|
+
type?: undefined;
|
|
6
|
+
label: React.ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* Text for typeahead, when `label` is not a plain string
|
|
9
|
+
*/
|
|
10
|
+
textValue?: string;
|
|
11
|
+
onSelect: () => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
startSlot?: React.ReactNode;
|
|
14
|
+
endSlot?: React.ReactNode;
|
|
15
|
+
};
|
|
16
|
+
export type MenuSeparatorItem = {
|
|
17
|
+
type: 'separator';
|
|
18
|
+
};
|
|
19
|
+
export type MenuRadioItem = {
|
|
20
|
+
value: string;
|
|
21
|
+
label: React.ReactNode;
|
|
22
|
+
/**
|
|
23
|
+
* Text for typeahead, when `label` is not a plain string
|
|
24
|
+
*/
|
|
25
|
+
textValue?: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
endSlot?: React.ReactNode;
|
|
28
|
+
};
|
|
29
|
+
export type MenuRadioGroupItem = {
|
|
30
|
+
type: 'radio-group';
|
|
31
|
+
value: string;
|
|
32
|
+
onChange: (value: string) => void;
|
|
33
|
+
items: MenuRadioItem[];
|
|
34
|
+
};
|
|
35
|
+
export type MenuItem = MenuActionItem | MenuSeparatorItem | MenuRadioGroupItem;
|
|
36
|
+
export type MenuProps = {
|
|
37
|
+
/**
|
|
38
|
+
* The trigger, usually a `Button` or `IconButton`
|
|
39
|
+
*/
|
|
40
|
+
children: ReactElement;
|
|
41
|
+
items: MenuItem[];
|
|
42
|
+
open?: boolean;
|
|
43
|
+
onOpenChange?: (open: boolean) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Side of the trigger to open on, and which of its edges to line up with
|
|
46
|
+
* @default 'bottom-start'
|
|
47
|
+
*/
|
|
48
|
+
placement?: PopoverProps['placement'];
|
|
49
|
+
} & Omit<HTMLElementProps<HTMLDivElement>, 'ref' | 'children'>;
|
|
50
|
+
export declare const Menu: React.FC<MenuProps>;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=Menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Menu.d.ts","sourceRoot":"","sources":["../../../../src/components/overlay/Menu/Menu.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAc9B,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAenE,KAAK,YAAY,GAAG,KAAK,CAAC,YAAY,CAEpC,GAAG,EAEH,MAAM,GAAG,KAAK,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAC1C,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,WAAW,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAE/E,MAAM,MAAM,SAAS,GAAG;IACtB;;OAEG;IACH,QAAQ,EAAE,YAAY,CAAC;IACvB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC;;;OAGG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;CACvC,GAAG,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,EAAE,KAAK,GAAG,UAAU,CAAC,CAAC;AAU/D,eAAO,MAAM,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,SAAS,CAyLpC,CAAC"}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, Fragment, jsx } from "react/jsx-runtime";
|
|
3
|
+
import { composeRefs, classnames } from "@blockle/blocks-core";
|
|
4
|
+
import { Children, useId, useRef, useState, useEffect, isValidElement, cloneElement, Fragment as Fragment$1 } from "react";
|
|
5
|
+
import { useComponentStyles } from "../../../hooks/useComponentStyles/useComponentStyles.js";
|
|
6
|
+
import { CheckIcon } from "../../../icons/icons.js";
|
|
7
|
+
import { Popover } from "../Popover/Popover.js";
|
|
8
|
+
import { isMenuNavigationKey, isTypeaheadKey, getEnabledItems, getNextIndex, getTypeaheadMatch, getItemText, getTextValue } from "./menu-utils.js";
|
|
9
|
+
const TYPEAHEAD_RESET_MS = 500;
|
|
10
|
+
function focusItem(list, which) {
|
|
11
|
+
const items = getEnabledItems(list);
|
|
12
|
+
items.at(which === "first" ? 0 : -1)?.focus();
|
|
13
|
+
}
|
|
14
|
+
const Menu = ({
|
|
15
|
+
children,
|
|
16
|
+
items,
|
|
17
|
+
open: openProp,
|
|
18
|
+
onOpenChange,
|
|
19
|
+
placement = "bottom-start",
|
|
20
|
+
className,
|
|
21
|
+
...restProps
|
|
22
|
+
}) => {
|
|
23
|
+
if (Children.count(children) !== 1) {
|
|
24
|
+
throw new Error("Menu component can only have one child");
|
|
25
|
+
}
|
|
26
|
+
const listId = useId();
|
|
27
|
+
const generatedTriggerId = useId();
|
|
28
|
+
const triggerRef = useRef(null);
|
|
29
|
+
const listRef = useRef(null);
|
|
30
|
+
const searchRef = useRef("");
|
|
31
|
+
const searchTimeoutRef = useRef(void 0);
|
|
32
|
+
const initialFocus = useRef("first");
|
|
33
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
34
|
+
const isControlled = openProp !== void 0;
|
|
35
|
+
const open = (isControlled ? openProp : internalOpen) && items.length > 0;
|
|
36
|
+
const listClassName = useComponentStyles("menu", { list: true }, false);
|
|
37
|
+
const setOpen = (next) => {
|
|
38
|
+
if (!isControlled) {
|
|
39
|
+
setInternalOpen(next);
|
|
40
|
+
}
|
|
41
|
+
onOpenChange?.(next);
|
|
42
|
+
};
|
|
43
|
+
const openAndFocus = (which) => {
|
|
44
|
+
if (items.length === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
initialFocus.current = which;
|
|
48
|
+
setOpen(true);
|
|
49
|
+
};
|
|
50
|
+
const closeMenu = () => {
|
|
51
|
+
setOpen(false);
|
|
52
|
+
};
|
|
53
|
+
const closeAndFocusTrigger = () => {
|
|
54
|
+
setOpen(false);
|
|
55
|
+
triggerRef.current?.focus();
|
|
56
|
+
};
|
|
57
|
+
const onRequestClose = (reason) => {
|
|
58
|
+
if (reason === "escape") {
|
|
59
|
+
closeAndFocusTrigger();
|
|
60
|
+
} else {
|
|
61
|
+
closeMenu();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
return () => window.clearTimeout(searchTimeoutRef.current);
|
|
66
|
+
}, []);
|
|
67
|
+
const child = Children.toArray(children)[0];
|
|
68
|
+
if (!isValidElement(child)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const trigger = child;
|
|
72
|
+
const triggerId = trigger.props.id ?? generatedTriggerId;
|
|
73
|
+
const onTriggerClick = (event) => {
|
|
74
|
+
trigger.props.onClick?.(event);
|
|
75
|
+
if (event.defaultPrevented) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!open) {
|
|
79
|
+
openAndFocus("first");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const onTriggerKeyDown = (event) => {
|
|
83
|
+
trigger.props.onKeyDown?.(event);
|
|
84
|
+
if (event.defaultPrevented || open) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
openAndFocus(event.key === "ArrowDown" ? "first" : "last");
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const onListKeyDown = (event) => {
|
|
93
|
+
if (event.key === "Tab") {
|
|
94
|
+
closeAndFocusTrigger();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const navigationKey = isMenuNavigationKey(event.key) ? event.key : null;
|
|
98
|
+
if (!navigationKey && !isTypeaheadKey(event)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const enabled = getEnabledItems(listRef.current);
|
|
102
|
+
const current = enabled.indexOf(document.activeElement);
|
|
103
|
+
if (navigationKey) {
|
|
104
|
+
if (enabled.length === 0) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
enabled[getNextIndex(navigationKey, current, enabled.length)]?.focus();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (event.key === " " && searchRef.current === "") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
window.clearTimeout(searchTimeoutRef.current);
|
|
116
|
+
searchRef.current += event.key;
|
|
117
|
+
searchTimeoutRef.current = window.setTimeout(() => {
|
|
118
|
+
searchRef.current = "";
|
|
119
|
+
}, TYPEAHEAD_RESET_MS);
|
|
120
|
+
enabled[getTypeaheadMatch(enabled.map(getItemText), current, searchRef.current)]?.focus();
|
|
121
|
+
};
|
|
122
|
+
const onPick = (select) => {
|
|
123
|
+
select();
|
|
124
|
+
closeAndFocusTrigger();
|
|
125
|
+
};
|
|
126
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
127
|
+
cloneElement(trigger, {
|
|
128
|
+
ref: composeRefs(triggerRef, trigger.props.ref),
|
|
129
|
+
id: triggerId,
|
|
130
|
+
"aria-haspopup": "menu",
|
|
131
|
+
"aria-expanded": open,
|
|
132
|
+
"aria-controls": open ? listId : void 0,
|
|
133
|
+
onClick: onTriggerClick,
|
|
134
|
+
onKeyDown: onTriggerKeyDown
|
|
135
|
+
}),
|
|
136
|
+
/* @__PURE__ */ jsx(
|
|
137
|
+
Popover,
|
|
138
|
+
{
|
|
139
|
+
...restProps,
|
|
140
|
+
ref: listRef,
|
|
141
|
+
id: listId,
|
|
142
|
+
role: "menu",
|
|
143
|
+
"aria-labelledby": triggerId,
|
|
144
|
+
anchorElement: triggerRef,
|
|
145
|
+
open,
|
|
146
|
+
onRequestClose,
|
|
147
|
+
placement,
|
|
148
|
+
limitHeight: true,
|
|
149
|
+
className: classnames(listClassName, className),
|
|
150
|
+
onKeyDown: onListKeyDown,
|
|
151
|
+
children: /* @__PURE__ */ jsx(
|
|
152
|
+
MenuItems,
|
|
153
|
+
{
|
|
154
|
+
items,
|
|
155
|
+
listRef,
|
|
156
|
+
initialFocus: initialFocus.current,
|
|
157
|
+
onPick
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
] });
|
|
163
|
+
};
|
|
164
|
+
const MenuItems = ({
|
|
165
|
+
items,
|
|
166
|
+
listRef,
|
|
167
|
+
initialFocus,
|
|
168
|
+
onPick
|
|
169
|
+
}) => {
|
|
170
|
+
const separatorClassName = useComponentStyles(
|
|
171
|
+
"menu",
|
|
172
|
+
{ separator: true },
|
|
173
|
+
false
|
|
174
|
+
);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
focusItem(listRef.current, initialFocus);
|
|
177
|
+
}, []);
|
|
178
|
+
return items.map((item, index) => {
|
|
179
|
+
if (item.type === "separator") {
|
|
180
|
+
return /* @__PURE__ */ jsx(
|
|
181
|
+
"hr",
|
|
182
|
+
{
|
|
183
|
+
className: separatorClassName
|
|
184
|
+
},
|
|
185
|
+
index
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (item.type === "radio-group") {
|
|
189
|
+
return (
|
|
190
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: items have no id
|
|
191
|
+
/* @__PURE__ */ jsx(Fragment$1, { children: item.items.map((radio) => /* @__PURE__ */ jsx(
|
|
192
|
+
MenuRadio,
|
|
193
|
+
{
|
|
194
|
+
item: radio,
|
|
195
|
+
checked: radio.value === item.value,
|
|
196
|
+
onActivate: () => onPick(() => item.onChange(radio.value))
|
|
197
|
+
},
|
|
198
|
+
radio.value
|
|
199
|
+
)) }, index)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return /* @__PURE__ */ jsxs(
|
|
203
|
+
MenuItemButton,
|
|
204
|
+
{
|
|
205
|
+
role: "menuitem",
|
|
206
|
+
item,
|
|
207
|
+
onActivate: () => onPick(item.onSelect),
|
|
208
|
+
children: [
|
|
209
|
+
item.startSlot,
|
|
210
|
+
item.label,
|
|
211
|
+
item.endSlot
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
index
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
const MenuItemButton = ({
|
|
219
|
+
role,
|
|
220
|
+
item,
|
|
221
|
+
onActivate,
|
|
222
|
+
children,
|
|
223
|
+
...ariaProps
|
|
224
|
+
}) => {
|
|
225
|
+
const className = useComponentStyles("menu", { item: true }, false);
|
|
226
|
+
return /* @__PURE__ */ jsx(
|
|
227
|
+
"button",
|
|
228
|
+
{
|
|
229
|
+
type: "button",
|
|
230
|
+
role,
|
|
231
|
+
tabIndex: -1,
|
|
232
|
+
"aria-disabled": item.disabled || void 0,
|
|
233
|
+
"data-text-value": getTextValue(item),
|
|
234
|
+
...ariaProps,
|
|
235
|
+
className,
|
|
236
|
+
onClick: () => {
|
|
237
|
+
if (!item.disabled) {
|
|
238
|
+
onActivate();
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
onPointerMove: (event) => {
|
|
242
|
+
if (!item.disabled) {
|
|
243
|
+
event.currentTarget.focus();
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
children
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
const MenuRadio = ({ item, checked, onActivate }) => {
|
|
251
|
+
const indicatorClassName = useComponentStyles(
|
|
252
|
+
"menu",
|
|
253
|
+
{ itemIndicator: true },
|
|
254
|
+
false
|
|
255
|
+
);
|
|
256
|
+
return /* @__PURE__ */ jsxs(
|
|
257
|
+
MenuItemButton,
|
|
258
|
+
{
|
|
259
|
+
role: "menuitemradio",
|
|
260
|
+
"aria-checked": checked,
|
|
261
|
+
item,
|
|
262
|
+
onActivate,
|
|
263
|
+
children: [
|
|
264
|
+
/* @__PURE__ */ jsx("span", { className: indicatorClassName, "data-radio-indicator": "", children: checked && /* @__PURE__ */ jsx(CheckIcon, {}) }),
|
|
265
|
+
item.label,
|
|
266
|
+
item.endSlot
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
export {
|
|
272
|
+
Menu
|
|
273
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare function getEnabledItems(list: HTMLElement | null): HTMLElement[];
|
|
2
|
+
type MenuNavigationKey = 'ArrowDown' | 'ArrowUp' | 'Home' | 'End';
|
|
3
|
+
export declare function isMenuNavigationKey(key: string): key is MenuNavigationKey;
|
|
4
|
+
/**
|
|
5
|
+
* Returns the index to focus after `key`, wrapping at both ends. `current` is
|
|
6
|
+
* -1 when none of the items has focus.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getNextIndex(key: MenuNavigationKey, current: number, count: number): number;
|
|
9
|
+
/**
|
|
10
|
+
* The text an item takes part in typeahead with: its `textValue`, else its
|
|
11
|
+
* `label` when that is a string.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getTextValue(item: {
|
|
14
|
+
label: React.ReactNode;
|
|
15
|
+
textValue?: string;
|
|
16
|
+
}): string | undefined;
|
|
17
|
+
export declare function getItemText(item: HTMLElement): string | undefined;
|
|
18
|
+
export declare function isTypeaheadKey(event: {
|
|
19
|
+
key: string;
|
|
20
|
+
ctrlKey: boolean;
|
|
21
|
+
metaKey: boolean;
|
|
22
|
+
altKey: boolean;
|
|
23
|
+
}): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the index of the item to focus for `search`, or -1 for no match.
|
|
26
|
+
* `texts` holds each item's text, `undefined` for items without any. Typing
|
|
27
|
+
* the same character repeatedly cycles through the items starting with it,
|
|
28
|
+
* beginning after `current`. A longer search matches from `current` itself, so
|
|
29
|
+
* refining a search doesn't skip past the item already in focus.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getTypeaheadMatch(texts: (string | undefined)[], current: number, search: string): number;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=menu-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-utils.d.ts","sourceRoot":"","sources":["../../../../src/components/overlay/Menu/menu-utils.ts"],"names":[],"mappings":"AAKA,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,WAAW,EAAE,CAMvE;AAED,KAAK,iBAAiB,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,KAAK,CAAC;AAElE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,iBAAiB,CAIzE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,CAWR;AAOD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,GAAG,SAAS,CAIrB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAEjE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAIV;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,EAC7B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,MAAM,CAgBR"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const ENABLED_ITEM_SELECTOR = '[role^="menuitem"]:not([aria-disabled="true"])';
|
|
2
|
+
function getEnabledItems(list) {
|
|
3
|
+
if (!list) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
return Array.from(list.querySelectorAll(ENABLED_ITEM_SELECTOR));
|
|
7
|
+
}
|
|
8
|
+
function isMenuNavigationKey(key) {
|
|
9
|
+
return key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End";
|
|
10
|
+
}
|
|
11
|
+
function getNextIndex(key, current, count) {
|
|
12
|
+
switch (key) {
|
|
13
|
+
case "Home":
|
|
14
|
+
return 0;
|
|
15
|
+
case "End":
|
|
16
|
+
return count - 1;
|
|
17
|
+
case "ArrowDown":
|
|
18
|
+
return (current + 1) % count;
|
|
19
|
+
case "ArrowUp":
|
|
20
|
+
return current <= 0 ? count - 1 : current - 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const TEXT_VALUE_ATTRIBUTE = "data-text-value";
|
|
24
|
+
function getTextValue(item) {
|
|
25
|
+
return item.textValue ?? (typeof item.label === "string" ? item.label : void 0);
|
|
26
|
+
}
|
|
27
|
+
function getItemText(item) {
|
|
28
|
+
return item.getAttribute(TEXT_VALUE_ATTRIBUTE) ?? void 0;
|
|
29
|
+
}
|
|
30
|
+
function isTypeaheadKey(event) {
|
|
31
|
+
return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
32
|
+
}
|
|
33
|
+
function getTypeaheadMatch(texts, current, search) {
|
|
34
|
+
const query = search.toLowerCase();
|
|
35
|
+
const repeated = query.length > 1 && [...query].every((c) => c === query[0]);
|
|
36
|
+
const prefix = repeated ? query.slice(0, 1) : query;
|
|
37
|
+
const start = repeated || query.length === 1 ? current + 1 : current;
|
|
38
|
+
for (let offset = 0; offset < texts.length; offset++) {
|
|
39
|
+
const index = ((start + offset) % texts.length + texts.length) % texts.length;
|
|
40
|
+
if (texts[index]?.toLowerCase().startsWith(prefix)) {
|
|
41
|
+
return index;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return -1;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
getEnabledItems,
|
|
48
|
+
getItemText,
|
|
49
|
+
getNextIndex,
|
|
50
|
+
getTextValue,
|
|
51
|
+
getTypeaheadMatch,
|
|
52
|
+
isMenuNavigationKey,
|
|
53
|
+
isTypeaheadKey
|
|
54
|
+
};
|