@godxjp/ui 25.0.0 → 25.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data-entry/select.d.ts +26 -0
- package/dist/components/data-entry/select.js +10 -2
- package/dist/components/general/button.js +6 -1
- package/dist/components/navigation/dropdown-menu.d.ts +2 -1
- package/dist/contracts/measurement.json +1 -1
- package/dist/styles/control.css +2 -0
- package/dist/styles/text-layout.css +5 -1
- package/dist/tokens/components/text.css +3 -1
- package/dist/tokens/foundation.css +3 -2
- package/docs/FRAME-A11Y-CI.md +122 -10
- package/docs/data-display/popover.tsx +3 -1
- package/docs/data-entry/date-picker.tsx +3 -1
- package/docs/data-entry/select-matrix.tsx +3 -1
- package/docs/data-entry/select.tsx +4 -1
- package/docs/feedback/dialog.tsx +4 -1
- package/docs/feedback/sheet.tsx +2 -1
- package/docs/navigation/dropdown-menu.tsx +8 -1
- package/package.json +2 -2
- package/dist/components/general/inert-background.d.ts +0 -10
- package/dist/components/general/inert-background.js +0 -73
|
@@ -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 {};
|
|
@@ -221,7 +221,7 @@ const SelectValue = React.forwardRef(function SelectValue2({ placeholder, childr
|
|
|
221
221
|
className: className ?? "",
|
|
222
222
|
style,
|
|
223
223
|
...props,
|
|
224
|
-
children: ({ isPlaceholder, defaultChildren }) => isPlaceholder ? placeholder ??
|
|
224
|
+
children: ({ isPlaceholder, defaultChildren }) => isPlaceholder ? placeholder ?? "" : children ?? defaultChildren
|
|
225
225
|
}
|
|
226
226
|
);
|
|
227
227
|
});
|
|
@@ -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
|
}
|
|
@@ -17,7 +17,12 @@ const buttonVariants = cva("ui-button", {
|
|
|
17
17
|
// (`--primary-hover`) could never take effect while `hover:bg-primary/90` was emitted here.
|
|
18
18
|
default: "ui-button--default",
|
|
19
19
|
destructive: "ui-button--destructive bg-destructive text-destructive-foreground",
|
|
20
|
-
|
|
20
|
+
// `shadow-xs` từng nằm ở đây và mắc ĐÚNG lỗi đã nêu ngay trên cho `default`:
|
|
21
|
+
// một utility thì out-rank tầng components, nên bóng của nút outline đi vòng
|
|
22
|
+
// qua tầng token — consumer đặt `--shadow-sm: none` thì `default` sạch bóng còn
|
|
23
|
+
// `outline` trơ ra. Nay bóng khai ở `.ui-button--outline` và đọc `var(--shadow-sm)`
|
|
24
|
+
// như mọi biến thể khác, nên nó tắt/đổi được qua theme. (gh#662)
|
|
25
|
+
outline: "ui-button--outline border bg-background hover:bg-accent hover:text-accent-foreground",
|
|
21
26
|
dashed: "ui-button--dashed border border-dashed bg-background hover:bg-accent hover:text-accent-foreground",
|
|
22
27
|
secondary: "ui-button--secondary bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
23
28
|
ghost: "ui-button--ghost hover:bg-accent hover:text-accent-foreground",
|
|
@@ -139,7 +139,8 @@ type DropdownMenuContentProps = React.PropsWithChildren<DropdownMenuContentProps
|
|
|
139
139
|
* phần tử mang `role="menu"`. `data-slot` và lớp `ui-dropdown-menu-content` ở lại trên HỘP, cùng
|
|
140
140
|
* chỗ với `data-state` / `data-side` mà các utility animate đang đọc.
|
|
141
141
|
*
|
|
142
|
-
* `useInertHiddenBackground` đã được gỡ ở đây
|
|
142
|
+
* `useInertHiddenBackground` đã được gỡ ở đây, và nay đã rời hẳn `src/` sang
|
|
143
|
+
* `data-entry/__tests__/inert-background.fixture.ts` (gh#643): RAC gọi `ariaHideOutside(..., {shouldUseInert:
|
|
143
144
|
* true})` — nền thành `inert` thật chứ không chỉ `aria-hidden`, nên axe `aria-hidden-focus` không
|
|
144
145
|
* còn cửa để nổ. Và hiệu ứng đó khoá theo `state.isOpen`, không theo unmount, nên `inert` được nhả
|
|
145
146
|
* NGAY ở ý định đóng: đúng lớp lỗi gh#385, đã sửa ở thượng nguồn. Phép đo là
|
|
@@ -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.
|
|
3
|
+
"version": "25.2.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,
|
package/dist/styles/control.css
CHANGED
|
@@ -120,10 +120,14 @@
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
[data-slot="text"][data-link] {
|
|
123
|
-
text-decoration-line:
|
|
123
|
+
text-decoration-line: var(--text-link-decoration-line);
|
|
124
124
|
text-underline-offset: var(--text-link-underline-offset);
|
|
125
125
|
text-decoration-thickness: var(--text-link-underline-width);
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
[data-slot="text"][data-link][data-tone="primary"]:not([data-disabled]) {
|
|
129
|
+
color: hsl(var(--text-link));
|
|
130
|
+
}
|
|
127
131
|
[data-slot="text"][data-link]:hover,
|
|
128
132
|
[data-slot="text"][data-link]:focus-visible {
|
|
129
133
|
text-decoration-line: underline;
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
|
|
40
40
|
--text-tertiary: 44 5% 55%;
|
|
41
41
|
--text-disabled: 40 6% 68%;
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
--text-link: 268.3 100% 41.6%;
|
|
43
44
|
--text-brand: 268.3 100% 41.6%;
|
|
44
45
|
|
|
45
46
|
--text-primary: 268 100% 34.5%;
|
|
@@ -351,7 +352,7 @@
|
|
|
351
352
|
|
|
352
353
|
--text-tertiary: 44 6% 56%;
|
|
353
354
|
--text-disabled: 45 6% 40%;
|
|
354
|
-
--text-link:
|
|
355
|
+
--text-link: 268.7 100% 86.9%;
|
|
355
356
|
--text-brand: 268.7 100% 86.9%;
|
|
356
357
|
|
|
357
358
|
--text-primary: 262.7 100% 92.7%;
|
package/docs/FRAME-A11Y-CI.md
CHANGED
|
@@ -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
|
|
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.
|
|
111
|
-
|
|
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
|
|
133
|
-
| sequential
|
|
134
|
-
| showcase only (`--scope=showcase`)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
package/docs/feedback/dialog.tsx
CHANGED
|
@@ -64,7 +64,10 @@ export default function Demo() {
|
|
|
64
64
|
<CardContent>
|
|
65
65
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
|
66
66
|
<DialogTrigger asChild>
|
|
67
|
-
|
|
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>
|
package/docs/feedback/sheet.tsx
CHANGED
|
@@ -104,7 +104,8 @@ export default function Demo() {
|
|
|
104
104
|
<CardContent>
|
|
105
105
|
<Sheet open={filterOpen} onOpenChange={setFilterOpen}>
|
|
106
106
|
<SheetTrigger asChild>
|
|
107
|
-
|
|
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
|
-
|
|
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,10 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
/**
|
|
3
|
-
* Call from an overlay's content wrapper, and spread the returned ref onto the Radix content
|
|
4
|
-
* element itself — that element is what carries `data-state`, and registering it is what lets the
|
|
5
|
-
* background be released on close intent rather than on unmount (gh#385).
|
|
6
|
-
*
|
|
7
|
-
* The caller's own forwarded ref is composed in, so a content component still hands its node to
|
|
8
|
-
* the consumer. Idempotent and refcounted: one observer serves every overlay on the page.
|
|
9
|
-
*/
|
|
10
|
-
export declare function useInertHiddenBackground<T extends HTMLElement>(forwarded?: React.Ref<T>): (node: T | null) => void;
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
const TABBABLE = 'a[href], area[href], button, input, select, textarea, iframe, object, embed, details, audio[controls], video[controls], [contenteditable="true"], [tabindex]:not([tabindex^="-"])';
|
|
4
|
-
const owned = /* @__PURE__ */ new Set();
|
|
5
|
-
const anchors = /* @__PURE__ */ new Set();
|
|
6
|
-
let observer = null;
|
|
7
|
-
let mounted = 0;
|
|
8
|
-
function anyOverlayOpen() {
|
|
9
|
-
for (const anchor of anchors) {
|
|
10
|
-
if (anchor.isConnected && anchor.getAttribute("data-state") !== "closed") return true;
|
|
11
|
-
}
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
function sync() {
|
|
15
|
-
if (!anyOverlayOpen()) {
|
|
16
|
-
release();
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
for (const el of document.querySelectorAll("[data-aria-hidden]")) {
|
|
20
|
-
if (owned.has(el) || el.hasAttribute("inert")) continue;
|
|
21
|
-
if (el.matches("[data-radix-focus-guard]")) continue;
|
|
22
|
-
if (el.matches(TABBABLE) || el.querySelector(TABBABLE)) {
|
|
23
|
-
el.setAttribute("inert", "");
|
|
24
|
-
owned.add(el);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
for (const el of [...owned]) {
|
|
28
|
-
if (el.isConnected && el.hasAttribute("data-aria-hidden")) continue;
|
|
29
|
-
el.removeAttribute("inert");
|
|
30
|
-
owned.delete(el);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function release() {
|
|
34
|
-
for (const el of owned) el.removeAttribute("inert");
|
|
35
|
-
owned.clear();
|
|
36
|
-
}
|
|
37
|
-
function useInertHiddenBackground(forwarded) {
|
|
38
|
-
React.useEffect(() => {
|
|
39
|
-
mounted += 1;
|
|
40
|
-
if (!observer) {
|
|
41
|
-
observer = new MutationObserver(sync);
|
|
42
|
-
observer.observe(document.body, {
|
|
43
|
-
subtree: true,
|
|
44
|
-
attributes: true,
|
|
45
|
-
// `data-state` is the close-intent signal; `data-aria-hidden` is the what-to-hide signal.
|
|
46
|
-
attributeFilter: ["data-aria-hidden", "data-state"]
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
sync();
|
|
50
|
-
return () => {
|
|
51
|
-
mounted -= 1;
|
|
52
|
-
if (mounted > 0) return;
|
|
53
|
-
observer?.disconnect();
|
|
54
|
-
observer = null;
|
|
55
|
-
release();
|
|
56
|
-
};
|
|
57
|
-
}, []);
|
|
58
|
-
const registered = React.useRef(null);
|
|
59
|
-
const forwardedRef = React.useRef(forwarded);
|
|
60
|
-
forwardedRef.current = forwarded;
|
|
61
|
-
return React.useCallback((node) => {
|
|
62
|
-
if (registered.current) anchors.delete(registered.current);
|
|
63
|
-
registered.current = node;
|
|
64
|
-
if (node) anchors.add(node);
|
|
65
|
-
sync();
|
|
66
|
-
const target = forwardedRef.current;
|
|
67
|
-
if (typeof target === "function") target(node);
|
|
68
|
-
else if (target) target.current = node;
|
|
69
|
-
}, []);
|
|
70
|
-
}
|
|
71
|
-
export {
|
|
72
|
-
useInertHiddenBackground
|
|
73
|
-
};
|