@godxjp/ui 25.0.0 → 25.1.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.
@@ -127,5 +127,31 @@ export declare const SelectItem: React.ForwardRefExoticComponent<Omit<React.HTML
127
127
  /** @deprecated Radix-only; has no effect. */
128
128
  asChild?: boolean;
129
129
  } & React.RefAttributes<HTMLDivElement>>;
130
+ /**
131
+ * A RULE, NOT A ROLE — `role="separator"` is not a child a `listbox` may own (gh#643).
132
+ *
133
+ * It rendered react-aria's `<Separator>`, so a divider between two `SelectGroup`s landed in the
134
+ * viewport as `<div role="separator">`, a direct child of `role="listbox"`. ARIA 1.2 lets a
135
+ * `listbox` own `option` and `group` and nothing else, so axe's `aria-required-children` fires
136
+ * CRITICAL on the whole viewport: "Element has children which are not allowed: [role=separator]".
137
+ * Measured by `check:frame-axe`'s overlay scope on `/isolate/data-entry-select-matrix`, 1 node at
138
+ * each of 1440 / 375 / 320 — and only ever with the list OPEN, which is why nothing saw it for the
139
+ * two releases the gate has existed. `menu` DOES own `separator`, so `DropdownMenuSeparator` is
140
+ * correct as it stands; this is a listbox-only rule.
141
+ *
142
+ * MAKING IT A `group` INSTEAD WAS REJECTED. It would satisfy the rule and it would lie: the
143
+ * grouping is already carried by the two `SelectGroup` sections either side of it, and a screen
144
+ * reader would then announce an empty third group between them. The divider is decoration over a
145
+ * structure that is already announced, so it says so — `aria-hidden`, which takes it out of the
146
+ * accessibility tree and therefore out of the listbox's owned children. Same call `SelectLabel`
147
+ * already makes one component up, where the group's `<header>` renders `role="presentation"`
148
+ * because `aria-labelledby` is what actually names the section.
149
+ *
150
+ * DROPPING `AriaSeparator` FOR A PLAIN `<div>` WAS TRIED FIRST AND IS WRONG. It is a
151
+ * `createLeafComponent` collection node, not a styled element: react-aria builds the listbox's
152
+ * collection from the element TYPES of its children, so an unrecognised `<div>` truncates the
153
+ * collection and every option after the divider stops existing. `select-groups.test.tsx` caught
154
+ * it immediately — "Unable to find an accessible element with the role option and name バナナ".
155
+ */
130
156
  export declare const SelectSeparator: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLElement> & React.RefAttributes<HTMLElement>>;
131
157
  export {};
@@ -533,7 +533,15 @@ const SelectSeparator = React.forwardRef(
533
533
  ref,
534
534
  "data-slot": "select-separator",
535
535
  className: cn("ui-select-separator", className),
536
- ...splitDomProps(props).attributes
536
+ ...splitDomProps(props).attributes,
537
+ render: (separatorProps) => /* @__PURE__ */ jsx(
538
+ "div",
539
+ {
540
+ ...separatorProps,
541
+ role: void 0,
542
+ "aria-hidden": "true"
543
+ }
544
+ )
537
545
  }
538
546
  );
539
547
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "25.0.0",
3
+ "version": "25.1.0",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -31,6 +31,7 @@ pnpm check:frame-axe -- /isolate/layout-topbar # one route, while fixing
31
31
  | tags | `wcag2a` · `wcag2aa` · `wcag21aa` · `wcag22aa` — **the consumer's set, verbatim** |
32
32
  | routes | every `/isolate/<id>` in `window.__STORY_MANIFEST__` + every `/showcase/<id>` |
33
33
  | viewports | 1440×900 · 375×667 · **320×568** |
34
+ | states | as rendered, **plus** one overlay OPEN on the routes that declare `data-axe-open` |
34
35
 
35
36
  320 is not decoration: it is WCAG 2.2 SC 1.4.10's reflow width, it is the width the consumer's
36
37
  nightly runs, and it is the width gh#639 failed at while 390 passed.
@@ -39,6 +40,114 @@ Showcases are included on purpose. They are the only frames here shaped like a r
39
40
  page, a landmark tree, a focus order — which is precisely the class the component frames cannot
40
41
  reach and the consumer has been carrying alone.
41
42
 
43
+ ## The overlay scope — opening the thing before measuring it
44
+
45
+ A menu, dialog, listbox or popover that is closed at rest **paints nothing**. Every axe rule whose
46
+ condition only exists while one is open was therefore outside this gate's field of view, not passing
47
+ it. The gate shipped in 24.1.0 without this half; it is back (gh#643 item 4), rebuilt from the
48
+ version #492 deleted.
49
+
50
+ A demo opts in **declaratively**, from the demo itself:
51
+
52
+ ```tsx
53
+ // the trigger, when it forwards DOM props
54
+ <DialogTrigger asChild>
55
+ <Button data-axe-open size="sm">仕訳新規作成</Button>
56
+ </DialogTrigger>
57
+
58
+ // the region that owns it, when the component renders its own trigger
59
+ <CardContent data-axe-open>
60
+ <FormField id="status" label="状態"><Select … /></FormField>
61
+ </CardContent>
62
+
63
+ // right-click, for the gesture nothing else reaches
64
+ <Button data-axe-open="contextmenu" variant="outline">…</Button>
65
+ ```
66
+
67
+ What the gate then does, per route/viewport, **after** the default-state scan (opening an overlay is
68
+ destructive to the state that scan measures):
69
+
70
+ 1. Take the **first** `[data-axe-open]` on the page. No declaration ⇒ the route skips this entirely
71
+ and costs nothing.
72
+ 2. Press **Escape** up to three times while any overlay is mounted. Several demos deliberately
73
+ render one open at rest (`popover`'s anchored panel, `dropdown-menu`'s `defaultOpen` card), and a
74
+ modal one makes the rest of the page unreachable. Dismissing first also makes the measurement a
75
+ real **closed → open** transition rather than whatever the demo happened to leave mounted.
76
+ 3. Resolve the click target: the declaring element itself when it matches
77
+ `button, [role=button], [role=combobox], [role=menuitem], a[href], summary`, otherwise the first
78
+ such control inside it. (`Select` and `DatePicker` own their trigger DOM and forward no `data-*`
79
+ to it — hence the declare-on-the-region form. `DatePicker`'s combobox `<input>` is what opens the
80
+ calendar, and it is what gets pressed.)
81
+ 4. Click (or right-click), then wait until **one more** overlay is mounted
82
+ (`[data-radix-popper-content-wrapper], [role=dialog], [role=alertdialog], [role=menu],
83
+ [role=listbox]`) — never a bare timer.
84
+ 5. Scan again, and key the rows `@<viewport>+open`:
85
+
86
+ ```
87
+ /isolate/data-entry-select-matrix @1440+open aria-required-children
88
+ ```
89
+
90
+ **A declaration whose overlay never opens is a gate FAILURE**, recorded as `overlay-did-not-open`
91
+ and red on the spot. A broken declaration otherwise reads exactly like a clean frame, which is the
92
+ one thing this scope exists to prevent.
93
+
94
+ **Routes declaring an open step today — 7 declared, 7 open:** `data-entry-select`,
95
+ `data-entry-select-matrix`, `data-entry-date-picker`, `data-display-popover`,
96
+ `navigation-dropdown-menu`, `feedback-dialog`, `feedback-sheet`. (The old `navigation-context-menu`
97
+ frame is gone — `ContextMenu` was folded into `DropdownMenu trigger={["contextMenu"]}` in v23 — so
98
+ the right-click gesture is declared on that card of `navigation-dropdown-menu` instead.)
99
+
100
+ ### What it found, and the proof it has teeth
101
+
102
+ **`aria-required-children`, CRITICAL, on `/isolate/data-entry-select-matrix` at all three
103
+ viewports.** `SelectSeparator` rendered react-aria's `<Separator>`, i.e. `role="separator"`, as a
104
+ direct child of the viewport's `role="listbox"`. ARIA 1.2 lets a `listbox` own `option` and `group`
105
+ and nothing else. Fixed in `src/components/data-entry/select.tsx` — the divider is decoration over a
106
+ structure the two `SelectGroup`s already announce, so it now renders `aria-hidden` with no role.
107
+ `menu` **does** own `separator`, so `DropdownMenuSeparator` is correct as it stands. Regression test:
108
+ `src/components/data-entry/__tests__/select-groups.test.tsx`.
109
+
110
+ The control run is the part worth keeping. On the **same defective build**:
111
+
112
+ | declarations | result |
113
+ | ------------ | --------------------------------------------------------------------------- |
114
+ | removed | ✓ green, `0 route(s) opened a declared overlay` |
115
+ | present | ✗ red, 3 × `aria-required-children` on `@1440+open / @375+open / @320+open` |
116
+
117
+ A `data-axe-open` on a `Button` that opens nothing turns the gate red with three
118
+ `overlay-did-not-open` rows, one per viewport.
119
+
120
+ **The original 2026-09 proof no longer reproduces, and the reason matters more than the proof.** That
121
+ commit disabled `src/components/general/inert-background.ts` and watched `aria-hidden-focus` go red
122
+ on `select`, `dropdown-menu` and `context-menu`. Disabling it today changes nothing measurable:
123
+ these overlays are **react-aria-components**, not Radix: react-aria inerts the background itself —
124
+ `#root` plus three focus sentinels carry `inert` the moment a listbox opens, with no `aria-hidden`
125
+ anywhere on them. Stripping every `inert` attribute immediately before the scan still leaves the
126
+ page clean and `aria-hidden-focus` in axe's `passes` bucket, because that rule needs an
127
+ `aria-hidden` ancestor to fire at all. No `src/` component imports `useInertHiddenBackground` any
128
+ more; the only importer is the Radix parity fixture
129
+ `src/components/data-entry/__tests__/radix-select.fixture.tsx`. So `inert-background.ts` is dead
130
+ for `Select` and `DropdownMenu`, and the overlay scope's value is no longer the rule it was built
131
+ for.
132
+
133
+ Be careful with the stronger version of that claim, because the first draft of this paragraph made
134
+ it and it is false: `data-aria-hidden` is **not** absent from the built preview. The `aria-hidden`
135
+ npm package ships inside the `command` chunk (`cmdk`'s dependency tree), so the marker exists in
136
+ the bundle even though no first-party overlay sets it. What is measured above is the narrow claim —
137
+ `Select` and `DropdownMenu` background-inert via `inert`, not `aria-hidden` — not a repo-wide
138
+ absence. It is still the rule class: `aria-required-children` is the same
139
+ shape, and nothing else here could see it.
140
+
141
+ ### What is still outside this scope, stated rather than hidden
142
+
143
+ - Only the **first** declaration per route is exercised. A route with several distinct overlays is
144
+ measured on one of them.
145
+ - Only **one step deep**: submenus, an overlay opened from inside another, and the state after a
146
+ selection are not reached.
147
+ - **Keyboard opening is not exercised** — the gate clicks. A trigger that opens on click but not on
148
+ `Enter`/`Space` passes.
149
+ - The overlay's **focus trap and focus order** are not asserted; axe cannot see a JS focus trap.
150
+
42
151
  ## What it found on its first run — and what came of it
43
152
 
44
153
  54 rows, **159 violation nodes**, on code that passed every other gate in this repository. Fixing
@@ -105,12 +214,10 @@ The gate fails on a key that is **not** in the baseline, and on a baselined key
105
214
 
106
215
  ## What this gate does NOT do — stated, not hidden
107
216
 
108
- The gate deleted in #492 did three things this one does not, and each is a real gap:
217
+ The gate deleted in #492 did three things this one does not. One is now back; two are still gaps:
109
218
 
110
- 1. **No overlay scope.** It scans the frame as rendered. A menu, dialog, listbox or popover that is
111
- closed at rest is never measured, so `aria-hidden-focus` and friends stay outside its field of
112
- view. The old gate opened one overlay per frame from a `data-axe-open` attribute; that attribute
113
- was removed from the demos along with the gate and would have to come back.
219
+ 1. ~~**No overlay scope.**~~ Restored — see [the overlay
220
+ scope](#the-overlay-scope--opening-the-thing-before-measuring-it) above.
114
221
  2. **No chrome/component split.** The old gate held the preview toolbar to zero violations and
115
222
  allowlisted the component scope separately. `/isolate/**` renders the demo alone, so there is
116
223
  little chrome to separate — but `/showcase/**` is scanned whole.
@@ -127,11 +234,16 @@ which changes what `color-contrast` resolves a background to). Two sweeps after
127
234
 
128
235
  ## Cost
129
236
 
130
- | | |
131
- | ---------------------------------- | ----------------------------------------------------------------------------------- |
132
- | full sweep | **3m20s** locally · **12m01s** on the self-hosted runner — 215 routes × 3 viewports |
133
- | sequential | ~55 min — the three viewport passes run concurrently, which is the whole difference |
134
- | showcase only (`--scope=showcase`) | 30s |
237
+ | | |
238
+ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
239
+ | full sweep | **3m20s** locally · **12m01s** on the self-hosted runner — 215 routes × 3 viewports |
240
+ | sequential | ~55 min — the three viewport passes run concurrently, which is the whole difference |
241
+ | showcase only (`--scope=showcase`) | 30s |
242
+ | full sweep **with the overlay scope** | **2m14s** on the machine gh#643 item 4 was built on, same 215 × 3 — the runner number has not been re-measured |
243
+
244
+ The overlay pass is bounded by declaration: only the 7 declaring routes pay it (21 extra scans plus
245
+ their open steps), and no `/showcase/**` route declares one, so `--scope=showcase` — the merge lane —
246
+ is unchanged.
135
247
 
136
248
  `--shard=i/n` is in the script for the day the sweep outgrows the lane. Using it adds check-run
137
249
  names, which costs nothing here because none of them is in `REQUIRED_CI_CHECK_RUNS`.
@@ -61,7 +61,9 @@ export default function Demo() {
61
61
  <CardContent>
62
62
  <Popover open={filterOpen} onOpenChange={setFilterOpen}>
63
63
  <PopoverTrigger asChild>
64
- <Button variant="outline">
64
+ {/* data-axe-open: check:frame-axe presses this before its overlay scan, so the
65
+ panel is measured OPEN as well as closed. See docs/FRAME-A11Y-CI.md. */}
66
+ <Button data-axe-open variant="outline">
65
67
  <SlidersHorizontal />
66
68
  絞り込み
67
69
  </Button>
@@ -41,7 +41,9 @@ export default function Demo() {
41
41
  形式に整形され、 解釈できない入力は元の値へ復帰する。
42
42
  </CardDescription>
43
43
  </CardHeader>
44
- <CardContent>
44
+ {/* data-axe-open on the region: DatePicker owns its trigger DOM. The gate presses the
45
+ `[role="combobox"]` input inside, which is what opens the calendar. */}
46
+ <CardContent data-axe-open>
45
47
  <FormField id="issue-date" label="発行日" required>
46
48
  <DatePicker
47
49
  id="issue-date"
@@ -37,7 +37,9 @@ export default function Demo() {
37
37
  content. Radix owns keyboard focus and collision handling.
38
38
  </CardDescription>
39
39
  </CardHeader>
40
- <CardContent>
40
+ {/* data-axe-open on the region: the gate presses the first `[role="combobox"]` inside it,
41
+ which is the compound SelectTrigger. */}
42
+ <CardContent data-axe-open>
41
43
  <Flex direction="col" gap="md">
42
44
  <FormField
43
45
  id="compound-controlled"
@@ -90,7 +90,10 @@ export default function Demo() {
90
90
  Pass an options array; name= submits the value with the form.
91
91
  </CardDescription>
92
92
  </CardHeader>
93
- <CardContent>
93
+ {/* data-axe-open on the REGION, not the trigger: the data-driven Select renders its own
94
+ trigger and forwards no `data-*` to it, so the gate resolves the `[role="combobox"]`
95
+ inside this element and presses that. See docs/FRAME-A11Y-CI.md. */}
96
+ <CardContent data-axe-open>
94
97
  <FormField id="status" label="状態">
95
98
  <Select
96
99
  id="status"
@@ -64,7 +64,10 @@ export default function Demo() {
64
64
  <CardContent>
65
65
  <Dialog open={createOpen} onOpenChange={setCreateOpen}>
66
66
  <DialogTrigger asChild>
67
- <Button size="sm">仕訳新規作成</Button>
67
+ {/* data-axe-open: check:frame-axe presses this before its overlay scan. */}
68
+ <Button data-axe-open size="sm">
69
+ 仕訳新規作成
70
+ </Button>
68
71
  </DialogTrigger>
69
72
  <DialogContent className="max-w-lg">
70
73
  <DialogHeader>
@@ -104,7 +104,8 @@ export default function Demo() {
104
104
  <CardContent>
105
105
  <Sheet open={filterOpen} onOpenChange={setFilterOpen}>
106
106
  <SheetTrigger asChild>
107
- <Button variant="outline" size="sm">
107
+ {/* data-axe-open: check:frame-axe presses this before its overlay scan. */}
108
+ <Button data-axe-open variant="outline" size="sm">
108
109
  <SlidersHorizontal />
109
110
  詳細検索
110
111
  </Button>
@@ -105,7 +105,14 @@ export default function Demo() {
105
105
  <Flex direction="row" align="center" gap="md" wrap>
106
106
  <DropdownMenu trigger={["contextMenu"]}>
107
107
  <DropdownMenuTrigger asChild>
108
- <Button variant="outline">仕訳 JE-0042 を右クリック</Button>
108
+ {/* data-axe-open="contextmenu": the gate RIGHT-clicks this before its overlay
109
+ scan. It is declared here rather than on one of the click menus above
110
+ because the `defaultOpen` card at the bottom of this page already holds a
111
+ click-opened menu mounted at rest, so the click path is measured either way
112
+ — the right-click path is the one nothing else on this page reaches. */}
113
+ <Button data-axe-open="contextmenu" variant="outline">
114
+ 仕訳 JE-0042 を右クリック
115
+ </Button>
109
116
  </DropdownMenuTrigger>
110
117
  <DropdownMenuContent>
111
118
  <DropdownMenuItem>編集</DropdownMenuItem>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "25.0.0",
4
- "godxUiMcp": "25.0.0",
3
+ "version": "25.1.0",
4
+ "godxUiMcp": "25.1.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",