@astryxdesign/core 0.4.6 → 0.4.7-canary.0b6762d
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 +35 -0
- package/dist/AspectRatio/AspectRatio.d.ts +24 -0
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +23 -4
- package/dist/BottomSheet/BottomSheet.d.ts +1 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +54 -15
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +6 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.js +62 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +10 -4
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +23 -5
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -5
- package/dist/Calendar/Calendar.d.ts +3 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +13 -14
- package/dist/Calendar/getInitialFocusDate.d.ts +46 -0
- package/dist/Calendar/getInitialFocusDate.d.ts.map +1 -0
- package/dist/Calendar/getInitialFocusDate.js +62 -0
- package/dist/Chat/ChatMessageList.d.ts +22 -3
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +6 -3
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +34 -1
- package/dist/Dialog/Dialog.d.ts +7 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +48 -22
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +6 -2
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +10 -3
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +38 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +149 -23
- package/dist/MultiSelector/MultiSelector.d.ts +15 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +17 -6
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +5 -0
- package/dist/Stepper/Step.d.ts +140 -0
- package/dist/Stepper/Step.d.ts.map +1 -0
- package/dist/Stepper/Step.js +1045 -0
- package/dist/Stepper/StepStatus.d.ts +24 -0
- package/dist/Stepper/StepStatus.d.ts.map +1 -0
- package/dist/Stepper/StepStatus.js +1 -0
- package/dist/Stepper/Stepper.d.ts +93 -0
- package/dist/Stepper/Stepper.d.ts.map +1 -0
- package/dist/Stepper/Stepper.js +184 -0
- package/dist/Stepper/StepperContext.d.ts +41 -0
- package/dist/Stepper/StepperContext.d.ts.map +1 -0
- package/dist/Stepper/StepperContext.js +34 -0
- package/dist/Stepper/index.d.ts +8 -0
- package/dist/Stepper/index.d.ts.map +1 -0
- package/dist/Stepper/index.js +7 -0
- package/dist/Stepper/stepper.stylex.d.ts +15 -0
- package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
- package/dist/Stepper/stepper.stylex.js +20 -0
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +5 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +4 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
- package/dist/Table/plugins/pagination/useTablePagination.d.ts +3 -0
- package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/useTablePagination.js +21 -4
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
- package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
- package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
- package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelection.js +19 -5
- package/dist/Table/types.d.ts +22 -4
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +5 -0
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +6 -2
- package/dist/astryx.css +43 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useHotkeys.d.ts.map +1 -1
- package/dist/hooks/useHotkeys.js +10 -3
- package/dist/hooks/useListFocus.d.ts +5 -2
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +12 -6
- package/dist/hooks/useMergedRefs.d.ts +18 -0
- package/dist/hooks/useMergedRefs.d.ts.map +1 -0
- package/dist/hooks/useMergedRefs.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/utils/timeParser.d.ts +1 -1
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +26 -12
- package/locales/en.json +23 -7
- package/locales/pseudo.json +12 -0
- package/package.json +8 -3
- package/src/AspectRatio/AspectRatio.doc.mjs +6 -3
- package/src/AspectRatio/AspectRatio.test.tsx +57 -10
- package/src/AspectRatio/AspectRatio.tsx +40 -1
- package/src/Avatar/Avatar.doc.mjs +2 -1
- package/src/BottomSheet/BottomSheet.test.tsx +123 -0
- package/src/BottomSheet/BottomSheet.tsx +36 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +225 -0
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +82 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +66 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +19 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +13 -0
- package/src/BottomSheet/useSheetGestures.test.ts +27 -0
- package/src/BottomSheet/useSheetGestures.ts +25 -5
- package/src/Breadcrumbs/BreadcrumbItem.tsx +2 -1
- package/src/Button/Button.doc.mjs +22 -0
- package/src/Calendar/Calendar.doc.mjs +4 -3
- package/src/Calendar/Calendar.test.tsx +52 -0
- package/src/Calendar/Calendar.tsx +18 -15
- package/src/Calendar/getInitialFocusDate.test.ts +94 -0
- package/src/Calendar/getInitialFocusDate.ts +93 -0
- package/src/Chat/ChatMessageList.doc.mjs +9 -1
- package/src/Chat/ChatMessageList.test.tsx +46 -0
- package/src/Chat/ChatMessageList.tsx +28 -4
- package/src/CodeBlock/CodeBlock.doc.mjs +6 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
- package/src/DateInput/DateInputTouch.test.tsx +36 -0
- package/src/DateInput/TouchDateField.tsx +35 -1
- package/src/Dialog/Dialog.doc.mjs +8 -3
- package/src/Dialog/Dialog.test.tsx +71 -0
- package/src/Dialog/Dialog.tsx +72 -20
- package/src/Field/Field.doc.mjs +11 -0
- package/src/Heading/Heading.test.tsx +23 -1
- package/src/Heading/Heading.tsx +6 -2
- package/src/Kbd/Kbd.test.tsx +17 -0
- package/src/Kbd/Kbd.tsx +10 -3
- package/src/Link/Link.doc.mjs +11 -0
- package/src/Markdown/index.ts +1 -0
- package/src/Markdown/parser.perf.test.ts +71 -1
- package/src/Markdown/parser.test.ts +145 -2
- package/src/Markdown/parser.ts +208 -22
- package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +10 -0
- package/src/MultiSelector/MultiSelector.test.tsx +105 -0
- package/src/MultiSelector/MultiSelector.tsx +50 -6
- package/src/MultiSelector/index.ts +1 -0
- package/src/NumberInput/NumberInput.doc.mjs +5 -0
- package/src/Popover/Popover.test.tsx +27 -1
- package/src/PowerSearch/PowerSearch.doc.mjs +10 -0
- package/src/Selector/Selector.doc.mjs +11 -0
- package/src/Selector/Selector.test.tsx +21 -0
- package/src/Selector/Selector.tsx +11 -1
- package/src/Stepper/Step.doc.mjs +98 -0
- package/src/Stepper/Step.tsx +1638 -0
- package/src/Stepper/StepStatus.ts +26 -0
- package/src/Stepper/Stepper.doc.mjs +370 -0
- package/src/Stepper/Stepper.test.tsx +1024 -0
- package/src/Stepper/Stepper.tsx +258 -0
- package/src/Stepper/StepperContext.ts +71 -0
- package/src/Stepper/index.ts +18 -0
- package/src/Stepper/stepper.stylex.ts +19 -0
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.test.tsx +21 -4
- package/src/Table/BaseTable.tsx +6 -3
- package/src/Table/Table.test.tsx +35 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +34 -4
- package/src/Table/plugins/pagination/useTablePagination.tsx +35 -3
- package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
- package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
- package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
- package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
- package/src/Table/types.ts +22 -4
- package/src/Table/useBaseTablePlugins.ts +5 -0
- package/src/Table/useTableGroupedRows.doc.mjs +5 -4
- package/src/Table/useTablePagination.doc.mjs +7 -0
- package/src/Table/useTableRowStatus.doc.mjs +1 -1
- package/src/Table/useTableSelection.doc.mjs +31 -0
- package/src/Text/Text.test.tsx +23 -1
- package/src/Text/Text.tsx +6 -2
- package/src/TextInput/TextInput.doc.mjs +10 -0
- package/src/Toast/Toast.doc.mjs +6 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +10 -0
- package/src/Toolbar/Toolbar.doc.mjs +5 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useHotkeys.test.ts +18 -0
- package/src/hooks/useHotkeys.ts +10 -3
- package/src/hooks/useListFocus.doc.mjs +2 -2
- package/src/hooks/useListFocus.test.tsx +65 -3
- package/src/hooks/useListFocus.ts +15 -7
- package/src/hooks/useMergedRefs.doc.mjs +79 -0
- package/src/hooks/useMergedRefs.test.tsx +62 -0
- package/src/hooks/useMergedRefs.ts +36 -0
- package/src/index.ts +1 -0
- package/src/theme/MediaTheme.doc.mjs +5 -5
- package/src/utils/timeParser.test.ts +6 -1
- package/src/utils/timeParser.ts +35 -13
- package/src/__tests__/inputWidthContract.test.tsx +0 -69
|
@@ -334,6 +334,106 @@ describe('BottomSheet', () => {
|
|
|
334
334
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
335
335
|
});
|
|
336
336
|
|
|
337
|
+
it('ignores Escape while an IME composition is active', () => {
|
|
338
|
+
const onOpenChange = vi.fn();
|
|
339
|
+
render(
|
|
340
|
+
<BottomSheet isOpen onOpenChange={onOpenChange} label="Filters">
|
|
341
|
+
<input aria-label="Search" />
|
|
342
|
+
</BottomSheet>,
|
|
343
|
+
);
|
|
344
|
+
const dialog = screen.getByRole('dialog');
|
|
345
|
+
|
|
346
|
+
fireEvent.keyDown(dialog, {key: 'Escape', isComposing: true});
|
|
347
|
+
fireEvent.keyDown(dialog, {key: 'Escape', keyCode: 229});
|
|
348
|
+
|
|
349
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('claims the composing Escape so no native close request follows', () => {
|
|
353
|
+
const onOpenChange = vi.fn();
|
|
354
|
+
render(
|
|
355
|
+
<BottomSheet isOpen onOpenChange={onOpenChange} label="Filters">
|
|
356
|
+
<input aria-label="Search" />
|
|
357
|
+
</BottomSheet>,
|
|
358
|
+
);
|
|
359
|
+
const dialog = screen.getByRole('dialog');
|
|
360
|
+
|
|
361
|
+
// An unclaimed Escape lets the browser raise its own close request, which
|
|
362
|
+
// arrives as `cancel` and dismisses the sheet on the same keypress. The
|
|
363
|
+
// guard has to swallow the composing Escape, not merely skip dismissal.
|
|
364
|
+
const wasNotClaimed = fireEvent.keyDown(dialog, {
|
|
365
|
+
key: 'Escape',
|
|
366
|
+
isComposing: true,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(wasNotClaimed).toBe(false);
|
|
370
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('keeps a form sheet open when Escape cancels a field composition', () => {
|
|
374
|
+
const onOpenChange = vi.fn();
|
|
375
|
+
render(
|
|
376
|
+
<BottomSheet
|
|
377
|
+
isOpen
|
|
378
|
+
purpose="form"
|
|
379
|
+
onOpenChange={onOpenChange}
|
|
380
|
+
label="Edit profile">
|
|
381
|
+
<input aria-label="Name" />
|
|
382
|
+
</BottomSheet>,
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// The reported path: the keydown starts at the composing field and bubbles
|
|
386
|
+
// to the sheet's <dialog>, so the guard has to survive the trip.
|
|
387
|
+
fireEvent.keyDown(screen.getByRole('textbox', {name: 'Name'}), {
|
|
388
|
+
key: 'Escape',
|
|
389
|
+
isComposing: true,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('still dismisses on the Escape after a composition is cancelled', () => {
|
|
396
|
+
const onOpenChange = vi.fn();
|
|
397
|
+
render(
|
|
398
|
+
<BottomSheet isOpen onOpenChange={onOpenChange} label="Filters">
|
|
399
|
+
<input aria-label="Search" />
|
|
400
|
+
</BottomSheet>,
|
|
401
|
+
);
|
|
402
|
+
const dialog = screen.getByRole('dialog');
|
|
403
|
+
|
|
404
|
+
fireEvent.keyDown(dialog, {key: 'Escape', isComposing: true});
|
|
405
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
406
|
+
|
|
407
|
+
fireEvent.keyDown(dialog, {key: 'Escape'});
|
|
408
|
+
|
|
409
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('leaves the IME the composition keys that are not Escape', () => {
|
|
413
|
+
const onOpenChange = vi.fn();
|
|
414
|
+
render(
|
|
415
|
+
<BottomSheet isOpen onOpenChange={onOpenChange} label="Filters">
|
|
416
|
+
<input aria-label="Search" />
|
|
417
|
+
</BottomSheet>,
|
|
418
|
+
);
|
|
419
|
+
const dialog = screen.getByRole('dialog');
|
|
420
|
+
|
|
421
|
+
// Enter commits a candidate and the arrows walk the candidate window. The
|
|
422
|
+
// sheet must claim neither, or the IME loses keys it owns.
|
|
423
|
+
const enterUnclaimed = fireEvent.keyDown(dialog, {
|
|
424
|
+
key: 'Enter',
|
|
425
|
+
isComposing: true,
|
|
426
|
+
});
|
|
427
|
+
const arrowUnclaimed = fireEvent.keyDown(dialog, {
|
|
428
|
+
key: 'ArrowDown',
|
|
429
|
+
isComposing: true,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
expect(enterUnclaimed).toBe(true);
|
|
433
|
+
expect(arrowUnclaimed).toBe(true);
|
|
434
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
435
|
+
});
|
|
436
|
+
|
|
337
437
|
it('requests close when the scrim (dialog element itself) is clicked', () => {
|
|
338
438
|
const onOpenChange = vi.fn();
|
|
339
439
|
render(
|
|
@@ -474,6 +574,29 @@ describe('BottomSheet', () => {
|
|
|
474
574
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
475
575
|
});
|
|
476
576
|
|
|
577
|
+
it('ignores Escape while an IME composition is active', () => {
|
|
578
|
+
const onOpenChange = vi.fn();
|
|
579
|
+
render(
|
|
580
|
+
<BottomSheet
|
|
581
|
+
isOpen
|
|
582
|
+
hasScrim={false}
|
|
583
|
+
onOpenChange={onOpenChange}
|
|
584
|
+
label="Filters">
|
|
585
|
+
<input aria-label="Search" />
|
|
586
|
+
</BottomSheet>,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// Escape is this sheet's only keyboard route out — there is no native
|
|
590
|
+
// close request behind it — so the guard is all that stands between a
|
|
591
|
+
// composing CJK user and a dismissed sheet.
|
|
592
|
+
fireEvent.keyDown(screen.getByRole('dialog'), {
|
|
593
|
+
key: 'Escape',
|
|
594
|
+
isComposing: true,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
598
|
+
});
|
|
599
|
+
|
|
477
600
|
it('still dismisses on a downward swipe past the threshold', () => {
|
|
478
601
|
const onOpenChange = vi.fn();
|
|
479
602
|
render(
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* SYNC: When modified, update these files to stay in sync:
|
|
18
18
|
* - /packages/core/src/BottomSheet/BottomSheetPanel.tsx
|
|
19
|
+
* - /packages/core/src/BottomSheet/BottomSheetEdgeTint.tsx
|
|
19
20
|
* - /packages/core/src/BottomSheet/BottomSheet.doc.mjs
|
|
20
21
|
* - /packages/core/src/BottomSheet/BottomSheet.test.tsx
|
|
21
22
|
* - /packages/core/src/BottomSheet/BottomSheetSwitcher.tsx
|
|
@@ -37,12 +38,13 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
37
38
|
import type {BaseProps} from '../BaseProps';
|
|
38
39
|
import type {DialogPurpose} from '../Dialog';
|
|
39
40
|
import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
|
|
40
|
-
import {useDevWarning, useScrollLock} from '../hooks';
|
|
41
|
+
import {isImeKeyEvent, useDevWarning, useScrollLock} from '../hooks';
|
|
41
42
|
import {
|
|
42
43
|
BottomSheetPanel,
|
|
43
44
|
type BottomSheetPanelMotion,
|
|
44
45
|
type BottomSheetPanelState,
|
|
45
46
|
} from './BottomSheetPanel';
|
|
47
|
+
import {BottomSheetEdgeTint} from './BottomSheetEdgeTint';
|
|
46
48
|
import {
|
|
47
49
|
BottomSheetSwitcherContext,
|
|
48
50
|
type BottomSheetSwitcherContextValue,
|
|
@@ -93,6 +95,20 @@ const styles = stylex.create({
|
|
|
93
95
|
},
|
|
94
96
|
},
|
|
95
97
|
},
|
|
98
|
+
/**
|
|
99
|
+
* The dim leaves with the sheet, on a curve that matches.
|
|
100
|
+
*
|
|
101
|
+
* A fade covers no distance, so the decelerate token front-loads its
|
|
102
|
+
* progress and simply ends it early: `--ease-standard` puts the scrim at 90%
|
|
103
|
+
* faded in 163ms of a 410ms close, leaving an undimmed page under a sheet
|
|
104
|
+
* that is still sliding across it. `linear` spends the duration it is given.
|
|
105
|
+
* Same reasoning the touch date picker's surface swap already carries.
|
|
106
|
+
*/
|
|
107
|
+
scrimClosing: {
|
|
108
|
+
'::backdrop': {
|
|
109
|
+
transitionTimingFunction: 'linear',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
96
112
|
positioner: {
|
|
97
113
|
position: 'absolute',
|
|
98
114
|
insetInline: 0,
|
|
@@ -298,6 +314,9 @@ function StandaloneBottomSheet({
|
|
|
298
314
|
|
|
299
315
|
const handleCancel = useCallback(
|
|
300
316
|
(event: React.SyntheticEvent<HTMLDialogElement>) => {
|
|
317
|
+
// No IME guard here: `cancel` is a plain Event carrying no composition
|
|
318
|
+
// state, and handleKeyDown claims a composing Escape before the browser
|
|
319
|
+
// can raise the close request that would arrive here.
|
|
301
320
|
event.preventDefault();
|
|
302
321
|
dismissOnEscape();
|
|
303
322
|
},
|
|
@@ -305,10 +324,20 @@ function StandaloneBottomSheet({
|
|
|
305
324
|
);
|
|
306
325
|
const handleKeyDown = useCallback(
|
|
307
326
|
(event: React.KeyboardEvent<HTMLDialogElement>) => {
|
|
308
|
-
if (event.key
|
|
309
|
-
|
|
310
|
-
dismissOnEscape();
|
|
327
|
+
if (event.key !== 'Escape') {
|
|
328
|
+
return;
|
|
311
329
|
}
|
|
330
|
+
// Claim the key before reading it: an unclaimed Escape lets the browser
|
|
331
|
+
// raise its own close request, which lands on handleCancel and dismisses
|
|
332
|
+
// on the same keypress.
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
// An IME fires this keydown to cancel an in-progress composition, ahead
|
|
335
|
+
// of compositionend. It is a composition cancel, not a dismissal command
|
|
336
|
+
// — see utils/ime; Dialog and BottomSheetSwitcher guard the same way.
|
|
337
|
+
if (isImeKeyEvent(event.nativeEvent)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
dismissOnEscape();
|
|
312
341
|
},
|
|
313
342
|
[dismissOnEscape],
|
|
314
343
|
);
|
|
@@ -327,6 +356,7 @@ function StandaloneBottomSheet({
|
|
|
327
356
|
styles.dialog,
|
|
328
357
|
shouldPresent && styles.dialogOpen,
|
|
329
358
|
hasScrim && styles.scrim,
|
|
359
|
+
hasScrim && !isOpen && isPresented && styles.scrimClosing,
|
|
330
360
|
!hasScrim && styles.dialogNonModal,
|
|
331
361
|
)}
|
|
332
362
|
ref={dialogRef}
|
|
@@ -355,6 +385,8 @@ function StandaloneBottomSheet({
|
|
|
355
385
|
{children}
|
|
356
386
|
</BottomSheetPanel>
|
|
357
387
|
</div>
|
|
388
|
+
{/* A modal sheet's ::backdrop already answers Safari's edge sampler. */}
|
|
389
|
+
{hasScrim ? null : <BottomSheetEdgeTint />}
|
|
358
390
|
</dialog>
|
|
359
391
|
);
|
|
360
392
|
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file BottomSheetEdgeTint.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, BottomSheet, BottomSheetSwitcher
|
|
6
|
+
* @output Tests which sheets carry the iOS Safari bottom edge tint, and pins
|
|
7
|
+
* the declarations WebKit's edge sampler reads
|
|
8
|
+
* @position Core testing; validates BottomSheetEdgeTint.tsx and its two hosts
|
|
9
|
+
*
|
|
10
|
+
* SYNC: When modified, update these files to stay in sync:
|
|
11
|
+
* - /packages/core/src/BottomSheet/BottomSheetEdgeTint.tsx
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
15
|
+
import {render} from '@testing-library/react';
|
|
16
|
+
import {BottomSheet} from './BottomSheet';
|
|
17
|
+
import {BottomSheetSwitcher} from './BottomSheetSwitcher';
|
|
18
|
+
|
|
19
|
+
// jsdom doesn't implement <dialog> open/close or pointer capture; stub them.
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
22
|
+
this: HTMLDialogElement,
|
|
23
|
+
) {
|
|
24
|
+
this.setAttribute('open', '');
|
|
25
|
+
});
|
|
26
|
+
HTMLDialogElement.prototype.show = vi.fn(function (this: HTMLDialogElement) {
|
|
27
|
+
this.setAttribute('open', '');
|
|
28
|
+
});
|
|
29
|
+
HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
|
|
30
|
+
this.removeAttribute('open');
|
|
31
|
+
});
|
|
32
|
+
if (!Element.prototype.setPointerCapture) {
|
|
33
|
+
Element.prototype.setPointerCapture = vi.fn();
|
|
34
|
+
Element.prototype.releasePointerCapture = vi.fn();
|
|
35
|
+
}
|
|
36
|
+
vi.stubGlobal(
|
|
37
|
+
'matchMedia',
|
|
38
|
+
vi.fn().mockReturnValue({
|
|
39
|
+
matches: false,
|
|
40
|
+
media: '',
|
|
41
|
+
onchange: null,
|
|
42
|
+
addEventListener: vi.fn(),
|
|
43
|
+
removeEventListener: vi.fn(),
|
|
44
|
+
addListener: vi.fn(),
|
|
45
|
+
removeListener: vi.fn(),
|
|
46
|
+
dispatchEvent: vi.fn(),
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
vi.stubGlobal(
|
|
50
|
+
'requestAnimationFrame',
|
|
51
|
+
vi.fn((callback: FrameRequestCallback) => {
|
|
52
|
+
callback(0);
|
|
53
|
+
return 1;
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
vi.stubGlobal('cancelAnimationFrame', vi.fn());
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.unstubAllGlobals();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function tints(): ReadonlyArray<Element> {
|
|
64
|
+
return Array.from(document.querySelectorAll('[data-sheet-edge-tint]'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function edgeTintSource(): Promise<string> {
|
|
68
|
+
const tint = (await fullSource()).match(/\n {2}tint: \{([\s\S]*?)\n {2}\},/);
|
|
69
|
+
expect(tint).not.toBeNull();
|
|
70
|
+
return tint![1];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The tint's declarations with its explanatory comments stripped out. */
|
|
74
|
+
async function edgeTintDeclarations(): Promise<string> {
|
|
75
|
+
return (await edgeTintSource())
|
|
76
|
+
.split('\n')
|
|
77
|
+
.filter(line => !line.trim().startsWith('//'))
|
|
78
|
+
.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function fullSource(): Promise<string> {
|
|
82
|
+
const fs = await import('fs');
|
|
83
|
+
const path = await import('path');
|
|
84
|
+
return fs.readFileSync(
|
|
85
|
+
path.resolve(__dirname, './BottomSheetEdgeTint.tsx'),
|
|
86
|
+
'utf-8',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('BottomSheetEdgeTint', () => {
|
|
91
|
+
it('gives a non-modal sheet an edge tint to colour the iOS toolbar strip', () => {
|
|
92
|
+
render(
|
|
93
|
+
<BottomSheet
|
|
94
|
+
isOpen
|
|
95
|
+
onOpenChange={() => {}}
|
|
96
|
+
hasScrim={false}
|
|
97
|
+
label="Place details">
|
|
98
|
+
Content
|
|
99
|
+
</BottomSheet>,
|
|
100
|
+
);
|
|
101
|
+
expect(tints()).toHaveLength(1);
|
|
102
|
+
});
|
|
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', () => {
|
|
107
|
+
render(
|
|
108
|
+
<BottomSheet isOpen onOpenChange={() => {}} label="Place details">
|
|
109
|
+
Content
|
|
110
|
+
</BottomSheet>,
|
|
111
|
+
);
|
|
112
|
+
expect(tints()).toHaveLength(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('renders the tint inside the dialog, so it unmounts with the sheet', () => {
|
|
116
|
+
const {rerender} = render(
|
|
117
|
+
<BottomSheet
|
|
118
|
+
isOpen
|
|
119
|
+
onOpenChange={() => {}}
|
|
120
|
+
hasScrim={false}
|
|
121
|
+
label="Place details">
|
|
122
|
+
Content
|
|
123
|
+
</BottomSheet>,
|
|
124
|
+
);
|
|
125
|
+
const [tint] = tints();
|
|
126
|
+
expect(tint?.closest('dialog')).not.toBeNull();
|
|
127
|
+
|
|
128
|
+
rerender(<div />);
|
|
129
|
+
expect(tints()).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// The switcher owns one shared dialog for the whole flow, so the tint
|
|
133
|
+
// belongs to that dialog and must not be minted per child sheet.
|
|
134
|
+
it('gives a non-modal switcher flow exactly one tint', () => {
|
|
135
|
+
render(
|
|
136
|
+
<BottomSheetSwitcher
|
|
137
|
+
activeSheet="comment"
|
|
138
|
+
hasScrim={false}
|
|
139
|
+
onActiveSheetChange={() => {}}>
|
|
140
|
+
<BottomSheet sheetId="comment" label="Add a comment">
|
|
141
|
+
Comment
|
|
142
|
+
</BottomSheet>
|
|
143
|
+
<BottomSheet sheetId="confirmation" label="Confirmation">
|
|
144
|
+
Confirmation
|
|
145
|
+
</BottomSheet>
|
|
146
|
+
</BottomSheetSwitcher>,
|
|
147
|
+
);
|
|
148
|
+
expect(tints()).toHaveLength(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('leaves a modal switcher flow to its ::backdrop', () => {
|
|
152
|
+
render(
|
|
153
|
+
<BottomSheetSwitcher activeSheet="comment" onActiveSheetChange={() => {}}>
|
|
154
|
+
<BottomSheet sheetId="comment" label="Add a comment">
|
|
155
|
+
Comment
|
|
156
|
+
</BottomSheet>
|
|
157
|
+
</BottomSheetSwitcher>,
|
|
158
|
+
);
|
|
159
|
+
expect(tints()).toHaveLength(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('keeps the tint out of the accessibility tree and out of hit testing', async () => {
|
|
163
|
+
render(
|
|
164
|
+
<BottomSheet
|
|
165
|
+
isOpen
|
|
166
|
+
onOpenChange={() => {}}
|
|
167
|
+
hasScrim={false}
|
|
168
|
+
label="Place details">
|
|
169
|
+
Content
|
|
170
|
+
</BottomSheet>,
|
|
171
|
+
);
|
|
172
|
+
const [tint] = tints();
|
|
173
|
+
expect(tint?.getAttribute('aria-hidden')).toBe('true');
|
|
174
|
+
expect(tint?.textContent).toBe('');
|
|
175
|
+
expect(await edgeTintSource()).toContain("pointerEvents: 'none'");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Every declaration below is load-bearing for a heuristic that lives in
|
|
179
|
+
// WebKit, not in this repo, and none of it is observable in jsdom or in any
|
|
180
|
+
// engine without retractable browser chrome — so it is asserted on the style
|
|
181
|
+
// definition, the way BottomSheetPanel pins its handle gradient.
|
|
182
|
+
describe('the declarations WebKit samples', () => {
|
|
183
|
+
it('is fixed and flush with the bottom edge of the viewport', async () => {
|
|
184
|
+
const tint = await edgeTintSource();
|
|
185
|
+
// Only a fixed or sticky box is a candidate, and only a box flush with
|
|
186
|
+
// the edge is the one Safari hit tests.
|
|
187
|
+
expect(tint).toContain("position: 'fixed'");
|
|
188
|
+
expect(tint).toContain('insetBlockEnd: 0');
|
|
189
|
+
expect(tint).toContain('insetInline: 0');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('clears the 10px floor below which WebKit ignores the declared colour', async () => {
|
|
193
|
+
const height = (await fullSource()).match(
|
|
194
|
+
/const SAMPLE_HEIGHT_PX = (\d+);/,
|
|
195
|
+
);
|
|
196
|
+
expect(height).not.toBeNull();
|
|
197
|
+
expect(Number(height![1])).toBeGreaterThan(10);
|
|
198
|
+
expect(await edgeTintSource()).toContain(
|
|
199
|
+
'height: `${SAMPLE_HEIGHT_PX}px`',
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('declares the sheet surface colour, so the strip matches the sheet', async () => {
|
|
204
|
+
expect(await edgeTintSource()).toContain(
|
|
205
|
+
"backgroundColor: colorVars['--color-background-surface']",
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// visibility: hidden, display: none and a low opacity all disqualify the
|
|
210
|
+
// element from sampling. A mask does not, which is the only reason the
|
|
211
|
+
// strip can be both readable by Safari and invisible to the user.
|
|
212
|
+
it('hides itself with a mask rather than with visibility or opacity', async () => {
|
|
213
|
+
const tint = await edgeTintDeclarations();
|
|
214
|
+
expect(tint).toContain(
|
|
215
|
+
"maskImage: 'linear-gradient(transparent, transparent)'",
|
|
216
|
+
);
|
|
217
|
+
expect(tint).toContain(
|
|
218
|
+
"WebkitMaskImage: 'linear-gradient(transparent, transparent)'",
|
|
219
|
+
);
|
|
220
|
+
expect(tint).not.toContain('visibility');
|
|
221
|
+
expect(tint).not.toContain('opacity');
|
|
222
|
+
expect(tint).not.toContain("display: 'none'");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file BottomSheetEdgeTint.tsx
|
|
7
|
+
* @input Uses StyleX and core color tokens
|
|
8
|
+
* @output Exports BottomSheetEdgeTint, an internal decorative element
|
|
9
|
+
* @position Private helper rendered inside a non-modal BottomSheet dialog
|
|
10
|
+
*
|
|
11
|
+
* iOS 26 Safari dropped `<meta name="theme-color">` and instead derives the
|
|
12
|
+
* colour it paints behind its translucent toolbars by sampling the page: it
|
|
13
|
+
* hit tests a point just inside each viewport edge, walks up to the nearest
|
|
14
|
+
* `fixed`/`sticky` ancestor, and extends that element's declared
|
|
15
|
+
* `background-color` into the browser chrome.
|
|
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.
|
|
24
|
+
*
|
|
25
|
+
* This element gives the heuristic something unambiguous to sample: fixed,
|
|
26
|
+
* full width, flush with the bottom edge, taller than WebKit's 10px minimum
|
|
27
|
+
* for reading a declared colour, and painted in the sheet's own surface
|
|
28
|
+
* colour. It is masked out so it never renders for the user; Safari reads the
|
|
29
|
+
* computed style, not the painted pixels.
|
|
30
|
+
*
|
|
31
|
+
* Everywhere else it is inert decoration.
|
|
32
|
+
*
|
|
33
|
+
* SYNC: When modified, update these files to stay in sync:
|
|
34
|
+
* - /packages/core/src/BottomSheet/BottomSheet.tsx
|
|
35
|
+
* - /packages/core/src/BottomSheet/BottomSheetSwitcher.tsx
|
|
36
|
+
* - /packages/core/src/BottomSheet/BottomSheetEdgeTint.test.tsx
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import * as stylex from '@stylexjs/stylex';
|
|
40
|
+
import {colorVars} from '../theme/tokens.stylex';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* WebKit ignores the declared `background-color` of a sampled box thinner than
|
|
44
|
+
* 10px and falls back to sampling painted pixels, which a masked element has
|
|
45
|
+
* none of. 12px clears that floor with room to spare and is still far below
|
|
46
|
+
* the ~40px of chrome it colours.
|
|
47
|
+
*/
|
|
48
|
+
const SAMPLE_HEIGHT_PX = 12;
|
|
49
|
+
|
|
50
|
+
const styles = stylex.create({
|
|
51
|
+
tint: {
|
|
52
|
+
position: 'fixed',
|
|
53
|
+
insetInline: 0,
|
|
54
|
+
insetBlockEnd: 0,
|
|
55
|
+
height: `${SAMPLE_HEIGHT_PX}px`,
|
|
56
|
+
backgroundColor: colorVars['--color-background-surface'],
|
|
57
|
+
// Above the panel so the edge hit test lands here, and never in the way of
|
|
58
|
+
// a touch that was meant for the sheet.
|
|
59
|
+
zIndex: 2,
|
|
60
|
+
pointerEvents: 'none',
|
|
61
|
+
// Invisible to the user. WebKit's sampler checks `visibility` and
|
|
62
|
+
// `opacity` — either would disqualify the element — but not `mask`, so
|
|
63
|
+
// this hides the strip while leaving the colour readable. Without it the
|
|
64
|
+
// strip would show as a hairline under the panel as the sheet slides out.
|
|
65
|
+
maskImage: 'linear-gradient(transparent, transparent)',
|
|
66
|
+
WebkitMaskImage: 'linear-gradient(transparent, transparent)',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Colours the iOS Safari toolbar strip below a non-modal sheet. Renders
|
|
72
|
+
* nothing visible; see the file header for why it exists.
|
|
73
|
+
*/
|
|
74
|
+
export function BottomSheetEdgeTint() {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
{...stylex.props(styles.tint)}
|
|
78
|
+
data-sheet-edge-tint=""
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -328,4 +328,70 @@ describe('BottomSheetPanel', () => {
|
|
|
328
328
|
"backgroundColor: colorVars['--color-background-surface']",
|
|
329
329
|
);
|
|
330
330
|
});
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The exit is not the entrance played backwards.
|
|
334
|
+
*
|
|
335
|
+
* `--ease-standard` is a decelerate curve: on it the sheet was half gone in
|
|
336
|
+
* 59ms of a 410ms transition and 90% gone in 163ms, so the close was over
|
|
337
|
+
* before it could be seen. The closing state therefore carries its own
|
|
338
|
+
* accelerating curve, and only the closing state does.
|
|
339
|
+
*/
|
|
340
|
+
it('closes on an accelerating curve of its own, not the entrance timing', () => {
|
|
341
|
+
const declarationsFor = (element: HTMLElement) => {
|
|
342
|
+
const classes = new Set(element.className.split(/\s+/));
|
|
343
|
+
const out: string[] = [];
|
|
344
|
+
for (const styleSheet of Array.from(document.styleSheets)) {
|
|
345
|
+
let rules: CSSRuleList;
|
|
346
|
+
try {
|
|
347
|
+
rules = styleSheet.cssRules;
|
|
348
|
+
} catch {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
for (const rule of Array.from(rules)) {
|
|
352
|
+
const selector = (rule as CSSStyleRule).selectorText;
|
|
353
|
+
const owner = selector?.match(/^\.([\w-]+)/)?.[1];
|
|
354
|
+
if (owner != null && classes.has(owner)) {
|
|
355
|
+
out.push(rule.cssText);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return out.join('\n');
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const {container: closing} = render(
|
|
363
|
+
<BottomSheetPanel
|
|
364
|
+
state={{kind: 'exiting'}}
|
|
365
|
+
height="hug"
|
|
366
|
+
onDismiss={() => {}}
|
|
367
|
+
onScrimOpacity={() => {}}>
|
|
368
|
+
Panel content
|
|
369
|
+
</BottomSheetPanel>,
|
|
370
|
+
);
|
|
371
|
+
const closingRules = declarationsFor(
|
|
372
|
+
closing.querySelector('.astryx-bottom-sheet') as HTMLElement,
|
|
373
|
+
);
|
|
374
|
+
// An accelerating curve: no vertical rise at the start (y1 = 0), so the
|
|
375
|
+
// travel lands inside the duration instead of ahead of it.
|
|
376
|
+
expect(closingRules).toContain(
|
|
377
|
+
'transition-timing-function: cubic-bezier(.3,0,.6,.6)',
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const {container: resting} = render(
|
|
381
|
+
<BottomSheetPanel
|
|
382
|
+
state={{kind: 'open', entering: false}}
|
|
383
|
+
height="hug"
|
|
384
|
+
onDismiss={() => {}}
|
|
385
|
+
onScrimOpacity={() => {}}>
|
|
386
|
+
Panel content
|
|
387
|
+
</BottomSheetPanel>,
|
|
388
|
+
);
|
|
389
|
+
const restingRules = declarationsFor(
|
|
390
|
+
resting.querySelector('.astryx-bottom-sheet') as HTMLElement,
|
|
391
|
+
);
|
|
392
|
+
expect(restingRules).toContain(
|
|
393
|
+
'transition-timing-function: var(--ease-standard)',
|
|
394
|
+
);
|
|
395
|
+
expect(restingRules).not.toContain('cubic-bezier(.3,0,.6,.6)');
|
|
396
|
+
});
|
|
331
397
|
});
|
|
@@ -149,8 +149,27 @@ const styles = stylex.create({
|
|
|
149
149
|
transitionDuration: '0.01s',
|
|
150
150
|
},
|
|
151
151
|
},
|
|
152
|
+
/**
|
|
153
|
+
* The exit is its own motion, not the entrance played backwards.
|
|
154
|
+
*
|
|
155
|
+
* `--ease-standard` is `cubic-bezier(0.24, 1, 0.4, 1)`, a decelerate curve:
|
|
156
|
+
* it spends its speed immediately and coasts. That is right for an entrance,
|
|
157
|
+
* where the sheet arrives fast and settles, and wrong for an exit. Measured
|
|
158
|
+
* on device, it put the sheet half off-screen in 59ms of a 410ms close and
|
|
159
|
+
* 90% off in 163ms -- the travel is over before the eye has followed it, and
|
|
160
|
+
* the rest of the duration moves pixels already below the fold.
|
|
161
|
+
*
|
|
162
|
+
* An exit accelerates instead: away from rest, gathering speed, quickest as
|
|
163
|
+
* it leaves the screen. The curve is deliberately gentle rather than a hard
|
|
164
|
+
* `ease-in` -- it is moving within ~50ms, so the close reads as one
|
|
165
|
+
* departure rather than a hesitation and a snap. The duration is the
|
|
166
|
+
* entrance's own band, so only the curve differs between the two
|
|
167
|
+
* directions; a shorter band leaves too little visible travel once a theme
|
|
168
|
+
* scales the motion scale down (neutral's medium is 300ms against 410ms).
|
|
169
|
+
*/
|
|
152
170
|
sheetClosing: {
|
|
153
171
|
transform: 'translateY(100%)',
|
|
172
|
+
transitionTimingFunction: 'cubic-bezier(0.3, 0, 0.6, 0.6)',
|
|
154
173
|
},
|
|
155
174
|
sheetFading: {
|
|
156
175
|
opacity: 0,
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* SYNC: When modified, update these files to stay in sync:
|
|
24
24
|
* - /packages/core/src/BottomSheet/BottomSheet.tsx
|
|
25
|
+
* - /packages/core/src/BottomSheet/BottomSheetEdgeTint.tsx
|
|
25
26
|
* - /packages/core/src/BottomSheet/BottomSheetSwitcher.doc.mjs
|
|
26
27
|
* - /packages/core/src/BottomSheet/BottomSheetSwitcher.test.tsx
|
|
27
28
|
* - /packages/core/src/BottomSheet/index.ts
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
useScrollLock,
|
|
52
53
|
} from '../hooks';
|
|
53
54
|
import {composeEventHandlers, mergeProps, mergeRefs} from '../utils';
|
|
55
|
+
import {BottomSheetEdgeTint} from './BottomSheetEdgeTint';
|
|
54
56
|
import {
|
|
55
57
|
BottomSheetSwitcherContext,
|
|
56
58
|
type BottomSheetSwitcherContextValue,
|
|
@@ -103,6 +105,14 @@ const styles = stylex.create({
|
|
|
103
105
|
},
|
|
104
106
|
},
|
|
105
107
|
},
|
|
108
|
+
// The flow's dim leaves with its last panel. Same rule, same reasoning, as
|
|
109
|
+
// a standalone sheet's -- see `scrimClosing` in BottomSheet.tsx. A handoff
|
|
110
|
+
// between two sheets is not a close, and keeps the entrance curve.
|
|
111
|
+
scrimClosing: {
|
|
112
|
+
'::backdrop': {
|
|
113
|
+
transitionTimingFunction: 'linear',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
106
116
|
});
|
|
107
117
|
|
|
108
118
|
type RetainedSheetPhase = 'covered' | 'aligning' | 'fading' | 'exiting';
|
|
@@ -593,6 +603,7 @@ export function BottomSheetSwitcher({
|
|
|
593
603
|
styles.dialog,
|
|
594
604
|
isFlowVisible && styles.dialogOpen,
|
|
595
605
|
hasScrim && styles.scrim,
|
|
606
|
+
hasScrim && isFlowVisible && activeSheet == null && styles.scrimClosing,
|
|
596
607
|
!hasScrim && styles.dialogNonModal,
|
|
597
608
|
xstyle,
|
|
598
609
|
);
|
|
@@ -620,6 +631,8 @@ export function BottomSheetSwitcher({
|
|
|
620
631
|
? {role: 'alertdialog'}
|
|
621
632
|
: undefined)}>
|
|
622
633
|
{children}
|
|
634
|
+
{/* A modal flow's ::backdrop already answers Safari's edge sampler. */}
|
|
635
|
+
{hasScrim ? null : <BottomSheetEdgeTint />}
|
|
623
636
|
</dialog>
|
|
624
637
|
</BottomSheetSwitcherContext>
|
|
625
638
|
);
|