@astryxdesign/core 0.5.2-canary.c9c8564 → 0.5.2-canary.e03511b

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 (216) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +3 -1
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +4 -3
  4. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
  5. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
  7. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
  9. package/dist/Button/Button.d.ts +2 -1
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.js +11 -4
  12. package/dist/Carousel/Carousel.d.ts.map +1 -1
  13. package/dist/Carousel/Carousel.js +2 -2
  14. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  15. package/dist/Chat/ChatToolCalls.js +34 -17
  16. package/dist/ContextMenu/ContextMenu.d.ts +14 -3
  17. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  18. package/dist/ContextMenu/ContextMenu.js +149 -26
  19. package/dist/ContextMenu/index.d.ts +1 -0
  20. package/dist/ContextMenu/index.d.ts.map +1 -1
  21. package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
  22. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  23. package/dist/DropdownMenu/DropdownMenu.js +340 -26
  24. package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
  25. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  26. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  27. package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
  28. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
  29. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
  31. package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
  32. package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
  33. package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
  34. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
  35. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
  36. package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
  37. package/dist/DropdownMenu/index.d.ts +2 -1
  38. package/dist/DropdownMenu/index.d.ts.map +1 -1
  39. package/dist/DropdownMenu/menuWidth.d.ts +14 -0
  40. package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
  41. package/dist/DropdownMenu/menuWidth.js +35 -0
  42. package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
  43. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
  44. package/dist/DropdownMenu/useMenuOverflow.js +60 -0
  45. package/dist/MoreMenu/MoreMenu.d.ts +8 -2
  46. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  47. package/dist/MoreMenu/MoreMenu.js +2 -0
  48. package/dist/MultiSelector/MultiSelector.d.ts +11 -1
  49. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  50. package/dist/MultiSelector/MultiSelector.js +97 -68
  51. package/dist/MultiSelector/index.d.ts +1 -1
  52. package/dist/MultiSelector/index.d.ts.map +1 -1
  53. package/dist/Popover/Popover.d.ts +10 -6
  54. package/dist/Popover/Popover.d.ts.map +1 -1
  55. package/dist/Popover/Popover.js +141 -16
  56. package/dist/Popover/usePopover.d.ts +4 -0
  57. package/dist/Popover/usePopover.d.ts.map +1 -1
  58. package/dist/Popover/usePopover.js +52 -5
  59. package/dist/Selector/Selector.d.ts +10 -0
  60. package/dist/Selector/Selector.d.ts.map +1 -1
  61. package/dist/Selector/Selector.js +106 -80
  62. package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
  63. package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
  64. package/dist/Selector/SelectorBottomSheet.js +83 -0
  65. package/dist/Selector/index.d.ts +1 -1
  66. package/dist/Selector/index.d.ts.map +1 -1
  67. package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
  68. package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
  69. package/dist/Selector/selectorPresentation.stylex.js +21 -0
  70. package/dist/Selector/useSelectorPresentation.d.ts +32 -0
  71. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
  72. package/dist/Selector/useSelectorPresentation.js +90 -0
  73. package/dist/Toast/useToastGesture.d.ts.map +1 -1
  74. package/dist/Toast/useToastGesture.js +46 -9
  75. package/dist/astryx.css +19 -1
  76. package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
  77. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
  78. package/dist/hooks/useAdaptivePresentation.js +19 -0
  79. package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
  80. package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
  81. package/dist/hooks/useFocusReturnVisibility.js +35 -0
  82. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  83. package/dist/hooks/useFocusTrap.js +12 -0
  84. package/locales/en.json +20 -0
  85. package/locales/pseudo.json +15 -0
  86. package/package.json +2 -2
  87. package/src/BottomSheet/BottomSheet.doc.mjs +37 -0
  88. package/src/BottomSheet/BottomSheet.spec.md +177 -0
  89. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  90. package/src/BottomSheet/BottomSheet.tsx +7 -4
  91. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
  92. package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
  93. package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
  94. package/src/Button/Button.doc.mjs +56 -0
  95. package/src/Button/Button.test.tsx +10 -0
  96. package/src/Button/Button.tsx +13 -5
  97. package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
  98. package/src/Carousel/Carousel.test.tsx +83 -0
  99. package/src/Carousel/Carousel.tsx +8 -2
  100. package/src/Chat/ChatToolCalls.test.tsx +44 -0
  101. package/src/Chat/ChatToolCalls.tsx +36 -15
  102. package/src/CheckboxList/CheckboxList.doc.mjs +172 -31
  103. package/src/CheckboxList/CheckboxList.spec.md +214 -0
  104. package/src/CommandPalette/CommandPalette.doc.mjs +72 -0
  105. package/src/CommandPalette/CommandPalette.spec.md +223 -0
  106. package/src/ComplexSelector/ComplexSelector.doc.mjs +42 -0
  107. package/src/ComplexSelector/ComplexSelector.spec.md +189 -0
  108. package/src/ContextMenu/ContextMenu.doc.mjs +61 -1
  109. package/src/ContextMenu/ContextMenu.spec.md +206 -0
  110. package/src/ContextMenu/ContextMenu.test.tsx +131 -1
  111. package/src/ContextMenu/ContextMenu.tsx +199 -32
  112. package/src/ContextMenu/index.ts +1 -0
  113. package/src/Divider/Divider.doc.mjs +24 -0
  114. package/src/Divider/Divider.spec.md +150 -0
  115. package/src/DropdownMenu/DropdownMenu.doc.mjs +305 -40
  116. package/src/DropdownMenu/DropdownMenu.spec.md +245 -0
  117. package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
  118. package/src/DropdownMenu/DropdownMenu.tsx +459 -28
  119. package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
  120. package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
  121. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
  122. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
  123. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
  124. package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
  125. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
  126. package/src/DropdownMenu/index.ts +2 -0
  127. package/src/DropdownMenu/menuWidth.ts +51 -0
  128. package/src/DropdownMenu/useMenuOverflow.ts +77 -0
  129. package/src/FieldStatus/FieldStatus.doc.mjs +21 -0
  130. package/src/FieldStatus/FieldStatus.spec.md +212 -0
  131. package/src/Grid/Grid.doc.mjs +19 -0
  132. package/src/Grid/Grid.spec.md +161 -0
  133. package/src/Icon/Icon.doc.mjs +196 -48
  134. package/src/Icon/Icon.spec.md +191 -0
  135. package/src/IconButton/IconButton.doc.mjs +38 -0
  136. package/src/Kbd/Kbd.doc.mjs +18 -0
  137. package/src/Kbd/Kbd.spec.md +141 -0
  138. package/src/Layout/Layout.doc.mjs +32 -0
  139. package/src/Layout/Layout.spec.md +199 -0
  140. package/src/Lightbox/Lightbox.doc.mjs +149 -33
  141. package/src/Lightbox/Lightbox.spec.md +197 -0
  142. package/src/Markdown/Markdown.doc.mjs +60 -0
  143. package/src/Markdown/Markdown.spec.md +200 -0
  144. package/src/MobileNav/MobileNav.doc.mjs +140 -32
  145. package/src/MobileNav/MobileNav.spec.md +208 -0
  146. package/src/MoreMenu/MoreMenu.doc.mjs +59 -0
  147. package/src/MoreMenu/MoreMenu.spec.md +221 -0
  148. package/src/MoreMenu/MoreMenu.test.tsx +47 -0
  149. package/src/MoreMenu/MoreMenu.tsx +13 -1
  150. package/src/MultiSelector/MultiSelector.doc.mjs +128 -0
  151. package/src/MultiSelector/MultiSelector.spec.md +266 -0
  152. package/src/MultiSelector/MultiSelector.test.tsx +127 -0
  153. package/src/MultiSelector/MultiSelector.tsx +121 -73
  154. package/src/MultiSelector/index.ts +1 -0
  155. package/src/NavIcon/NavIcon.doc.mjs +17 -0
  156. package/src/NavIcon/NavIcon.spec.md +133 -0
  157. package/src/NavMenu/NavHeadingMenu.spec.md +205 -0
  158. package/src/NavMenu/NavMenu.doc.mjs +37 -0
  159. package/src/Outline/Outline.doc.mjs +35 -0
  160. package/src/Outline/Outline.spec.md +161 -0
  161. package/src/OverflowList/OverflowList.doc.mjs +24 -0
  162. package/src/OverflowList/OverflowList.spec.md +156 -0
  163. package/src/Pagination/Pagination.doc.mjs +74 -0
  164. package/src/Pagination/Pagination.spec.md +225 -0
  165. package/src/Popover/Popover.doc.mjs +11 -11
  166. package/src/Popover/Popover.test.tsx +397 -2
  167. package/src/Popover/Popover.tsx +228 -21
  168. package/src/Popover/usePopover.tsx +92 -15
  169. package/src/ProgressBar/ProgressBar.doc.mjs +198 -38
  170. package/src/ProgressBar/ProgressBar.spec.md +260 -0
  171. package/src/Resizable/Resizable.doc.mjs +59 -16
  172. package/src/Resizable/Resizable.spec.md +194 -0
  173. package/src/Section/Section.doc.mjs +17 -0
  174. package/src/Section/Section.spec.md +138 -0
  175. package/src/SegmentedControl/SegmentedControl.doc.mjs +83 -0
  176. package/src/SegmentedControl/SegmentedControl.spec.md +151 -0
  177. package/src/Selector/Selector.doc.mjs +12 -0
  178. package/src/Selector/Selector.spec.md +202 -0
  179. package/src/Selector/Selector.test.tsx +135 -0
  180. package/src/Selector/Selector.tsx +141 -82
  181. package/src/Selector/SelectorBottomSheet.tsx +97 -0
  182. package/src/Selector/index.ts +1 -0
  183. package/src/Selector/selectorPresentation.stylex.ts +20 -0
  184. package/src/Selector/useSelectorPresentation.ts +147 -0
  185. package/src/Skeleton/Skeleton.doc.mjs +12 -0
  186. package/src/Skeleton/Skeleton.spec.md +126 -0
  187. package/src/Slider/Slider.doc.mjs +184 -30
  188. package/src/Slider/Slider.spec.md +223 -0
  189. package/src/Stack/Stack.doc.mjs +22 -0
  190. package/src/Stack/Stack.spec.md +154 -0
  191. package/src/StatusDot/StatusDot.doc.mjs +17 -0
  192. package/src/StatusDot/StatusDot.spec.md +137 -0
  193. package/src/Switch/Switch.doc.mjs +40 -0
  194. package/src/Switch/Switch.spec.md +174 -0
  195. package/src/Table/Table.doc.mjs +102 -24
  196. package/src/Table/Table.spec.md +283 -0
  197. package/src/Text/Text.doc.mjs +190 -43
  198. package/src/Text/Text.spec.md +205 -0
  199. package/src/TextArea/TextArea.doc.mjs +255 -45
  200. package/src/TextArea/TextArea.spec.md +204 -0
  201. package/src/Toast/ToastViewport.test.tsx +118 -4
  202. package/src/Toast/useToastGesture.ts +61 -8
  203. package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
  204. package/src/Toolbar/Toolbar.doc.mjs +19 -0
  205. package/src/Toolbar/Toolbar.spec.md +173 -0
  206. package/src/Tooltip/Tooltip.doc.mjs +17 -0
  207. package/src/Tooltip/Tooltip.spec.md +141 -0
  208. package/src/TreeList/TreeList.doc.mjs +58 -0
  209. package/src/TreeList/TreeList.spec.md +220 -0
  210. package/src/Typeahead/Typeahead.doc.mjs +83 -1
  211. package/src/Typeahead/Typeahead.spec.md +250 -0
  212. package/src/hooks/useAdaptivePresentation.ts +35 -0
  213. package/src/hooks/useFocusReturnVisibility.ts +45 -0
  214. package/src/hooks/useFocusTrap.test.tsx +40 -0
  215. package/src/hooks/useFocusTrap.ts +13 -0
  216. package/src/theme/themingTargets.test.ts +91 -25
@@ -1,4 +1,33 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Sheet panel',
7
+ required: true,
8
+ description:
9
+ 'Painted surface that rises from the bottom edge and contains the sheet.',
10
+ },
11
+ {
12
+ name: 'Content area',
13
+ required: true,
14
+ description:
15
+ 'Scrollable area that presents the caller-provided sheet content.',
16
+ },
17
+ {
18
+ name: 'Handle',
19
+ required: true,
20
+ description:
21
+ 'Decorative grab affordance and drag region at the top of the panel.',
22
+ },
23
+ {
24
+ name: 'Scrim',
25
+ required: false,
26
+ description:
27
+ 'Backdrop that dims and blocks the page in a scrim-backed presentation.',
28
+ },
29
+ ];
30
+
2
31
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
3
32
 
4
33
  export const docs = {
@@ -71,6 +100,12 @@ export const docs = {
71
100
  description:
72
101
  'For a standalone sheet, called when it requests an open-state change. Automatic calls follow purpose: info dismisses on Escape, scrim click, or swipe; form dismisses on Escape only; required never dismisses implicitly. Omit inside BottomSheetSwitcher.',
73
102
  },
103
+ {
104
+ name: 'finalFocusRef',
105
+ type: 'RefObject<HTMLElement | null>',
106
+ description:
107
+ 'Optional explicit focus-return target for a standalone sheet. Use when the opener can remount or the active element is not a reliable trigger, such as an adaptive presentation switch. Omit inside BottomSheetSwitcher.',
108
+ },
74
109
  {
75
110
  name: 'purpose',
76
111
  type: "'required' | 'form' | 'info'",
@@ -120,6 +155,7 @@ export const docs = {
120
155
  },
121
156
  ],
122
157
  usage: {
158
+ anatomy,
123
159
  description:
124
160
  'A mobile touch surface for filters, actions, forms, and detail views that should rise from the bottom of the viewport; use BottomSheetSwitcher for multi-step flows.',
125
161
  bestPractices: [
@@ -271,6 +307,7 @@ export const docsDense = {
271
307
  description:
272
308
  'mobile touch sheet rising from the bottom edge (native <dialog>): grab handle, opt-in transform-based drag-to-resize snap points (snapPoints: viewport fraction, percent or px length), scrolling area resizes to the snapped visible height on release (a peek stop, a quarter of the sheet or less, keeps the full height and slides instead), Dialog-aligned dismissal purpose (info/form/required), purpose-gated swipe-to-dismiss, fully-expanded Tall visual-viewport mobile-keyboard handling, named height scale, modal (default) or non-modal (hasScrim={false}) presentation',
273
309
  usage: {
310
+ anatomy,
274
311
  description:
275
312
  'Mobile touch surface for filters, actions, forms, and detail views that should rise from the bottom of the viewport; use BottomSheetSwitcher for multi-step flows.',
276
313
  bestPractices: [
@@ -0,0 +1,177 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 3
4
+ kind: component
5
+ id: component:BottomSheet
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: [theming]
13
+ verified_by:
14
+ [
15
+ packages/core/src/BottomSheet/BottomSheet.test.tsx,
16
+ packages/core/src/BottomSheet/BottomSheetPanel.test.tsx,
17
+ scripts/check-knowledge.mjs,
18
+ ]
19
+ families: [family:overlay-dismissal]
20
+ design_specs: []
21
+ architecture:
22
+ [architecture:component-theming-surface, architecture:layer-runtime]
23
+ contributing: []
24
+ system_specs: []
25
+ ---
26
+
27
+ # BottomSheet component contract
28
+
29
+ ## Intent
30
+
31
+ BottomSheet presents caller-provided content in a panel that rises from the
32
+ bottom edge. This draft records current consumer anatomy and theming reachability
33
+ without changing runtime behavior, styling, targets, or public API.
34
+
35
+ ## Compatibility and migration
36
+
37
+ - Released default preserved: `yes`
38
+ - Compatibility class: additive documentation only; runtime, DOM, styling,
39
+ targets, and public API remain unchanged
40
+ - Controlled/uncontrolled behavior: unchanged
41
+ - Migration decision: none
42
+
43
+ Consumer migration instructions belong in consumer docs and release notes.
44
+
45
+ ## Ownership boundary
46
+
47
+ **Owns**
48
+
49
+ - The visual Sheet panel and its current `bottom-sheet` target.
50
+ - The scrolling Content area and decorative Handle inside the panel.
51
+ - Standalone sheet presentation, including its optional native-dialog Scrim.
52
+
53
+ **Does not own / non-goals**
54
+
55
+ - Caller-provided content rendered inside the Content area.
56
+ - A separate target for Content area, Handle, or Scrim; none exists on current
57
+ `main`.
58
+ - A shared switcher dialog or scrim — owned by BottomSheetSwitcher when the sheet
59
+ participates in that host.
60
+ - Shared layer hosting or dismissal policy.
61
+
62
+ ## Public concepts
63
+
64
+ No new public concept is introduced. Consumer props and usage remain documented
65
+ in `BottomSheet.doc.mjs`.
66
+
67
+ ## Behavioral and layout contract
68
+
69
+ | ID | Candidate invariant | Basis | Draft review state |
70
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | -------------------------------------------------- |
71
+ | FR1 | The current presented sheet contains one Sheet panel, one scrolling Content area, and one decorative Handle; a Scrim is present only in scrim-backed presentation. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
72
+ | FR2 | The Sheet panel carries `bottom-sheet`; Content area, Handle, and Scrim carry no BottomSheet public target. | Current source, docs, and tests | Verified current behavior; no target change |
73
+
74
+ ### Allowed variation
75
+
76
+ - Caller-provided children may vary without becoming BottomSheet-owned anatomy.
77
+ - Height, snap points, motion phase, and standalone versus switcher hosting may
78
+ vary without changing the four-part anatomy recorded here.
79
+
80
+ ### Representative states
81
+
82
+ - A standalone modal sheet renders Sheet panel, Content area, Handle, and Scrim.
83
+ - A standalone non-modal sheet renders Sheet panel, Content area, and Handle
84
+ without a Scrim.
85
+ - A switcher-managed sheet renders the same panel anatomy while
86
+ BottomSheetSwitcher owns the shared dialog and optional Scrim.
87
+
88
+ ### Transformation and precedence order
89
+
90
+ - No new height, gesture, motion, or styling precedence rule is introduced.
91
+
92
+ ### Performance and resources
93
+
94
+ - No new performance or resource rule is introduced.
95
+
96
+ ## Accessibility contract
97
+
98
+ This draft does not change or extend BottomSheet's existing dialog naming,
99
+ focus, Handle, keyboard, or dismissal behavior.
100
+
101
+ ## Design relationships
102
+
103
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
104
+ | ---------------- | ------------------------------------------------------------------------------------- | ------------------------------ | -------------- | ------------------ |
105
+ | Sheet panel | Presents the painted bottom-edge surface and owns panel geometry and motion. | Current source and public docs | Prominent | FR1, FR2 |
106
+ | Content area | Provides the scrolling area for caller-provided sheet content. | Current source and tests | Prominent | FR1, FR2 |
107
+ | Handle | Presents the decorative grab affordance and owns the panel's drag interaction region. | Current source and tests | Supporting | FR1, FR2 |
108
+ | Scrim | Dims and blocks the page in a scrim-backed host. | Current source and public docs | Supporting | FR1, FR2 |
109
+
110
+ Content area, Handle, and Scrim are stable visible parts, but no current public
111
+ target reaches them. Their `none` dispositions record factual reachability, not
112
+ a decision that they must remain unthemeable.
113
+
114
+ ### Theming anatomy
115
+
116
+ <!-- anatomy-theming:v1 -->
117
+
118
+ ```json
119
+ {
120
+ "Sheet panel": {"target": "bottom-sheet"},
121
+ "Content area": {
122
+ "none": {
123
+ "reason": "reachability-gap: No current public target reaches the scrolling content area."
124
+ }
125
+ },
126
+ "Handle": {
127
+ "none": {
128
+ "reason": "reachability-gap: No current public target reaches the handle bar or pill."
129
+ }
130
+ },
131
+ "Scrim": {
132
+ "none": {
133
+ "reason": "reachability-gap: No BottomSheet public target reaches the native dialog backdrop or switcher-owned scrim."
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## Family and system relationships
140
+
141
+ - `architecture:component-theming-surface` owns anatomy qualification, target
142
+ mapping, and factual `none` dispositions.
143
+ - `architecture:layer-runtime` owns the current native-dialog host distinction
144
+ and records that sheets retain local scrim and swipe behavior.
145
+ - `family:overlay-dismissal` owns shared Escape and platform-close ordering and
146
+ records BottomSheet's current local-only adoption gap. This anatomy backfill
147
+ does not migrate that runtime behavior.
148
+
149
+ ## Verification map
150
+
151
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
152
+ | ------------------- | ------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------- |
153
+ | FR1 | `BottomSheet.test.tsx` render, content, Handle, and scrim behavior suites | Modal, non-modal, and switcher presentations | Removing a documented part fails existing content, structure, or dismissal assertions. | `audit:BottomSheet/anatomy` |
154
+ | FR2 | Panel target assertion, source inspection, and theming target inventories | Sheet panel, Content area, Handle, and Scrim | Removing the panel target or documenting an unshipped child target fails evidence or inventory. | `audit:BottomSheet/theming` |
155
+ | Layout evidence | `BottomSheetPanel.test.tsx` | Floating Handle and scrolling Content area | Reordering or merging the stable parts fails existing panel structure and style assertions. | `audit:BottomSheet/anatomy` |
156
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target inventory | Missing, extra, prefixed, stale, or multiply assigned mappings fail repository validation. | `audit:BottomSheet/theming` |
157
+
158
+ Existing tests directly assert the Sheet panel target, Content area placement,
159
+ Handle structure, and scrim behavior. Source and public target metadata confirm
160
+ that the other three parts have no public target.
161
+
162
+ ## Decision log
163
+
164
+ None. This draft records current facts and introduces no component-local design,
165
+ API, theming, or layer-system decision.
166
+
167
+ ## Open questions
168
+
169
+ - **OQ1 — Should Content area, Handle, or Scrim gain a stable public theming
170
+ target?** (`human-api`) Their current lack of reachability is an audit gap, not
171
+ settled intent.
172
+
173
+ ## Content boundary
174
+
175
+ This file does not duplicate consumer prop tables, examples, gesture algorithms,
176
+ implementation steps, or shared layer and theming rules. It links to their
177
+ owners.
@@ -2319,6 +2319,35 @@ describe('BottomSheet', () => {
2319
2319
 
2320
2320
  expect(document.activeElement).toBe(opener);
2321
2321
  });
2322
+
2323
+ it('prefers an explicit final focus target', () => {
2324
+ const finalFocusRef = createRef<HTMLButtonElement>();
2325
+ function ExplicitFocusHarness() {
2326
+ const [isOpen, setIsOpen] = useState(true);
2327
+ return (
2328
+ <>
2329
+ <button ref={finalFocusRef} type="button">
2330
+ Adaptive trigger
2331
+ </button>
2332
+ <BottomSheet
2333
+ isOpen={isOpen}
2334
+ onOpenChange={setIsOpen}
2335
+ finalFocusRef={finalFocusRef}
2336
+ label="Filters">
2337
+ <button type="button" onClick={() => setIsOpen(false)}>
2338
+ Done
2339
+ </button>
2340
+ </BottomSheet>
2341
+ </>
2342
+ );
2343
+ }
2344
+
2345
+ render(<ExplicitFocusHarness />);
2346
+ fireEvent.click(screen.getByRole('button', {name: 'Done'}));
2347
+ finishSheetExit();
2348
+
2349
+ expect(document.activeElement).toBe(finalFocusRef.current);
2350
+ });
2322
2351
  });
2323
2352
 
2324
2353
  describe('initial focus', () => {
@@ -32,6 +32,7 @@ import {
32
32
  useLayoutEffect,
33
33
  useRef,
34
34
  useState,
35
+ type RefObject,
35
36
  type ReactNode,
36
37
  } from 'react';
37
38
  import * as stylex from '@stylexjs/stylex';
@@ -162,6 +163,8 @@ interface StandaloneBottomSheetProps extends BottomSheetSharedProps {
162
163
  isOpen: boolean;
163
164
  onOpenChange: (isOpen: boolean) => void;
164
165
  hasScrim?: boolean;
166
+ /** Element that receives focus after the sheet closes. */
167
+ finalFocusRef?: RefObject<HTMLElement | null>;
165
168
  sheetId?: never;
166
169
  }
167
170
 
@@ -221,6 +224,7 @@ function StandaloneBottomSheet({
221
224
  height = 'capped',
222
225
  snapPoints,
223
226
  hasScrim = true,
227
+ finalFocusRef,
224
228
  purpose = 'info',
225
229
  xstyle,
226
230
  ...props
@@ -298,10 +302,10 @@ function StandaloneBottomSheet({
298
302
  dialog.close();
299
303
  }
300
304
  setIsPresented(false);
301
- triggerRef.current?.focus();
305
+ (finalFocusRef?.current ?? triggerRef.current)?.focus();
302
306
  triggerRef.current = null;
303
307
  },
304
- [isOpen],
308
+ [finalFocusRef, isOpen],
305
309
  );
306
310
 
307
311
  useScrollLock(shouldPresent && hasScrim);
@@ -385,8 +389,7 @@ function StandaloneBottomSheet({
385
389
  {children}
386
390
  </BottomSheetPanel>
387
391
  </div>
388
- {/* A modal sheet's ::backdrop already answers Safari's edge sampler. */}
389
- {hasScrim ? null : <BottomSheetEdgeTint />}
392
+ <BottomSheetEdgeTint />
390
393
  </dialog>
391
394
  );
392
395
  }
@@ -101,15 +101,13 @@ describe('BottomSheetEdgeTint', () => {
101
101
  expect(tints()).toHaveLength(1);
102
102
  });
103
103
 
104
- // A modal sheet's ::backdrop is a case WebKit's sampler handles on its own,
105
- // so a second sampling target there would only fight it.
106
- it('leaves a modal sheet to its ::backdrop', () => {
104
+ it('gives a modal sheet the same surface-coloured edge tint', () => {
107
105
  render(
108
106
  <BottomSheet isOpen onOpenChange={() => {}} label="Place details">
109
107
  Content
110
108
  </BottomSheet>,
111
109
  );
112
- expect(tints()).toHaveLength(0);
110
+ expect(tints()).toHaveLength(1);
113
111
  });
114
112
 
115
113
  it('renders the tint inside the dialog, so it unmounts with the sheet', () => {
@@ -148,7 +146,7 @@ describe('BottomSheetEdgeTint', () => {
148
146
  expect(tints()).toHaveLength(1);
149
147
  });
150
148
 
151
- it('leaves a modal switcher flow to its ::backdrop', () => {
149
+ it('gives a modal switcher flow exactly one tint', () => {
152
150
  render(
153
151
  <BottomSheetSwitcher activeSheet="comment" onActiveSheetChange={() => {}}>
154
152
  <BottomSheet sheetId="comment" label="Add a comment">
@@ -156,7 +154,7 @@ describe('BottomSheetEdgeTint', () => {
156
154
  </BottomSheet>
157
155
  </BottomSheetSwitcher>,
158
156
  );
159
- expect(tints()).toHaveLength(0);
157
+ expect(tints()).toHaveLength(1);
160
158
  });
161
159
 
162
160
  it('keeps the tint out of the accessibility tree and out of hit testing', async () => {
@@ -6,7 +6,7 @@
6
6
  * @file BottomSheetEdgeTint.tsx
7
7
  * @input Uses StyleX and core color tokens
8
8
  * @output Exports BottomSheetEdgeTint, an internal decorative element
9
- * @position Private helper rendered inside a non-modal BottomSheet dialog
9
+ * @position Private helper rendered inside every BottomSheet dialog
10
10
  *
11
11
  * iOS 26 Safari dropped `<meta name="theme-color">` and instead derives the
12
12
  * colour it paints behind its translucent toolbars by sampling the page: it
@@ -14,13 +14,10 @@
14
14
  * `fixed`/`sticky` ancestor, and extends that element's declared
15
15
  * `background-color` into the browser chrome.
16
16
  *
17
- * A modal sheet is served by that heuristic already — WebKit has a dedicated
18
- * branch for a dialog's `::backdrop`. A non-modal sheet is not: the nearest
19
- * fixed ancestor of the panel is the sheet's own full-viewport `<dialog>`,
20
- * which is transparent and viewport-sized, and WebKit answers a viewport-sized
21
- * candidate by *keeping the colour it already had* — the host page's. The page
22
- * then shows through behind the address bar while the sheet covers the screen
23
- * above it.
17
+ * WebKit may otherwise extend either the dialog backdrop or the host page into
18
+ * the browser chrome. Which one wins varies with dialog mode and the page's
19
+ * own fixed or sticky edge content, so relying on the backdrop makes otherwise
20
+ * identical BottomSheets produce different toolbar colours.
24
21
  *
25
22
  * This element gives the heuristic something unambiguous to sample: fixed,
26
23
  * full width, flush with the bottom edge, taller than WebKit's 10px minimum
@@ -68,8 +65,8 @@ const styles = stylex.create({
68
65
  });
69
66
 
70
67
  /**
71
- * Colours the iOS Safari toolbar strip below a non-modal sheet. Renders
72
- * nothing visible; see the file header for why it exists.
68
+ * Colours the iOS Safari toolbar strip below a sheet. Renders nothing visible;
69
+ * see the file header for why it exists.
73
70
  */
74
71
  export function BottomSheetEdgeTint() {
75
72
  return (
@@ -632,8 +632,7 @@ export function BottomSheetSwitcher({
632
632
  ? {role: 'alertdialog'}
633
633
  : undefined)}>
634
634
  {children}
635
- {/* A modal flow's ::backdrop already answers Safari's edge sampler. */}
636
- {hasScrim ? null : <BottomSheetEdgeTint />}
635
+ <BottomSheetEdgeTint />
637
636
  </dialog>
638
637
  </BottomSheetSwitcherContext>
639
638
  );
@@ -23,6 +23,62 @@ export const docs = {
23
23
  {guidance: false, description: 'Use the destructive variant without a confirmation step for irreversible actions like deleting data.'},
24
24
  {guidance: false, description: 'Use a button for navigation. If it only takes the user to another page, use a link instead. Buttons are for actions like saving, deleting, or submitting.'},
25
25
  ],
26
+ accessibility: [
27
+ {
28
+ name: 'Text label',
29
+ category: 'Color contrast',
30
+ criterion: '1.4.3 Contrast (Minimum)',
31
+ requirement: '4.5:1',
32
+ states: ['Rest', 'Hover', 'Pointer down'],
33
+ description:
34
+ 'Button text must have at least 4.5:1 contrast with the button background in every state. For Hover and Pointer down, measure the final background after the overlay is applied.',
35
+ },
36
+ {
37
+ name: 'Essential icon or spinner arc',
38
+ category: 'Color contrast',
39
+ criterion: '1.4.11 Non-text Contrast',
40
+ requirement: '3:1',
41
+ states: ['Icon only', 'Loading'],
42
+ description:
43
+ 'An icon used instead of text must have at least 3:1 contrast with the button background. The moving spinner arc must also meet 3:1. An icon beside a visible label does not need its own check.',
44
+ },
45
+ {
46
+ name: 'Badge text',
47
+ category: 'Color contrast',
48
+ criterion: '1.4.3 Contrast (Minimum)',
49
+ requirement: '4.5:1',
50
+ states: ['Rest', 'Hover', 'Pointer down'],
51
+ description:
52
+ 'Badge text inside a button must have at least 4.5:1 contrast with the Badge background. Check all 14 built-in Badge colors in Rest, Hover, and Pointer down on page and surface backgrounds. This covers 336 pairs per mode. Check custom end content separately.',
53
+ },
54
+ {
55
+ name: 'Visible control boundary',
56
+ category: 'Color contrast',
57
+ criterion: '1.4.11 Non-text Contrast',
58
+ requirement: '3:1 if needed',
59
+ states: ['Rest'],
60
+ description:
61
+ 'The button edge needs 3:1 contrast only when users need it to see the control. A text-only button can rely on its label.',
62
+ },
63
+ {
64
+ name: 'Keyboard focus indicator',
65
+ category: 'Color contrast',
66
+ criterion: '1.4.11 Non-text Contrast',
67
+ requirement: '3:1',
68
+ states: ['Focus visible'],
69
+ description:
70
+ 'The focus outline needs at least 3:1 contrast with the area around the button. Check every style. Destructive buttons use a red outline.',
71
+ },
72
+ {
73
+ name: 'Disabled appearance',
74
+ category: 'Color contrast',
75
+ criterion: '1.4.3 and 1.4.11 exceptions',
76
+ requirement: 'Not required',
77
+ states: ['Disabled'],
78
+ description:
79
+ 'Disabled controls do not need to meet these contrast ratios.',
80
+ },
81
+ ],
26
82
  anatomy: [
27
83
  {name: 'Icon', required: false, description: 'A leading icon that reinforces the label, like a trash icon on a Delete button.'},
28
84
  {name: 'Label', required: true, description: 'The visible text describing the action. Also used as the accessible name.'},
@@ -70,6 +70,16 @@ describe('Button', () => {
70
70
  const button = screen.getByRole('button');
71
71
  // Button should be disabled when loading
72
72
  expect(button).toBeDisabled();
73
+ expect(button.className).toContain('styles.inactive');
74
+ expect(button.className).not.toContain('styles.disabled');
75
+ });
76
+
77
+ it('keeps the dimmed treatment for explicitly disabled buttons', () => {
78
+ render(<Button label="Submit" isDisabled />);
79
+ const button = screen.getByRole('button');
80
+ expect(button).toBeDisabled();
81
+ expect(button.className).toContain('styles.inactive');
82
+ expect(button.className).toContain('styles.disabled');
73
83
  });
74
84
 
75
85
  it('sets aria-busy synchronously while clickAction is pending', async () => {
@@ -99,18 +99,20 @@ const styles = stylex.create({
99
99
  pressable: {
100
100
  transform: {
101
101
  default: 'scale(1)',
102
- ':active': 'scale(0.98)',
102
+ ':active:where(:not(:disabled,[aria-disabled="true"]))': 'scale(0.98)',
103
103
  },
104
104
  },
105
- disabled: {
105
+ inactive: {
106
106
  cursor: 'default',
107
- opacity: 0.5,
108
107
  backgroundImage: 'none',
109
108
  transform: {
110
109
  default: 'none',
111
110
  ':active': 'none',
112
111
  },
113
112
  },
113
+ disabled: {
114
+ opacity: 0.5,
115
+ },
114
116
  ariaDisabled: {
115
117
  // The variants' hover treatment already steps aside for
116
118
  // `[aria-disabled]`; `:active` still matches a press on an aria-disabled
@@ -270,7 +272,8 @@ export interface ButtonProps extends BaseProps<HTMLButtonElement> {
270
272
  */
271
273
  isDisabled?: boolean;
272
274
  /**
273
- * Whether the button is in a loading state.
275
+ * Whether the button is in a loading state. Loading prevents interaction
276
+ * without dimming the spinner; explicit disabled states remain dimmed.
274
277
  * @default false
275
278
  */
276
279
  isLoading?: boolean;
@@ -573,6 +576,10 @@ export function Button({
573
576
  // not disabled, so clicks keep landing and can interrupt the in-flight action.
574
577
  const buttonDisabled =
575
578
  isDisabled || groupDisabled || (isLoadingState && !isInterruptible);
579
+ // A loading button remains non-interactive, but its spinner communicates an
580
+ // active state and must retain contrast. Only explicitly disabled controls
581
+ // receive the visually dimmed treatment.
582
+ const visuallyDisabled = isDisabled || groupDisabled;
576
583
  // isIconOnly prop is the source of truth for icon-only rendering.
577
584
  // When false (default), label is always rendered as visible text.
578
585
 
@@ -639,7 +646,8 @@ export function Button({
639
646
  sizeStyles[size],
640
647
  isIconOnly && styles.iconOnly,
641
648
  interactionOverlayStyles.backgroundImage,
642
- buttonDisabled && styles.disabled,
649
+ buttonDisabled && styles.inactive,
650
+ visuallyDisabled && styles.disabled,
643
651
  useAriaDisabled && styles.ariaDisabled,
644
652
  renderAsLink && styles.link,
645
653
  !buttonGroup && styles.pressable,
@@ -39,6 +39,53 @@ export const docs = {
39
39
  usage: {
40
40
  description:
41
41
  'ButtonGroup joins related actions into a single connected control. Use it when multiple buttons represent related choices or operations that belong together visually, like copy/cut/paste, or undo/redo.',
42
+ accessibility: [
43
+ {
44
+ name: 'Text label',
45
+ category: 'Color contrast',
46
+ criterion: '1.4.3 Contrast (Minimum)',
47
+ requirement: '4.5:1',
48
+ states: ['Rest', 'Hover', 'Pointer down'],
49
+ description:
50
+ 'Text in each button must have at least 4.5:1 contrast with its background in every state. For Hover and Pointer down, measure the final background after the overlay is applied.',
51
+ },
52
+ {
53
+ name: 'Essential icon or spinner arc',
54
+ category: 'Color contrast',
55
+ criterion: '1.4.11 Non-text Contrast',
56
+ requirement: '3:1',
57
+ states: ['Icon only', 'Loading'],
58
+ description:
59
+ 'An icon used instead of text must have at least 3:1 contrast with the button background. The moving spinner arc must also meet 3:1. An icon beside a visible label does not need its own check.',
60
+ },
61
+ {
62
+ name: 'Visible control boundary',
63
+ category: 'Color contrast',
64
+ criterion: '1.4.11 Non-text Contrast',
65
+ requirement: '3:1 if needed',
66
+ states: ['Rest'],
67
+ description:
68
+ 'Some groups need a divider or edge to show each button. That divider or edge must have at least 3:1 contrast.',
69
+ },
70
+ {
71
+ name: 'Keyboard focus indicator',
72
+ category: 'Color contrast',
73
+ criterion: '1.4.11 Non-text Contrast',
74
+ requirement: '3:1',
75
+ states: ['Focus visible'],
76
+ description:
77
+ 'The focus outline must have at least 3:1 contrast with the area around it.',
78
+ },
79
+ {
80
+ name: 'Disabled appearance',
81
+ category: 'Color contrast',
82
+ criterion: '1.4.3 and 1.4.11 exceptions',
83
+ requirement: 'Not required',
84
+ states: ['Disabled'],
85
+ description:
86
+ 'Disabled controls do not need to meet these contrast ratios.',
87
+ },
88
+ ],
42
89
  bestPractices: [
43
90
  {guidance: true, description: 'Group buttons that perform related actions on the same object, like copy, cut, paste on selected text.'},
44
91
  {guidance: true, description: 'Use the same variant for all buttons in a group so they look like a single connected unit.'},