@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.
Files changed (75) hide show
  1. package/dist/components/display/Divider/divider.css.js +3 -3
  2. package/dist/components/form/Label/Label.d.ts +1 -1
  3. package/dist/components/form/Label/Label.d.ts.map +1 -1
  4. package/dist/components/form/Select/select.css.js +1 -1
  5. package/dist/components/navigation/Tabs/Tab.d.ts +21 -0
  6. package/dist/components/navigation/Tabs/Tab.d.ts.map +1 -0
  7. package/dist/components/navigation/Tabs/Tab.js +138 -0
  8. package/dist/components/navigation/Tabs/TabIndicator.d.ts +6 -0
  9. package/dist/components/navigation/Tabs/TabIndicator.d.ts.map +1 -0
  10. package/dist/components/navigation/Tabs/TabIndicator.js +19 -0
  11. package/dist/components/navigation/Tabs/TabList.d.ts +10 -0
  12. package/dist/components/navigation/Tabs/TabList.d.ts.map +1 -0
  13. package/dist/components/navigation/Tabs/TabList.js +101 -0
  14. package/dist/components/navigation/Tabs/TabListContext.d.ts +13 -0
  15. package/dist/components/navigation/Tabs/TabListContext.d.ts.map +1 -0
  16. package/dist/components/navigation/Tabs/TabListContext.js +6 -0
  17. package/dist/components/navigation/Tabs/TabNav.d.ts +15 -0
  18. package/dist/components/navigation/Tabs/TabNav.d.ts.map +1 -0
  19. package/dist/components/navigation/Tabs/TabNav.js +44 -0
  20. package/dist/components/navigation/Tabs/TabPanel.d.ts +14 -0
  21. package/dist/components/navigation/Tabs/TabPanel.d.ts.map +1 -0
  22. package/dist/components/navigation/Tabs/TabPanel.js +31 -0
  23. package/dist/components/navigation/Tabs/Tabs.d.ts +13 -0
  24. package/dist/components/navigation/Tabs/Tabs.d.ts.map +1 -0
  25. package/dist/components/navigation/Tabs/Tabs.js +33 -0
  26. package/dist/components/navigation/Tabs/TabsContext.d.ts +13 -0
  27. package/dist/components/navigation/Tabs/TabsContext.d.ts.map +1 -0
  28. package/dist/components/navigation/Tabs/TabsContext.js +27 -0
  29. package/dist/components/navigation/Tabs/tabs.css.d.ts +7 -0
  30. package/dist/components/navigation/Tabs/tabs.css.d.ts.map +1 -0
  31. package/dist/components/navigation/Tabs/tabs.css.js +122 -0
  32. package/dist/components/overlay/Dialog/Dialog.d.ts +1 -1
  33. package/dist/components/overlay/Dialog/Dialog.d.ts.map +1 -1
  34. package/dist/components/overlay/Dialog/Dialog.js +9 -7
  35. package/dist/components/overlay/Drawer/Drawer.d.ts +1 -1
  36. package/dist/components/overlay/Drawer/Drawer.d.ts.map +1 -1
  37. package/dist/components/overlay/Drawer/Drawer.js +9 -7
  38. package/dist/components/overlay/Menu/Menu.d.ts +52 -0
  39. package/dist/components/overlay/Menu/Menu.d.ts.map +1 -0
  40. package/dist/components/overlay/Menu/Menu.js +273 -0
  41. package/dist/components/overlay/Menu/menu-utils.d.ts +33 -0
  42. package/dist/components/overlay/Menu/menu-utils.d.ts.map +1 -0
  43. package/dist/components/overlay/Menu/menu-utils.js +54 -0
  44. package/dist/components/overlay/Popover/Popover.d.ts +9 -0
  45. package/dist/components/overlay/Popover/Popover.d.ts.map +1 -1
  46. package/dist/components/overlay/Popover/Popover.js +70 -31
  47. package/dist/components/overlay/Popover/popover-utils.d.ts +32 -1
  48. package/dist/components/overlay/Popover/popover-utils.d.ts.map +1 -1
  49. package/dist/components/overlay/Popover/popover-utils.js +77 -22
  50. package/dist/components/overlay/modalOverlayStack.d.ts +8 -0
  51. package/dist/components/overlay/modalOverlayStack.d.ts.map +1 -0
  52. package/dist/components/overlay/modalOverlayStack.js +37 -0
  53. package/dist/components/overlay/useModalOverlay.d.ts +6 -14
  54. package/dist/components/overlay/useModalOverlay.d.ts.map +1 -1
  55. package/dist/components/overlay/useModalOverlay.js +8 -31
  56. package/dist/components/typography/textOverflow.css.d.ts.map +1 -1
  57. package/dist/hooks/useModalDialog/useModalDialog.d.ts +5 -0
  58. package/dist/hooks/useModalDialog/useModalDialog.d.ts.map +1 -1
  59. package/dist/hooks/useModalDialog/useModalDialog.js +3 -2
  60. package/dist/hooks/usePreventBodyScroll/usePreventBodyScroll.d.ts +5 -0
  61. package/dist/hooks/usePreventBodyScroll/usePreventBodyScroll.d.ts.map +1 -1
  62. package/dist/hooks/usePreventBodyScroll/usePreventBodyScroll.js +31 -16
  63. package/dist/index.d.ts +6 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +12 -0
  66. package/dist/testUtils/browser/contrast.d.ts +6 -2
  67. package/dist/testUtils/browser/contrast.d.ts.map +1 -1
  68. package/dist/testUtils/browser/emulateNoAnchorPositioning.d.ts +8 -0
  69. package/dist/testUtils/browser/emulateNoAnchorPositioning.d.ts.map +1 -0
  70. package/dist/testUtils/browser/render.d.ts +6 -2
  71. package/dist/testUtils/browser/render.d.ts.map +1 -1
  72. package/dist/testUtils/browser/screenshot.d.ts +2 -0
  73. package/dist/testUtils/browser/screenshot.d.ts.map +1 -0
  74. package/dist/testUtils/testTheme.css.d.ts.map +1 -1
  75. 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;AAExC,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAkDxC,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, DialogContext } from "../useModalOverlay.js";
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 { contextValue, closeRequestProps } = useModalOverlay(dialogRef, {
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(dialogRef, open);
33
+ const { visible, onAnimationEnd, openAttribute } = useModalDialog(
34
+ dialogRef,
35
+ open
36
+ );
34
37
  if (!visible) {
35
38
  return null;
36
39
  }
37
- const dataOpen = typeof window === "undefined" && open ? true : void 0;
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: dataOpen,
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;AAExC,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAyDxC,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, DialogContext } from "../useModalOverlay.js";
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 dataOpen = typeof window === "undefined" && open ? true : void 0;
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(dialogRef, open);
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(DialogContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
41
+ return /* @__PURE__ */ jsx(
40
42
  "dialog",
41
43
  {
42
44
  ref: composeRefs(ref, dialogRef),
43
- open: dataOpen,
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
+ };