@astryxdesign/core 0.4.2 → 0.4.3-canary.07f9998
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +79 -45
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +3 -19
- package/dist/Banner/Banner.d.ts +9 -2
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +84 -33
- package/dist/Banner/index.d.ts +14 -12
- package/dist/Banner/index.d.ts.map +1 -1
- package/dist/Banner/index.js +10 -8
- package/dist/BottomSheet/BottomSheet.d.ts +66 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheet.js +381 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +57 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetPanel.js +395 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
- package/dist/BottomSheet/index.d.ts +11 -0
- package/dist/BottomSheet/index.d.ts.map +1 -0
- package/dist/BottomSheet/index.js +12 -0
- package/dist/BottomSheet/snapOffsets.d.ts +72 -0
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
- package/dist/BottomSheet/snapOffsets.js +130 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts +25 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
- package/dist/BottomSheet/useMobileKeyboard.js +384 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +170 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +881 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +3 -4
- package/dist/Chat/ChatTokenizedText.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +98 -31
- package/dist/ComplexSelector/index.d.ts +2 -2
- package/dist/ComplexSelector/index.d.ts.map +1 -1
- package/dist/ComplexSelector/index.js +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +9 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +31 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +55 -12
- package/dist/MobileNav/MobileNav.d.ts +12 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +102 -30
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +10 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +9 -1
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +4 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +2 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +10 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -1
- package/dist/Table/columnUtils.d.ts.map +1 -1
- package/dist/Table/columnUtils.js +4 -1
- package/dist/TextArea/TextArea.d.ts +6 -3
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +17 -6
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +8 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +4 -1
- package/dist/TreeList/TreeList.js +2 -1
- package/dist/Typeahead/BaseTypeahead.js +1 -1
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +10 -1
- package/dist/astryx.css +25 -2
- package/dist/hooks/index.d.ts +8 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +8 -1
- package/dist/hooks/useFocusTrap.d.ts +0 -11
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +1 -11
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +30 -7
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +36 -47
- package/dist/theme/expandColorScale.d.ts +17 -2
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +63 -33
- package/dist/theme/mergeComponents.d.ts +20 -0
- package/dist/theme/mergeComponents.d.ts.map +1 -0
- package/dist/theme/mergeComponents.js +56 -0
- package/dist/theme/onMediaTokens.d.ts +6 -1
- package/dist/theme/onMediaTokens.d.ts.map +1 -1
- package/dist/theme/onMediaTokens.js +11 -3
- package/dist/utils/characters.d.ts +27 -0
- package/dist/utils/characters.d.ts.map +1 -0
- package/dist/utils/characters.js +83 -0
- package/dist/utils/ime.d.ts +5 -0
- package/dist/utils/ime.d.ts.map +1 -0
- package/dist/utils/ime.js +63 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/package.json +9 -8
- package/src/Avatar/Avatar.test.tsx +6 -1
- package/src/Avatar/Avatar.tsx +3 -21
- package/src/Banner/Banner.doc.mjs +12 -10
- package/src/Banner/Banner.test.tsx +108 -0
- package/src/Banner/Banner.tsx +124 -38
- package/src/Banner/index.ts +15 -13
- package/src/BottomSheet/BottomSheet.doc.mjs +266 -0
- package/src/BottomSheet/BottomSheet.test.tsx +1865 -0
- package/src/BottomSheet/BottomSheet.tsx +539 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +542 -0
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
- package/src/BottomSheet/index.ts +15 -0
- package/src/BottomSheet/snapOffsets.test.ts +187 -0
- package/src/BottomSheet/snapOffsets.ts +159 -0
- package/src/BottomSheet/useMobileKeyboard.ts +566 -0
- package/src/BottomSheet/useSheetGestures.test.ts +833 -0
- package/src/BottomSheet/useSheetGestures.ts +1333 -0
- package/src/Chat/ChatComposerInput.tsx +3 -4
- package/src/Chat/ChatTokenizedText.tsx +1 -1
- package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
- package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
- package/src/ComplexSelector/ComplexSelector.tsx +156 -28
- package/src/ComplexSelector/index.ts +3 -1
- package/src/ContextMenu/ContextMenu.tsx +4 -2
- package/src/DateInput/DateInput.test.tsx +21 -0
- package/src/DateInput/DateInput.tsx +9 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
- package/src/DateTimeInput/DateTimeInput.tsx +39 -2
- package/src/Dialog/Dialog.tsx +2 -1
- package/src/Markdown/parser.test.ts +53 -0
- package/src/Markdown/parser.ts +53 -12
- package/src/MobileNav/MobileNav.tsx +128 -37
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
- package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
- package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
- package/src/MultiSelector/MultiSelector.test.tsx +31 -0
- package/src/MultiSelector/MultiSelector.tsx +10 -1
- package/src/NumberInput/NumberInput.test.tsx +18 -0
- package/src/NumberInput/NumberInput.tsx +9 -1
- package/src/PowerSearch/PowerSearch.test.tsx +48 -3
- package/src/PowerSearch/PowerSearch.tsx +4 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/PowerSearch/formatFilterValue.test.ts +22 -0
- package/src/PowerSearch/formatFilterValue.ts +2 -4
- package/src/Selector/Selector.test.tsx +33 -0
- package/src/Selector/Selector.tsx +10 -1
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +7 -0
- package/src/Table/Table.test.tsx +6 -0
- package/src/Table/columnUtils.ts +3 -1
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextArea/TextArea.test.tsx +72 -0
- package/src/TextArea/TextArea.tsx +26 -8
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +8 -0
- package/src/Tooltip/useTooltip.tsx +4 -1
- package/src/TreeList/TreeList.doc.mjs +2 -2
- package/src/TreeList/TreeList.tsx +1 -1
- package/src/Typeahead/BaseTypeahead.tsx +1 -1
- package/src/Typeahead/Typeahead.test.tsx +35 -0
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/stubMatchMedia.test.ts +79 -0
- package/src/__tests__/stubMatchMedia.ts +95 -0
- package/src/hooks/index.ts +9 -5
- package/src/hooks/useFocusTrap.ts +1 -14
- package/src/hooks/useInputStatusIcon.test.tsx +19 -0
- package/src/hooks/useInputStatusIcon.tsx +7 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.test.ts +127 -0
- package/src/theme/defineTheme.ts +70 -55
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/theme/extensibleAxes.test.ts +365 -0
- package/src/theme/mergeComponents.ts +59 -0
- package/src/theme/onMediaTokens.ts +9 -2
- package/src/utils/characters.test.ts +141 -0
- package/src/utils/characters.ts +88 -0
- package/src/utils/ime.test.ts +32 -0
- package/src/utils/ime.ts +69 -0
- package/src/utils/index.ts +4 -0
- package/dist/astryx.umd.js +0 -147
- package/dist/astryx.umd.js.map +0 -7
package/src/Banner/Banner.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Banner.tsx
|
|
7
|
-
* @input Uses React useState, Button, Icon (with registry string names), StyleX
|
|
7
|
+
* @input Uses React useState/useRef/useId, Button, Icon (with registry string names), StyleX
|
|
8
8
|
* @output Exports Banner component, BannerProps, BannerStatus, BannerContainer types
|
|
9
9
|
* @position Core implementation; consumed by index.ts, tested by Banner.test.tsx
|
|
10
10
|
*
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* - Each visual area owns its own border-radius (no overflow:clip on the container)
|
|
20
20
|
* - When children are provided, a collapse/expand toggle button appears in the end area
|
|
21
21
|
*
|
|
22
|
+
* A status added through `BannerStatusMap` augmentation has no entry in the
|
|
23
|
+
* status lookups, so it renders with no status fill, no default glyph and the
|
|
24
|
+
* polite `role="status"` rather than losing its ARIA role entirely.
|
|
25
|
+
*
|
|
22
26
|
* Title and description render as <div> (not <p>): they accept arbitrary
|
|
23
27
|
* ReactNode content, and <p> cannot legally contain block-level children
|
|
24
28
|
* (the HTML parser reparents them, desyncing SSR markup from the hydrated
|
|
@@ -34,12 +38,12 @@
|
|
|
34
38
|
* - /packages/cli/assets/templates/blocks/components/Banner/ (showcase blocks)
|
|
35
39
|
*/
|
|
36
40
|
|
|
37
|
-
import {useId, useState, type ReactNode} from 'react';
|
|
41
|
+
import {useId, useRef, useState, type ReactNode} from 'react';
|
|
38
42
|
import * as stylex from '@stylexjs/stylex';
|
|
39
43
|
import type {BaseProps} from '../BaseProps';
|
|
40
44
|
import {Button} from '../Button';
|
|
41
45
|
import {Icon} from '../Icon';
|
|
42
|
-
import type {IconName} from '../Icon';
|
|
46
|
+
import type {IconName, IconColor} from '../Icon';
|
|
43
47
|
import {
|
|
44
48
|
colorVars,
|
|
45
49
|
spacingVars,
|
|
@@ -51,7 +55,7 @@ import {
|
|
|
51
55
|
easeVars,
|
|
52
56
|
shadowVars,
|
|
53
57
|
} from '../theme/tokens.stylex';
|
|
54
|
-
import {mergeProps} from '../utils';
|
|
58
|
+
import {composeEventHandlers, isRenderable, mergeProps} from '../utils';
|
|
55
59
|
import type {Elevation} from '../utils/types';
|
|
56
60
|
import {edgeCompSlot} from '../Layout/edgeCompensation.stylex';
|
|
57
61
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -112,6 +116,9 @@ export interface BannerProps extends BaseProps<HTMLDivElement> {
|
|
|
112
116
|
* Action button rendered in the header area (end-aligned).
|
|
113
117
|
* Typically an Button with a secondary or ghost variant.
|
|
114
118
|
*
|
|
119
|
+
* When the header is too narrow to hold the actions and a readable text
|
|
120
|
+
* column, the whole end area wraps to its own row below the text.
|
|
121
|
+
*
|
|
115
122
|
* @example
|
|
116
123
|
* ```
|
|
117
124
|
* endContent={<Button label="Retry" variant="ghost" onClick={handleRetry} />}
|
|
@@ -147,37 +154,38 @@ export interface BannerProps extends BaseProps<HTMLDivElement> {
|
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
// =============================================================================
|
|
150
|
-
// Status
|
|
157
|
+
// Status lookups
|
|
151
158
|
// =============================================================================
|
|
152
159
|
|
|
153
|
-
|
|
160
|
+
// `BannerStatus` is `keyof BannerStatusMap`, and index.ts documents augmenting
|
|
161
|
+
// that interface to add a status. Every lookup below is therefore partial: an
|
|
162
|
+
// augmented status the library has never heard of falls through to the base
|
|
163
|
+
// treatment (no status fill, no glyph, the polite role) instead of resolving to
|
|
164
|
+
// `undefined` and dropping the ARIA role along with it.
|
|
165
|
+
|
|
166
|
+
const defaultIconNames: Partial<Record<BannerStatus, IconName>> = {
|
|
154
167
|
info: 'info',
|
|
155
168
|
warning: 'warning',
|
|
156
169
|
error: 'error',
|
|
157
170
|
success: 'success',
|
|
158
171
|
};
|
|
159
172
|
|
|
160
|
-
|
|
161
|
-
// Status → ARIA role mapping
|
|
162
|
-
// =============================================================================
|
|
163
|
-
|
|
164
|
-
const statusRole: Record<BannerStatus, 'alert' | 'status'> = {
|
|
173
|
+
const statusRole: Partial<Record<BannerStatus, 'alert' | 'status'>> = {
|
|
165
174
|
info: 'status',
|
|
166
175
|
warning: 'alert',
|
|
167
176
|
error: 'alert',
|
|
168
177
|
success: 'status',
|
|
169
178
|
};
|
|
170
179
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// =============================================================================
|
|
180
|
+
/** An unknown status is not urgent by definition, so it announces politely. */
|
|
181
|
+
const FALLBACK_ROLE = 'status';
|
|
174
182
|
|
|
175
|
-
const statusIconColor = {
|
|
183
|
+
const statusIconColor: Partial<Record<BannerStatus, IconColor>> = {
|
|
176
184
|
info: 'accent',
|
|
177
185
|
warning: 'warning',
|
|
178
186
|
error: 'error',
|
|
179
187
|
success: 'success',
|
|
180
|
-
}
|
|
188
|
+
};
|
|
181
189
|
|
|
182
190
|
// =============================================================================
|
|
183
191
|
// Styles
|
|
@@ -193,24 +201,28 @@ const styles = stylex.create({
|
|
|
193
201
|
// When elevated, a card-container banner rounds its root so the shadow
|
|
194
202
|
// follows the same silhouette as the header/content border-radius.
|
|
195
203
|
rootElevatedCard: {
|
|
196
|
-
borderRadius: radiusVars['--radius-container']
|
|
204
|
+
borderRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
197
205
|
},
|
|
198
206
|
// Header area — colored status background with icon, title, description, actions
|
|
199
207
|
// This is the primary theme target ('banner')
|
|
200
208
|
header: {
|
|
201
209
|
display: 'flex',
|
|
202
210
|
alignItems: 'flex-start',
|
|
203
|
-
|
|
211
|
+
flexWrap: 'wrap',
|
|
212
|
+
columnGap: spacingVars['--spacing-2'],
|
|
213
|
+
// The end area carries a -4px block margin (see endArea), so the wrapped
|
|
214
|
+
// row reads as one step of spacing rather than two.
|
|
215
|
+
rowGap: spacingVars['--spacing-3'],
|
|
204
216
|
paddingBlock: spacingVars['--spacing-3'],
|
|
205
217
|
paddingInline: spacingVars['--spacing-4'],
|
|
206
218
|
},
|
|
207
219
|
// Border-radius for card container: all corners when standalone, top-only when content is visible
|
|
208
220
|
headerCardStandalone: {
|
|
209
|
-
borderRadius: radiusVars['--radius-container']
|
|
221
|
+
borderRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
210
222
|
},
|
|
211
223
|
headerCardWithContent: {
|
|
212
|
-
borderStartStartRadius: radiusVars['--radius-container']
|
|
213
|
-
borderStartEndRadius: radiusVars['--radius-container']
|
|
224
|
+
borderStartStartRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
225
|
+
borderStartEndRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
214
226
|
borderEndStartRadius: 0,
|
|
215
227
|
borderEndEndRadius: 0,
|
|
216
228
|
},
|
|
@@ -226,6 +238,15 @@ const styles = stylex.create({
|
|
|
226
238
|
flex: 1,
|
|
227
239
|
minWidth: 0,
|
|
228
240
|
},
|
|
241
|
+
// The wrap threshold, applied only when there is `endContent` to wrap. Flex
|
|
242
|
+
// breaks a line when the items no longer fit at their base size, so this
|
|
243
|
+
// basis is what moves the end area to its own row instead of letting it hold
|
|
244
|
+
// the header and squeeze the title down to one word per line. In rem so it
|
|
245
|
+
// tracks the user's font size. The dismiss and expand controls alone are
|
|
246
|
+
// narrow enough never to need it.
|
|
247
|
+
headerContentWithEndContent: {
|
|
248
|
+
flexBasis: '8rem',
|
|
249
|
+
},
|
|
229
250
|
title: {
|
|
230
251
|
margin: 0,
|
|
231
252
|
fontFamily: 'inherit',
|
|
@@ -233,6 +254,10 @@ const styles = stylex.create({
|
|
|
233
254
|
fontWeight: fontWeightVars['--font-weight-semibold'],
|
|
234
255
|
lineHeight: typeScaleVars['--text-label-leading'],
|
|
235
256
|
color: colorVars['--color-text-primary'],
|
|
257
|
+
// A single unbroken token (a URL, an ID, a German compound) otherwise sets
|
|
258
|
+
// the flex item's min-content width and pushes the page into horizontal
|
|
259
|
+
// scrolling at 320px, which is a WCAG 1.4.10 reflow failure.
|
|
260
|
+
overflowWrap: 'anywhere',
|
|
236
261
|
},
|
|
237
262
|
description: {
|
|
238
263
|
margin: 0,
|
|
@@ -241,6 +266,7 @@ const styles = stylex.create({
|
|
|
241
266
|
fontWeight: fontWeightVars['--font-weight-normal'],
|
|
242
267
|
lineHeight: typeScaleVars['--text-supporting-leading'],
|
|
243
268
|
color: colorVars['--color-text-secondary'],
|
|
269
|
+
overflowWrap: 'anywhere',
|
|
244
270
|
},
|
|
245
271
|
iconWrapper: {
|
|
246
272
|
display: 'flex',
|
|
@@ -250,8 +276,13 @@ const styles = stylex.create({
|
|
|
250
276
|
endArea: {
|
|
251
277
|
display: 'flex',
|
|
252
278
|
alignItems: 'center',
|
|
279
|
+
flexWrap: 'wrap',
|
|
280
|
+
justifyContent: 'flex-end',
|
|
253
281
|
gap: spacingVars['--spacing-2'],
|
|
254
282
|
flexShrink: 0,
|
|
283
|
+
// Bounded so a group of actions wider than the row wraps within itself
|
|
284
|
+
// rather than pushing the banner past the viewport (WCAG 1.4.10).
|
|
285
|
+
maxWidth: '100%',
|
|
255
286
|
marginInlineStart: 'auto',
|
|
256
287
|
marginBlock: `calc(-1 * (${spacingVars['--spacing-3']} - ${spacingVars['--spacing-2']}))`,
|
|
257
288
|
},
|
|
@@ -262,17 +293,17 @@ const styles = stylex.create({
|
|
|
262
293
|
paddingInline: spacingVars['--spacing-4'],
|
|
263
294
|
borderInlineStartWidth: borderVars['--border-width'],
|
|
264
295
|
borderInlineEndWidth: borderVars['--border-width'],
|
|
265
|
-
|
|
296
|
+
borderBlockEndWidth: borderVars['--border-width'],
|
|
266
297
|
borderInlineStartStyle: 'solid',
|
|
267
298
|
borderInlineEndStyle: 'solid',
|
|
268
|
-
|
|
299
|
+
borderBlockEndStyle: 'solid',
|
|
269
300
|
borderInlineStartColor: colorVars['--color-border'],
|
|
270
301
|
borderInlineEndColor: colorVars['--color-border'],
|
|
271
|
-
|
|
302
|
+
borderBlockEndColor: colorVars['--color-border'],
|
|
272
303
|
},
|
|
273
304
|
contentAreaCard: {
|
|
274
|
-
borderEndStartRadius: radiusVars['--radius-container']
|
|
275
|
-
borderEndEndRadius: radiusVars['--radius-container']
|
|
305
|
+
borderEndStartRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
306
|
+
borderEndEndRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
276
307
|
},
|
|
277
308
|
// Applied to the chevron <Icon> itself (via `xstyle`) rather than a wrapper,
|
|
278
309
|
// so the element that rotates is the element a theme targets.
|
|
@@ -304,6 +335,17 @@ const statusStyles = stylex.create({
|
|
|
304
335
|
},
|
|
305
336
|
});
|
|
306
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Narrows an augmented `BannerStatus` to one the style map actually carries,
|
|
340
|
+
* so an unknown status renders with no status fill rather than crashing the
|
|
341
|
+
* lookup. StyleX style maps cannot be declared `Partial`, hence the guard.
|
|
342
|
+
*/
|
|
343
|
+
function hasStatusStyle(
|
|
344
|
+
status: BannerStatus,
|
|
345
|
+
): status is BannerStatus & keyof typeof statusStyles {
|
|
346
|
+
return Object.prototype.hasOwnProperty.call(statusStyles, status);
|
|
347
|
+
}
|
|
348
|
+
|
|
307
349
|
// Resting elevation for the banner. Applied to the root so the shadow wraps
|
|
308
350
|
// the whole banner (header + optional content). 'none' is the default and
|
|
309
351
|
// leaves the layout-only root untouched. For the `card` container the root is
|
|
@@ -381,28 +423,63 @@ export function Banner({
|
|
|
381
423
|
className,
|
|
382
424
|
style,
|
|
383
425
|
ref,
|
|
426
|
+
onFocusCapture,
|
|
427
|
+
onPointerDownCapture,
|
|
384
428
|
...rest
|
|
385
429
|
}: BannerProps) {
|
|
386
430
|
const t = useTranslator();
|
|
387
431
|
const [isDismissed, setIsDismissed] = useState(false);
|
|
388
432
|
const [isExpanded, setIsExpanded] = useState(defaultIsExpanded);
|
|
433
|
+
// The element focus came from before it entered the banner. Dismissing
|
|
434
|
+
// unmounts the whole banner, dismiss button included, so without a handoff
|
|
435
|
+
// the browser drops focus to <body> and a keyboard user loses their place.
|
|
436
|
+
// ToastViewport makes the same handoff when a focused toast is dismissed.
|
|
437
|
+
const focusOriginRef = useRef<HTMLElement | null>(null);
|
|
389
438
|
// Links the expand/collapse toggle to the content region it shows/hides so
|
|
390
439
|
// assistive tech can move from the button to its controlled content
|
|
391
440
|
// (disclosure pattern). The region is conditionally rendered, so aria-controls
|
|
392
441
|
// below is set only while it's mounted to avoid a dangling reference.
|
|
393
442
|
const contentId = useId();
|
|
394
443
|
const defaultIconName = defaultIconNames[status];
|
|
395
|
-
const role = statusRole[status];
|
|
444
|
+
const role = statusRole[status] ?? FALLBACK_ROLE;
|
|
396
445
|
const iconColor = statusIconColor[status];
|
|
397
|
-
const hasChildren = children
|
|
446
|
+
const hasChildren = isRenderable(children);
|
|
398
447
|
|
|
399
448
|
if (isDismissed) {
|
|
400
449
|
return null;
|
|
401
450
|
}
|
|
402
451
|
|
|
452
|
+
// `focusin` reports the element focus came *from* as relatedTarget; on
|
|
453
|
+
// pointerdown focus has not moved yet, so document.activeElement is it.
|
|
454
|
+
const rememberFocusOrigin = (candidate: EventTarget | null, root: Node) => {
|
|
455
|
+
if (
|
|
456
|
+
candidate instanceof HTMLElement &&
|
|
457
|
+
candidate !== document.body &&
|
|
458
|
+
!root.contains(candidate)
|
|
459
|
+
) {
|
|
460
|
+
focusOriginRef.current = candidate;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const handleFocusCapture = (event: React.FocusEvent<HTMLDivElement>) => {
|
|
465
|
+
rememberFocusOrigin(event.relatedTarget, event.currentTarget);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const handlePointerDownCapture = (
|
|
469
|
+
event: React.PointerEvent<HTMLDivElement>,
|
|
470
|
+
) => {
|
|
471
|
+
rememberFocusOrigin(document.activeElement, event.currentTarget);
|
|
472
|
+
};
|
|
473
|
+
|
|
403
474
|
const handleDismiss = () => {
|
|
475
|
+
const origin = focusOriginRef.current;
|
|
404
476
|
setIsDismissed(true);
|
|
405
477
|
onDismiss?.();
|
|
478
|
+
// Move focus before React removes the subtree, so the browser never has a
|
|
479
|
+
// frame where the focused node is gone.
|
|
480
|
+
if (origin?.isConnected) {
|
|
481
|
+
origin.focus();
|
|
482
|
+
}
|
|
406
483
|
};
|
|
407
484
|
|
|
408
485
|
const handleToggleExpand = () => {
|
|
@@ -410,11 +487,11 @@ export function Banner({
|
|
|
410
487
|
};
|
|
411
488
|
|
|
412
489
|
// Show the end area if there are actions, dismiss, or a collapsible toggle
|
|
413
|
-
const showEndArea = endContent
|
|
490
|
+
const showEndArea = isRenderable(endContent) || isDismissable || hasChildren;
|
|
414
491
|
// Center items vertically when there's only a title (no description)
|
|
415
492
|
// and the banner has action buttons
|
|
416
|
-
const hasActions = endContent
|
|
417
|
-
const isSingleLine = description
|
|
493
|
+
const hasActions = isRenderable(endContent) || isDismissable;
|
|
494
|
+
const isSingleLine = !isRenderable(description) && hasActions;
|
|
418
495
|
|
|
419
496
|
const showContent = hasChildren && isExpanded;
|
|
420
497
|
const isCard = container === 'card';
|
|
@@ -423,6 +500,11 @@ export function Banner({
|
|
|
423
500
|
<div
|
|
424
501
|
ref={ref}
|
|
425
502
|
role={role}
|
|
503
|
+
onFocusCapture={composeEventHandlers(onFocusCapture, handleFocusCapture)}
|
|
504
|
+
onPointerDownCapture={composeEventHandlers(
|
|
505
|
+
onPointerDownCapture,
|
|
506
|
+
handlePointerDownCapture,
|
|
507
|
+
)}
|
|
426
508
|
{...mergeProps(
|
|
427
509
|
stylex.props(
|
|
428
510
|
styles.root,
|
|
@@ -441,7 +523,7 @@ export function Banner({
|
|
|
441
523
|
stylex.props(
|
|
442
524
|
styles.header,
|
|
443
525
|
isSingleLine && styles.headerCentered,
|
|
444
|
-
statusStyles[status],
|
|
526
|
+
hasStatusStyle(status) && statusStyles[status],
|
|
445
527
|
isCard &&
|
|
446
528
|
(showContent
|
|
447
529
|
? styles.headerCardWithContent
|
|
@@ -454,16 +536,16 @@ export function Banner({
|
|
|
454
536
|
overrides reach it via inheritance). The wrapper itself stays
|
|
455
537
|
layout-only. */}
|
|
456
538
|
<div
|
|
457
|
-
{...(icon
|
|
539
|
+
{...(isRenderable(icon)
|
|
458
540
|
? mergeProps(
|
|
459
541
|
themeProps('banner-icon', {status}),
|
|
460
542
|
stylex.props(styles.iconWrapper),
|
|
461
543
|
)
|
|
462
544
|
: stylex.props(styles.iconWrapper))}
|
|
463
545
|
aria-hidden="true">
|
|
464
|
-
{icon
|
|
546
|
+
{isRenderable(icon) ? (
|
|
465
547
|
icon
|
|
466
|
-
) : (
|
|
548
|
+
) : defaultIconName != null ? (
|
|
467
549
|
// Applied to the status <Icon> itself rather than the wrapper, so
|
|
468
550
|
// the element that paints the glyph is the element a theme
|
|
469
551
|
// targets — a 'banner-icon' 'status:X' color override beats the
|
|
@@ -474,11 +556,15 @@ export function Banner({
|
|
|
474
556
|
color={iconColor}
|
|
475
557
|
{...themeProps('banner-icon', {status})}
|
|
476
558
|
/>
|
|
477
|
-
)}
|
|
559
|
+
) : null}
|
|
478
560
|
</div>
|
|
479
|
-
<div
|
|
561
|
+
<div
|
|
562
|
+
{...stylex.props(
|
|
563
|
+
styles.headerContent,
|
|
564
|
+
isRenderable(endContent) && styles.headerContentWithEndContent,
|
|
565
|
+
)}>
|
|
480
566
|
<div {...stylex.props(styles.title)}>{title}</div>
|
|
481
|
-
{description
|
|
567
|
+
{isRenderable(description) && (
|
|
482
568
|
<div {...stylex.props(styles.description)}>{description}</div>
|
|
483
569
|
)}
|
|
484
570
|
</div>
|
package/src/Banner/index.ts
CHANGED
|
@@ -11,19 +11,6 @@
|
|
|
11
11
|
* SYNC: When modified, update this header and /packages/core/src/Banner/Banner.doc.mjs
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* Extensible status map for Banner.
|
|
16
|
-
*
|
|
17
|
-
* Theme packages can add custom statuses via TypeScript module augmentation:
|
|
18
|
-
* @example
|
|
19
|
-
* ```
|
|
20
|
-
* declare module '@astryxdesign/core/Banner' {
|
|
21
|
-
* interface BannerStatusMap {
|
|
22
|
-
* 'neutral': true;
|
|
23
|
-
* }
|
|
24
|
-
* }
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
14
|
/**
|
|
28
15
|
* Extensible container map for Banner.
|
|
29
16
|
*
|
|
@@ -42,6 +29,21 @@ export interface BannerContainerMap {
|
|
|
42
29
|
section: true;
|
|
43
30
|
}
|
|
44
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Extensible status map for Banner.
|
|
34
|
+
*
|
|
35
|
+
* Theme packages can add custom statuses via TypeScript module augmentation.
|
|
36
|
+
* A status the library does not know falls through to the base treatment: no
|
|
37
|
+
* status fill, no default glyph, and `role="status"`.
|
|
38
|
+
* @example
|
|
39
|
+
* ```
|
|
40
|
+
* declare module '@astryxdesign/core/Banner' {
|
|
41
|
+
* interface BannerStatusMap {
|
|
42
|
+
* 'neutral': true;
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
45
47
|
export interface BannerStatusMap {
|
|
46
48
|
info: true;
|
|
47
49
|
warning: true;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
|
|
3
|
+
|
|
4
|
+
export const docs = {
|
|
5
|
+
name: 'BottomSheet',
|
|
6
|
+
displayName: 'Bottom Sheet',
|
|
7
|
+
group: 'BottomSheet',
|
|
8
|
+
category: 'Overlay',
|
|
9
|
+
keywords: [
|
|
10
|
+
'bottom sheet',
|
|
11
|
+
'sheet',
|
|
12
|
+
'mobile',
|
|
13
|
+
'touch',
|
|
14
|
+
'drag',
|
|
15
|
+
'swipe',
|
|
16
|
+
'dismiss',
|
|
17
|
+
'grab handle',
|
|
18
|
+
'dialog',
|
|
19
|
+
'overlay',
|
|
20
|
+
'modal',
|
|
21
|
+
'form',
|
|
22
|
+
'mobile keyboard',
|
|
23
|
+
'visual viewport',
|
|
24
|
+
],
|
|
25
|
+
playground: {
|
|
26
|
+
overlay: true,
|
|
27
|
+
defaults: {
|
|
28
|
+
isOpen: false,
|
|
29
|
+
label: 'Filters',
|
|
30
|
+
height: 'hug',
|
|
31
|
+
children: {
|
|
32
|
+
__element: 'Section',
|
|
33
|
+
props: {padding: 4},
|
|
34
|
+
children: {
|
|
35
|
+
__element: 'VStack',
|
|
36
|
+
props: {gap: 2},
|
|
37
|
+
children: [
|
|
38
|
+
{
|
|
39
|
+
__element: 'Heading',
|
|
40
|
+
props: {level: 3},
|
|
41
|
+
children: 'Filters',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
__element: 'Text',
|
|
45
|
+
props: {type: 'body'},
|
|
46
|
+
children: 'Adjust the properties below, then open the preview.',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
theming: {
|
|
54
|
+
targets: [{className: 'astryx-bottom-sheet', visualProps: []}],
|
|
55
|
+
},
|
|
56
|
+
description:
|
|
57
|
+
"A mobile touch sheet that rises from the bottom edge, with animated entrance and exit, a grab handle, drag-to-resize snap points, and purpose-controlled dismissal. A standalone sheet owns a native <dialog>; inside BottomSheetSwitcher it renders a panel in the switcher's shared dialog. In both modes, ref and shared DOM props target the visual panel <div>.",
|
|
58
|
+
props: [
|
|
59
|
+
{
|
|
60
|
+
name: 'isOpen',
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
description:
|
|
63
|
+
'Whether a standalone sheet is open. Fully controlled; pair with onOpenChange. Omit inside BottomSheetSwitcher.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'onOpenChange',
|
|
67
|
+
type: '(isOpen: boolean) => void',
|
|
68
|
+
description:
|
|
69
|
+
'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.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'purpose',
|
|
73
|
+
type: "'required' | 'form' | 'info'",
|
|
74
|
+
description:
|
|
75
|
+
"Controls implicit dismissal behavior, matching Dialog. info allows Escape, scrim click, and swipe-to-dismiss. form protects entered data by blocking scrim click and swipe while allowing Escape. required blocks every implicit dismissal path and uses role='alertdialog'. Explicit controls may still update the controlled state. Works for standalone and BottomSheetSwitcher-managed sheets.",
|
|
76
|
+
default: "'info'",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'sheetId',
|
|
80
|
+
type: 'string',
|
|
81
|
+
description:
|
|
82
|
+
'Unique ID for this sheet inside BottomSheetSwitcher. The switcher opens it when activeSheet matches. Omit isOpen and onOpenChange when sheetId is used.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'label',
|
|
86
|
+
type: 'string',
|
|
87
|
+
description:
|
|
88
|
+
'Accessible label for the sheet. Required; the sheet has no built-in heading to derive a name from.',
|
|
89
|
+
required: true,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'children',
|
|
93
|
+
type: 'ReactNode',
|
|
94
|
+
description:
|
|
95
|
+
"Sheet content, rendered below the grab handle in a scrollable area. If it includes a text-entry control that can bring up the mobile keyboard, use height='tall' and keep the sheet fully expanded while editing.",
|
|
96
|
+
required: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'height',
|
|
100
|
+
type: "'hug' | 'capped' | 'tall' | number | string",
|
|
101
|
+
description:
|
|
102
|
+
"How tall the sheet is. Named budgets: 'hug' fits its content up to 92% of the viewport, 'capped' is a scrolling mid-height panel (~62%), and 'tall' is a pinned near-full panel (~92%) for content that streams in. Or pass a number (px) / CSS length for a custom budget. The user can drag between snap points regardless. On shorter viewports the sheet fills the available height. Only a fully expanded 'tall' sheet provides mobile-keyboard accommodation; shorter Tall detents, Hug, Capped, numeric, and CSS-length heights do not move or add keyboard scroll space.",
|
|
103
|
+
default: "'capped'",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'hasScrim',
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
description:
|
|
109
|
+
"For a standalone BottomSheet, whether to render a scrim, the semi-transparent overlay that covers and blocks the background. true (default) uses showModal(): top layer, focus trap, ::backdrop scrim, body scroll lock, and tap-scrim-to-dismiss when purpose='info', with the background inert. false uses show() with no scrim, leaving the page behind interactive and scrollable. For a multi-step flow, configure hasScrim on BottomSheetSwitcher instead; it owns one shared dialog across every child.",
|
|
110
|
+
default: 'true',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
usage: {
|
|
114
|
+
description:
|
|
115
|
+
'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.',
|
|
116
|
+
bestPractices: [
|
|
117
|
+
{
|
|
118
|
+
guidance: true,
|
|
119
|
+
description:
|
|
120
|
+
'Use for mobile-first surfaces (filters, share sheets, quick actions) where the content should rise from the bottom edge.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
guidance: true,
|
|
124
|
+
description:
|
|
125
|
+
"Pick the starting height that fits the content: 'hug' for short bounded content, 'capped' for lists, and 'tall' for forms or streaming/resizing content.",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
guidance: true,
|
|
129
|
+
description:
|
|
130
|
+
"Use purpose='form' to protect entered data from scrim clicks and swipes while keeping Escape available; reserve purpose='required' for flows that must end through an explicit action.",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
guidance: false,
|
|
134
|
+
description:
|
|
135
|
+
"Don't make the sheet content overly long. Consider breaking it into steps and using Bottom Sheet Switcher.",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
examples: [
|
|
140
|
+
{
|
|
141
|
+
label: 'Basic',
|
|
142
|
+
code: `const [isOpen, setIsOpen] = useState(false);
|
|
143
|
+
<BottomSheet
|
|
144
|
+
isOpen={isOpen}
|
|
145
|
+
onOpenChange={setIsOpen}
|
|
146
|
+
label="Filters">
|
|
147
|
+
<FilterControls />
|
|
148
|
+
</BottomSheet>`,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
label: 'Tall sheet (a list)',
|
|
152
|
+
code: `const [isOpen, setIsOpen] = useState(false);
|
|
153
|
+
<BottomSheet
|
|
154
|
+
isOpen={isOpen}
|
|
155
|
+
onOpenChange={setIsOpen}
|
|
156
|
+
label="Nearby places"
|
|
157
|
+
height="tall">
|
|
158
|
+
<PlaceList />
|
|
159
|
+
</BottomSheet>`,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
label: 'Hug height (fits content)',
|
|
163
|
+
code: `const [isOpen, setIsOpen] = useState(false);
|
|
164
|
+
<BottomSheet
|
|
165
|
+
isOpen={isOpen}
|
|
166
|
+
onOpenChange={setIsOpen}
|
|
167
|
+
label="Share"
|
|
168
|
+
height="hug">
|
|
169
|
+
<ShareActions />
|
|
170
|
+
</BottomSheet>`,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
label: 'Long scrolling content',
|
|
174
|
+
code: `const [isOpen, setIsOpen] = useState(false);
|
|
175
|
+
<BottomSheet
|
|
176
|
+
isOpen={isOpen}
|
|
177
|
+
onOpenChange={setIsOpen}
|
|
178
|
+
label="Release notes"
|
|
179
|
+
height="hug">
|
|
180
|
+
<ReleaseNotes />
|
|
181
|
+
</BottomSheet>`,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
label: 'Multi-step flow (one sheet at a time)',
|
|
185
|
+
code: `const [activeSheet, setActiveSheet] = useState(null);
|
|
186
|
+
<>
|
|
187
|
+
<Button label="Start" onClick={() => setActiveSheet('details')} />
|
|
188
|
+
<BottomSheetSwitcher
|
|
189
|
+
activeSheet={activeSheet}
|
|
190
|
+
onActiveSheetChange={setActiveSheet}>
|
|
191
|
+
<BottomSheet sheetId="details" label="Details">
|
|
192
|
+
<Button label="Continue" onClick={() => setActiveSheet('confirm')} />
|
|
193
|
+
</BottomSheet>
|
|
194
|
+
<BottomSheet sheetId="confirm" label="Confirm">
|
|
195
|
+
<Button label="Back" onClick={() => setActiveSheet('details')} />
|
|
196
|
+
</BottomSheet>
|
|
197
|
+
</BottomSheetSwitcher>
|
|
198
|
+
</>`,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
label: 'Mobile keyboard',
|
|
202
|
+
code: `const [isOpen, setIsOpen] = useState(false);
|
|
203
|
+
<BottomSheet
|
|
204
|
+
isOpen={isOpen}
|
|
205
|
+
onOpenChange={setIsOpen}
|
|
206
|
+
label="Add a comment"
|
|
207
|
+
height="tall">
|
|
208
|
+
<LongCommentForm />
|
|
209
|
+
</BottomSheet>`,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
label: 'No scrim',
|
|
213
|
+
code: `const [isOpen, setIsOpen] = useState(true);
|
|
214
|
+
<BottomSheet
|
|
215
|
+
isOpen={isOpen}
|
|
216
|
+
onOpenChange={setIsOpen}
|
|
217
|
+
label="Nearby places"
|
|
218
|
+
hasScrim={false}>
|
|
219
|
+
<PlaceList />
|
|
220
|
+
</BottomSheet>`,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
label: 'Protect form input',
|
|
224
|
+
code: `const [isOpen, setIsOpen] = useState(false);
|
|
225
|
+
<BottomSheet
|
|
226
|
+
isOpen={isOpen}
|
|
227
|
+
onOpenChange={setIsOpen}
|
|
228
|
+
purpose="form"
|
|
229
|
+
label="Edit profile">
|
|
230
|
+
<ProfileForm onSave={() => setIsOpen(false)} />
|
|
231
|
+
</BottomSheet>`,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
|
|
237
|
+
export const docsDense = {
|
|
238
|
+
description:
|
|
239
|
+
'mobile touch sheet rising from the bottom edge (native <dialog>): grab handle, transform-based drag-to-resize snap points, scrolling area resizes to the snapped visible height on release (the peek detent 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',
|
|
240
|
+
usage: {
|
|
241
|
+
description:
|
|
242
|
+
'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.',
|
|
243
|
+
bestPractices: [
|
|
244
|
+
{
|
|
245
|
+
guidance: true,
|
|
246
|
+
description:
|
|
247
|
+
'Use for mobile-first surfaces (filters, share sheets, quick actions) where the content should rise from the bottom edge.',
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
guidance: true,
|
|
251
|
+
description:
|
|
252
|
+
"Pick the starting height that fits the content: 'hug' for short bounded content, 'capped' for lists, and 'tall' for forms or streaming/resizing content.",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
guidance: true,
|
|
256
|
+
description:
|
|
257
|
+
"Use purpose='form' to protect entered data from scrim clicks and swipes while keeping Escape available; reserve purpose='required' for flows that must end through an explicit action.",
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
guidance: false,
|
|
261
|
+
description:
|
|
262
|
+
"Don't make the sheet content overly long. Consider breaking it into steps and using Bottom Sheet Switcher.",
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
};
|