@jonmilner/ui 0.8.1 → 0.8.2

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.
@@ -1,2 +1,2 @@
1
1
  @layer tokens, reset, atoms, components.box, components.checkbox, components.clickable, components.icon, components.icon-button, components.inline, components.stack, components.text, components.avatar, components.badge, components.button, components.chip, components.field, components.color-field, components.heading, components.dialog, components.alert-dialog, components.menu, components.multiselect, components.search-field, components.tabs, components.tag, components.text-field, components.toast, components.visually-hidden;
2
- @layer components.menu{._393eil1{border:var(--space-1) solid var(--color-border);transform-origin:var(--transform-origin);transition:opacity var(--duration-fast) var(--easing-standard), scale var(--duration-fast) var(--easing-enter)}._393eil1:focus-visible{outline:var(--space-2) solid var(--color-focus);outline-offset:var(--space-2)}._393eil1[data-starting-style],._393eil1[data-ending-style]{opacity:0;scale:calc(1 - var(--scale-med))}._393eil2{cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none}._393eil2:focus-visible{outline:var(--space-2) solid var(--color-focus);outline-offset:var(--space-2)}._393eil2[data-highlighted]{background-color:light-dark(var(--color-bg-muted),color-mix(in srgb, var(--color-fg) 10%, var(--color-bg-raised)))}._393eil2[aria-disabled=true]{color:var(--color-fg-muted);cursor:not-allowed}._393eil3{text-decoration:none}._393eil4{border-block-start-color:var(--color-border)}._393eil4:before{content:"";border-block-start:var(--space-1) solid;border-block-start-color:inherit;display:block}@media (forced-colors:active){._393eil2[data-highlighted]:not([aria-disabled=true]){forced-color-adjust:none;color:highlighttext;background-color:highlight}._393eil2[aria-disabled=true]{color:graytext}}}
2
+ @layer components.menu{._393eil1{border:var(--space-1) solid var(--color-border);max-inline-size:var(--available-width);transform-origin:var(--transform-origin);transition:opacity var(--duration-fast) var(--easing-standard), scale var(--duration-fast) var(--easing-enter)}._393eil1:focus-visible{outline:var(--space-2) solid var(--color-focus);outline-offset:var(--space-2)}._393eil1[data-starting-style],._393eil1[data-ending-style]{opacity:0;scale:calc(1 - var(--scale-med))}._393eil2{cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none}._393eil2:focus-visible{outline:var(--space-2) solid var(--color-focus);outline-offset:calc(-1 * var(--space-2))}._393eil2[data-highlighted]{background-color:light-dark(var(--color-bg-muted),color-mix(in srgb, var(--color-fg) 10%, var(--color-bg-raised)))}._393eil2[aria-disabled=true]{color:var(--color-fg-muted);cursor:not-allowed}._393eil3{color:var(--color-fg-muted)}._393eil4{text-decoration:none}._393eil5{border-block-start-color:var(--color-border)}._393eil5:before{content:"";border-block-start:var(--space-1) solid;border-block-start-color:inherit;display:block}@media (forced-colors:active){._393eil2[data-highlighted]:not([aria-disabled=true]){forced-color-adjust:none;color:highlighttext;background-color:highlight}._393eil2[data-highlighted]:not([aria-disabled=true]):focus-visible{color:canvastext;background-color:canvas}._393eil2[aria-disabled=true]{color:graytext}._393eil3{color:inherit}}}
@@ -3,6 +3,6 @@ import "./Menu.css";
3
3
  /* empty css */
4
4
  /* empty css */
5
5
  //#region src/Menu/Menu.css.ts
6
- var e = "_393eil0", t = "_393eil1", n = "_393eil2", r = "_393eil3", i = "_393eil4";
6
+ var e = "_393eil0", t = "_393eil1", n = "_393eil2", r = "_393eil3", i = "_393eil4", a = "_393eil5";
7
7
  //#endregion
8
- export { n as item, r as linkItem, t as popup, e as positioner, i as separator };
8
+ export { r as description, n as item, i as linkItem, t as popup, e as positioner, a as separator };
@@ -1,9 +1,24 @@
1
+ import { IconProps } from "../Icon/index.mjs";
1
2
  import { TriggerElementProps, TriggerLookProps } from "../popup/client.mjs";
2
3
  import "./index.mjs";
4
+ import { ComponentType, ReactNode } from "react";
3
5
  import { Menu } from "@base-ui/react/menu";
4
6
  //#region src/Menu/client.d.ts
5
7
  type TriggerOwnProps = Omit<Menu.Trigger.Props, "render" | "className" | "style" | "nativeButton">;
6
- export type MenuTriggerProps = TriggerOwnProps & TriggerLookProps & Omit<TriggerElementProps, keyof TriggerLookProps | keyof TriggerOwnProps>;
7
- export declare function MenuTrigger({ disabled, openOnHover, delay, closeDelay, handle, payload, ...props }: MenuTriggerProps): import("react").JSX.Element;
8
+ type TriggerIconForm = {
9
+ "aria-label"?: never;
10
+ "aria-labelledby"?: never;
11
+ children?: never;
12
+ icon: ComponentType<IconProps>;
13
+ iconEnd?: never;
14
+ iconStart?: never;
15
+ label: string;
16
+ };
17
+ type TriggerTextForm = {
18
+ icon?: never;
19
+ label?: never;
20
+ };
21
+ export type MenuTriggerProps = TriggerOwnProps & TriggerLookProps & Omit<TriggerElementProps, keyof TriggerLookProps | keyof TriggerOwnProps> & (TriggerIconForm | TriggerTextForm);
22
+ export declare function MenuTrigger({ disabled, openOnHover, delay, closeDelay, handle, payload, icon, label, ...props }: MenuTriggerProps): import("react").JSX.Element;
8
23
  //#endregion
9
24
  export {};
@@ -2,47 +2,90 @@
2
2
  import { join as e } from "../styles/join.mjs";
3
3
  import { splitStyleProps as t } from "../styles/split-style-props.mjs";
4
4
  import { Box as n } from "../Box/index.mjs";
5
- import { PopupScope as r, triggerElement as i, useClosingRule as a, usePopupScope as o, useRecordOpenChange as s } from "../popup/client.mjs";
6
- import { positioner as c } from "./Menu.css.mjs";
7
- import { jsx as l } from "react/jsx-runtime";
8
- import { Menu as u } from "@base-ui/react/menu";
5
+ import { Text as r } from "../Text/index.mjs";
6
+ import { PopupScope as i, triggerElement as a, useClosingRule as o, usePopupScope as s, useRecordOpenChange as c } from "../popup/client.mjs";
7
+ import { IconButton as l } from "../IconButton/index.mjs";
8
+ import { description as u, positioner as d } from "./Menu.css.mjs";
9
+ import { Fragment as f, jsx as p, jsxs as m } from "react/jsx-runtime";
10
+ import { useId as h } from "react";
11
+ import { Menu as g } from "@base-ui/react/menu";
9
12
  //#region src/Menu/client.tsx
10
- function d({ disabled: e = !1, openOnHover: t, delay: n, closeDelay: r, handle: a, payload: s, ...c }) {
11
- let { ref: d } = o();
12
- return /* @__PURE__ */ l(u.Trigger, {
13
+ function _({ disabled: e = !1, openOnHover: t, delay: n, closeDelay: r, handle: i, payload: o, icon: c, label: u, ...d }) {
14
+ let { ref: f } = s();
15
+ return /* @__PURE__ */ p(g.Trigger, {
13
16
  disabled: e,
14
17
  openOnHover: t,
15
18
  delay: n,
16
19
  closeDelay: r,
17
- handle: a,
18
- payload: s,
19
- ref: d,
20
- render: i(c)
20
+ handle: i,
21
+ payload: o,
22
+ ref: f,
23
+ render: c ? /* @__PURE__ */ p(l, {
24
+ ...d,
25
+ icon: c,
26
+ label: u
27
+ }) : a(d)
21
28
  });
22
29
  }
23
- function f({ children: e }) {
24
- let { container: t } = o();
25
- return /* @__PURE__ */ l(u.Portal, {
30
+ function v(e, t, i) {
31
+ let a = h();
32
+ return t === void 0 ? { content: e } : {
33
+ aria: { "aria-describedby": i ? `${a} ${i}` : a },
34
+ content: /* @__PURE__ */ m(f, { children: [/* @__PURE__ */ p(n, {
35
+ alignItems: "center",
36
+ as: "span",
37
+ display: "flex",
38
+ gap: "8",
39
+ children: e
40
+ }), /* @__PURE__ */ p(r, {
41
+ "aria-hidden": !0,
42
+ as: "span",
43
+ className: u,
44
+ id: a,
45
+ size: "xs",
46
+ children: t
47
+ })] })
48
+ };
49
+ }
50
+ function y({ description: e, children: t, ...n }) {
51
+ let { aria: r, content: i } = v(t, e, n["aria-describedby"]);
52
+ return /* @__PURE__ */ p(g.Item, {
53
+ ...n,
54
+ ...r,
55
+ children: i
56
+ });
57
+ }
58
+ function b({ description: e, children: t, ...n }) {
59
+ let { aria: r, content: i } = v(t, e, n["aria-describedby"]);
60
+ return /* @__PURE__ */ p(g.LinkItem, {
61
+ ...n,
62
+ ...r,
63
+ children: i
64
+ });
65
+ }
66
+ function x({ children: e }) {
67
+ let { container: t } = s();
68
+ return /* @__PURE__ */ p(g.Portal, {
26
69
  container: t,
27
70
  children: e
28
71
  });
29
72
  }
30
- function p(e) {
31
- return /* @__PURE__ */ l(r, { children: /* @__PURE__ */ l(m, { ...e }) });
73
+ function S(e) {
74
+ return /* @__PURE__ */ p(i, { children: /* @__PURE__ */ p(C, { ...e }) });
32
75
  }
33
- function m({ onOpenChange: e, ...t }) {
34
- return /* @__PURE__ */ l(u.Root, {
76
+ function C({ onOpenChange: e, ...t }) {
77
+ return /* @__PURE__ */ p(g.Root, {
35
78
  ...t,
36
- onOpenChange: s(e)
79
+ onOpenChange: c(e)
37
80
  });
38
81
  }
39
- function h({ finalFocus: e, ...n }) {
40
- return /* @__PURE__ */ l(u.Positioner, {
82
+ function w({ finalFocus: e, ...n }) {
83
+ return /* @__PURE__ */ p(g.Positioner, {
41
84
  ...n,
42
85
  collisionPadding: 8,
43
86
  render: (n, { open: r }) => {
44
87
  let [, i] = t(n);
45
- return /* @__PURE__ */ l(g, {
88
+ return /* @__PURE__ */ p(T, {
46
89
  ...i,
47
90
  finalFocus: e,
48
91
  open: r
@@ -51,16 +94,16 @@ function h({ finalFocus: e, ...n }) {
51
94
  sideOffset: 4
52
95
  });
53
96
  }
54
- function g({ open: t, finalFocus: r, ref: i, className: o, ...s }) {
55
- return /* @__PURE__ */ l(n, {
97
+ function T({ open: t, finalFocus: r, ref: i, className: a, ...s }) {
98
+ return /* @__PURE__ */ p(n, {
56
99
  ...s,
57
- ...a(i, {
100
+ ...o(i, {
58
101
  finalFocus: r,
59
102
  open: t
60
103
  }),
61
- className: e(c, o),
104
+ className: e(d, a),
62
105
  zIndex: "popover"
63
106
  });
64
107
  }
65
108
  //#endregion
66
- export { f as MenuPortal, h as MenuPositioner, p as MenuRootScope, d as MenuTrigger };
109
+ export { y as MenuItemPart, b as MenuLinkItemPart, x as MenuPortal, w as MenuPositioner, S as MenuRootScope, _ as MenuTrigger };
@@ -11,10 +11,13 @@ export type MenuProps = Pick<Menu$1.Root.Props, "open" | "defaultOpen" | "onOpen
11
11
  children?: ReactNode;
12
12
  };
13
13
  export type MenuPopupProps = PartProps<Menu$1.Popup.Props> & Pick<Menu$1.Positioner.Props, "side" | "align">;
14
- export type MenuItemProps = PartProps<Menu$1.Item.Props> & {
14
+ type ItemDescription = {
15
+ description?: ReactNode;
16
+ };
17
+ export type MenuItemProps = PartProps<Menu$1.Item.Props> & ItemDescription & {
15
18
  variant?: "neutral" | "danger";
16
19
  };
17
- export type MenuLinkItemProps = PartProps<Menu$1.LinkItem.Props> & {
20
+ export type MenuLinkItemProps = PartProps<Menu$1.LinkItem.Props> & ItemDescription & {
18
21
  disabled?: boolean;
19
22
  href: string;
20
23
  };
@@ -24,8 +27,8 @@ export type MenuGroupProps = PartProps<Menu$1.Group.Props> & {
24
27
  };
25
28
  declare function MenuRoot(props: MenuProps): import("react").JSX.Element;
26
29
  declare function MenuPopup({ side, align, backgroundColor, borderRadius, boxShadow, padding, className, style, ...props }: MenuPopupProps): import("react").JSX.Element;
27
- declare function MenuItem({ variant, display, alignItems, gap, minBlockSize, paddingInline, borderRadius, color, className, style, children, ...props }: MenuItemProps): import("react").JSX.Element;
28
- declare function MenuLinkItem({ href, disabled, closeOnClick, onClick, display, alignItems, gap, minBlockSize, paddingInline, borderRadius, color, className, style, children, ...props }: MenuLinkItemProps): import("react").JSX.Element;
30
+ declare function MenuItem({ variant, description, display, minBlockSize, paddingInline, borderRadius, color, className, style, children, ...props }: MenuItemProps): import("react").JSX.Element;
31
+ declare function MenuLinkItem({ href, disabled, closeOnClick, onClick, description, display, minBlockSize, paddingInline, borderRadius, color, className, style, children, ...props }: MenuLinkItemProps): import("react").JSX.Element;
29
32
  declare function MenuSeparator({ paddingBlock, paddingInline, className, style, ...props }: MenuSeparatorProps): import("react").JSX.Element;
30
33
  declare function MenuGroup({ label, className, style, children, ...props }: MenuGroupProps): import("react").JSX.Element;
31
34
  export declare const Menu: typeof MenuRoot & {
@@ -3,90 +3,102 @@ import { splitStyleProps as t } from "../styles/split-style-props.mjs";
3
3
  import { Box as n } from "../Box/index.mjs";
4
4
  import { Text as r } from "../Text/index.mjs";
5
5
  import { item as i, linkItem as a, popup as o, separator as s } from "./Menu.css.mjs";
6
- import { MenuPortal as c, MenuPositioner as l, MenuRootScope as u, MenuTrigger as d } from "./client.mjs";
7
- import { jsx as f, jsxs as p } from "react/jsx-runtime";
8
- import { Children as m } from "react";
9
- import { Menu as h } from "@base-ui/react/menu";
6
+ import { MenuItemPart as c, MenuLinkItemPart as l, MenuPortal as u, MenuPositioner as d, MenuRootScope as f, MenuTrigger as p } from "./client.mjs";
7
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
8
+ import { Children as g } from "react";
9
+ import { Menu as _ } from "@base-ui/react/menu";
10
10
  //#region src/Menu/index.tsx
11
- function g(e) {
12
- return /* @__PURE__ */ f(u, { ...e });
11
+ function v(e) {
12
+ return /* @__PURE__ */ m(f, { ...e });
13
13
  }
14
- function _({ side: r = "bottom", align: i = "start", backgroundColor: a = "bgRaised", borderRadius: s = "med", boxShadow: u = "med", padding: d = "4", className: p, style: m, ...g }) {
15
- let [_, v] = t(g);
16
- return /* @__PURE__ */ f(c, { children: /* @__PURE__ */ f(l, {
14
+ function y({ side: r = "bottom", align: i = "start", backgroundColor: a = "bgRaised", borderRadius: s = "med", boxShadow: c = "med", padding: l = "4", className: f, style: p, ...h }) {
15
+ let [g, v] = t(h);
16
+ return /* @__PURE__ */ m(u, { children: /* @__PURE__ */ m(d, {
17
17
  align: i,
18
18
  finalFocus: v.finalFocus !== void 0 && v.finalFocus !== !0,
19
19
  side: r,
20
- children: /* @__PURE__ */ f(h.Popup, {
20
+ children: /* @__PURE__ */ m(_.Popup, {
21
21
  ...v,
22
- render: /* @__PURE__ */ f(n, {
22
+ render: /* @__PURE__ */ m(n, {
23
23
  backgroundColor: a,
24
24
  borderRadius: s,
25
- boxShadow: u,
26
- padding: d,
27
- ..._,
28
- className: e(o, p),
29
- style: m
25
+ boxShadow: c,
26
+ padding: l,
27
+ ...g,
28
+ className: e(o, f),
29
+ style: p
30
30
  })
31
31
  })
32
32
  }) });
33
33
  }
34
- function v(e) {
35
- return m.map(e, (e) => typeof e == "string" || typeof e == "number" ? /* @__PURE__ */ f(r, {
34
+ function b(e) {
35
+ return g.map(e, (e) => typeof e == "string" || typeof e == "number" ? /* @__PURE__ */ m(r, {
36
36
  as: "span",
37
37
  font: "label",
38
38
  size: "sm",
39
39
  children: e
40
40
  }) : e);
41
41
  }
42
- function y({ variant: r = "neutral", display: a = "flex", alignItems: o = "center", gap: s = "8", minBlockSize: c = "sm", paddingInline: l = "12", borderRadius: u = "sm", color: d = r === "danger" ? "danger" : "fg", className: p, style: m, children: g, ..._ }) {
43
- let [y, b] = t(_);
44
- return /* @__PURE__ */ f(h.Item, {
45
- ...b,
46
- render: /* @__PURE__ */ f(n, {
47
- display: a,
48
- alignItems: o,
49
- gap: s,
50
- minBlockSize: c,
42
+ var x = (e) => e == null || e === !1 || e === "" ? void 0 : e, S = (e, t) => t !== void 0 && (typeof e == "string" || typeof e == "number") ? String(e) : void 0, C = (e) => e ? {
43
+ flexDirection: "column",
44
+ alignItems: "stretch",
45
+ justifyContent: "center",
46
+ gap: "4",
47
+ paddingBlock: "8"
48
+ } : {
49
+ alignItems: "center",
50
+ gap: "8"
51
+ };
52
+ function w({ variant: r = "neutral", description: a, display: o = "flex", minBlockSize: s = "sm", paddingInline: l = "12", borderRadius: u = "sm", color: d = r === "danger" ? "danger" : "fg", className: f, style: p, children: h, ...g }) {
53
+ let [_, v] = t(g), y = x(a);
54
+ return /* @__PURE__ */ m(c, {
55
+ label: S(h, y),
56
+ ...v,
57
+ description: y,
58
+ render: /* @__PURE__ */ m(n, {
59
+ ...C(y !== void 0),
60
+ display: o,
61
+ minBlockSize: s,
51
62
  paddingInline: l,
52
63
  borderRadius: u,
53
64
  color: d,
54
- ...y,
55
- className: e(i, p),
56
- style: m
65
+ ..._,
66
+ className: e(i, f),
67
+ style: p
57
68
  }),
58
- children: v(g)
69
+ children: b(h)
59
70
  });
60
71
  }
61
- function b({ href: r, disabled: o = !1, closeOnClick: s = !0, onClick: c, display: l = "flex", alignItems: u = "center", gap: d = "8", minBlockSize: p = "sm", paddingInline: m = "12", borderRadius: g = "sm", color: _ = "fg", className: y, style: b, children: x, ...S }) {
62
- let [C, w] = t(S);
63
- return /* @__PURE__ */ f(h.LinkItem, {
64
- ...w,
72
+ function T({ href: r, disabled: o = !1, closeOnClick: s = !0, onClick: c, description: u, display: d = "flex", minBlockSize: f = "sm", paddingInline: p = "12", borderRadius: h = "sm", color: g = "fg", className: _, style: v, children: y, ...w }) {
73
+ let [T, E] = t(w), D = x(u);
74
+ return /* @__PURE__ */ m(l, {
75
+ label: S(y, D),
76
+ ...E,
65
77
  "aria-disabled": o || void 0,
66
78
  closeOnClick: !o && s,
79
+ description: D,
67
80
  href: o ? void 0 : r,
68
81
  onClick: o ? void 0 : c,
69
- render: /* @__PURE__ */ f(n, {
82
+ render: /* @__PURE__ */ m(n, {
70
83
  as: "a",
71
- display: l,
72
- alignItems: u,
73
- gap: d,
74
- minBlockSize: p,
75
- paddingInline: m,
76
- borderRadius: g,
77
- color: _,
78
- ...C,
79
- className: e(i, a, y),
80
- style: b
84
+ ...C(D !== void 0),
85
+ display: d,
86
+ minBlockSize: f,
87
+ paddingInline: p,
88
+ borderRadius: h,
89
+ color: g,
90
+ ...T,
91
+ className: e(i, a, _),
92
+ style: v
81
93
  }),
82
- children: v(x)
94
+ children: b(y)
83
95
  });
84
96
  }
85
- function x({ paddingBlock: r = "4", paddingInline: i = "8", className: a, style: o, ...c }) {
97
+ function E({ paddingBlock: r = "4", paddingInline: i = "8", className: a, style: o, ...c }) {
86
98
  let [l, u] = t(c);
87
- return /* @__PURE__ */ f(h.Separator, {
99
+ return /* @__PURE__ */ m(_.Separator, {
88
100
  ...u,
89
- render: /* @__PURE__ */ f(n, {
101
+ render: /* @__PURE__ */ m(n, {
90
102
  paddingBlock: r,
91
103
  paddingInline: i,
92
104
  ...l,
@@ -95,17 +107,17 @@ function x({ paddingBlock: r = "4", paddingInline: i = "8", className: a, style:
95
107
  })
96
108
  });
97
109
  }
98
- function S({ label: e, className: i, style: a, children: o, ...s }) {
110
+ function D({ label: e, className: i, style: a, children: o, ...s }) {
99
111
  let [c, l] = t(s);
100
- return /* @__PURE__ */ p(h.Group, {
112
+ return /* @__PURE__ */ h(_.Group, {
101
113
  ...l,
102
- render: /* @__PURE__ */ f(n, {
114
+ render: /* @__PURE__ */ m(n, {
103
115
  ...c,
104
116
  className: i,
105
117
  style: a
106
118
  }),
107
- children: [/* @__PURE__ */ f(h.GroupLabel, {
108
- render: /* @__PURE__ */ f(r, {
119
+ children: [/* @__PURE__ */ m(_.GroupLabel, {
120
+ render: /* @__PURE__ */ m(r, {
109
121
  as: "div",
110
122
  color: "fgMuted",
111
123
  font: "label",
@@ -117,13 +129,13 @@ function S({ label: e, className: i, style: a, children: o, ...s }) {
117
129
  }), o]
118
130
  });
119
131
  }
120
- var C = Object.assign(g, {
121
- Trigger: d,
122
- Popup: _,
123
- Item: y,
124
- LinkItem: b,
125
- Separator: x,
126
- Group: S
132
+ var O = Object.assign(v, {
133
+ Trigger: p,
134
+ Popup: y,
135
+ Item: w,
136
+ LinkItem: T,
137
+ Separator: E,
138
+ Group: D
127
139
  });
128
140
  //#endregion
129
- export { C as Menu };
141
+ export { O as Menu };
package/dist/docs/Menu.md CHANGED
@@ -20,12 +20,14 @@ A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a
20
20
  ## Rules
21
21
 
22
22
  - Compose the parts: `Menu` > `Menu.Trigger` + `Menu.Popup` > `Menu.Item`, `Menu.LinkItem`, `Menu.Separator`, `Menu.Group`. `Menu` holds the open state and renders no element, so it takes no `className` or `style`; every other part does, plus style props.
23
- - `Menu.Trigger` is a bare `Clickable`: give it its look with style props (or your own class), and an accessible name. An icon-only trigger needs `aria-label`.
23
+ - `Menu.Trigger` is a bare `Clickable`: give it its look with style props (or your own class), and an accessible name.
24
+ - For an icon-only trigger ("More actions"), pass `icon` and `label`: it renders an `IconButton`, the same square as one at each `size` (`sm`, `med`, `lg`; `med` by default), named by `label`, with IconButton's `variant` (`secondary` by default) and the open look below. It takes no children, `iconStart`, `iconEnd` or `aria-label` (ADR 0123). Never build one from `iconStart` and `aria-label`: Button's padding makes it wider than the IconButtons beside it.
24
25
  - For a trigger that reads as a button ("Actions", "New"), pass Button's `variant`, `size`, `iconStart` or `iconEnd` (a `ChevronDownIcon` for "Actions ▾"): it renders a `Button`, with its fill, border and height, and keeps the trigger's behavior and `aria-disabled`. While the menu is open it shows Button's open look, a held tint and an inset ring (ADR 0063); a bare trigger has none, so style it yourself off `aria-expanded`. Style props still override the defaults.
25
26
  - `Menu.Item` runs its `onClick` on click, Enter and Space, then the menu closes. Pass `closeOnClick={false}` to keep it open.
26
27
  - `Menu.LinkItem` is a real `<a role="menuitem">` for an entry that goes somewhere. `href` is required; `target`, `rel` and `download` pass through. Hover shows the URL, Cmd/Ctrl-click and middle-click open a new tab, and a click, Enter or Space follows it and closes the menu. Never navigate from a `Menu.Item`'s `onClick` (`location.assign`, a router's `navigate`): it loses all of that. Router-aware (client-side) links are not offered. It takes no `variant`.
27
28
  - `variant="danger"` marks a destructive item. It still runs on one click, so open a confirm dialog from it (Cancel focused first) before anything irreversible happens.
28
29
  - `disabled` on an item renders `aria-disabled`: it stays announced and reachable with the arrow keys, and does nothing. A disabled `Menu.LinkItem` also drops its `href`, so no click or context menu reaches the URL. On the trigger it keeps the button focusable and stops it opening.
30
+ - `description` on `Menu.Item` or `Menu.LinkItem` puts a short line of muted text below the label, such as why a disabled item cannot act ("Already running"). The item grows to fit both lines. Put secondary text there, never as a second line in the children, which overlaps the label and joins its name.
29
31
  - `Menu.Group` takes a `label` that is shown and names the group. Separate groups with `Menu.Separator`.
30
32
  - `Menu.Separator` draws a line inside its own padding (`paddingBlock="4"`, `paddingInline="8"`) and sets no margin. Change its spacing with `paddingBlock` and `paddingInline` (the `padding` shorthand loses to these defaults, decision 0065); a `margin*` prop adds space outside the padding. `style={{ borderBlockStartColor }}` recolours the line.
31
33
  - The popup portals into the trigger's nearest `ThemeProvider`, so it keeps the brand and mode. That provider must not clip or stack its content: wrap the app root, and keep a nested provider off elements with `overflow: hidden`, a `transform` or their own `z-index`, or the popup is clipped or drawn under siblings. It sits on `zIndex` `popover`. A menu opened only through `open`, with no `Menu.Trigger`, has no provider to find and portals to `<body>`.
@@ -38,9 +40,7 @@ A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a
38
40
 
39
41
  ```tsx
40
42
  <Menu>
41
- <Menu.Trigger aria-label="Actions for Invoice 42" paddingInline="8" borderRadius="sm">
42
- <MoreIcon />
43
- </Menu.Trigger>
43
+ <Menu.Trigger icon={EllipsisIcon} label="Actions for Invoice 42" />
44
44
  <Menu.Popup>
45
45
  <Menu.Item onClick={edit}>Edit</Menu.Item>
46
46
  <Menu.Item onClick={duplicate}>Duplicate</Menu.Item>
@@ -61,6 +61,20 @@ A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a
61
61
  </Menu.Popup>
62
62
  ```
63
63
 
64
+ ```tsx
65
+ <Menu>
66
+ <Menu.Trigger icon={EllipsisIcon} label="More actions" size="sm" />
67
+ <Menu.Popup>
68
+ <Menu.Item onClick={retry} description="Runs it again from the start">
69
+ Retry
70
+ </Menu.Item>
71
+ <Menu.Item disabled description="Already running">
72
+ Unqueue
73
+ </Menu.Item>
74
+ </Menu.Popup>
75
+ </Menu>
76
+ ```
77
+
64
78
  ```tsx
65
79
  <Menu>
66
80
  <Menu.Trigger variant="secondary" size="sm">
@@ -93,6 +107,19 @@ A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a
93
107
  </Menu.Item>
94
108
  ```
95
109
 
110
+ ```tsx
111
+ // Button's padding makes this wider than an IconButton. Use icon={EllipsisIcon} label="More actions".
112
+ <Menu.Trigger iconStart={EllipsisIcon} aria-label="More actions" size="sm" />
113
+ ```
114
+
115
+ ```tsx
116
+ // The second line overlaps the label and joins the item's name. Use description="Already running".
117
+ <Menu.Item disabled>
118
+ Unqueue
119
+ <Text size="xs">Already running</Text>
120
+ </Menu.Item>
121
+ ```
122
+
96
123
  ```tsx
97
124
  // Base UI already moves focus with the arrow keys; this fights it.
98
125
  <Menu.Item onKeyDown={(event) => event.key === "ArrowDown" && next()}>Edit</Menu.Item>
@@ -116,7 +143,10 @@ A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a
116
143
  - The trigger has `aria-haspopup="menu"` and `aria-expanded`; the popup is `role="menu"` labelled by the trigger, items and link items are `role="menuitem"` (a link item is an `<a>` with an `href`, so the browser treats it as a link), groups are `role="group"` named by their label.
117
144
  - Opening from the keyboard focuses the first item; from the pointer, the popup. Focus stays in the menu until it closes. Closed by Esc, an item or `open={false}`, focus returns at once to the trigger that opened it, so a Tab pressed while it fades goes on to the control after the trigger. A press outside, a Tab out, a hover menu the pointer left, or a popup with `finalFocus` leaves focus to Base UI, which places it when the fade ends.
118
145
  - A closed menu stays on the page while it fades out, but is `inert` and `aria-hidden` from the moment it closes, so a menu opened during the fade (the next row's) is the only one a screen reader or a role query finds, and nothing in the fading one takes focus or a click.
119
- - The highlighted item gets a fill (`bgMuted` in light mode; in dark mode `bgRaised` with 10% `fg` mixed in, since `bgMuted` sits too close to the popup there), and the shared focus ring when reached from the keyboard. Under forced colors it is `Highlight` with `HighlightText`, disabled items are `GrayText` on the page's fill even when highlighted (`GrayText` pairs with `Canvas` only), and the popup keeps its border.
146
+ - The icon trigger's name is its `label`, as `aria-label`.
147
+ - An item's `description` is its accessible description (`aria-describedby`), read after its name; the name stays the label alone. An `aria-describedby` of the item's own is kept, read after the description. With plain-text children, typeahead matches the label alone, not the description; otherwise pass `label`.
148
+ - The popup is never wider than the viewport beside its trigger, so at 320 px and 200% text a long item wraps inside it (WCAG 1.4.10).
149
+ - The highlighted item gets a fill (`bgMuted` in light mode; in dark mode `bgRaised` with 10% `fg` mixed in, since `bgMuted` sits too close to the popup there), the same from the pointer and the keyboard. From the keyboard it also gets the shared focus ring, drawn inside the item so it never meets the popup's edge; the fill alone is under 3:1 on the popup, the ring passes 3:1 on the fill (`Menu.test.tsx` checks it). Under forced colors it is `Highlight` with `HighlightText`; from the keyboard the fill drops to `Canvas` so the `Highlight` ring alone marks the item; disabled items are `GrayText` on the page's fill even when highlighted (`GrayText` pairs with `Canvas` only) and marked by the ring, and the popup keeps its border.
120
150
  - The danger item's `danger` color passes 4.5:1 on the popup and on the highlight fill in every preset (`Menu.test.tsx` checks it, with `fg` and `fgMuted`); the word ("Delete") carries the meaning, not the color.
121
151
 
122
152
  ## Props
@@ -152,6 +182,7 @@ Also accepts the style props (see `manifest.json` `styleProps`).
152
182
  | --- | --- | --- | --- |
153
183
  | `className` | `string` | no | |
154
184
  | `closeOnClick` | `boolean` | no | Whether to close the menu when the item is clicked. |
185
+ | `description` | `ReactNode` | no | |
155
186
  | `disabled` | `boolean` | no | Whether the component should ignore user interaction. |
156
187
  | `id` | `string` | no | |
157
188
  | `label` | `string` | no | Overrides the text used for keyboard text navigation and filtering. Falls back to the rendered text when not provided. |
@@ -168,6 +199,7 @@ Also accepts the style props (see `manifest.json` `styleProps`).
168
199
  | --- | --- | --- | --- |
169
200
  | `className` | `string` | no | |
170
201
  | `closeOnClick` | `boolean` | no | Whether to close the menu when the item is clicked. |
202
+ | `description` | `ReactNode` | no | |
171
203
  | `disabled` | `boolean` | no | |
172
204
  | `href` | `string` | yes | |
173
205
  | `id` | `string` | no | |
@@ -204,13 +236,17 @@ Also accepts the style props (see `manifest.json` `styleProps`).
204
236
 
205
237
  | Prop | Type | Required | Description |
206
238
  | --- | --- | --- | --- |
239
+ | `aria-label` | `string` | no | Defines a string value that labels the current element. |
240
+ | `aria-labelledby` | `string` | no | Identifies the element (or elements) that labels the current element. |
207
241
  | `children` | `ReactNode` | no | |
208
242
  | `closeDelay` | `number` | no | How long to wait before closing the menu that was opened on hover. Specified in milliseconds. Requires the `openOnHover` prop. |
209
243
  | `delay` | `number` | no | How long to wait before the menu may be opened on hover. Specified in milliseconds. Requires the `openOnHover` prop. |
210
244
  | `disabled` | `boolean` | no | Whether the component should ignore user interaction. |
211
245
  | `handle` | `MenuHandle<unknown>` | no | A handle to associate the trigger with a menu. |
246
+ | `icon` | `ComponentType` | no | |
212
247
  | `iconEnd` | `ComponentType` | no | |
213
248
  | `iconStart` | `ComponentType` | no | |
249
+ | `label` | `string` | no | |
214
250
  | `openOnHover` | `boolean` | no | Whether the menu should also open when the trigger is hovered. |
215
251
  | `payload` | `unknown` | no | A payload to pass to the menu when it is opened. |
216
252
  | `size` | `"lg" \| "med" \| "sm"` | no | |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "./manifest.schema.json",
3
3
  "package": "@jonmilner/ui",
4
- "version": "0.8.1",
4
+ "version": "0.8.2",
5
5
  "components": [
6
6
  {
7
7
  "name": "AlertDialog",
@@ -1301,6 +1301,11 @@
1301
1301
  "required": false,
1302
1302
  "description": "Whether to close the menu when the item is clicked."
1303
1303
  },
1304
+ {
1305
+ "name": "description",
1306
+ "type": "ReactNode",
1307
+ "required": false
1308
+ },
1304
1309
  {
1305
1310
  "name": "disabled",
1306
1311
  "type": "boolean",
@@ -1361,6 +1366,11 @@
1361
1366
  "required": false,
1362
1367
  "description": "Whether to close the menu when the item is clicked."
1363
1368
  },
1369
+ {
1370
+ "name": "description",
1371
+ "type": "ReactNode",
1372
+ "required": false
1373
+ },
1364
1374
  {
1365
1375
  "name": "disabled",
1366
1376
  "type": "boolean",
@@ -1476,6 +1486,18 @@
1476
1486
  "name": "Menu.Trigger",
1477
1487
  "styleProps": true,
1478
1488
  "props": [
1489
+ {
1490
+ "name": "aria-label",
1491
+ "type": "string",
1492
+ "required": false,
1493
+ "description": "Defines a string value that labels the current element."
1494
+ },
1495
+ {
1496
+ "name": "aria-labelledby",
1497
+ "type": "string",
1498
+ "required": false,
1499
+ "description": "Identifies the element (or elements) that labels the current element."
1500
+ },
1479
1501
  {
1480
1502
  "name": "children",
1481
1503
  "type": "ReactNode",
@@ -1505,6 +1527,11 @@
1505
1527
  "required": false,
1506
1528
  "description": "A handle to associate the trigger with a menu."
1507
1529
  },
1530
+ {
1531
+ "name": "icon",
1532
+ "type": "ComponentType",
1533
+ "required": false
1534
+ },
1508
1535
  {
1509
1536
  "name": "iconEnd",
1510
1537
  "type": "ComponentType",
@@ -1515,6 +1542,11 @@
1515
1542
  "type": "ComponentType",
1516
1543
  "required": false
1517
1544
  },
1545
+ {
1546
+ "name": "label",
1547
+ "type": "string",
1548
+ "required": false
1549
+ },
1518
1550
  {
1519
1551
  "name": "openOnHover",
1520
1552
  "type": "boolean",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonmilner/ui",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "files": [
5
5
  "dist",
6
6
  "THIRD_PARTY_NOTICES.md"