-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathComplexSelector.doc.mjs
More file actions
311 lines (307 loc) · 11.3 KB
/
Copy pathComplexSelector.doc.mjs
File metadata and controls
311 lines (307 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// Copyright (c) Meta Platforms, Inc. and affiliates.
/**
* @file ComplexSelector.doc.mjs
* @input ComplexSelector public API and composition contract
* @output Exports full and dense component documentation
* @position Core documentation consumed by CLI and Storybook autodocs
*
* SYNC: When modified, update ComplexSelector.tsx, tests, and stories.
*/
/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
export const docs = {
name: 'ComplexSelector',
displayName: 'Complex Selector',
group: 'Selector',
category: 'Data Input',
keywords: [
'selector',
'picker',
'popover',
'dialog',
'custom',
'rich',
'matrix',
'grid',
],
theming: {
targets: [
{
className: 'astryx-complex-selector',
visualProps: ['variant', 'size', 'status'],
},
{
className: 'astryx-complex-selector-indicator-icon',
states: ['state'],
},
{className: 'astryx-complex-selector-popup'},
],
},
components: [
{
name: 'ComplexSelector',
displayName: 'Complex Selector',
description:
'An input or toolbar trigger and dialog-popover shell for custom selector content.',
props: [
{
name: 'label',
type: 'string',
description: 'Label text for accessibility and the field label.',
required: true,
},
{
name: 'value',
type: 'Value',
description: 'Current controlled value.',
required: true,
},
{
name: 'onChange',
type: '(value: Value) => void',
description: 'Called when custom content commits a new value.',
},
{
name: 'changeAction',
type: '(value: Value) => void | Promise<void>',
description:
'Async action after onChange. ComplexSelector exposes optimistic value and busy state while pending.',
},
{
name: 'children',
type: '(value: Value, onChange: (value: Value) => void, close: () => void, state: ComplexSelectorRenderState) => ReactNode',
description:
'Custom dialog content. Receives positional value, onChange, close, and state helpers.',
required: true,
},
{
name: 'triggerLabel',
type: 'ReactNode',
description: 'Label/content shown in the closed trigger.',
},
{
name: 'placeholder',
type: 'ReactNode',
description: 'Placeholder shown when triggerLabel is omitted.',
default: "'Select...'",
},
{
name: 'isDisabled',
type: 'boolean',
description: 'Disables the selector.',
},
{
name: 'isLoading',
type: 'boolean',
description: 'Shows loading state on the trigger.',
},
{
name: 'status',
type: "{type: 'warning' | 'error' | 'success', message?: string}",
description: 'Validation status.',
},
{
name: 'size',
type: "'sm' | 'md' | 'lg'",
description: 'Exact trigger height: sm 28px, md 32px, or lg 36px.',
default: "'md'",
},
{
name: 'variant',
type: "'input' | 'ghost'",
description:
'Visual trigger style. Input is the bordered form treatment; ghost matches toolbar buttons.',
default: "'input'",
},
{
name: 'startIcon',
type: 'ReactNode | IconType',
description: 'Icon displayed at the start of the trigger.',
},
{
name: 'hasChevron',
type: 'boolean',
description:
'Shows the chevron at the end of the trigger. Set false when the trigger content already reads as "this opens something", or when the composed content supplies its own end affordance. The chevron is decorative (aria-hidden) and sits outside the trigger button, so the accessible name, focus order, and keyboard behaviour are unchanged.',
default: 'true',
},
{
name: 'width',
type: 'SizeValue',
description: 'Width of the field.',
},
{
name: 'placement',
type: "'above' | 'below' | 'start' | 'end'",
description: 'Popup placement.',
default: "'below'",
},
{
name: 'alignment',
type: "'start' | 'center' | 'end'",
description: 'Popup alignment along the placement axis.',
default: "'start'",
},
{
name: 'handleRef',
type: 'React.Ref<ComplexSelectorHandle>',
description:
'Imperative handle for programmatic control. Exposes open(), close(), toggle(), and isOpen().',
},
{
name: 'contentXstyle',
type: 'StyleXStyles',
description: 'StyleX styles for the popup content container.',
},
],
},
],
usage: {
description:
'Use ComplexSelector when a selection needs richer custom content than a Selector option row. It is intentionally one component: ComplexSelector owns the field, trigger, popover, focus restore, and changeAction flow, while the content render prop owns the selector-specific accessible structure.',
bestPractices: [
{
guidance: true,
description:
'Use variant="ghost" with a startIcon when the selector is triggered from a toolbar. Use alignment="end" when a wide surface should align its end edge to the trigger.',
},
{
guidance: true,
description:
'For staged editors, keep draft state in the composed content and call the provided onChange helper only from Apply. Cancel or dismiss without committing.',
},
{
guidance: true,
description:
'Compose the dialog content from the appropriate accessible structure for the job: RadioList for a simple choice, Calendar/date inputs for date picking, TreeList or a searchable list for hierarchy, or a custom grid when two-dimensional arrow navigation is useful.',
},
{
guidance: true,
description:
'Use the provided onChange helper from children; it already calls both onChange and changeAction and updates optimistic busy state.',
},
{
guidance: true,
description:
'Call close() from custom content when a selection should dismiss the popup. Keep it open for multi-step content or freeform entry flows.',
},
{
guidance: true,
description:
'Use Astryx focus hooks for custom content: useGridFocus for two-dimensional grids, useTreeFocus through TreeList for hierarchies, and useListFocus for custom linear collections.',
},
{
guidance: true,
description:
'Evaluate custom content against WCAG 2.2: keyboard operation, focus visible/not obscured, names and roles, labels/instructions, target size, and contrast/non-text contrast are especially relevant for selector popovers.',
},
{
guidance: false,
description:
'Do not rebuild trigger ARIA, popover focus management, or changeAction handling in product code.',
},
{
guidance: false,
description:
'Do not use ComplexSelector for a plain single-column text list; use Selector instead.',
},
],
},
examples: [
{
label: 'Trigger without the chevron',
code: `
// ComplexSelector has no built-in clear button, so the end slot holds only
// the chevron. Drop it when the composed content puts its own affordance
// there, or when the trigger already reads as openable on its own.
<ComplexSelector
label="Date range"
value={range}
onChange={setRange}
triggerLabel={formatRange(range)}
variant="ghost"
startIcon="calendar"
hasChevron={false}>
{(value, onChange, close) => (
<RangeCalendar value={value} onChange={onChange} onDone={close} />
)}
</ComplexSelector>
`,
},
],
};
export const docsDense = {
name: 'ComplexSelector',
displayName: 'Complex Selector',
group: 'Selector',
category: 'Data Input',
description:
'Input/ghost trigger + dialog-popover shell for rich custom selectors. Content gets value/onChange/close/state; content owns semantics. Use focus hooks and evaluate custom content against WCAG 2.2.',
usage: {
description:
'Use when a selection needs richer custom content than a Selector row. One component: it owns field, trigger, popover, focus restore, and changeAction; the render prop owns the selector-specific accessible structure.',
bestPractices: [
{
guidance: true,
description:
'Use variant="ghost" with a startIcon for a toolbar trigger. Use alignment="end" when a wide surface should align its end edge to the trigger.',
},
{
guidance: true,
description:
'For staged editors, keep draft state in the composed content and call the provided onChange helper only from Apply. Cancel or dismiss without committing.',
},
{
guidance: true,
description:
'Compose content from the right accessible structure: RadioList for a simple choice, Calendar/date inputs for dates, TreeList or a searchable list for hierarchy, a custom grid for 2D arrow navigation.',
},
{
guidance: true,
description:
'Use the onChange helper from children; it calls both onChange and changeAction and updates optimistic busy state.',
},
{
guidance: true,
description:
'Call close() from custom content when a selection should dismiss the popup. Keep it open for multi-step or freeform flows.',
},
{
guidance: true,
description:
'Use Astryx focus hooks: useGridFocus for 2D grids, useTreeFocus via TreeList for hierarchies, useListFocus for custom linear collections.',
},
{
guidance: true,
description:
'Evaluate custom content against WCAG 2.2: keyboard operation, focus visible/not obscured, names and roles, labels/instructions, target size, and contrast.',
},
{
guidance: false,
description:
'Do not rebuild trigger ARIA, popover focus management, or changeAction handling in product code.',
},
{
guidance: false,
description:
'Do not use ComplexSelector for a plain single-column text list; use Selector instead.',
},
],
},
propDescriptions: {
label: 'Accessible field label.',
value: 'Controlled value.',
onChange: 'Commit value.',
changeAction: 'Async action after onChange; drives optimistic value/busy.',
children: 'Render custom dialog content from (value,onChange,close,state).',
triggerLabel: 'Closed trigger label/content.',
variant: 'input for forms; ghost for toolbar triggers.',
startIcon: 'Leading trigger icon.',
hasChevron:
'false => drop the trigger chevron when the trigger supplies its own end affordance. Chevron is aria-hidden and outside the button, so name/focus/keyboard are unchanged. Defaults to true.',
placement: 'Popup placement.',
alignment: 'Popup alignment.',
handleRef: 'Imperative open/close/toggle handle.',
accessibility:
'Custom content must provide its own accessible structure. Use focus hooks and evaluate against WCAG 2.2.',
},
};