@astryxdesign/core 0.6.1 → 0.6.2-canary.0faf070
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/CHANGELOG.md +33 -0
- package/dist/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +3 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +56 -19
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +12 -2
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +6 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts +3 -0
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +16 -9
- package/dist/Dialog/DialogHeader.d.ts +1 -1
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +10 -7
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +9 -3
- package/dist/Kbd/Kbd.d.ts +5 -3
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +36 -42
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +6 -2
- package/dist/Markdown/Markdown.d.ts +10 -2
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +58 -14
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +126 -12
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +369 -34
- package/dist/Markdown/utils.d.ts +1 -1
- package/dist/Markdown/utils.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +13 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
- package/dist/SideNav/SideNav.d.ts +2 -1
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +9 -3
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +19 -10
- package/dist/Spinner/Spinner.d.ts +1 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +23 -15
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +11 -0
- package/dist/TabList/Tab.d.ts +1 -1
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +20 -7
- package/dist/ToggleButton/ToggleButton.d.ts +2 -1
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +7 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +15 -6
- package/dist/astryx.css +13 -2
- package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
- package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
- package/dist/hooks/scrollKeyboardDelegation.js +146 -0
- package/dist/hooks/useScrollableArea.d.ts +6 -2
- package/dist/hooks/useScrollableArea.d.ts.map +1 -1
- package/dist/hooks/useScrollableArea.js +17 -5
- package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +9 -0
- package/locales/en.json +16 -0
- package/locales/pseudo.json +12 -0
- package/package.json +7 -5
- package/scripts/agent-doc-state.mjs +1 -1
- package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
- package/src/BottomSheet/BottomSheet.spec.md +46 -20
- package/src/BottomSheet/BottomSheet.test.tsx +6 -3
- package/src/BottomSheet/BottomSheet.tsx +3 -1
- package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
- package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
- package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
- package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
- package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
- package/src/Button/__tests__/Button.a11y.states.ts +9 -0
- package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +21 -1
- package/src/ClickableCard/ClickableCard.test.tsx +102 -5
- package/src/Collapsible/Collapsible.doc.mjs +11 -0
- package/src/Collapsible/Collapsible.test.tsx +21 -0
- package/src/Collapsible/Collapsible.tsx +5 -0
- package/src/DateRangeInput/DateRangeInput.doc.mjs +35 -7
- package/src/DateRangeInput/DateRangeInput.spec.md +203 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +100 -4
- package/src/DateRangeInput/DateRangeInput.tsx +29 -20
- package/src/Dialog/Dialog.doc.mjs +3 -0
- package/src/Dialog/Dialog.spec.md +1 -1
- package/src/Dialog/DialogHeader.doc.mjs +38 -0
- package/src/Dialog/DialogHeader.test.tsx +49 -0
- package/src/Dialog/DialogHeader.tsx +23 -4
- package/src/Dialog/modules/DialogHeader.spec.md +152 -0
- package/src/FileInput/FileInput.doc.mjs +2 -0
- package/src/FileInput/FileInput.spec.md +199 -0
- package/src/FileInput/FileInput.test.tsx +14 -0
- package/src/FileInput/FileInput.tsx +13 -3
- package/src/Kbd/Kbd.doc.mjs +3 -3
- package/src/Kbd/Kbd.test.tsx +57 -1
- package/src/Kbd/Kbd.tsx +57 -37
- package/src/Link/Link.doc.mjs +11 -0
- package/src/Link/Link.test.tsx +24 -0
- package/src/Link/Link.tsx +5 -0
- package/src/Markdown/Markdown.doc.mjs +167 -42
- package/src/Markdown/Markdown.public.test.ts +157 -0
- package/src/Markdown/Markdown.spec.md +255 -71
- package/src/Markdown/Markdown.test.tsx +107 -3
- package/src/Markdown/Markdown.tsx +116 -35
- package/src/Markdown/incremental.test.ts +175 -7
- package/src/Markdown/index.ts +6 -0
- package/src/Markdown/parser.perf.test.ts +3 -1
- package/src/Markdown/parser.test.ts +122 -0
- package/src/Markdown/parser.ts +609 -81
- package/src/Markdown/utils.ts +6 -0
- package/src/Outline/Outline.spec.md +1 -1
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
- package/src/RadioList/RadioList.doc.mjs +11 -0
- package/src/RadioList/RadioList.test.tsx +32 -0
- package/src/RadioList/RadioListItem.tsx +25 -1
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
- package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
- package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
- package/src/SideNav/SideNav.doc.mjs +1 -1
- package/src/SideNav/SideNav.test.tsx +10 -0
- package/src/SideNav/SideNav.tsx +14 -2
- package/src/Slider/Slider.doc.mjs +27 -0
- package/src/Slider/Slider.spec.md +61 -47
- package/src/Slider/Slider.test.tsx +146 -0
- package/src/Slider/Slider.tsx +37 -13
- package/src/Spinner/Spinner.doc.mjs +6 -3
- package/src/Spinner/Spinner.test.tsx +37 -0
- package/src/Spinner/Spinner.tsx +31 -14
- package/src/Switch/Switch.doc.mjs +11 -0
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +28 -0
- package/src/TabList/Tab.tsx +35 -7
- package/src/TabList/TabList.doc.mjs +11 -0
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
- package/src/ToggleButton/ToggleButton.test.tsx +133 -0
- package/src/ToggleButton/ToggleButton.tsx +9 -2
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
- package/src/Tokenizer/Tokenizer.spec.md +142 -75
- package/src/Typeahead/BaseTypeahead.spec.md +4 -3
- package/src/Typeahead/BaseTypeahead.tsx +15 -6
- package/src/Typeahead/Typeahead.test.tsx +53 -0
- package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
- package/src/__tests__/pressState.ts +93 -0
- package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
- package/src/hooks/scrollKeyboardDelegation.ts +233 -0
- package/src/hooks/useScrollableArea.doc.mjs +15 -3
- package/src/hooks/useScrollableArea.test.tsx +59 -1
- package/src/hooks/useScrollableArea.ts +34 -10
- package/src/theme/derivedVarRegistry.test.ts +6 -4
- package/src/utils/interactionOverlay.stylex.ts +20 -0
package/src/Markdown/utils.ts
CHANGED
|
@@ -16,9 +16,15 @@ export {
|
|
|
16
16
|
|
|
17
17
|
export type {
|
|
18
18
|
InlineNode,
|
|
19
|
+
InlineNodeWithMath,
|
|
20
|
+
MathInlineNode,
|
|
19
21
|
BlockNode,
|
|
22
|
+
BlockNodeWithMath,
|
|
23
|
+
MathBlockNode,
|
|
20
24
|
ListItemNode,
|
|
21
25
|
TableCellNode,
|
|
22
26
|
TableAlignment,
|
|
27
|
+
ParseOptions,
|
|
28
|
+
MathParseOptions,
|
|
23
29
|
IncrementalState,
|
|
24
30
|
} from './parser';
|
|
@@ -12,7 +12,7 @@ owners: [cixzhang]
|
|
|
12
12
|
review_triggers: [theming]
|
|
13
13
|
verified_by:
|
|
14
14
|
[packages/core/src/Outline/Outline.test.tsx, scripts/check-knowledge.mjs]
|
|
15
|
-
modules: []
|
|
15
|
+
modules: [module:Outline/parseOutlineFromMarkdown]
|
|
16
16
|
families: []
|
|
17
17
|
design_specs: []
|
|
18
18
|
architecture: [architecture:component-theming-surface]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 1
|
|
4
|
+
kind: module
|
|
5
|
+
id: module:Outline/parseOutlineFromMarkdown
|
|
6
|
+
authority: current
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: cixzhang
|
|
10
|
+
approved_at: 2026-09-15
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers: [public-api, behavior]
|
|
13
|
+
verified_by: [packages/core/src/Outline/parseOutlineFromMarkdown.test.ts]
|
|
14
|
+
parent_component: component:Outline
|
|
15
|
+
references:
|
|
16
|
+
[architecture:public-component-api, component:Markdown, spec:AST-036]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# parseOutlineFromMarkdown module contract
|
|
20
|
+
|
|
21
|
+
## Intent
|
|
22
|
+
|
|
23
|
+
`parseOutlineFromMarkdown` and `useOutlineFromMarkdown` derive same-document
|
|
24
|
+
heading navigation from the same Markdown parse configuration, ordered immutable
|
|
25
|
+
transforms, text projection, and collision allocator used by the rendered document.
|
|
26
|
+
This focused module owns that utility behavior without deciding the draft Outline
|
|
27
|
+
component's anatomy or theming.
|
|
28
|
+
|
|
29
|
+
## Compatibility and migration
|
|
30
|
+
|
|
31
|
+
- Released default preserved: `yes`
|
|
32
|
+
- Compatibility class: additive parser options; calls without plugins preserve
|
|
33
|
+
released labels, levels, IDs, and return types
|
|
34
|
+
- Migration decision: `spec:AST-036`
|
|
35
|
+
|
|
36
|
+
## Ownership boundary
|
|
37
|
+
|
|
38
|
+
**Owns**
|
|
39
|
+
|
|
40
|
+
- Deriving ordered `OutlineItem` values from Markdown headings.
|
|
41
|
+
- Accepting the same parser-affecting options and ordered plugin list as Markdown.
|
|
42
|
+
- Applying the same validated immutable transforms before selecting headings.
|
|
43
|
+
- Using the same extension text projection and collision-safe slug allocation as
|
|
44
|
+
Markdown heading rendering.
|
|
45
|
+
|
|
46
|
+
**Does not own / non-goals**
|
|
47
|
+
|
|
48
|
+
- Outline component structure, active state, targets, layout, or theming.
|
|
49
|
+
- Plugin rendering, block-extension navigation entries, or renderer-derived text.
|
|
50
|
+
- A second parser, plugin registry, or independent heading-ID policy.
|
|
51
|
+
|
|
52
|
+
## Public API and concepts
|
|
53
|
+
|
|
54
|
+
`parseOutlineFromMarkdown(source, options?)` performs direct derivation.
|
|
55
|
+
`useOutlineFromMarkdown(source, options?)` memoizes the same derivation for React
|
|
56
|
+
callers. `options.plugins` uses the canonical opaque Markdown plugin entries. The
|
|
57
|
+
same ordered transforms run before heading selection; inline extension-node `toText`
|
|
58
|
+
projections then contribute to heading labels and IDs.
|
|
59
|
+
|
|
60
|
+
## Behavioral contract
|
|
61
|
+
|
|
62
|
+
| ID | Invariant | Basis | Acceptance and implementation state |
|
|
63
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ----------------------------------- |
|
|
64
|
+
| FR1 | Both utilities accept the same Markdown parser options and ordered plugin list as the rendered Markdown document. | `spec:AST-036` FR13, FR16 | pending implementation |
|
|
65
|
+
| FR2 | Both utilities apply the same validated immutable transforms before selecting headings, without allowing renderer output to affect identity. | `spec:AST-036` FR13, FR16 | pending implementation |
|
|
66
|
+
| FR3 | Plugin-enabled labels and IDs use the same extension `toText` projection, slugger, and cross-base collision allocator as Markdown, producing unique matching targets. | `spec:AST-036` FR16 | pending implementation |
|
|
67
|
+
| FR4 | An extension node never creates an Outline entry of its own, and extension containers do not change heading traversal: entries come from the same blocks Markdown assigns heading IDs to, so a heading nested in a container is omitted exactly as one nested in a blockquote or list item is. A container never restarts, extends, or re-scopes heading identity or slug allocation. | `spec:AST-036` FR16, FR25 | pending implementation |
|
|
68
|
+
| FR5 | Omitting plugins and passing an empty list preserve released heading selection, labels, levels, IDs, return type, and memoization behavior. | `spec:AST-036` FR2 | pending implementation |
|
|
69
|
+
| FR6 | The hook memoizes against source and every parse-affecting or transform-affecting option so changed configuration cannot return stale headings. | `component:Markdown` FR15–FR16 | pending implementation |
|
|
70
|
+
| FR7 | A transform that removes a heading removes its Outline entry, and a transform that inserts a synthetic heading adds one, in both surfaces identically. Source-backed heading depth cannot change, so an entry's level always matches the rendered heading. | `spec:AST-036` FR26 | pending implementation |
|
|
71
|
+
| FR8 | When a plugin fails, derivation uses the same last valid root the rendered document uses, so labels, levels, and IDs still agree. Failures are reported through the shared diagnostic channel; this module adds no failure mode, fallback, or throw of its own. | `spec:AST-036` FR12, FR29 | pending implementation |
|
|
72
|
+
| FR9 | Derivation runs on the canonical tree through the shared parse path and requires no DOM, renderer, client boundary, or second parser. Because transforms are idempotent under streaming, an outline derived from a growing source prefix converges rather than oscillates. | `spec:AST-036` FR32, FR33, FR36 | pending implementation |
|
|
73
|
+
|
|
74
|
+
### Transformation and precedence order
|
|
75
|
+
|
|
76
|
+
Markdown parse configuration → ordered immutable transforms → heading nodes →
|
|
77
|
+
shared perceivable-text projection → shared slug normalization → document-order
|
|
78
|
+
collision allocation → Outline items.
|
|
79
|
+
|
|
80
|
+
### Performance and resources
|
|
81
|
+
|
|
82
|
+
- The hook recomputes only when source or parser/transform configuration changes.
|
|
83
|
+
- Renderer output does not enter heading identity.
|
|
84
|
+
- No DOM, renderer mount, registry, Remark runtime, or optional plugin resource is
|
|
85
|
+
required.
|
|
86
|
+
|
|
87
|
+
## Accessibility contract
|
|
88
|
+
|
|
89
|
+
Each derived label is the same perceivable heading text used for Markdown identity,
|
|
90
|
+
and each derived ID targets the matching rendered heading. This module adds no
|
|
91
|
+
interaction or ARIA behavior; the Outline component owns navigation presentation.
|
|
92
|
+
|
|
93
|
+
## Design relationships
|
|
94
|
+
|
|
95
|
+
No visual representation is owned by this parser utility.
|
|
96
|
+
|
|
97
|
+
## Parent and system relationships
|
|
98
|
+
|
|
99
|
+
- `component:Markdown` owns aggregate parsing, heading rendering, and the shared
|
|
100
|
+
projection/collision rules.
|
|
101
|
+
- `spec:AST-036` owns the canonical plugin protocol and cross-surface parity.
|
|
102
|
+
- Draft `component:Outline` owns presentation only after its separate anatomy and
|
|
103
|
+
theming decisions become current.
|
|
104
|
+
|
|
105
|
+
## Verification map
|
|
106
|
+
|
|
107
|
+
| Contract | Verification | Representative states | Mutation or failure expectation |
|
|
108
|
+
| -------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| FR1–FR6 | Outline parser tests, plugin transform parity tests, and Core typecheck | transformed, formatted, and extension headings; headings nested in extension containers, blockquotes, and list items; duplicate and cross-base slugs; omitted/empty plugins | A transform is skipped, a label/ID diverges from Markdown, a container changes which headings have identity, or default output changes. |
|
|
110
|
+
| FR7–FR9 | Heading insert/remove parity, failure parity, and streaming derivation | headings removed with/without sections; inserted synthetic headings; failing transform; growing source prefix; server derivation | Outline/document disagreement, divergent fallback, a throw, stale IDs, or streamed oscillation. |
|
|
111
|
+
|
|
112
|
+
## Decision log
|
|
113
|
+
|
|
114
|
+
### DEC-1 — Markdown owns heading identity; Outline projects it
|
|
115
|
+
|
|
116
|
+
**Reference:** `module:Outline/parseOutlineFromMarkdown/DEC-1`
|
|
117
|
+
**Decider:** cixzhang, 2026-09-15
|
|
118
|
+
|
|
119
|
+
Outline utilities consume Markdown's parse configuration, ordered transforms, text
|
|
120
|
+
projection, and collision allocator rather than defining a second heading-identity
|
|
121
|
+
system. This keeps labels and link targets aligned without coupling plugin renderer
|
|
122
|
+
output to navigation identity.
|
|
123
|
+
|
|
124
|
+
### DEC-2 — One traversal survives containers and heading edits
|
|
125
|
+
|
|
126
|
+
**Reference:** `module:Outline/parseOutlineFromMarkdown/DEC-2`
|
|
127
|
+
**Decider:** cixzhang, 2026-09-16
|
|
128
|
+
|
|
129
|
+
Extension containers hold ordinary Markdown but do not change which headings have identity. Markdown assigns IDs by one traversal and this module reads the same one, so Outline never links to a heading Markdown did not identify. A heading inside a container remains omitted like one inside a blockquote or list item, and the extension node is never an Outline entry. Widening traversal is a separate decision.
|
|
130
|
+
|
|
131
|
+
Transforms may remove and insert headings, so Outline follows the rendered document rather than source: a dropped heading disappears, an inserted heading appears, and immutable source-backed depth keeps levels aligned. Both surfaces run the same pipeline rather than independent rules.
|
|
132
|
+
|
|
133
|
+
Rejected: a container-only traversal exception; extension nodes as Outline entries; source-only derivation; module-local plugin failure fallback.
|
|
134
|
+
|
|
135
|
+
## Open questions
|
|
136
|
+
|
|
137
|
+
None.
|
|
138
|
+
|
|
139
|
+
## Content boundary
|
|
140
|
+
|
|
141
|
+
This file does not duplicate plugin protocol details, Markdown parser mechanics,
|
|
142
|
+
consumer examples, or the draft Outline component's anatomy and theming contract.
|
|
@@ -10,7 +10,14 @@ import {
|
|
|
10
10
|
afterAll,
|
|
11
11
|
afterEach,
|
|
12
12
|
} from 'vitest';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
render,
|
|
15
|
+
screen,
|
|
16
|
+
fireEvent,
|
|
17
|
+
act,
|
|
18
|
+
waitFor,
|
|
19
|
+
within,
|
|
20
|
+
} from '@testing-library/react';
|
|
14
21
|
import React, {useState} from 'react';
|
|
15
22
|
import * as stylex from '@stylexjs/stylex';
|
|
16
23
|
import {spacingVars} from '../theme/tokens.stylex';
|
|
@@ -170,6 +177,148 @@ describe('PowerSearch', () => {
|
|
|
170
177
|
expect(popoverText).toContain('equals');
|
|
171
178
|
});
|
|
172
179
|
|
|
180
|
+
describe('value editor identity follows the field and operator', () => {
|
|
181
|
+
// Two fields with the same value type, so an unkeyed editor would be
|
|
182
|
+
// reused across the switch and keep the first field's menu contents.
|
|
183
|
+
const sameTypeConfig: PowerSearchConfig = {
|
|
184
|
+
name: 'test-same-type',
|
|
185
|
+
fields: [
|
|
186
|
+
{
|
|
187
|
+
key: 'creator',
|
|
188
|
+
label: 'Creator',
|
|
189
|
+
operators: [
|
|
190
|
+
{
|
|
191
|
+
key: 'is_any_of',
|
|
192
|
+
label: 'is any of',
|
|
193
|
+
value: {
|
|
194
|
+
type: 'enum_list',
|
|
195
|
+
values: [{value: 'creator-1', label: 'Creator Person'}],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
key: 'owner',
|
|
202
|
+
label: 'Owner',
|
|
203
|
+
operators: [
|
|
204
|
+
{
|
|
205
|
+
key: 'is_any_of',
|
|
206
|
+
label: 'is any of',
|
|
207
|
+
value: {
|
|
208
|
+
type: 'enum_list',
|
|
209
|
+
values: [{value: 'owner-1', label: 'Owner Person'}],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const creatorOption = () =>
|
|
218
|
+
screen.queryByRole('option', {name: 'Creator Person', hidden: true});
|
|
219
|
+
const ownerOption = () =>
|
|
220
|
+
screen.queryByRole('option', {name: 'Owner Person', hidden: true});
|
|
221
|
+
|
|
222
|
+
it('replaces an open value menu when the field changes', async () => {
|
|
223
|
+
function Harness() {
|
|
224
|
+
const internalConfig = useInternalConfig(sameTypeConfig);
|
|
225
|
+
return (
|
|
226
|
+
<PowerSearchEditPopover
|
|
227
|
+
config={internalConfig}
|
|
228
|
+
filter={{
|
|
229
|
+
field: 'creator',
|
|
230
|
+
operator: 'is_any_of',
|
|
231
|
+
value: {type: 'enum_list', value: []},
|
|
232
|
+
}}
|
|
233
|
+
mode="edit"
|
|
234
|
+
onSave={() => {}}
|
|
235
|
+
onCancel={() => {}}
|
|
236
|
+
/>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
render(<Harness />);
|
|
241
|
+
|
|
242
|
+
// Open the Creator value menu and leave it open.
|
|
243
|
+
fireEvent.focus(screen.getByRole('combobox', {name: 'Values'}));
|
|
244
|
+
await waitFor(() => expect(creatorOption()).toBeInTheDocument());
|
|
245
|
+
|
|
246
|
+
// Switch the field underneath the open menu.
|
|
247
|
+
fireEvent.click(screen.getByRole('combobox', {name: 'Field'}));
|
|
248
|
+
fireEvent.click(
|
|
249
|
+
screen.getByRole('option', {name: 'Owner', hidden: true}),
|
|
250
|
+
);
|
|
251
|
+
act(() => flushRAF());
|
|
252
|
+
|
|
253
|
+
// The old field's options are gone, the new field's are shown.
|
|
254
|
+
expect(creatorOption()).not.toBeInTheDocument();
|
|
255
|
+
await waitFor(() => expect(ownerOption()).toBeInTheDocument());
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('replaces an open value menu on a nested sub-filter row', async () => {
|
|
259
|
+
const nestedConfig: PowerSearchConfig = {
|
|
260
|
+
name: 'test-nested-same-type',
|
|
261
|
+
fields: [
|
|
262
|
+
{
|
|
263
|
+
key: 'group',
|
|
264
|
+
label: 'Group',
|
|
265
|
+
operators: [
|
|
266
|
+
{key: 'all_of', label: 'all of', value: {type: 'nested'}},
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
...sameTypeConfig.fields,
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
function Harness() {
|
|
274
|
+
const internalConfig = useInternalConfig(nestedConfig);
|
|
275
|
+
return (
|
|
276
|
+
<PowerSearchEditPopover
|
|
277
|
+
config={internalConfig}
|
|
278
|
+
filter={{
|
|
279
|
+
field: 'group',
|
|
280
|
+
operator: 'all_of',
|
|
281
|
+
value: {
|
|
282
|
+
type: 'nested',
|
|
283
|
+
value: [
|
|
284
|
+
{
|
|
285
|
+
field: 'creator',
|
|
286
|
+
operator: 'is_any_of',
|
|
287
|
+
value: {type: 'enum_list', value: []},
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
}}
|
|
292
|
+
mode="edit"
|
|
293
|
+
onSave={() => {}}
|
|
294
|
+
onCancel={() => {}}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
render(<Harness />);
|
|
300
|
+
|
|
301
|
+
fireEvent.focus(screen.getByRole('combobox', {name: 'Values'}));
|
|
302
|
+
await waitFor(() => expect(creatorOption()).toBeInTheDocument());
|
|
303
|
+
|
|
304
|
+
// The nested row's own field selector is the one currently on
|
|
305
|
+
// "Creator"; the root selector stays on "Group".
|
|
306
|
+
const rowField = screen
|
|
307
|
+
.getAllByRole('combobox', {name: 'Field'})
|
|
308
|
+
.find(el => el.textContent === 'Creator');
|
|
309
|
+
expect(rowField).toBeDefined();
|
|
310
|
+
fireEvent.click(rowField!);
|
|
311
|
+
const rowListbox = document.getElementById(
|
|
312
|
+
rowField!.getAttribute('aria-controls')!,
|
|
313
|
+
)!;
|
|
314
|
+
fireEvent.click(
|
|
315
|
+
within(rowListbox).getByRole('option', {name: 'Owner', hidden: true}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(creatorOption()).not.toBeInTheDocument();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
173
322
|
it('edit popover shows correct filter after removing a preceding filter', () => {
|
|
174
323
|
const filters: PowerSearchFilter[] = [
|
|
175
324
|
{field: 'status', operator: 'is', value: {type: 'string', value: 'open'}},
|
|
@@ -22,7 +22,10 @@ import {TreeList, type TreeListItemData} from '../TreeList';
|
|
|
22
22
|
import {useTranslator} from '../i18n';
|
|
23
23
|
import {isImeKeyEvent} from '../utils/ime';
|
|
24
24
|
import {spacingVars, typeScaleVars} from '../theme/tokens.stylex';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
PowerSearchValueEditor,
|
|
27
|
+
type PowerSearchValueEditorProps,
|
|
28
|
+
} from './PowerSearchValueEditor';
|
|
26
29
|
import {resolveOperatorLabel} from './resolveOperatorLabel';
|
|
27
30
|
import type {InternalConfig} from './useInternalConfig';
|
|
28
31
|
import type {
|
|
@@ -356,6 +359,7 @@ function NestedSubFilterRow({
|
|
|
356
359
|
{operatorValue && !isEmptyType && !isNestedType && (
|
|
357
360
|
<div {...stylex.props(styles.nestedRowValueEditor)}>
|
|
358
361
|
<PowerSearchValueEditor
|
|
362
|
+
key={valueEditorKey(subFilter)}
|
|
359
363
|
operatorValue={operatorValue}
|
|
360
364
|
filterValue={subFilter.value}
|
|
361
365
|
onChange={handleValueChange}
|
|
@@ -606,6 +610,42 @@ function NestedEditor({
|
|
|
606
610
|
return <TreeList items={items} density="balanced" />;
|
|
607
611
|
}
|
|
608
612
|
|
|
613
|
+
// =============================================================================
|
|
614
|
+
// Value editor cell
|
|
615
|
+
// =============================================================================
|
|
616
|
+
|
|
617
|
+
// A field switch must replace the editor, not update it in place: a reused
|
|
618
|
+
// editor keeps its open menu and old results.
|
|
619
|
+
function valueEditorKey(filter: {field: string; operator?: string}): string {
|
|
620
|
+
return `${filter.field}\u0000${filter.operator ?? ''}`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Keyed on field + operator, so this also mounts on a field switch, not just
|
|
624
|
+
// when the popover opens; the focus handoff below runs for both.
|
|
625
|
+
function ValueEditorCell(props: PowerSearchValueEditorProps) {
|
|
626
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
627
|
+
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
const frame = requestAnimationFrame(() => {
|
|
630
|
+
const container = ref.current;
|
|
631
|
+
if (!container) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const focusable = container.querySelector<HTMLElement>(
|
|
635
|
+
'input:not([disabled]), button:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
636
|
+
);
|
|
637
|
+
focusable?.focus();
|
|
638
|
+
});
|
|
639
|
+
return () => cancelAnimationFrame(frame);
|
|
640
|
+
}, []);
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<div ref={ref} {...stylex.props(styles.valueEditor)}>
|
|
644
|
+
<PowerSearchValueEditor {...props} />
|
|
645
|
+
</div>
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
609
649
|
// =============================================================================
|
|
610
650
|
// Main popover
|
|
611
651
|
// =============================================================================
|
|
@@ -625,22 +665,6 @@ export function PowerSearchEditPopover({
|
|
|
625
665
|
saveButtonLabelFromProps ?? t('@astryx.powersearch.editor.apply');
|
|
626
666
|
const [partialFilter, setPartialFilter] =
|
|
627
667
|
useState<PartialFilter>(initialFilter);
|
|
628
|
-
const valueEditorRef = useRef<HTMLDivElement>(null);
|
|
629
|
-
|
|
630
|
-
// Focus the first focusable element inside the value editor after mount
|
|
631
|
-
useEffect(() => {
|
|
632
|
-
const frame = requestAnimationFrame(() => {
|
|
633
|
-
const container = valueEditorRef.current;
|
|
634
|
-
if (!container) {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
const focusable = container.querySelector<HTMLElement>(
|
|
638
|
-
'input:not([disabled]), button:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
639
|
-
);
|
|
640
|
-
focusable?.focus();
|
|
641
|
-
});
|
|
642
|
-
return () => cancelAnimationFrame(frame);
|
|
643
|
-
}, []);
|
|
644
668
|
|
|
645
669
|
const currentOperator = partialFilter.operator
|
|
646
670
|
? config.getOperator(partialFilter.field, partialFilter.operator)
|
|
@@ -854,17 +878,16 @@ export function PowerSearchEditPopover({
|
|
|
854
878
|
</div>
|
|
855
879
|
)}
|
|
856
880
|
{operatorValue && !isEmptyType && (
|
|
857
|
-
<
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
</div>
|
|
881
|
+
<ValueEditorCell
|
|
882
|
+
key={valueEditorKey(partialFilter)}
|
|
883
|
+
operatorValue={operatorValue}
|
|
884
|
+
filterValue={partialFilter.value}
|
|
885
|
+
onChange={handleValueChange}
|
|
886
|
+
onEnter={handleSave}
|
|
887
|
+
config={config}
|
|
888
|
+
maxMenuItems={maxMenuItems}
|
|
889
|
+
isDisabled={isReadOnly}
|
|
890
|
+
/>
|
|
868
891
|
)}
|
|
869
892
|
</HStack>
|
|
870
893
|
</div>
|
|
@@ -132,6 +132,17 @@ export const docs = {
|
|
|
132
132
|
{name: 'RadioListItem'},
|
|
133
133
|
],
|
|
134
134
|
usage: {
|
|
135
|
+
accessibility: [
|
|
136
|
+
{
|
|
137
|
+
name: 'Radio circle',
|
|
138
|
+
category: 'Color contrast',
|
|
139
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
140
|
+
requirement: '3:1',
|
|
141
|
+
states: ['Rest', 'Hover', 'Pointer down', 'Selected'],
|
|
142
|
+
description:
|
|
143
|
+
'The circle edge (unselected) and fill (selected) must have at least 3:1 contrast with the surface behind them. For Hover and Pointer down, measure the final colors after the tint and the pressed overlay are applied.',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
135
146
|
description:
|
|
136
147
|
'A group of options where only one can be selected at a time. All options are visible at once, making it easy to compare choices. Use it when users need to pick one option from a small set.',
|
|
137
148
|
bestPractices: [
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
16
16
|
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
17
17
|
import userEvent from '@testing-library/user-event';
|
|
18
|
+
import {hasPressedArm} from '../__tests__/pressState';
|
|
18
19
|
import {RadioList} from './RadioList';
|
|
19
20
|
import {RadioListItem} from './RadioListItem';
|
|
20
21
|
import {getForcedColorsRules} from '../__tests__/forcedColors';
|
|
@@ -856,3 +857,34 @@ describe('forced colors (WCAG 1.4.11)', () => {
|
|
|
856
857
|
expect(getForcedColorsRules()).toContain('background-color: canvastext;');
|
|
857
858
|
});
|
|
858
859
|
});
|
|
860
|
+
|
|
861
|
+
describe('pressed state', () => {
|
|
862
|
+
it('paints the pressed overlay over the radio indicator while the row is pressed', () => {
|
|
863
|
+
const {container} = render(
|
|
864
|
+
<RadioList label="Plan" value="a" onChange={() => {}}>
|
|
865
|
+
<RadioListItem label="Option A" value="a" />
|
|
866
|
+
<RadioListItem label="Option B" value="b" />
|
|
867
|
+
</RadioList>,
|
|
868
|
+
);
|
|
869
|
+
const circle = container.querySelector('.astryx-radio-indicator');
|
|
870
|
+
const wrapper = circle?.parentElement?.parentElement;
|
|
871
|
+
if (wrapper == null) {
|
|
872
|
+
throw new Error('the radio has no indicator wrapper to press');
|
|
873
|
+
}
|
|
874
|
+
expect(hasPressedArm(wrapper)).toBe(true);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('does not expose a pressed arm on a disabled radio', () => {
|
|
878
|
+
const {container} = render(
|
|
879
|
+
<RadioList label="Plan" value="a" onChange={() => {}}>
|
|
880
|
+
<RadioListItem label="Unavailable" value="b" isDisabled />
|
|
881
|
+
</RadioList>,
|
|
882
|
+
);
|
|
883
|
+
const circle = container.querySelector('.astryx-radio-indicator');
|
|
884
|
+
const wrapper = circle?.parentElement?.parentElement;
|
|
885
|
+
if (wrapper == null) {
|
|
886
|
+
throw new Error('the radio has no indicator wrapper');
|
|
887
|
+
}
|
|
888
|
+
expect(hasPressedArm(wrapper)).toBe(false);
|
|
889
|
+
});
|
|
890
|
+
});
|
|
@@ -23,6 +23,7 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
23
23
|
import type {StyleXStyles} from '@stylexjs/stylex';
|
|
24
24
|
import type {BaseProps} from '../BaseProps';
|
|
25
25
|
import {RadioListContext} from './RadioList';
|
|
26
|
+
import {colorVars, radiusVars} from '../theme/tokens.stylex';
|
|
26
27
|
import {mergeProps, isRenderable, rtlStyles} from '../utils';
|
|
27
28
|
import {indicatorScope} from '../Indicator/indicator.markers.stylex';
|
|
28
29
|
import {useIndicatorFocusRing} from '../hooks/useIndicatorFocusRing';
|
|
@@ -39,6 +40,25 @@ const styles = stylex.create({
|
|
|
39
40
|
flexShrink: 0,
|
|
40
41
|
isolation: 'isolate',
|
|
41
42
|
},
|
|
43
|
+
// The row remains the large press target, but nested links and buttons keep
|
|
44
|
+
// their independent action and do not make the radio appear pressed. This
|
|
45
|
+
// owner-drawn layer also survives a theme replacing the indicator component.
|
|
46
|
+
indicatorPressOverlay: {
|
|
47
|
+
'::after': {
|
|
48
|
+
content: '""',
|
|
49
|
+
position: 'absolute',
|
|
50
|
+
inset: 0,
|
|
51
|
+
borderRadius: radiusVars['--radius-full'],
|
|
52
|
+
pointerEvents: 'none',
|
|
53
|
+
backgroundColor: {
|
|
54
|
+
default: 'transparent',
|
|
55
|
+
[stylex.when.ancestor(
|
|
56
|
+
':active:not(:has(a:active,button:active,[role="button"]:active,[role="link"]:active))',
|
|
57
|
+
indicatorScope,
|
|
58
|
+
)]: colorVars['--color-overlay-pressed'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
42
62
|
input: {
|
|
43
63
|
position: 'absolute',
|
|
44
64
|
top: '50%',
|
|
@@ -222,7 +242,11 @@ export function RadioListItem({
|
|
|
222
242
|
|
|
223
243
|
const radioCircle = (
|
|
224
244
|
<div
|
|
225
|
-
{...stylex.props(
|
|
245
|
+
{...stylex.props(
|
|
246
|
+
styles.radioWrapper,
|
|
247
|
+
wrapperSizeStyles[size],
|
|
248
|
+
!isDisabled && styles.indicatorPressOverlay,
|
|
249
|
+
)}
|
|
226
250
|
{...focusProps}>
|
|
227
251
|
<input
|
|
228
252
|
ref={radioRef}
|