@astryxdesign/core 0.7.3-canary.f94e962 → 0.7.3

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +8 -4
  4. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  5. package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
  6. package/dist/Button/Button.d.ts.map +1 -1
  7. package/dist/Button/Button.js +15 -2
  8. package/dist/Calendar/Calendar.js +3 -2
  9. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  10. package/dist/Chat/ChatComposer.js +25 -2
  11. package/dist/Chat/ChatSystemMessage.d.ts +4 -0
  12. package/dist/Chat/ChatSystemMessage.d.ts.map +1 -1
  13. package/dist/Chat/ChatSystemMessage.js +4 -2
  14. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  15. package/dist/CodeBlock/CodeBlock.js +11 -1
  16. package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
  17. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  18. package/dist/DateRangeInput/DateRangeInput.js +27 -18
  19. package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
  20. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  21. package/dist/DateTimeInput/DateTimeInput.js +25 -27
  22. package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
  23. package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
  24. package/dist/DateTimeInput/NativeDateSegment.js +5 -5
  25. package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
  26. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
  27. package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
  28. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  29. package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
  30. package/dist/Dialog/Dialog.d.ts.map +1 -1
  31. package/dist/Dialog/Dialog.js +9 -4
  32. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +9 -5
  33. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  34. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +2 -0
  35. package/dist/Markdown/Markdown.d.ts.map +1 -1
  36. package/dist/Markdown/Markdown.js +48 -15
  37. package/dist/MultiSelector/MultiSelector.d.ts +4 -4
  38. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  39. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
  40. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
  41. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
  42. package/dist/MultiSelector/MultiSelector.js +25 -18
  43. package/dist/Pagination/Pagination.d.ts.map +1 -1
  44. package/dist/Pagination/Pagination.js +5 -2
  45. package/dist/Switch/Switch.d.ts.map +1 -1
  46. package/dist/Switch/Switch.js +13 -3
  47. package/dist/TimeInput/TimeInput.d.ts +5 -5
  48. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  49. package/dist/TimeInput/TimeInput.js +37 -20
  50. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  51. package/dist/TimeInput/TouchTimeField.js +17 -6
  52. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  53. package/dist/Timestamp/Timestamp.js +41 -8
  54. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  55. package/dist/TreeList/TreeListItem.js +8 -3
  56. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  57. package/dist/hooks/useGridFocus.js +10 -1
  58. package/package.json +4 -4
  59. package/scripts/agent-doc-state.mjs +24 -2
  60. package/src/BottomSheet/BottomSheet.spec.md +26 -53
  61. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  62. package/src/BottomSheet/BottomSheet.tsx +8 -4
  63. package/src/BottomSheet/BottomSheetSwitcher.spec.md +45 -48
  64. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
  65. package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
  66. package/src/Button/Button.test.tsx +13 -2
  67. package/src/Button/Button.tsx +23 -2
  68. package/src/Button/__tests__/Button.a11y.states.ts +4 -3
  69. package/src/Calendar/Calendar.test.tsx +68 -24
  70. package/src/Calendar/Calendar.tsx +2 -1
  71. package/src/Carousel/Carousel.spec.md +146 -0
  72. package/src/Chat/ChatComposer.spec.md +26 -26
  73. package/src/Chat/ChatComposer.test.tsx +58 -2
  74. package/src/Chat/ChatComposer.tsx +25 -1
  75. package/src/Chat/ChatSystemMessage.doc.mjs +1 -1
  76. package/src/Chat/ChatSystemMessage.test.tsx +25 -3
  77. package/src/Chat/ChatSystemMessage.tsx +6 -4
  78. package/src/Chat/__tests__/ChatSystemMessage.a11y.chromium.spec.ts +8 -9
  79. package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +7 -6
  80. package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
  81. package/src/CodeBlock/CodeBlock.tsx +11 -1
  82. package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
  83. package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
  84. package/src/DateRangeInput/DateRangeInput.tsx +40 -20
  85. package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
  86. package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
  87. package/src/DateTimeInput/DateTimeInput.tsx +42 -26
  88. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
  89. package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
  90. package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
  91. package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
  92. package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
  93. package/src/Dialog/Dialog.test.tsx +39 -0
  94. package/src/Dialog/Dialog.tsx +9 -4
  95. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
  96. package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
  97. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +62 -7
  98. package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +190 -0
  99. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +10 -4
  100. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +93 -0
  101. package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
  102. package/src/DropdownMenu/__tests__/DropdownMenuRadioGroupAudit.a11y.chromium.spec.ts +327 -0
  103. package/src/FieldStatus/FieldStatus.spec.md +4 -4
  104. package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +0 -18
  105. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +3 -18
  106. package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +1 -2
  107. package/src/Markdown/Markdown.helpers.perf.test.ts +25 -19
  108. package/src/Markdown/Markdown.rerender.test.tsx +143 -0
  109. package/src/Markdown/Markdown.tsx +89 -35
  110. package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
  111. package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
  112. package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
  113. package/src/MultiSelector/MultiSelector.test.tsx +49 -7
  114. package/src/MultiSelector/MultiSelector.tsx +53 -29
  115. package/src/Pagination/Pagination.test.tsx +5 -1
  116. package/src/Pagination/Pagination.tsx +3 -1
  117. package/src/Switch/Switch.test.tsx +56 -0
  118. package/src/Switch/Switch.tsx +13 -2
  119. package/src/Table/plugins/columnResize/useTableColumnResize.spec.md +11 -11
  120. package/src/Table/plugins/rowStatus/useTableRowStatus.spec.md +4 -3
  121. package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
  122. package/src/TimeInput/Presentation.test.tsx +23 -0
  123. package/src/TimeInput/TimeInput.doc.mjs +8 -7
  124. package/src/TimeInput/TimeInput.test.tsx +119 -7
  125. package/src/TimeInput/TimeInput.tsx +51 -23
  126. package/src/TimeInput/TouchTimeField.tsx +27 -5
  127. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  128. package/src/Timestamp/Timestamp.test.tsx +168 -0
  129. package/src/Timestamp/Timestamp.tsx +62 -11
  130. package/src/TreeList/TreeList.test.tsx +28 -0
  131. package/src/TreeList/TreeListItem.tsx +6 -2
  132. package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
  133. package/src/hooks/useGridFocus.ts +16 -1
@@ -80,7 +80,8 @@ export type NativeTimeSegmentProps = {
80
80
  max?: ISOTimeString;
81
81
  hourFormat: '12h' | '24h';
82
82
  isEffectivelyDisabled: boolean;
83
- hasDisabledMessage: boolean;
83
+ /** While disabled, stay focusable: aria-disabled and readOnly, not `disabled`. */
84
+ isFocusableDisabled: boolean;
84
85
  isEffectivelyRequired: boolean;
85
86
  isBusy: boolean;
86
87
  statusType?: InputStatusType;
@@ -112,7 +113,7 @@ export function NativeTimeSegment({
112
113
  max,
113
114
  hourFormat,
114
115
  isEffectivelyDisabled,
115
- hasDisabledMessage,
116
+ isFocusableDisabled,
116
117
  isEffectivelyRequired,
117
118
  isBusy,
118
119
  statusType,
@@ -285,7 +286,7 @@ export function NativeTimeSegment({
285
286
  <button
286
287
  type="button"
287
288
  onClick={openPicker}
288
- disabled={isEffectivelyDisabled}
289
+ aria-disabled={isEffectivelyDisabled || undefined}
289
290
  aria-label={openPickerLabel}
290
291
  tabIndex={-1}
291
292
  {...stylex.props(
@@ -309,9 +310,11 @@ export function NativeTimeSegment({
309
310
  min={min}
310
311
  max={max}
311
312
  step={60}
312
- disabled={isEffectivelyDisabled && !hasDisabledMessage}
313
- aria-disabled={hasDisabledMessage ? 'true' : undefined}
314
- readOnly={hasDisabledMessage || undefined}
313
+ disabled={isEffectivelyDisabled && !isFocusableDisabled}
314
+ aria-disabled={
315
+ isEffectivelyDisabled && isFocusableDisabled ? 'true' : undefined
316
+ }
317
+ readOnly={(isEffectivelyDisabled && isFocusableDisabled) || undefined}
315
318
  // The segment's own label applies only when nothing else names it;
316
319
  // otherwise a caller aria-label from the spread above stays.
317
320
  aria-label={
@@ -159,6 +159,17 @@ const sizeStyles = stylex.create({
159
159
  const HORIZONTAL_SEGMENT_BASIS = 196;
160
160
 
161
161
  const styles = stylex.create({
162
+ // Same as DateTimeInput's: the focus ring for a focused disabled field.
163
+ disabledFocusRing: {
164
+ borderColor: {
165
+ default: colorVars['--color-border-emphasized'],
166
+ ':focus-within': colorVars['--color-accent'],
167
+ },
168
+ boxShadow: {
169
+ default: 'none',
170
+ ':focus-within': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
171
+ },
172
+ },
162
173
  iconButton: {
163
174
  display: 'flex',
164
175
  alignItems: 'center',
@@ -1330,6 +1341,7 @@ export function TouchDateTimeField({
1330
1341
  sizeStyles[size],
1331
1342
  styles.touchDateWrapper,
1332
1343
  isEffectivelyDisabled && inputWrapperStyles.disabled,
1344
+ isEffectivelyDisabled && styles.disabledFocusRing,
1333
1345
  status && inputStatusBorderStyles[status.type],
1334
1346
  status &&
1335
1347
  !isEffectivelyDisabled &&
@@ -1340,7 +1352,7 @@ export function TouchDateTimeField({
1340
1352
  <button
1341
1353
  type="button"
1342
1354
  onClick={() => openSheet('date')}
1343
- disabled={isEffectivelyDisabled}
1355
+ aria-disabled={isEffectivelyDisabled || undefined}
1344
1356
  aria-label={t('@astryx.dateInput.openCalendar')}
1345
1357
  tabIndex={-1}
1346
1358
  {...stylex.props(
@@ -1367,7 +1379,11 @@ export function TouchDateTimeField({
1367
1379
  readOnly
1368
1380
  inputMode="none"
1369
1381
  onChange={() => {}}
1370
- onClick={chainEventHandlers(() => openSheet('date'), onClickProp)}
1382
+ onClick={
1383
+ isEffectivelyDisabled
1384
+ ? undefined
1385
+ : chainEventHandlers(() => openSheet('date'), onClickProp)
1386
+ }
1371
1387
  onKeyDown={chainEventHandlers(
1372
1388
  (event: React.KeyboardEvent<HTMLInputElement>) =>
1373
1389
  handleInputKeyDown(event, 'date'),
@@ -1375,8 +1391,7 @@ export function TouchDateTimeField({
1375
1391
  )}
1376
1392
  aria-labelledby={controlLabelledBy}
1377
1393
  placeholder={placeholder}
1378
- disabled={isEffectivelyDisabled && !showsDisabledMessage}
1379
- aria-disabled={showsDisabledMessage ? 'true' : undefined}
1394
+ aria-disabled={isEffectivelyDisabled ? 'true' : undefined}
1380
1395
  aria-describedby={ariaDescribedBy}
1381
1396
  aria-required={isEffectivelyRequired ? 'true' : undefined}
1382
1397
  aria-invalid={status?.type === 'error' ? 'true' : undefined}
@@ -1417,6 +1432,7 @@ export function TouchDateTimeField({
1417
1432
  sizeStyles[size],
1418
1433
  styles.touchTimeWrapper,
1419
1434
  isEffectivelyDisabled && inputWrapperStyles.disabled,
1435
+ isEffectivelyDisabled && styles.disabledFocusRing,
1420
1436
  status && inputStatusBorderStyles[status.type],
1421
1437
  status &&
1422
1438
  !isEffectivelyDisabled &&
@@ -1427,7 +1443,7 @@ export function TouchDateTimeField({
1427
1443
  <button
1428
1444
  type="button"
1429
1445
  onClick={() => openSheet('time')}
1430
- disabled={isEffectivelyDisabled}
1446
+ aria-disabled={isEffectivelyDisabled || undefined}
1431
1447
  aria-label={t('@astryx.dateTimeInput.openTimePicker', {
1432
1448
  label: resolvedTimeLabel,
1433
1449
  })}
@@ -1456,8 +1472,7 @@ export function TouchDateTimeField({
1456
1472
  onClick={() => openSheet('time')}
1457
1473
  onKeyDown={event => handleInputKeyDown(event, 'time')}
1458
1474
  placeholder={timePlaceholder}
1459
- disabled={isEffectivelyDisabled && !showsDisabledMessage}
1460
- aria-disabled={showsDisabledMessage ? 'true' : undefined}
1475
+ aria-disabled={isEffectivelyDisabled ? 'true' : undefined}
1461
1476
  aria-label={resolvedTimeLabel}
1462
1477
  aria-describedby={ariaDescribedBy}
1463
1478
  aria-required={isEffectivelyRequired ? 'true' : undefined}
@@ -286,6 +286,45 @@ describe('Dialog', () => {
286
286
  });
287
287
  });
288
288
 
289
+ describe('a cancel from inside the dialog', () => {
290
+ // A file input fires a bubbling `cancel` at itself when its picker is
291
+ // dismissed (Escape, or Cancel in the chooser). It reaches the <dialog>
292
+ // like a close request but is not one.
293
+ function DialogWithFilePicker({onOpenChange}: {onOpenChange: () => void}) {
294
+ return (
295
+ <Dialog isOpen onOpenChange={onOpenChange} aria-label="Upload">
296
+ <input type="file" aria-label="Picture" />
297
+ </Dialog>
298
+ );
299
+ }
300
+
301
+ it('stays open when a file picker inside it is dismissed', () => {
302
+ const onOpenChange = vi.fn();
303
+ render(<DialogWithFilePicker onOpenChange={onOpenChange} />);
304
+
305
+ const fromPicker = new Event('cancel', {bubbles: true, cancelable: true});
306
+ screen.getByLabelText('Picture').dispatchEvent(fromPicker);
307
+
308
+ expect(onOpenChange).not.toHaveBeenCalled();
309
+ expect(fromPicker.defaultPrevented).toBe(false);
310
+ });
311
+
312
+ it('still closes on its own close request', () => {
313
+ const onOpenChange = vi.fn();
314
+ render(<DialogWithFilePicker onOpenChange={onOpenChange} />);
315
+
316
+ screen
317
+ .getByLabelText('Picture')
318
+ .dispatchEvent(new Event('cancel', {bubbles: true, cancelable: true}));
319
+ screen
320
+ .getByRole('dialog')
321
+ .dispatchEvent(new Event('cancel', {cancelable: true}));
322
+
323
+ expect(onOpenChange).toHaveBeenCalledTimes(1);
324
+ expect(onOpenChange).toHaveBeenCalledWith(false);
325
+ });
326
+ });
327
+
289
328
  describe('IME composition', () => {
290
329
  // A CJK user presses Escape to cancel a half-formed character several times
291
330
  // a sentence. jsdom models neither composition nor the close watcher, so
@@ -643,11 +643,16 @@ export function Dialog({
643
643
  // Escape presses the stack owns never arrive here — it preventDefault()s
644
644
  // those, which suppresses the close watcher.
645
645
  //
646
- // Always preventDefault so the browser cannot close a controlled <dialog>
647
- // behind React's back, then answer the request with the stack's own rules:
648
- // only the top-most layer dismisses, a `required` dialog swallows it, and
649
- // nothing dismisses while an IME composition is in progress.
646
+ // A `cancel` that bubbles up from inside is not a close request for this
647
+ // dialog: a file input fires one when its picker is dismissed. It is left
648
+ // alone. Otherwise always preventDefault so the browser cannot close a
649
+ // controlled <dialog> behind React's back, then answer the request with the
650
+ // stack's own rules: only the top-most layer dismisses, a `required` dialog
651
+ // swallows it, and nothing dismisses while an IME composition is in progress.
650
652
  const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {
653
+ if (event.target !== event.currentTarget) {
654
+ return;
655
+ }
651
656
  event.preventDefault();
652
657
  if (!shouldDismissOnCloseRequest()) {
653
658
  return;
@@ -8,7 +8,50 @@ export const docs = {
8
8
  displayName: 'Dropdown Menu Item',
9
9
  isHiddenFromOverview: true,
10
10
  description:
11
- 'Helper component for custom item rendering with consistent styling. Host attributes and DOM event handlers (aria-*, data-*, id, drag and drop) reach the row root, the element carrying role="menuitem".',
11
+ 'One action or destination row in the pointer presentation of DropdownMenu. The parent menu owns keyboard navigation; the row owns activation, disabled state, destination behavior, content layout, and its theme target. Supported host attributes, DOM handlers, className, style, xstyle, and ref reach or compose on the role="menuitem" root.',
12
+ usage: {
13
+ description:
14
+ 'Use DropdownMenuItem inside DropdownMenu compound mode for one action or destination. Keep label, description, icon, and endContent non-interactive; controls belong in separate rows or the menu header/footer.',
15
+ bestPractices: [
16
+ {
17
+ guidance: true,
18
+ description:
19
+ 'Use href for a destination so LinkProvider, modified clicks, middle clicks, and browser link affordances remain available.',
20
+ },
21
+ {
22
+ guidance: true,
23
+ description:
24
+ 'Use hasCloseOnSelect=false only when activation reports a result in place on the same row.',
25
+ },
26
+ {
27
+ guidance: false,
28
+ description:
29
+ 'Put buttons, links, or other focusable controls inside label, description, or endContent. Each menu row is one interactive target.',
30
+ },
31
+ ],
32
+ anatomy: [
33
+ {
34
+ name: 'Row',
35
+ required: true,
36
+ description:
37
+ 'The role="menuitem" root that owns activation, disabled state, destination behavior, and theme state.',
38
+ },
39
+ {
40
+ name: 'Content',
41
+ required: true,
42
+ description:
43
+ 'The non-interactive label plus optional icon, description, and end content.',
44
+ },
45
+ ],
46
+ },
47
+ theming: {
48
+ targets: [
49
+ {
50
+ className: 'astryx-dropdown-menu-item',
51
+ visualProps: ['size', 'variant'],
52
+ },
53
+ ],
54
+ },
12
55
  playground: {
13
56
  // Standalone DropdownMenuItem has no required props, so the properties-tab
14
57
  // preview renders an empty row without seeded content. Seed a label and
@@ -26,6 +69,7 @@ export const docs = {
26
69
  name: 'label',
27
70
  type: 'ReactNode',
28
71
  description: 'Primary label text.',
72
+ required: true,
29
73
  },
30
74
  {
31
75
  name: 'description',
@@ -55,11 +99,18 @@ export const docs = {
55
99
  description:
56
100
  'Link relationship. noopener noreferrer is added for target="_blank". Only used with href.',
57
101
  },
102
+ {
103
+ name: 'isDisabled',
104
+ type: 'boolean',
105
+ description:
106
+ 'Whether the row is unavailable. Disabled rows expose aria-disabled, remove link destinations, and suppress activation.',
107
+ default: 'false',
108
+ },
58
109
  {
59
110
  name: 'endContent',
60
111
  type: 'ReactNode',
61
112
  description:
62
- 'Additional content rendered after the label and description.',
113
+ 'Non-interactive content rendered after the label and description.',
63
114
  },
64
115
  {
65
116
  name: 'hasCloseOnSelect',
@@ -95,7 +146,7 @@ export const docsZh = {
95
146
  isHiddenFromOverview: true,
96
147
  displayName: 'Dropdown Menu Item',
97
148
  description:
98
- '用于自定义项渲染的辅助组件,提供一致的样式。宿主属性和 DOM 事件处理器(aria-*、data-*、id、拖放)会落在行根元素上,即带 role="menuitem" 的元素。',
149
+ 'DropdownMenu 指针呈现中的一个操作或目标行。父菜单负责键盘导航;该行负责激活、禁用状态、目标行为、内容布局和主题目标。支持的宿主属性、DOM 处理程序、className、style、xstyle 和 ref 会落在或组合到 role="menuitem" 根元素上。',
99
150
  props: [
100
151
  {
101
152
  name: 'icon',
@@ -106,6 +157,7 @@ export const docsZh = {
106
157
  name: 'label',
107
158
  type: 'ReactNode',
108
159
  description: '主标签文本。',
160
+ required: true,
109
161
  },
110
162
  {
111
163
  name: 'description',
@@ -134,10 +186,16 @@ export const docsZh = {
134
186
  type: 'string',
135
187
  description: '链接关系。target="_blank" 时自动加入 noopener noreferrer。',
136
188
  },
189
+ {
190
+ name: 'isDisabled',
191
+ type: 'boolean',
192
+ description: '该行是否不可用;不可用的行不会激活或导航。',
193
+ default: 'false',
194
+ },
137
195
  {
138
196
  name: 'endContent',
139
197
  type: 'ReactNode',
140
- description: '在标签和描述之后渲染的附加内容。',
198
+ description: '在标签和描述之后渲染的非交互式附加内容。',
141
199
  },
142
200
  {
143
201
  name: 'hasCloseOnSelect',
@@ -168,9 +226,9 @@ export const docsZh = {
168
226
 
169
227
  export const docsDense = {
170
228
  name: 'DropdownMenuItem',
171
- isHiddenFromOverview: true,
172
229
  displayName: 'Dropdown Menu Item',
173
- description: 'helper for custom item rendering w/ consistent styling',
230
+ description:
231
+ 'one pointer-menu action/destination row; parent owns keyboard, row owns activation, state, destination, content, and root inputs',
174
232
  propDescriptions: {
175
233
  icon: 'icon before label',
176
234
  label: 'primary label text',
@@ -180,7 +238,8 @@ export const docsDense = {
180
238
  href: 'row is a real anchor w/ role=menuitem; modified/middle click keeps browser meaning',
181
239
  target: "link target ('_blank' | '_self'), with href",
182
240
  rel: 'link rel; noopener noreferrer added for _blank',
183
- endContent: 'additional content after label+description',
241
+ isDisabled: 'unavailable; removes destinations and suppresses activation',
242
+ endContent: 'non-interactive content after label+description',
184
243
  hasCloseOnSelect:
185
244
  'false keeps the menu open on activation (in-place result on the item)',
186
245
  variant:
@@ -0,0 +1,209 @@
1
+ ---
2
+ schema_version: 3
3
+ template_version: 7
4
+ kind: component
5
+ id: component:DropdownMenuItem
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers: [public-api, behavior, accessibility, theming, testing]
13
+ verified_by:
14
+ [
15
+ packages/core/src/DropdownMenu/DropdownMenu.test.tsx,
16
+ packages/core/src/DropdownMenu/__tests__/Menu.a11y.test.tsx,
17
+ packages/core/src/theme/themingTargets.test.ts,
18
+ apps/storybook/stories/DropdownMenuItem.stories.tsx,
19
+ apps/storybook/rtl-audit/verified-not-applicable.json,
20
+ scripts/check-knowledge.mjs,
21
+ ]
22
+ modules: []
23
+ families: [family:navigation-destinations]
24
+ design_specs: []
25
+ architecture:
26
+ [
27
+ architecture:public-component-api,
28
+ architecture:component-theming-surface,
29
+ architecture:component-test-sufficiency,
30
+ architecture:interaction-modality,
31
+ ]
32
+ contributing: []
33
+ system_specs: [spec:AST-020, spec:AST-029]
34
+ ---
35
+
36
+ # DropdownMenuItem component contract
37
+
38
+ ## Contract at a glance
39
+
40
+ | Area | Contract |
41
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | Public contract | One compound pointer-menu row for an action or destination, with label, supporting content, disabled state, destructive paint, optional close suppression, root inputs, and a forwarded ref. |
43
+ | Behavior | The parent menu owns roving focus and keyboard activation; the row owns click order, disabled suppression, close-on-select, destination handoff, hover focus, and its visible content composition. |
44
+ | End-user impact | Actions and destinations preserve keyboard, pointer, and browser link behavior while unavailable rows remain inert and destructive actions remain identifiable. |
45
+ | Builder impact | Destinations use `href`; supporting content stays non-interactive; `hasCloseOnSelect=false` is reserved for in-place result feedback. |
46
+ | Compatibility/readiness | Patch-compatible observational contract. Runtime, exports, defaults, and theme targets remain unchanged; authority stays `draft` until exact-head owner approval. |
47
+ | Review checks | Reject nested controls, disabled activation, lost destination safety, broken modified-click behavior, dropped root inputs, a second keyboard owner, or state/target reflection moved off the row. |
48
+ | Governing rules | `family:navigation-destinations/FR1–FR5`; `architecture:public-component-api/INV5–INV9`; `architecture:component-theming-surface/INV3–INV6`; `spec:AST-020/FR1,FR2,FR6,FR15`. |
49
+
50
+ This table is a review projection; the body below is authoritative.
51
+
52
+ ## Intent
53
+
54
+ DropdownMenuItem renders one compound-mode row in the pointer presentation of
55
+ DropdownMenu. It owns the row element, content slots, action-versus-destination
56
+ branch, activation order, disabled and destructive state, root composition, and
57
+ `dropdown-menu-item` theme target. DropdownMenu owns the surrounding menu,
58
+ keyboard collection, focus movement, typeahead, presentation choice, and close
59
+ lifecycle.
60
+
61
+ ## Compatibility and migration
62
+
63
+ - Released default preserved: yes.
64
+ - Compatibility class: observational patch; no behavior or public surface changes.
65
+ - Controlled/uncontrolled behavior: not applicable.
66
+ - Migration decision: none.
67
+
68
+ Consumer migration instructions belong in consumer docs and release notes.
69
+
70
+ ## Ownership boundary
71
+
72
+ **Owns**
73
+
74
+ - One `role="menuitem"` root and its label, description, icon, end content,
75
+ disabled state, destructive variant, ref, supported host inputs, and styling inputs.
76
+ - Action callback order and `hasCloseOnSelect` behavior.
77
+ - Link-row rendering, destination handoff, modified/middle-click preservation,
78
+ target and relationship handling, and disabled-link inertness.
79
+ - Pointer hover focus and the row's press/hover/focus paint.
80
+ - The `dropdown-menu-item` target and its size/variant state reflection.
81
+
82
+ **Does not own / non-goals**
83
+
84
+ - Menu naming, open/close state, focus entry, roving focus, typeahead, and keyboard
85
+ click synthesis — owned by DropdownMenu and its shared hooks.
86
+ - Touch and bottom-sheet row rendering — the parent presentation delegates those
87
+ rows to List.
88
+ - Destination acceptance and blocking — owned by `family:navigation-destinations`.
89
+ - Interactive controls inside label, description, or end content. Controls belong
90
+ beside the menu or as separate rows.
91
+
92
+ ## Public concepts
93
+
94
+ | Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid-value behavior |
95
+ | ------------ | --------------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------- | ------- | ------------------------------- | --------- | ------------------------------------------------------------ |
96
+ | Row act | action; destination | Runs a callback or exposes a browser/router destination from the row itself. | Every enabled row | action | component and navigation family | stable | A blocked destination is inert; a disabled link has no href. |
97
+ | Availability | enabled; disabled | Controls whether the row can receive focus or activate. | Every row | enabled | component and parent menu | stable | Disabled suppresses activation and destination. |
98
+ | Close policy | close; remain open | Whether successful activation closes the parent menu. | Action and destination rows | close | component | stable | Omission closes. |
99
+ | Visual state | default; destructive; hover; focus; press; disabled | Paints row meaning and interaction state without changing menuitem semantics. | Pointer row | default | component | stable | Unknown variants are rejected by the public type. |
100
+ | Root surface | ref; DOM/data/ARIA; class/style/xstyle/events | Extends the row root while preserving component-owned role and keyboard state. | Every pointer row | omitted | public component API | stable | Accepted inputs compose; owned role/tab stop remain fixed. |
101
+
102
+ ## Behavioral and layout contract
103
+
104
+ Draft requirements identify their basis so observed code is not mistaken for an
105
+ intentional decision.
106
+
107
+ | ID | Candidate invariant | Basis | Draft review state |
108
+ | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ |
109
+ | FR1 | The pointer row MUST expose one `role="menuitem"` root. Label, description, icon, and end content remain presentational content of that row rather than nested controls. | WAI-ARIA menu pattern; released source/docs | verify |
110
+ | FR2 | Enabled activation MUST run the row callback before optional close. Disabled rows MUST expose `aria-disabled=true`, remove link destinations, and run neither callback nor close. | Released behavior; focused tests | verify |
111
+ | FR3 | A destination row MUST render as the link root through LinkProvider. Plain activation runs the callback then closes and navigates; modified and middle clicks preserve browser behavior without running the callback; blocked schemes are inert. | `family:navigation-destinations/FR1–FR5`; released tests | verify |
112
+ | FR4 | The parent menu MUST remain the only keyboard owner. It synthesizes click activation with modifiers, moves focus, and runs typeahead; the row MAY focus itself on pointer hover but MUST NOT create a second roving-focus model. | `architecture:interaction-modality`; current parent tests | verify |
113
+ | FR5 | The visible root MUST carry `dropdown-menu-item`; size and destructive state MUST reflect through `themeProps`; semantic tokens own layout and paint before caller styling inputs compose. | `architecture:component-theming-surface/INV3–INV6`; source/tests | verify |
114
+ | FR6 | Ref, supported host attributes and handlers, `className`, `style`, and `xstyle` MUST reach or compose on the row root without replacing owned role, tab stop, disabled behavior, destination handling, or activation order. | `architecture:public-component-api/INV5–INV9`; focused tests | verify |
115
+ | FR7 | Disabled menu rows remain inert. Whether the menu keyboard model should visit them is unresolved at the shared owner and MUST NOT be changed by a component-local audit. | WAI-ARIA menu pattern; parent `component:DropdownMenu/OQ1` | human API decision |
116
+ | FR8 | Keyboard focus MUST remain perceivable, including in forced-colors mode. The current background-only treatment is an objective gap; a remediation must preserve modality and row-content visibility without inventing new ordinary-theme appearance. | WCAG 2.2 SC 1.4.11 and 2.4.7; current rendered behavior | objective gap; owner must confirm the shared visual boundary |
117
+
118
+ ### Allowed variation
119
+
120
+ - **AV1 — Content.** Non-interactive icons, text, descriptions, shortcuts, badges,
121
+ and status text may vary without changing the row's menuitem semantics.
122
+ - **AV2 — Theme paint.** A theme may restyle the target's admitted properties while
123
+ preserving visible state distinctions and forced-colors focus.
124
+ - **AV3 — Parent size.** The parent menu supplies `sm`, `md`, or `lg`; the row reflects
125
+ that state and may vary its tokenized insets.
126
+
127
+ ### Representative states
128
+
129
+ | State | Required invariant | Allowed variation |
130
+ | ---------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------- |
131
+ | Enabled action | Callback runs, then close follows the close policy. | Content, size, and visual variant. |
132
+ | In-place result action | Callback runs and the menu remains open. | Label/end content may update. |
133
+ | Disabled action | `aria-disabled=true`; callback, hover focus, and close are suppressed. | Content remains visible. |
134
+ | Destination | One anchor root preserves safe destination and browser activation behavior. | Target, rel, router provider, and content. |
135
+ | Destructive | Error-role paint applies to label, icon, and description without changing behavior. | Other row content remains consumer-authored. |
136
+ | Keyboard focus | Parent focus lands on the row and remains visible. | Exact theme-owned paint may vary. |
137
+ | RTL / narrow viewport | Logical insets and start alignment follow direction without horizontal overflow. | Menu placement remains parent-owned. |
138
+
139
+ ### Transformation and precedence order
140
+
141
+ - **ORD1 — Activation.** Disabled guard → browser-navigation classification → row
142
+ callback where applicable → close policy → browser/router navigation.
143
+ - **ORD2 — Root composition.** Parent size → base row styles → variant/disabled state
144
+ → caller `xstyle`; target metadata and caller class/style compose on the same root.
145
+
146
+ ### Performance and resources
147
+
148
+ - **PR1 — Shared press lifecycle.** DropdownMenuItem uses the shared counted press
149
+ controller through `usePressFeedback`; the first mounted pressable installs the
150
+ window listeners and the last removes them. The item adds no per-row listener,
151
+ observer, timer, request, or local resource owner.
152
+
153
+ ## Accessibility contract
154
+
155
+ - **AR1 — Menu item semantics.** The root is one named `menuitem`; supporting content
156
+ does not introduce a nested interactive descendant.
157
+ - **AR2 — Disabled state.** Disabled rows expose state and cannot activate or navigate.
158
+ - **AR3 — Keyboard ownership.** DropdownMenu owns focus collection and activation;
159
+ the item remains a single row in that model.
160
+ - **AR4 — Focus perception.** Focus remains visually perceivable in supported themes
161
+ and platform accessibility modes; the open forced-colors gap remains explicit.
162
+
163
+ ## Design relationships
164
+
165
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
166
+ | ---------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------- | ------------------ |
167
+ | Row | One full-width action/destination target with tokenized insets and start alignment. | Current source and parent menu composition | prominent | FR1–FR6 |
168
+ | Supporting text | Description and end content remain subordinate to the row label. | Delegated Item layout and current consumer docs | supporting | FR1 |
169
+ | Interaction | Hover, focus, press, disabled, and destructive states remain distinguishable. | Current source; forced-colors focus is an unresolved objective gap | prominent | FR5, FR8, AR4 |
170
+
171
+ No new component-local visual decision is settled by this draft.
172
+
173
+ ## Family and system relationships
174
+
175
+ - `family:navigation-destinations` owns destination acceptance and blocked-scheme
176
+ behavior; this component supplies the destination-bearing row.
177
+ - `architecture:public-component-api` owns root input composition, refs, and released
178
+ compatibility.
179
+ - `architecture:component-theming-surface` owns target placement and state reflection.
180
+ - `architecture:component-test-sufficiency` owns proportionate observable evidence.
181
+ - `architecture:interaction-modality` and DropdownMenu own parent keyboard behavior.
182
+ - `spec:AST-020` supplies WCAG/APG adoption and evidence-layer boundaries.
183
+ - `spec:AST-029` keeps this observational draft separate from the audit receipt.
184
+
185
+ ## Verification map
186
+
187
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
188
+ | ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
189
+ | FR1–FR4, AR1–AR3 | `DropdownMenu.test.tsx`; reusable Menu accessibility tests; scoped axe | action, destination, disabled, keyboard, modified clicks | A nested control, activation while disabled, lost browser affordance, or second keyboard owner fails observable tests | `audit:DropdownMenuItem/§1,§4,§6` |
190
+ | FR5, FR8, AR4 | theming-target tests; exact-head Neutral/Butter/RTL/narrow/focus/hover frames | default, destructive, disabled, focus, hover | Target/state reflection disappears, paint is absent, or focus becomes unobservable | `audit:DropdownMenuItem/§2,§5` |
191
+ | FR6 | root/ref/handler tests; lint and typecheck | ref, host attributes, class/style/xstyle, pointer handlers | An accepted input is dropped or overrides component-owned semantics | `audit:DropdownMenuItem/§3` |
192
+ | RTL / responsive | source-hashed verified-N/A receipt plus exact-head browser frames | LTR, RTL, 320px | Physical direction leaks into the row or narrow layout overflows | `audit:DropdownMenuItem/§9,§10` |
193
+
194
+ ## Decision log
195
+
196
+ None. This draft records current facts and introduces no product decision.
197
+
198
+ ## Open questions
199
+
200
+ - **OQ1 — Which shared keyboard owner should keep disabled menu items discoverable
201
+ while preserving their inert behavior?** (`human-api`)
202
+ - **OQ2 — Should forced-colors focus use a component-specific system outline, or a
203
+ shared row-focus treatment that also governs ordinary themes?** (`human-design`)
204
+
205
+ ## Content boundary
206
+
207
+ This file does not duplicate consumer prop tables/examples, current audit scores,
208
+ screenshot matrices, implementation steps, or family/system rules. The audit pull
209
+ request owns the closed run evidence required by `spec:AST-029/FR5`.