@astryxdesign/core 0.6.2-canary.bfee8a4 → 0.6.2-canary.c8d5849

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 (261) hide show
  1. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  2. package/dist/Chat/ChatComposerInput.js +2 -2
  3. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  4. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  5. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  6. package/dist/Collapsible/Collapsible.js +6 -1
  7. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  8. package/dist/CommandPalette/CommandPaletteInput.js +1 -1
  9. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  10. package/dist/ComplexSelector/ComplexSelector.js +1 -1
  11. package/dist/DateInput/DateInput.d.ts.map +1 -1
  12. package/dist/DateInput/DateInput.js +4 -4
  13. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  14. package/dist/DateInput/NativeDateField.js +16 -16
  15. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  16. package/dist/DateInput/TouchDateField.js +2 -2
  17. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  18. package/dist/DateRangeInput/DateRangeInput.js +4 -4
  19. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  20. package/dist/DateTimeInput/DateTimeInput.js +8 -8
  21. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  22. package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
  23. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
  24. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
  25. package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
  26. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  27. package/dist/Field/PanelSearchInput.js +1 -1
  28. package/dist/FileInput/FileInput.d.ts.map +1 -1
  29. package/dist/FileInput/FileInput.js +7 -7
  30. package/dist/Kbd/Kbd.d.ts +5 -3
  31. package/dist/Kbd/Kbd.d.ts.map +1 -1
  32. package/dist/Kbd/Kbd.js +36 -42
  33. package/dist/Link/Link.d.ts.map +1 -1
  34. package/dist/Link/Link.js +6 -2
  35. package/dist/Markdown/Markdown.d.ts +8 -2
  36. package/dist/Markdown/Markdown.d.ts.map +1 -1
  37. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  38. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  39. package/dist/Markdown/Markdown.fr23.bench.js +150 -0
  40. package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
  41. package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
  42. package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
  43. package/dist/Markdown/Markdown.js +262 -109
  44. package/dist/Markdown/ast.d.ts +162 -0
  45. package/dist/Markdown/ast.d.ts.map +1 -0
  46. package/dist/Markdown/ast.js +74 -0
  47. package/dist/Markdown/index.d.ts +5 -3
  48. package/dist/Markdown/index.d.ts.map +1 -1
  49. package/dist/Markdown/index.js +3 -2
  50. package/dist/Markdown/parser/index.d.ts +10 -0
  51. package/dist/Markdown/parser/index.d.ts.map +1 -0
  52. package/dist/Markdown/parser/index.js +10 -0
  53. package/dist/Markdown/parser.d.ts +126 -58
  54. package/dist/Markdown/parser.d.ts.map +1 -1
  55. package/dist/Markdown/parser.js +716 -167
  56. package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
  57. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
  58. package/dist/Markdown/plugins/frontmatter.js +132 -0
  59. package/dist/Markdown/plugins/index.d.ts +16 -0
  60. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  61. package/dist/Markdown/plugins/index.js +13 -0
  62. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  63. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  64. package/dist/Markdown/plugins/protocol.js +957 -0
  65. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  66. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  67. package/dist/Markdown/plugins/semanticFence.js +213 -0
  68. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  69. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  70. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  71. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  72. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  73. package/dist/Markdown/plugins/textTransform.js +374 -0
  74. package/dist/Markdown/remark.d.ts +196 -0
  75. package/dist/Markdown/remark.d.ts.map +1 -0
  76. package/dist/Markdown/remark.js +1062 -0
  77. package/dist/Markdown/url.d.ts +4 -0
  78. package/dist/Markdown/url.d.ts.map +1 -0
  79. package/dist/Markdown/url.js +25 -0
  80. package/dist/Markdown/utils.d.ts +4 -2
  81. package/dist/Markdown/utils.d.ts.map +1 -1
  82. package/dist/Markdown/utils.js +2 -1
  83. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  84. package/dist/NumberInput/NumberInput.js +4 -4
  85. package/dist/Outline/index.d.ts +1 -0
  86. package/dist/Outline/index.d.ts.map +1 -1
  87. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  88. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  89. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  90. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  91. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  92. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  93. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  94. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  95. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  96. package/dist/ProgressBar/ProgressBar.js +36 -31
  97. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  98. package/dist/RadioList/RadioListItem.js +13 -1
  99. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  100. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  101. package/dist/Selector/Selector.d.ts.map +1 -1
  102. package/dist/Selector/Selector.js +1 -1
  103. package/dist/SideNav/SideNav.d.ts +2 -1
  104. package/dist/SideNav/SideNav.d.ts.map +1 -1
  105. package/dist/SideNav/SideNav.js +9 -3
  106. package/dist/Slider/Slider.d.ts.map +1 -1
  107. package/dist/Slider/Slider.js +14 -8
  108. package/dist/Spinner/Spinner.d.ts.map +1 -1
  109. package/dist/Spinner/Spinner.js +23 -7
  110. package/dist/Switch/Switch.d.ts.map +1 -1
  111. package/dist/Switch/Switch.js +11 -0
  112. package/dist/TabList/Tab.d.ts +1 -1
  113. package/dist/TabList/Tab.d.ts.map +1 -1
  114. package/dist/TabList/Tab.js +20 -7
  115. package/dist/Table/Table.d.ts +1 -0
  116. package/dist/Table/Table.d.ts.map +1 -1
  117. package/dist/Table/Table.js +29 -13
  118. package/dist/TextArea/TextArea.d.ts.map +1 -1
  119. package/dist/TextArea/TextArea.js +1 -1
  120. package/dist/TextInput/TextInput.d.ts.map +1 -1
  121. package/dist/TextInput/TextInput.js +2 -2
  122. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  123. package/dist/TimeInput/TimeInput.js +4 -4
  124. package/dist/ToggleButton/ToggleButton.d.ts +2 -1
  125. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  126. package/dist/ToggleButton/ToggleButton.js +7 -1
  127. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  128. package/dist/Typeahead/BaseTypeahead.js +16 -7
  129. package/dist/astryx.css +14 -6
  130. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  131. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  132. package/dist/utils/interactionOverlay.stylex.js +9 -0
  133. package/package.json +18 -3
  134. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  135. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  136. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  137. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  138. package/src/Chat/ChatComposerInput.doc.mjs +3 -3
  139. package/src/Chat/ChatComposerInput.tsx +10 -2
  140. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  141. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  142. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  143. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  144. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  145. package/src/Collapsible/Collapsible.test.tsx +21 -0
  146. package/src/Collapsible/Collapsible.tsx +5 -0
  147. package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
  148. package/src/ComplexSelector/ComplexSelector.tsx +6 -1
  149. package/src/DateInput/DateInput.tsx +6 -1
  150. package/src/DateInput/NativeDateField.tsx +24 -8
  151. package/src/DateInput/TouchDateField.tsx +6 -2
  152. package/src/DateRangeInput/DateRangeInput.tsx +6 -1
  153. package/src/DateTimeInput/DateTimeInput.tsx +6 -1
  154. package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
  155. package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
  156. package/src/Field/PanelSearchInput.tsx +5 -2
  157. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
  158. package/src/FileInput/FileInput.tsx +11 -2
  159. package/src/Kbd/Kbd.doc.mjs +3 -3
  160. package/src/Kbd/Kbd.test.tsx +57 -1
  161. package/src/Kbd/Kbd.tsx +57 -37
  162. package/src/Layout/LayoutFooter.doc.mjs +17 -0
  163. package/src/Link/Link.doc.mjs +11 -0
  164. package/src/Link/Link.test.tsx +24 -0
  165. package/src/Link/Link.tsx +5 -0
  166. package/src/Markdown/Markdown.doc.mjs +272 -0
  167. package/src/Markdown/Markdown.fr23.bench.ts +174 -0
  168. package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
  169. package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
  170. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  171. package/src/Markdown/Markdown.public.test.ts +126 -3
  172. package/src/Markdown/Markdown.spec.md +197 -44
  173. package/src/Markdown/Markdown.test.tsx +11 -0
  174. package/src/Markdown/Markdown.tsx +414 -135
  175. package/src/Markdown/ast.test.ts +307 -0
  176. package/src/Markdown/ast.ts +337 -0
  177. package/src/Markdown/incremental.test.ts +12 -0
  178. package/src/Markdown/index.ts +37 -1
  179. package/src/Markdown/modules/remark.spec.md +269 -0
  180. package/src/Markdown/parser/index.ts +64 -0
  181. package/src/Markdown/parser.public.test.ts +103 -0
  182. package/src/Markdown/parser.ts +1180 -235
  183. package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
  184. package/src/Markdown/plugins/frontmatter.ts +198 -0
  185. package/src/Markdown/plugins/index.ts +53 -0
  186. package/src/Markdown/plugins/protocol.test.tsx +1022 -0
  187. package/src/Markdown/plugins/protocol.ts +1618 -0
  188. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  189. package/src/Markdown/plugins/semanticFence.ts +424 -0
  190. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  191. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  192. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  193. package/src/Markdown/plugins/textTransform.ts +554 -0
  194. package/src/Markdown/remark.test.tsx +1877 -0
  195. package/src/Markdown/remark.ts +1574 -0
  196. package/src/Markdown/url.ts +30 -0
  197. package/src/Markdown/utils.ts +13 -1
  198. package/src/MultiSelector/MultiSelector.test.tsx +1 -35
  199. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
  200. package/src/NumberInput/NumberInput.tsx +6 -1
  201. package/src/Outline/Outline.doc.mjs +7 -5
  202. package/src/Outline/Outline.spec.md +1 -1
  203. package/src/Outline/index.ts +1 -0
  204. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  205. package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
  206. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  207. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  208. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  209. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  210. package/src/ProgressBar/ProgressBar.test.tsx +29 -11
  211. package/src/ProgressBar/ProgressBar.tsx +59 -72
  212. package/src/RadioList/RadioList.doc.mjs +11 -0
  213. package/src/RadioList/RadioList.test.tsx +32 -0
  214. package/src/RadioList/RadioListItem.tsx +25 -1
  215. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  216. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  217. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  218. package/src/Selector/Selector.spec.md +12 -9
  219. package/src/Selector/Selector.tsx +6 -1
  220. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
  221. package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
  222. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
  223. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
  224. package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
  225. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
  226. package/src/SideNav/SideNav.doc.mjs +1 -1
  227. package/src/SideNav/SideNav.test.tsx +10 -0
  228. package/src/SideNav/SideNav.tsx +14 -2
  229. package/src/Slider/Slider.doc.mjs +11 -0
  230. package/src/Slider/Slider.test.tsx +128 -0
  231. package/src/Slider/Slider.tsx +25 -7
  232. package/src/Spinner/Spinner.test.tsx +4 -0
  233. package/src/Spinner/Spinner.tsx +31 -18
  234. package/src/Switch/Switch.doc.mjs +11 -0
  235. package/src/Switch/Switch.test.tsx +16 -0
  236. package/src/Switch/Switch.tsx +28 -0
  237. package/src/TabList/Tab.tsx +35 -7
  238. package/src/TabList/TabList.doc.mjs +11 -0
  239. package/src/TabList/TabList.test.tsx +66 -0
  240. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  241. package/src/Table/Table.doc.mjs +1 -1
  242. package/src/Table/Table.spec.md +54 -40
  243. package/src/Table/Table.test.tsx +83 -10
  244. package/src/Table/Table.tsx +30 -22
  245. package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
  246. package/src/TextArea/TextArea.tsx +6 -1
  247. package/src/TextInput/TextInput.test.tsx +20 -1
  248. package/src/TextInput/TextInput.tsx +6 -1
  249. package/src/TimeInput/TimeInput.tsx +6 -1
  250. package/src/ToggleButton/ToggleButton.test.tsx +133 -0
  251. package/src/ToggleButton/ToggleButton.tsx +9 -2
  252. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  253. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  254. package/src/TreeList/TreeList.spec.md +123 -75
  255. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  256. package/src/Typeahead/BaseTypeahead.tsx +21 -7
  257. package/src/Typeahead/Typeahead.test.tsx +53 -0
  258. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  259. package/src/__tests__/pressState.ts +93 -0
  260. package/src/inputFontFloor.test.ts +106 -0
  261. package/src/utils/interactionOverlay.stylex.ts +20 -0
@@ -82,6 +82,17 @@ export const docs = {
82
82
  {name: 'CollapsibleGroup'},
83
83
  ],
84
84
  usage: {
85
+ accessibility: [
86
+ {
87
+ name: 'Trigger label',
88
+ category: 'Color contrast',
89
+ criterion: '1.4.3 Contrast (Minimum)',
90
+ requirement: '4.5:1',
91
+ states: ['Rest', 'Pointer down'],
92
+ description:
93
+ 'The trigger text must have at least 4.5:1 contrast with the surface behind it. For Pointer down, measure against the pressed overlay the trigger row paints while it is pressed.',
94
+ },
95
+ ],
85
96
  description: 'Collapsible hides and reveals content behind a trigger button. Use it in settings panels, FAQ pages, or detail views to keep the page scannable while letting users drill into sections they care about. Wrap multiple collapsibles in CollapsibleGroup for accordion behavior. For custom collapsible components, use the `useCollapsible` hook directly (`astryx hook useCollapsible`).',
86
97
  bestPractices: [
87
98
  { guidance: true, description: 'Use hasDividers on CollapsibleGroup for FAQ-style lists: built-in row hairlines with themed border tokens, no hand-rolled borders.' },
@@ -12,6 +12,7 @@
12
12
  import {describe, it, expect, vi} from 'vitest';
13
13
  import {render, screen, within} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
+ import {hasPressedArm} from '../__tests__/pressState';
15
16
  import {Collapsible} from './Collapsible';
16
17
  import {CollapsibleGroup} from './CollapsibleGroup';
17
18
 
@@ -615,3 +616,23 @@ describe('Collapsible', () => {
615
616
  });
616
617
  });
617
618
  });
619
+
620
+ describe('pressed state', () => {
621
+ it('paints the pressed overlay on the trigger row while it is pressed', () => {
622
+ render(
623
+ <Collapsible trigger="Details">
624
+ <p>Body</p>
625
+ </Collapsible>,
626
+ );
627
+ expect(hasPressedArm(screen.getByRole('button'))).toBe(true);
628
+ });
629
+
630
+ it('does not press a disabled trigger', () => {
631
+ render(
632
+ <Collapsible trigger="Details" isDisabled>
633
+ <p>Body</p>
634
+ </Collapsible>,
635
+ );
636
+ expect(hasPressedArm(screen.getByRole('button'))).toBe(false);
637
+ });
638
+ });
@@ -48,6 +48,7 @@ import {mergeProps} from '../utils';
48
48
  import type {BaseProps} from '../BaseProps';
49
49
  import {themeProps} from '../utils/themeProps';
50
50
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
51
+ import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
51
52
 
52
53
  const styles = stylex.create({
53
54
  root: {
@@ -411,6 +412,10 @@ export function Collapsible({
411
412
  focusOutlineProps.focusVisible(
412
413
  styles.trigger,
413
414
  density != null && triggerDensity[density],
415
+ // The system's pressed overlay on the disclosure row. The trigger
416
+ // has no hover surface of its own, so this is the one background
417
+ // it paints, and only while it is pressed.
418
+ !isDisabled && interactionOverlayStyles.pressedBackgroundColor,
414
419
  isDisabled && styles.triggerDisabled,
415
420
  ),
416
421
  )}>
@@ -73,9 +73,14 @@ const styles = stylex.create({
73
73
  backgroundColor: 'transparent',
74
74
  color: colorVars['--color-text-primary'],
75
75
  fontFamily: typographyVars['--font-family-body'],
76
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
77
+ // control sits under 16px, and only iOS WebKit implements
78
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
76
79
  fontSize: {
77
80
  default: typeScaleVars['--text-body-size'],
78
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
81
+ '@media (pointer: coarse)': {
82
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
83
+ },
79
84
  },
80
85
  lineHeight: typeScaleVars['--text-body-leading'],
81
86
  padding: 0,
@@ -65,9 +65,14 @@ const styles = stylex.create({
65
65
  paddingBlock: spacingVars['--spacing-2'],
66
66
  paddingInline: spacingVars['--spacing-3'],
67
67
  fontFamily: typographyVars['--font-family-body'],
68
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
69
+ // control sits under 16px, and only iOS WebKit implements
70
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
68
71
  fontSize: {
69
72
  default: typeScaleVars['--text-label-size'],
70
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-label-size']})`,
73
+ '@media (pointer: coarse)': {
74
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-label-size']})`,
75
+ },
71
76
  },
72
77
  lineHeight: typeScaleVars['--text-label-leading'],
73
78
  color: colorVars['--color-text-primary'],
@@ -99,9 +99,14 @@ const styles = stylex.create({
99
99
  borderStyle: 'none',
100
100
  padding: 0,
101
101
  fontFamily: typographyVars['--font-family-body'],
102
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
103
+ // control sits under 16px, and only iOS WebKit implements
104
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
102
105
  fontSize: {
103
106
  default: typeScaleVars['--text-body-size'],
104
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
107
+ '@media (pointer: coarse)': {
108
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
109
+ },
105
110
  },
106
111
  lineHeight: typeScaleVars['--text-body-leading'],
107
112
  color: colorVars['--color-text-primary'],
@@ -100,10 +100,14 @@ const styles = stylex.create({
100
100
  borderStyle: 'none',
101
101
  padding: 0,
102
102
  fontFamily: typographyVars['--font-family-body'],
103
- // Below 16px iOS zooms the page when the field takes focus.
103
+ // Below 16px iOS zooms the page when the field takes focus. Only iOS
104
+ // WebKit implements -webkit-touch-callout, so the coarse-pointer floor is
105
+ // keyed to iOS alone.
104
106
  fontSize: {
105
107
  default: typeScaleVars['--text-body-size'],
106
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
108
+ '@media (pointer: coarse)': {
109
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
110
+ },
107
111
  },
108
112
  lineHeight: typeScaleVars['--text-body-leading'],
109
113
  color: colorVars['--color-text-primary'],
@@ -112,11 +116,20 @@ const styles = stylex.create({
112
116
  // A date control's intrinsic height comes from its inner edit fields, not
113
117
  // from `line-height`, so it renders ~2px taller than a text input and its
114
118
  // value sits off the shared baseline inside the same flex row. One line
115
- // box is exactly what the text field occupies.
116
- height: stylex.firstThatWorks(
117
- '1lh',
118
- `calc(max(1rem, ${typeScaleVars['--text-body-size']}) * ${typeScaleVars['--text-body-leading']})`,
119
- ),
119
+ // box is exactly what the text field occupies. The calc fallback mirrors
120
+ // the iOS-only floor above; browsers with `1lh` track it for free.
121
+ height: {
122
+ default: stylex.firstThatWorks(
123
+ '1lh',
124
+ `calc(${typeScaleVars['--text-body-size']} * ${typeScaleVars['--text-body-leading']})`,
125
+ ),
126
+ '@media (pointer: coarse)': {
127
+ '@supports (-webkit-touch-callout: none)': stylex.firstThatWorks(
128
+ '1lh',
129
+ `calc(max(1rem, ${typeScaleVars['--text-body-size']}) * ${typeScaleVars['--text-body-leading']})`,
130
+ ),
131
+ },
132
+ },
120
133
  // iOS gives date controls their own button-like chrome, with inner
121
134
  // spacing and a centred value that no reset of ours can reach.
122
135
  WebkitAppearance: 'none',
@@ -187,9 +200,12 @@ const styles = stylex.create({
187
200
  // covers — one line of that leading fills its height exactly, which puts
188
201
  // the glyphs on the input's own baseline.
189
202
  display: 'block',
203
+ // ...with the input's own iOS floor, so the two baselines stay shared.
190
204
  fontSize: {
191
205
  default: typeScaleVars['--text-body-size'],
192
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
206
+ '@media (pointer: coarse)': {
207
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
208
+ },
193
209
  },
194
210
  lineHeight: typeScaleVars['--text-body-leading'],
195
211
  // A tap has to reach the control underneath — that is what raises the
@@ -194,10 +194,14 @@ const styles = stylex.create({
194
194
  padding: 0,
195
195
  fontFamily: typographyVars['--font-family-body'],
196
196
  // Below 16px iOS zooms the page on focus. The field is focusable even
197
- // though it is not typable, so it needs the same floor DateInput has.
197
+ // though it is not typable, so it needs the same floor DateInput has —
198
+ // keyed to iOS alone, since only iOS WebKit implements
199
+ // -webkit-touch-callout.
198
200
  fontSize: {
199
201
  default: typeScaleVars['--text-body-size'],
200
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
202
+ '@media (pointer: coarse)': {
203
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
204
+ },
201
205
  },
202
206
  lineHeight: typeScaleVars['--text-body-leading'],
203
207
  color: colorVars['--color-text-primary'],
@@ -92,9 +92,14 @@ const styles = stylex.create({
92
92
  borderStyle: 'none',
93
93
  padding: 0,
94
94
  fontFamily: typographyVars['--font-family-body'],
95
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
96
+ // control sits under 16px, and only iOS WebKit implements
97
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
95
98
  fontSize: {
96
99
  default: typeScaleVars['--text-body-size'],
97
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
100
+ '@media (pointer: coarse)': {
101
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
102
+ },
98
103
  },
99
104
  lineHeight: typeScaleVars['--text-body-leading'],
100
105
  color: colorVars['--color-text-primary'],
@@ -173,9 +173,14 @@ const styles = stylex.create({
173
173
  borderStyle: 'none',
174
174
  padding: 0,
175
175
  fontFamily: typographyVars['--font-family-body'],
176
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
177
+ // control sits under 16px, and only iOS WebKit implements
178
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
176
179
  fontSize: {
177
180
  default: typeScaleVars['--text-body-size'],
178
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
181
+ '@media (pointer: coarse)': {
182
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
183
+ },
179
184
  },
180
185
  lineHeight: typeScaleVars['--text-body-leading'],
181
186
  color: colorVars['--color-text-primary'],
@@ -182,9 +182,14 @@ const styles = stylex.create({
182
182
  borderStyle: 'none',
183
183
  padding: 0,
184
184
  fontFamily: typographyVars['--font-family-body'],
185
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
186
+ // control sits under 16px, and only iOS WebKit implements
187
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
185
188
  fontSize: {
186
189
  default: typeScaleVars['--text-body-size'],
187
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
190
+ '@media (pointer: coarse)': {
191
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
192
+ },
188
193
  },
189
194
  lineHeight: typeScaleVars['--text-body-leading'],
190
195
  color: colorVars['--color-text-primary'],
@@ -42,21 +42,34 @@ export const nativePickerSegmentStyles = stylex.create({
42
42
  borderStyle: 'none',
43
43
  padding: 0,
44
44
  fontFamily: typographyVars['--font-family-body'],
45
- // Below 16px iOS zooms the page when the field takes focus.
45
+ // Below 16px iOS zooms the page when the field takes focus. Only iOS
46
+ // WebKit implements -webkit-touch-callout, so the coarse-pointer floor is
47
+ // keyed to iOS alone.
46
48
  fontSize: {
47
49
  default: typeScaleVars['--text-body-size'],
48
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
50
+ '@media (pointer: coarse)': {
51
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
52
+ },
49
53
  },
50
54
  lineHeight: typeScaleVars['--text-body-leading'],
51
55
  color: colorVars['--color-text-primary'],
52
56
  backgroundColor: 'transparent',
53
57
  outline: 'none',
54
58
  // Native date/time controls size from their internal edit fields. Pin the
55
- // box to one text line so both segments stay aligned.
56
- height: stylex.firstThatWorks(
57
- '1lh',
58
- `calc(max(1rem, ${typeScaleVars['--text-body-size']}) * ${typeScaleVars['--text-body-leading']})`,
59
- ),
59
+ // box to one text line so both segments stay aligned. The calc fallback
60
+ // mirrors the iOS-only floor above; browsers with `1lh` track it for free.
61
+ height: {
62
+ default: stylex.firstThatWorks(
63
+ '1lh',
64
+ `calc(${typeScaleVars['--text-body-size']} * ${typeScaleVars['--text-body-leading']})`,
65
+ ),
66
+ '@media (pointer: coarse)': {
67
+ '@supports (-webkit-touch-callout: none)': stylex.firstThatWorks(
68
+ '1lh',
69
+ `calc(max(1rem, ${typeScaleVars['--text-body-size']}) * ${typeScaleVars['--text-body-leading']})`,
70
+ ),
71
+ },
72
+ },
60
73
  WebkitAppearance: 'none',
61
74
  appearance: 'none',
62
75
  // DateTimeInput renders its own calendar/clock affordances.
@@ -101,9 +114,12 @@ export const nativePickerSegmentStyles = stylex.create({
101
114
  insetInlineEnd: 0,
102
115
  insetBlock: 0,
103
116
  display: 'block',
117
+ // The same iOS-only floor as the input it covers, shared baselines.
104
118
  fontSize: {
105
119
  default: typeScaleVars['--text-body-size'],
106
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
120
+ '@media (pointer: coarse)': {
121
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
122
+ },
107
123
  },
108
124
  lineHeight: typeScaleVars['--text-body-leading'],
109
125
  pointerEvents: 'none',
@@ -119,10 +119,13 @@ const styles = stylex.create({
119
119
  color: colorVars['--color-text-primary'],
120
120
  fontFamily: typographyVars['--font-family-body'],
121
121
  // Matches the option rows below it, so the query reads as the first line of
122
- // the list. The coarse-pointer floor keeps iOS from zooming on focus.
122
+ // the list. The floor keeps iOS from zooming on focus — keyed to iOS
123
+ // alone, since only iOS WebKit implements -webkit-touch-callout.
123
124
  fontSize: {
124
125
  default: typeScaleVars['--text-label-size'],
125
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-label-size']})`,
126
+ '@media (pointer: coarse)': {
127
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-label-size']})`,
128
+ },
126
129
  },
127
130
  lineHeight: typeScaleVars['--text-label-leading'],
128
131
  // The field draws the focus ring (see `field`), so the bare input must not
@@ -256,7 +256,7 @@ export const CORE_STATUS_MESSAGE_BINDING_STATES = [
256
256
  minValue: 0,
257
257
  maxValue: 100,
258
258
  },
259
- focusSelector: '[role="progressbar"] [tabindex="0"]',
259
+ focusSelector: '[tabindex="0"]',
260
260
  storyId: 'a11y-status-message-pattern--progress-mark-focused-update',
261
261
  },
262
262
  ] as const satisfies ReadonlyArray<CoreStatusMessageBindingDefinition>;
@@ -231,9 +231,14 @@ const styles = stylex.create({
231
231
  },
232
232
  placeholderText: {
233
233
  fontFamily: typographyVars['--font-family-body'],
234
+ // The 16px floor is iOS-only: iOS Safari zooms the page when a focused
235
+ // control sits under 16px, and only iOS WebKit implements
236
+ // -webkit-touch-callout to key the coarse-pointer floor to it.
234
237
  fontSize: {
235
238
  default: typeScaleVars['--text-body-size'],
236
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
239
+ '@media (pointer: coarse)': {
240
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
241
+ },
237
242
  },
238
243
  lineHeight: typeScaleVars['--text-body-leading'],
239
244
  color: colorVars['--color-text-secondary'],
@@ -242,9 +247,13 @@ const styles = stylex.create({
242
247
  },
243
248
  fileNameText: {
244
249
  fontFamily: typographyVars['--font-family-body'],
250
+ // ...and the filename beside it keeps the same floor, so both lines the
251
+ // control shows stay the same size.
245
252
  fontSize: {
246
253
  default: typeScaleVars['--text-body-size'],
247
- '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
254
+ '@media (pointer: coarse)': {
255
+ '@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
256
+ },
248
257
  },
249
258
  lineHeight: typeScaleVars['--text-body-leading'],
250
259
  color: colorVars['--color-text-primary'],
@@ -27,7 +27,7 @@ export const docs = {
27
27
  name: 'keys',
28
28
  type: 'string',
29
29
  description:
30
- 'Keyboard shortcut string. Use "+" to separate keys. Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right.',
30
+ 'Keyboard shortcut string. Use "+" to separate keys. Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right, plus. Aliases: esc for escape and return for enter.',
31
31
  required: true,
32
32
  },
33
33
  {
@@ -72,7 +72,7 @@ export const docsZh = {
72
72
  name: 'keys',
73
73
  type: 'string',
74
74
  description:
75
- '键盘快捷键字符串。使用 "+" 分隔各按键。特殊按键:mod(Mac 上为 Cmd)、ctrl、alt、shift、enter、backspace、escape、tab、up、down、left、right。',
75
+ '键盘快捷键字符串。使用 "+" 分隔各按键。特殊按键:mod(Mac 上为 Cmd)、ctrl、alt、shift、enter、backspace、escape、tab、up、down、left、right、plus。别名:esc 等同于 escape,return 等同于 enter。',
76
76
  required: true,
77
77
  },
78
78
  {
@@ -122,7 +122,7 @@ export const docsDense = {
122
122
  ],
123
123
  },
124
124
  propDescriptions: {
125
- keys: 'Shortcut string. "+" separates keys. Special: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right.',
125
+ keys: 'Shortcut string. "+" separates keys. Special: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right, plus. Aliases: esc for escape, return for enter.',
126
126
  xstyle: 'StyleX styles for layout customization. Must be stylex.create() value.',
127
127
  className: 'CSS class for root element. Prefer xstyle; className for non-StyleX integration.',
128
128
  style: 'Inline styles for root element. Prefer xstyle; inline styles bypass StyleX optimization.',
@@ -10,13 +10,14 @@
10
10
  */
11
11
 
12
12
  import {render, screen} from '@testing-library/react';
13
- import {describe, it, expect, afterEach} from 'vitest';
13
+ import {describe, it, expect, afterEach, vi} from 'vitest';
14
14
  import {Kbd} from './Kbd';
15
15
 
16
16
  describe('Kbd', () => {
17
17
  const originalPlatform = navigator.platform;
18
18
 
19
19
  afterEach(() => {
20
+ vi.restoreAllMocks();
20
21
  // Restore platform after any test that spoofs it \u2014 ensures no
21
22
  // test pollution even if an assertion fails mid-test.
22
23
  Object.defineProperty(navigator, 'platform', {
@@ -72,6 +73,61 @@ describe('Kbd', () => {
72
73
  expect(screen.getByText('Esc')).toBeInTheDocument();
73
74
  });
74
75
 
76
+ it.each([
77
+ {alias: 'esc', display: 'Esc', label: 'Escape'},
78
+ {alias: 'return', display: '\u21B5', label: 'Enter'},
79
+ ])('normalizes the $alias key alias', ({alias, display, label}) => {
80
+ render(<Kbd keys={alias} />);
81
+ expect(screen.getByText(display)).toBeInTheDocument();
82
+ expect(screen.getByRole('img')).toHaveAttribute('aria-label', label);
83
+ });
84
+
85
+ it.each([
86
+ {alias: 'esc', canonical: 'escape'},
87
+ {alias: 'return', canonical: 'enter'},
88
+ ])('$alias renders identically to $canonical', ({alias, canonical}) => {
89
+ const {container, rerender} = render(<Kbd keys={canonical} />);
90
+ const text = container.textContent;
91
+ const label = screen.getByRole('img').getAttribute('aria-label');
92
+ rerender(<Kbd keys={alias} />);
93
+ expect(container.textContent).toBe(text);
94
+ expect(screen.getByRole('img').getAttribute('aria-label')).toBe(label);
95
+ });
96
+
97
+ it.each([
98
+ {keys: 'ctrl + ESC', display: 'Esc', label: 'Control + Escape'},
99
+ {keys: 'shift + RETURN', display: '\u21B5', label: 'Shift + Enter'},
100
+ ])('normalizes aliases inside the $keys combo', ({keys, display, label}) => {
101
+ render(<Kbd keys={keys} />);
102
+ expect(screen.getByText(display)).toBeInTheDocument();
103
+ expect(screen.getByRole('img')).toHaveAttribute('aria-label', label);
104
+ });
105
+
106
+ it('keeps child keys unique after alias normalization', () => {
107
+ const consoleError = vi
108
+ .spyOn(console, 'error')
109
+ .mockImplementation(() => {});
110
+ const {container} = render(
111
+ <Kbd keys="escape+esc+ESC+enter+return+return" />,
112
+ );
113
+ expect(consoleError).not.toHaveBeenCalled();
114
+ expect(
115
+ Array.from(container.querySelectorAll('kbd'), key => key.textContent),
116
+ ).toEqual(['Esc', 'Esc', 'Esc', '↵', '↵', '↵']);
117
+ });
118
+
119
+ it.each(['constructor', '__proto__'])(
120
+ 'renders the unknown %s key without consulting object prototypes',
121
+ key => {
122
+ render(<Kbd keys={key} />);
123
+ expect(screen.getByText(key.toUpperCase())).toBeInTheDocument();
124
+ expect(screen.getByRole('img')).toHaveAttribute(
125
+ 'aria-label',
126
+ key.toUpperCase(),
127
+ );
128
+ },
129
+ );
130
+
75
131
  it('exposes a spoken accessible name and hides the glyphs (obs-1)', () => {
76
132
  render(<Kbd keys="mod+shift+k" />);
77
133
  // The wrapper carries a screen-reader name built from spoken key labels
package/src/Kbd/Kbd.tsx CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file Kbd.tsx
7
- * @input Uses React, StyleX, theme tokens
8
- * @output Exports Kbd component and KbdProps
7
+ * @input Uses React, StyleX, theme tokens, shortcut key names and aliases
8
+ * @output Exports Kbd component and KbdProps with canonical shortcut labels
9
9
  * @position Core implementation; renders styled keyboard shortcut indicators
10
10
  *
11
11
  * SYNC: When modified, update:
@@ -61,20 +61,25 @@ const styles = stylex.create({
61
61
  * Note: `mod` is not in this map — it resolves dynamically via platform
62
62
  * detection inside the component.
63
63
  */
64
- const KEY_DISPLAY: Record<string, string> = {
65
- ctrl: '\u2303', // ⌃
66
- alt: '\u2325', // ⌥
67
- shift: '\u21E7', // ⇧
68
- enter: '\u21B5', // ↵
69
- backspace: '\u232B', // ⌫
70
- escape: 'Esc',
71
- tab: '\u21E5', // ⇥
72
- up: '\u2191',
73
- down: '\u2193',
74
- left: '\u2190',
75
- right: '\u2192',
76
- plus: '+',
77
- };
64
+ const KEY_DISPLAY = new Map<string, string>([
65
+ ['ctrl', '\u2303'], // ⌃
66
+ ['alt', '\u2325'], // ⌥
67
+ ['shift', '\u21E7'], // ⇧
68
+ ['enter', '\u21B5'], // ↵
69
+ ['backspace', '\u232B'], // ⌫
70
+ ['escape', 'Esc'],
71
+ ['tab', '\u21E5'], // ⇥
72
+ ['up', '\u2191'],
73
+ ['down', '\u2193'],
74
+ ['left', '\u2190'],
75
+ ['right', '\u2192'],
76
+ ['plus', '+'],
77
+ ]);
78
+
79
+ const KBD_KEY_ALIASES = new Map<string, string>([
80
+ ['esc', 'escape'],
81
+ ['return', 'enter'],
82
+ ]);
78
83
 
79
84
  /**
80
85
  * Resolves a key name to its display string. Handles the platform-aware
@@ -85,7 +90,7 @@ function getKeyDisplay(key: string, isMac: boolean): string {
85
90
  if (key === 'mod') {
86
91
  return isMac ? '\u2318' : 'Ctrl';
87
92
  }
88
- return KEY_DISPLAY[key] ?? key.toUpperCase();
93
+ return KEY_DISPLAY.get(key) ?? key.toUpperCase();
89
94
  }
90
95
 
91
96
  /**
@@ -93,26 +98,26 @@ function getKeyDisplay(key: string, isMac: boolean): string {
93
98
  * (⌘, ⇧, ↵, …) that assistive tech cannot announce meaningfully, so the
94
99
  * accessible name for the shortcut is built from these words instead.
95
100
  */
96
- const KEY_LABEL: Record<string, string> = {
97
- ctrl: 'Control',
98
- alt: 'Alt',
99
- shift: 'Shift',
100
- enter: 'Enter',
101
- backspace: 'Backspace',
102
- escape: 'Escape',
103
- tab: 'Tab',
104
- up: 'Up arrow',
105
- down: 'Down arrow',
106
- left: 'Left arrow',
107
- right: 'Right arrow',
108
- plus: 'Plus',
109
- };
101
+ const KEY_LABEL = new Map<string, string>([
102
+ ['ctrl', 'Control'],
103
+ ['alt', 'Alt'],
104
+ ['shift', 'Shift'],
105
+ ['enter', 'Enter'],
106
+ ['backspace', 'Backspace'],
107
+ ['escape', 'Escape'],
108
+ ['tab', 'Tab'],
109
+ ['up', 'Up arrow'],
110
+ ['down', 'Down arrow'],
111
+ ['left', 'Left arrow'],
112
+ ['right', 'Right arrow'],
113
+ ['plus', 'Plus'],
114
+ ]);
110
115
 
111
116
  function getKeyLabel(key: string, isMac: boolean): string {
112
117
  if (key === 'mod') {
113
118
  return isMac ? 'Command' : 'Control';
114
119
  }
115
- return KEY_LABEL[key] ?? key.toUpperCase();
120
+ return KEY_LABEL.get(key) ?? key.toUpperCase();
116
121
  }
117
122
 
118
123
  function subscribeToPlatformChanges(): () => void {
@@ -127,7 +132,9 @@ export interface KbdProps extends BaseProps<HTMLSpanElement> {
127
132
  ref?: React.Ref<HTMLSpanElement>;
128
133
  /**
129
134
  * Keyboard shortcut string. Use "+" to separate keys.
130
- * Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape.
135
+ * Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape,
136
+ * tab, up, down, left, and right.
137
+ * Aliases: "esc" for "escape" and "return" for "enter".
131
138
  * Use "plus" to render a literal "+" key (e.g. "shift+plus").
132
139
  *
133
140
  * @example
@@ -163,11 +170,24 @@ export function Kbd({keys, ref, xstyle, className, style, ...rest}: KbdProps) {
163
170
  getServerPlatformSnapshot,
164
171
  );
165
172
 
166
- const parts = keys.split('+').map(key => key.trim().toLowerCase());
173
+ const keyOccurrences = new Map<string, number>();
174
+ const parts = keys
175
+ .split('+')
176
+ .map(key => key.trim().toLowerCase())
177
+ .map(sourceKey => {
178
+ const occurrence = keyOccurrences.get(sourceKey) ?? 0;
179
+ keyOccurrences.set(sourceKey, occurrence + 1);
180
+ return {
181
+ key: KBD_KEY_ALIASES.get(sourceKey) ?? sourceKey,
182
+ reactKey: `${sourceKey}:${occurrence}`,
183
+ };
184
+ });
167
185
 
168
186
  // Screen-reader name: the joined spoken labels (e.g. "Command + K"), since
169
187
  // the visual glyphs below are announced meaninglessly by assistive tech.
170
- const accessibleName = parts.map(key => getKeyLabel(key, isMac)).join(' + ');
188
+ const accessibleName = parts
189
+ .map(({key}) => getKeyLabel(key, isMac))
190
+ .join(' + ');
171
191
 
172
192
  return (
173
193
  <span
@@ -181,8 +201,8 @@ export function Kbd({keys, ref, xstyle, className, style, ...rest}: KbdProps) {
181
201
  className,
182
202
  style,
183
203
  )}>
184
- {parts.map(key => (
185
- <kbd key={key} aria-hidden="true" {...stylex.props(styles.kbd)}>
204
+ {parts.map(({key, reactKey}) => (
205
+ <kbd key={reactKey} aria-hidden="true" {...stylex.props(styles.kbd)}>
186
206
  {getKeyDisplay(key, isMac)}
187
207
  </kbd>
188
208
  ))}
@@ -8,6 +8,23 @@ export const docs = {
8
8
  displayName: 'Layout Footer',
9
9
  isHiddenFromOverview: true,
10
10
  description: 'Bottom bar for action bars, pagination, and status bars.',
11
+ playground: {
12
+ defaults: {
13
+ children: 'Footer content: status bar or actions',
14
+ hasDivider: true,
15
+ },
16
+ wrapper: {
17
+ component: 'Layout',
18
+ slotProp: 'footer',
19
+ props: {
20
+ content: {
21
+ __element: 'LayoutContent',
22
+ props: {},
23
+ children: 'Main content area',
24
+ },
25
+ },
26
+ },
27
+ },
11
28
  props: [
12
29
  {
13
30
  name: 'children',