@jonmilner/ui 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,8 +60,9 @@ function p({ size: e = "sm", font: n, weight: i, leading: a, truncate: o, color:
60
60
  function m({ children: n, match: a, size: u = "sm", font: d, weight: f, leading: p, truncate: m, color: h = "danger", className: g, style: _, ...v }) {
61
61
  let [y, b] = t(v), x = l.toArray(n).some((e) => e !== "");
62
62
  return /* @__PURE__ */ c(r, {
63
+ "aria-atomic": !0,
64
+ "aria-live": "assertive",
63
65
  as: "div",
64
- role: "alert",
65
66
  size: u,
66
67
  font: d,
67
68
  weight: f,
@@ -1,5 +1,4 @@
1
1
  import { TriggerElementProps, TriggerLookProps } from "../popup/client.mjs";
2
- import { ReactNode } from "react";
3
2
  import { Menu } from "@base-ui/react/menu";
4
3
  //#region src/Menu/client.d.ts
5
4
  type TriggerOwnProps = Omit<Menu.Trigger.Props, "render" | "className" | "style" | "nativeButton">;
@@ -1,27 +1,44 @@
1
1
  "use client";
2
- import { triggerElement as e, usePopupScope as t } from "../popup/client.mjs";
3
- import { jsx as n } from "react/jsx-runtime";
4
- import { Menu as r } from "@base-ui/react/menu";
2
+ import { splitStyleProps as e } from "../styles/split-style-props.mjs";
3
+ import { Box as t } from "../Box/index.mjs";
4
+ import { triggerElement as n, usePopupScope as r } from "../popup/client.mjs";
5
+ import { jsx as i } from "react/jsx-runtime";
6
+ import { Menu as a } from "@base-ui/react/menu";
5
7
  //#region src/Menu/client.tsx
6
- function i({ disabled: i = !1, openOnHover: a, delay: o, closeDelay: s, handle: c, payload: l, ...u }) {
7
- let { ref: d } = t();
8
- return /* @__PURE__ */ n(r.Trigger, {
9
- disabled: i,
10
- openOnHover: a,
8
+ function o({ disabled: e = !1, openOnHover: t, delay: o, closeDelay: s, handle: c, payload: l, ...u }) {
9
+ let { ref: d } = r();
10
+ return /* @__PURE__ */ i(a.Trigger, {
11
+ disabled: e,
12
+ openOnHover: t,
11
13
  delay: o,
12
14
  closeDelay: s,
13
15
  handle: c,
14
16
  payload: l,
15
17
  ref: d,
16
- render: e(u)
18
+ render: n(u)
17
19
  });
18
20
  }
19
- function a({ children: e }) {
20
- let { container: i } = t();
21
- return /* @__PURE__ */ n(r.Portal, {
22
- container: i,
21
+ function s({ children: e }) {
22
+ let { container: t } = r();
23
+ return /* @__PURE__ */ i(a.Portal, {
24
+ container: t,
23
25
  children: e
24
26
  });
25
27
  }
28
+ function c(n) {
29
+ return /* @__PURE__ */ i(a.Positioner, {
30
+ ...n,
31
+ collisionPadding: 8,
32
+ render: (n, { open: r }) => {
33
+ let [, a] = e(n);
34
+ return /* @__PURE__ */ i(t, {
35
+ ...a,
36
+ "aria-hidden": !r || void 0,
37
+ zIndex: "popover"
38
+ });
39
+ },
40
+ sideOffset: 4
41
+ });
42
+ }
26
43
  //#endregion
27
- export { a as MenuPortal, i as MenuTrigger };
44
+ export { s as MenuPortal, c as MenuPositioner, o as MenuTrigger };
@@ -3,57 +3,54 @@ 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 { PopupScope as i } from "../popup/client.mjs";
6
- import { MenuPortal as a, MenuTrigger as o } from "./client.mjs";
7
- import { item as s, popup as c, separator as l } from "./Menu.css.mjs";
8
- import { jsx as u, jsxs as d } from "react/jsx-runtime";
9
- import { Children as f } from "react";
10
- import { Menu as p } from "@base-ui/react/menu";
6
+ import { MenuPortal as a, MenuPositioner as o, MenuTrigger as s } from "./client.mjs";
7
+ import { item as c, popup as l, separator as u } from "./Menu.css.mjs";
8
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
9
+ import { Children as p } from "react";
10
+ import { Menu as m } from "@base-ui/react/menu";
11
11
  //#region src/Menu/index.tsx
12
- function m({ children: e, ...t }) {
13
- return /* @__PURE__ */ u(i, { children: /* @__PURE__ */ u(p.Root, {
12
+ function h({ children: e, ...t }) {
13
+ return /* @__PURE__ */ d(i, { children: /* @__PURE__ */ d(m.Root, {
14
14
  ...t,
15
15
  children: e
16
16
  }) });
17
17
  }
18
- function h({ side: r = "bottom", align: i = "start", backgroundColor: o = "bgRaised", borderRadius: s = "med", boxShadow: l = "med", padding: d = "4", className: f, style: m, ...h }) {
19
- let [g, _] = t(h);
20
- return /* @__PURE__ */ u(a, { children: /* @__PURE__ */ u(p.Positioner, {
18
+ function g({ side: r = "bottom", align: i = "start", backgroundColor: s = "bgRaised", borderRadius: c = "med", boxShadow: u = "med", padding: f = "4", className: p, style: h, ...g }) {
19
+ let [_, v] = t(g);
20
+ return /* @__PURE__ */ d(a, { children: /* @__PURE__ */ d(o, {
21
21
  align: i,
22
- collisionPadding: 8,
23
- render: /* @__PURE__ */ u(n, { zIndex: "popover" }),
24
22
  side: r,
25
- sideOffset: 4,
26
- children: /* @__PURE__ */ u(p.Popup, {
27
- ..._,
28
- render: /* @__PURE__ */ u(n, {
29
- backgroundColor: o,
30
- borderRadius: s,
31
- boxShadow: l,
32
- padding: d,
33
- ...g,
34
- className: e(c, f),
35
- style: m
23
+ children: /* @__PURE__ */ d(m.Popup, {
24
+ ...v,
25
+ render: /* @__PURE__ */ d(n, {
26
+ backgroundColor: s,
27
+ borderRadius: c,
28
+ boxShadow: u,
29
+ padding: f,
30
+ ..._,
31
+ className: e(l, p),
32
+ style: h
36
33
  })
37
34
  })
38
35
  }) });
39
36
  }
40
- function g({ variant: i = "neutral", display: a = "flex", alignItems: o = "center", gap: c = "8", minBlockSize: l = "sm", paddingInline: d = "12", borderRadius: m = "sm", color: h = i === "danger" ? "danger" : "fg", className: g, style: _, children: v, ...y }) {
37
+ function _({ variant: i = "neutral", display: a = "flex", alignItems: o = "center", gap: s = "8", minBlockSize: l = "sm", paddingInline: u = "12", borderRadius: f = "sm", color: h = i === "danger" ? "danger" : "fg", className: g, style: _, children: v, ...y }) {
41
38
  let [b, x] = t(y);
42
- return /* @__PURE__ */ u(p.Item, {
39
+ return /* @__PURE__ */ d(m.Item, {
43
40
  ...x,
44
- render: /* @__PURE__ */ u(n, {
41
+ render: /* @__PURE__ */ d(n, {
45
42
  display: a,
46
43
  alignItems: o,
47
- gap: c,
44
+ gap: s,
48
45
  minBlockSize: l,
49
- paddingInline: d,
50
- borderRadius: m,
46
+ paddingInline: u,
47
+ borderRadius: f,
51
48
  color: h,
52
49
  ...b,
53
- className: e(s, g),
50
+ className: e(c, g),
54
51
  style: _
55
52
  }),
56
- children: f.map(v, (e) => typeof e == "string" || typeof e == "number" ? /* @__PURE__ */ u(r, {
53
+ children: p.map(v, (e) => typeof e == "string" || typeof e == "number" ? /* @__PURE__ */ d(r, {
57
54
  as: "span",
58
55
  font: "label",
59
56
  size: "sm",
@@ -61,30 +58,30 @@ function g({ variant: i = "neutral", display: a = "flex", alignItems: o = "cente
61
58
  }) : e)
62
59
  });
63
60
  }
64
- function _({ marginBlock: r = "4", marginInline: i = "8", className: a, style: o, ...s }) {
65
- let [c, d] = t(s);
66
- return /* @__PURE__ */ u(p.Separator, {
67
- ...d,
68
- render: /* @__PURE__ */ u(n, {
61
+ function v({ marginBlock: r = "4", marginInline: i = "8", className: a, style: o, ...s }) {
62
+ let [c, l] = t(s);
63
+ return /* @__PURE__ */ d(m.Separator, {
64
+ ...l,
65
+ render: /* @__PURE__ */ d(n, {
69
66
  marginBlock: r,
70
67
  marginInline: i,
71
68
  ...c,
72
- className: e(l, a),
69
+ className: e(u, a),
73
70
  style: o
74
71
  })
75
72
  });
76
73
  }
77
- function v({ label: e, className: i, style: a, children: o, ...s }) {
74
+ function y({ label: e, className: i, style: a, children: o, ...s }) {
78
75
  let [c, l] = t(s);
79
- return /* @__PURE__ */ d(p.Group, {
76
+ return /* @__PURE__ */ f(m.Group, {
80
77
  ...l,
81
- render: /* @__PURE__ */ u(n, {
78
+ render: /* @__PURE__ */ d(n, {
82
79
  ...c,
83
80
  className: i,
84
81
  style: a
85
82
  }),
86
- children: [/* @__PURE__ */ u(p.GroupLabel, {
87
- render: /* @__PURE__ */ u(r, {
83
+ children: [/* @__PURE__ */ d(m.GroupLabel, {
84
+ render: /* @__PURE__ */ d(r, {
88
85
  as: "div",
89
86
  color: "fgMuted",
90
87
  font: "label",
@@ -96,12 +93,12 @@ function v({ label: e, className: i, style: a, children: o, ...s }) {
96
93
  }), o]
97
94
  });
98
95
  }
99
- var y = Object.assign(m, {
100
- Trigger: o,
101
- Popup: h,
102
- Item: g,
103
- Separator: _,
104
- Group: v
96
+ var b = Object.assign(h, {
97
+ Trigger: s,
98
+ Popup: g,
99
+ Item: _,
100
+ Separator: v,
101
+ Group: y
105
102
  });
106
103
  //#endregion
107
- export { y as Menu };
104
+ export { b as Menu };
@@ -81,7 +81,7 @@ import { Field as BaseField } from "@base-ui/react/field";
81
81
 
82
82
  - The label is the control's accessible name; clicking it focuses the control.
83
83
  - The description and the error join the control's `aria-describedby`, so they are read with it. Base UI adds them after hydration; server HTML has the label link and `aria-invalid`.
84
- - The error slot is a `role="alert"` live region that stays in the page, so a message is announced when it appears, also after a client-side submit. Several fields failing at once each announce.
84
+ - The error slot is an assertive, atomic live region (`aria-live="assertive"`, `aria-atomic="true"`) that stays in the page, so a message is announced when it appears, also after a client-side submit. Several fields failing at once each announce. It has no `role="alert"`, so an empty slot is not an alert: a test finds a field's error by its text or the control's description, not `getByRole("alert")`.
85
85
  - The error is never color alone: a decorative alert icon (`aria-hidden`, at the text's size) leads the message, so it reads as an error under forced colors too, and the message says what is wrong and how to fix it.
86
86
 
87
87
  ## Props
package/dist/docs/Menu.md CHANGED
@@ -92,6 +92,7 @@ A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a
92
92
 
93
93
  - The trigger has `aria-haspopup="menu"` and `aria-expanded`; the popup is `role="menu"` labelled by the trigger, items are `role="menuitem"`, groups are `role="group"` named by their label.
94
94
  - Opening from the keyboard focuses the first item; from the pointer, the popup. Focus stays in the menu until it closes, then returns to the trigger.
95
+ - A closed menu stays on the page while it fades out, but is `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. Focus stays in it until the fade ends, then returns to the trigger.
95
96
  - 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`, and the popup keeps its border.
96
97
  - 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.
97
98
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "./manifest.schema.json",
3
3
  "package": "@jonmilner/ui",
4
- "version": "0.1.2",
4
+ "version": "0.2.1",
5
5
  "components": [
6
6
  {
7
7
  "name": "AlertDialog",
@@ -0,0 +1,51 @@
1
+ # 0.2.0
2
+
3
+ `Field.Error`'s slot, in every `Field`, `MultiSelect` and `SearchField`, is now an `aria-live="assertive"` `aria-atomic="true"` region with no `role="alert"`, so an empty slot is no longer an alert (decision 0097).
4
+
5
+ ## Find
6
+
7
+ In the app's tests (`*.test.tsx`, `*.stories.tsx`, Playwright specs), queries for a field's error by the alert role:
8
+
9
+ - `getByRole("alert")`, `findByRole("alert")`, `queryByRole("alert")`, `getAllByRole("alert")`: regex `(get|find|query)(All)?ByRole\(\s*["']alert["']`
10
+ - CSS selectors in tests: the exact string `[role="alert"]` or `role="alert"`
11
+ - In the app's stylesheets (`*.css`, `*.css.ts`), the same `[role="alert"]` string: a rule that styled field errors by the role no longer matches
12
+
13
+ Skip matches that target a `Toast` or the app's own alerts; only field errors changed.
14
+
15
+ ## Replace
16
+
17
+ Find the error by its text, or check the control's description:
18
+
19
+ ```tsx
20
+ // before
21
+ expect(screen.getByRole("alert")).toHaveTextContent("Enter an email address.");
22
+ // after
23
+ expect(screen.getByText("Enter an email address.")).toBeInTheDocument();
24
+ expect(screen.getByRole("textbox", { name: "Email" })).toHaveAccessibleDescription(
25
+ expect.stringContaining("Enter an email address."),
26
+ );
27
+ ```
28
+
29
+ ```ts
30
+ // before
31
+ field.querySelector('[role="alert"]');
32
+ // after
33
+ field.querySelector('[aria-live="assertive"]');
34
+ ```
35
+
36
+ ```css
37
+ /* before */
38
+ .form [role="alert"] {
39
+ margin-block-start: 4px;
40
+ }
41
+ /* after */
42
+ .form [aria-live="assertive"] {
43
+ margin-block-start: 4px;
44
+ }
45
+ ```
46
+
47
+ A test that asserted no alert was present, to work around the empty slot, no longer needs the workaround: `queryByRole("alert")` is now `null` while a field has no error.
48
+
49
+ ## Verify
50
+
51
+ Run the app's test command (`pnpm test`); it passes with no query left that finds a field's error by `role="alert"`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonmilner/ui",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "files": [
5
5
  "dist",
6
6
  "THIRD_PARTY_NOTICES.md"