@astryxdesign/core 0.4.2 → 0.4.3-canary.00bed29
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 +392 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +60 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetPanel.js +398 -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 +35 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
- package/dist/BottomSheet/useMobileKeyboard.js +373 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +180 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +942 -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/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +9 -3
- 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 +21 -7
- 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 +14 -3
- 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/Tokenizer/Tokenizer.js +11 -5
- 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.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -3
- 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/locales/en.json +48 -0
- package/locales/pseudo.json +36 -0
- 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 +2114 -0
- package/src/BottomSheet/BottomSheet.tsx +545 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +547 -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 +499 -0
- package/src/BottomSheet/useSheetGestures.test.ts +1014 -0
- package/src/BottomSheet/useSheetGestures.ts +1407 -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/FileInput/FileInput.test.tsx +53 -9
- package/src/Lightbox/Lightbox.test.tsx +49 -28
- package/src/Lightbox/Lightbox.tsx +7 -3
- 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 +195 -51
- package/src/MultiSelector/MultiSelector.tsx +22 -8
- 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 +85 -26
- package/src/Selector/Selector.tsx +13 -4
- 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/Tokenizer/Tokenizer.test.tsx +50 -32
- package/src/Tokenizer/Tokenizer.tsx +5 -5
- 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 +10 -3
- package/src/Typeahead/Typeahead.test.tsx +116 -7
- 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
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file snapOffsets.test.ts
|
|
5
|
+
* @input Uses vitest, snapOffsets pure helpers
|
|
6
|
+
* @output Unit tests for the snap-point -> translateY offset geometry
|
|
7
|
+
* @position Testing; validates snapOffsets.ts (no DOM needed)
|
|
8
|
+
*
|
|
9
|
+
* SYNC: When snapOffsets.ts changes, update these tests to match.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {describe, it, expect} from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
snapFractionsToHeights,
|
|
15
|
+
computeDetentOffsets,
|
|
16
|
+
nearestOffset,
|
|
17
|
+
resolveSettleOffset,
|
|
18
|
+
scrimOpacityForOffset,
|
|
19
|
+
isPeekOffset,
|
|
20
|
+
DETENT_DEDUP_PX,
|
|
21
|
+
} from './snapOffsets';
|
|
22
|
+
|
|
23
|
+
describe('snapFractionsToHeights', () => {
|
|
24
|
+
it('scales in-range fractions to px', () => {
|
|
25
|
+
expect(snapFractionsToHeights([0.5, 0.92], 800)).toEqual([400, 736]);
|
|
26
|
+
});
|
|
27
|
+
it('drops out-of-range fractions', () => {
|
|
28
|
+
expect(snapFractionsToHeights([0, 0.5, 1, 1.2, -0.3], 1000)).toEqual([
|
|
29
|
+
500, 1000,
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('computeDetentOffsets', () => {
|
|
35
|
+
it('always includes fully-open (0) first, ascending', () => {
|
|
36
|
+
// sheet 800; detent visible heights 400, 640 -> offsets 400, 160 -> sorted
|
|
37
|
+
expect(computeDetentOffsets(800, [400, 640])).toEqual([0, 160, 400]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('drops detents at or taller than the sheet (cannot translate up)', () => {
|
|
41
|
+
// 800 and 900 are >= sheet 800 -> dropped; only 500 (offset 300) remains
|
|
42
|
+
expect(computeDetentOffsets(800, [500, 800, 900])).toEqual([0, 300]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('de-dupes near-equal detents, keeping the taller (smaller offset)', () => {
|
|
46
|
+
// sheet 800; heights 760 (offset 40) and 400 (offset 400). 40 is within
|
|
47
|
+
// DETENT_DEDUP_PX of the fully-open 0 -> collapses to just [0, 400].
|
|
48
|
+
expect(computeDetentOffsets(800, [760, 400])).toEqual([0, 400]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps detents that are farther apart than the dedup threshold', () => {
|
|
52
|
+
// offsets 0, 100, 400 with default 48px dedup -> all kept
|
|
53
|
+
expect(computeDetentOffsets(800, [700, 400])).toEqual([0, 100, 400]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('respects a custom dedup threshold', () => {
|
|
57
|
+
// offsets 0, 100, 400; dedup 150 -> 100 collapses into 0
|
|
58
|
+
expect(computeDetentOffsets(800, [700, 400], 150)).toEqual([0, 400]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('a single-height sheet has just the fully-open detent', () => {
|
|
62
|
+
expect(computeDetentOffsets(600, [])).toEqual([0]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('nearestOffset', () => {
|
|
67
|
+
it('picks the closest offset', () => {
|
|
68
|
+
expect(nearestOffset(180, [0, 200, 400])).toBe(200);
|
|
69
|
+
expect(nearestOffset(90, [0, 200, 400])).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('resolveSettleOffset', () => {
|
|
74
|
+
const offsets = [0, 200, 400]; // full, mid, short
|
|
75
|
+
|
|
76
|
+
it('a down-drag never settles above the starting detent', () => {
|
|
77
|
+
// start at mid (200), drag down to ~240 -> nearest at/below is 200 (mid),
|
|
78
|
+
// never back up to 0.
|
|
79
|
+
expect(resolveSettleOffset(240, offsets, 1, 200)).toBe(200);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('a down-drag past the midpoint moves to the next lower detent', () => {
|
|
83
|
+
// start mid (200), drag to 320 -> nearest at/below is 400 (short)
|
|
84
|
+
expect(resolveSettleOffset(320, offsets, 1, 200)).toBe(400);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('an up-drag never settles below the starting detent', () => {
|
|
88
|
+
// start mid (200), drag up to ~160 -> nearest at/above is 0 or 200; 160 is
|
|
89
|
+
// closer to 200, so it holds mid rather than dropping to short.
|
|
90
|
+
expect(resolveSettleOffset(160, offsets, -1, 200)).toBe(200);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('an up-drag past the midpoint expands to the next higher detent', () => {
|
|
94
|
+
// start mid (200), drag up to 80 -> nearest at/above is 0 (full)
|
|
95
|
+
expect(resolveSettleOffset(80, offsets, -1, 200)).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('with no direction, picks the plain nearest', () => {
|
|
99
|
+
expect(resolveSettleOffset(180, offsets, 0, 200)).toBe(200);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('isPeekOffset', () => {
|
|
104
|
+
const offsets = [0, 100, 200]; // full, mid, peek
|
|
105
|
+
|
|
106
|
+
it('is true only for the shortest detent', () => {
|
|
107
|
+
expect(isPeekOffset(200, offsets)).toBe(true);
|
|
108
|
+
expect(isPeekOffset(100, offsets)).toBe(false);
|
|
109
|
+
expect(isPeekOffset(0, offsets)).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('tolerates sub-pixel rounding around the peek', () => {
|
|
113
|
+
expect(isPeekOffset(200.4, offsets)).toBe(true);
|
|
114
|
+
expect(isPeekOffset(199.6, offsets)).toBe(true);
|
|
115
|
+
expect(isPeekOffset(196, offsets)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('has no peek when the sheet has no collapsed stop', () => {
|
|
119
|
+
expect(isPeekOffset(200, [0])).toBe(false);
|
|
120
|
+
expect(isPeekOffset(0, [0])).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('treats the only collapsed stop as the peek', () => {
|
|
124
|
+
expect(isPeekOffset(200, [0, 200])).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('agrees with the detent scrimOpacityForOffset treats as the peek', () => {
|
|
128
|
+
const dismissOffset = 280;
|
|
129
|
+
const peek = offsets[offsets.length - 1];
|
|
130
|
+
expect(isPeekOffset(peek, offsets)).toBe(true);
|
|
131
|
+
expect(scrimOpacityForOffset(peek, offsets, dismissOffset)).toBeCloseTo(
|
|
132
|
+
0.3,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('scrimOpacityForOffset', () => {
|
|
138
|
+
describe('with a peek detent (>= 2 detents)', () => {
|
|
139
|
+
const offsets = [0, 100, 200]; // full, mid, peek
|
|
140
|
+
const dismissOffset = 280;
|
|
141
|
+
|
|
142
|
+
it('is full at or above the mid detent', () => {
|
|
143
|
+
expect(scrimOpacityForOffset(0, offsets, dismissOffset)).toBe(1);
|
|
144
|
+
expect(scrimOpacityForOffset(100, offsets, dismissOffset)).toBe(1);
|
|
145
|
+
expect(scrimOpacityForOffset(60, offsets, dismissOffset)).toBe(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('fades linearly from the mid detent toward the peek floor', () => {
|
|
149
|
+
// Halfway from mid (100) to peek (200), opacity is halfway from 1 to the
|
|
150
|
+
// 0.3 floor => 0.65.
|
|
151
|
+
expect(scrimOpacityForOffset(150, offsets, dismissOffset)).toBeCloseTo(
|
|
152
|
+
0.65,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('thins to the peek floor at or below the peek detent (still modal)', () => {
|
|
157
|
+
expect(scrimOpacityForOffset(200, offsets, dismissOffset)).toBeCloseTo(
|
|
158
|
+
0.3,
|
|
159
|
+
);
|
|
160
|
+
expect(scrimOpacityForOffset(240, offsets, dismissOffset)).toBeCloseTo(
|
|
161
|
+
0.3,
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('with a single detent (no peek)', () => {
|
|
167
|
+
const offsets = [0];
|
|
168
|
+
const dismissOffset = 160;
|
|
169
|
+
|
|
170
|
+
it('stays full until the drag begins collapsing', () => {
|
|
171
|
+
expect(scrimOpacityForOffset(0, offsets, dismissOffset)).toBe(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('fades across the dismiss overshoot toward the threshold', () => {
|
|
175
|
+
expect(scrimOpacityForOffset(80, offsets, dismissOffset)).toBeCloseTo(
|
|
176
|
+
0.5,
|
|
177
|
+
);
|
|
178
|
+
expect(scrimOpacityForOffset(160, offsets, dismissOffset)).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('DETENT_DEDUP_PX', () => {
|
|
184
|
+
it('is a sane positive threshold', () => {
|
|
185
|
+
expect(DETENT_DEDUP_PX).toBeGreaterThan(0);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file snapOffsets.ts
|
|
5
|
+
* @input Pure geometry helpers — no React, no DOM.
|
|
6
|
+
* @output Converts snap points to translateY offsets for the sheet surface.
|
|
7
|
+
* @position Internal to BottomSheet; consumed by useSheetGestures, tested by
|
|
8
|
+
* snapOffsets.test.ts.
|
|
9
|
+
*
|
|
10
|
+
* A detent is represented as an offset in px from the fully-open position
|
|
11
|
+
* (0 = fully open, larger = more collapsed). The panel may render that offset
|
|
12
|
+
* as a translate or as a reduction in layout height. These
|
|
13
|
+
* helpers turn snap points into the shared offset list; they are pure so the
|
|
14
|
+
* geometry can be unit-tested without a DOM.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detents whose resting offsets land within this many px of each other are
|
|
19
|
+
* treated as the same stop (the taller one wins). Stops the sheet from having
|
|
20
|
+
* two near-identical rest positions — e.g. when a content-hugging height sits
|
|
21
|
+
* a hair away from a fractional snap point.
|
|
22
|
+
*/
|
|
23
|
+
export const DETENT_DEDUP_PX = 48;
|
|
24
|
+
|
|
25
|
+
/** Resolve snap points given as viewport fractions (0, 1] to px heights. */
|
|
26
|
+
export function snapFractionsToHeights(
|
|
27
|
+
fractions: ReadonlyArray<number>,
|
|
28
|
+
viewportPx: number,
|
|
29
|
+
): number[] {
|
|
30
|
+
return fractions.filter(f => f > 0 && f <= 1).map(f => f * viewportPx);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Given the sheet's full height (px, as rendered fully open) and a set of
|
|
35
|
+
* candidate detent *visible heights* (px), return the resting offsets from
|
|
36
|
+
* fully-open, ascending and de-duplicated.
|
|
37
|
+
*
|
|
38
|
+
* - `0` (fully open) is always the first detent.
|
|
39
|
+
* - Only heights strictly shorter than the sheet become collapsed detents; a
|
|
40
|
+
* height >= the sheet is already covered by the fully-open stop.
|
|
41
|
+
* - Offsets closer than `dedupPx` collapse to the smaller (taller) offset, so
|
|
42
|
+
* near-identical detents — e.g. a hug height ≈ a snap point — become one stop.
|
|
43
|
+
*/
|
|
44
|
+
export function computeDetentOffsets(
|
|
45
|
+
sheetHeight: number,
|
|
46
|
+
detentHeights: ReadonlyArray<number>,
|
|
47
|
+
dedupPx: number = DETENT_DEDUP_PX,
|
|
48
|
+
): number[] {
|
|
49
|
+
const collapsed = detentHeights
|
|
50
|
+
.filter(h => h > 0 && h < sheetHeight)
|
|
51
|
+
.map(h => sheetHeight - h);
|
|
52
|
+
const ascending = [0, ...collapsed].sort((a, b) => a - b);
|
|
53
|
+
|
|
54
|
+
const deduped: number[] = [];
|
|
55
|
+
for (const offset of ascending) {
|
|
56
|
+
const last = deduped[deduped.length - 1];
|
|
57
|
+
if (last === undefined || offset - last >= dedupPx) {
|
|
58
|
+
deduped.push(offset);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return deduped;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Nearest value in `offsets` to `value` (offsets must be non-empty). */
|
|
65
|
+
export function nearestOffset(
|
|
66
|
+
value: number,
|
|
67
|
+
offsets: ReadonlyArray<number>,
|
|
68
|
+
): number {
|
|
69
|
+
return offsets.reduce(
|
|
70
|
+
(best, o) => (Math.abs(o - value) < Math.abs(best - value) ? o : best),
|
|
71
|
+
offsets[0],
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Minimum scrim opacity at the peek detent. The scrim thins to a glance state
|
|
77
|
+
* but never fully vanishes, because a modal sheet keeps the background inert —
|
|
78
|
+
* a fully clear backdrop would read as "interactive" when it isn't. (For a
|
|
79
|
+
* genuinely interactive, undimmed peek, use a non-modal sheet, `hasScrim=false`.)
|
|
80
|
+
*/
|
|
81
|
+
export const MIN_PEEK_SCRIM_OPACITY = 0.3;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether `offset` is the peek detent: the shortest stop, where the sheet is a
|
|
85
|
+
* glance rather than a working surface. A sheet with no collapsed stop at all
|
|
86
|
+
* has no peek — the same rule `scrimOpacityForOffset` thins the scrim by.
|
|
87
|
+
*
|
|
88
|
+
* The peek is the one detent the sheet does NOT express as layout height: at a
|
|
89
|
+
* sliver of viewport there is nothing useful to lay out, and reflowing the
|
|
90
|
+
* content into it (then back out on the way up) is churn the user sees. It
|
|
91
|
+
* keeps the full layout height and slides below the viewport instead.
|
|
92
|
+
*/
|
|
93
|
+
export function isPeekOffset(
|
|
94
|
+
offset: number,
|
|
95
|
+
offsets: ReadonlyArray<number>,
|
|
96
|
+
tolerancePx: number = 0.5,
|
|
97
|
+
): boolean {
|
|
98
|
+
if (offsets.length < 2) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return Math.abs(offset - offsets[offsets.length - 1]) <= tolerancePx;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Scrim opacity (1 = fully visible) for a drag/settle `offset`.
|
|
106
|
+
* The scrim stays full while the sheet is at or above its second-shortest
|
|
107
|
+
* detent, then fades as it collapses onto the shortest ("peek") detent — a
|
|
108
|
+
* glance state that thins the backdrop to `MIN_PEEK_SCRIM_OPACITY` (not fully
|
|
109
|
+
* gone, since the sheet is still modal) and holds there below it.
|
|
110
|
+
* A single-detent sheet has no peek, so it instead fades all the way to 0
|
|
111
|
+
* across the dismiss overshoot toward `dismissOffset` (the sheet is leaving).
|
|
112
|
+
*/
|
|
113
|
+
export function scrimOpacityForOffset(
|
|
114
|
+
offset: number,
|
|
115
|
+
offsets: ReadonlyArray<number>,
|
|
116
|
+
dismissOffset: number,
|
|
117
|
+
): number {
|
|
118
|
+
const shortest = offsets[offsets.length - 1];
|
|
119
|
+
const hasPeek = offsets.length >= 2;
|
|
120
|
+
const fadeStart = hasPeek ? offsets[offsets.length - 2] : 0;
|
|
121
|
+
const fadeEnd = hasPeek ? shortest : dismissOffset;
|
|
122
|
+
// Peek is a resting state on a still-modal sheet, so keep a floor; the
|
|
123
|
+
// dismiss overshoot is a sheet on its way out, so let it clear completely.
|
|
124
|
+
const floor = hasPeek ? MIN_PEEK_SCRIM_OPACITY : 0;
|
|
125
|
+
if (offset <= fadeStart) {
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
if (offset >= fadeEnd) {
|
|
129
|
+
return floor;
|
|
130
|
+
}
|
|
131
|
+
return 1 - (1 - floor) * ((offset - fadeStart) / (fadeEnd - fadeStart));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Settle target for a released drag. Restricts candidates to the drag
|
|
136
|
+
* direction so a committed drag never snaps *back past* where it started
|
|
137
|
+
* (a down-drag settles at/below the start, an up-drag at/above), then picks
|
|
138
|
+
* the nearest remaining detent. `dir`: 1 = dragged down, -1 = up, 0 = neither.
|
|
139
|
+
*/
|
|
140
|
+
export function resolveSettleOffset(
|
|
141
|
+
value: number,
|
|
142
|
+
offsets: ReadonlyArray<number>,
|
|
143
|
+
dir: number,
|
|
144
|
+
baseOffset: number,
|
|
145
|
+
): number {
|
|
146
|
+
let candidates = offsets;
|
|
147
|
+
if (dir > 0) {
|
|
148
|
+
const downward = offsets.filter(o => o >= baseOffset);
|
|
149
|
+
if (downward.length > 0) {
|
|
150
|
+
candidates = downward;
|
|
151
|
+
}
|
|
152
|
+
} else if (dir < 0) {
|
|
153
|
+
const upward = offsets.filter(o => o <= baseOffset);
|
|
154
|
+
if (upward.length > 0) {
|
|
155
|
+
candidates = upward;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return nearestOffset(value, candidates);
|
|
159
|
+
}
|