@cdx-ui/components 0.2.5 → 0.3.0
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/docs/components/bottom-sheet/content.md +2 -0
- package/docs/components/calendar/content.md +25 -1
- package/docs/components/month-calendar/content.md +2 -0
- package/docs/components/range-calendar/content.md +3 -1
- package/docs/llms.txt +1 -0
- package/docs/resources/release-notes/0.2.5/content.md +25 -0
- package/docs/resources/release-notes/content.md +1 -0
- package/lib/commonjs/components/BottomSheet/index.js +25 -1
- package/lib/commonjs/components/BottomSheet/index.js.map +1 -1
- package/lib/commonjs/components/BottomSheet/styles.js +8 -2
- package/lib/commonjs/components/BottomSheet/styles.js.map +1 -1
- package/lib/commonjs/components/Calendar/CalendarMonthHeader.js +15 -5
- package/lib/commonjs/components/Calendar/CalendarMonthHeader.js.map +1 -1
- package/lib/commonjs/components/Calendar/CalendarYearList.js +116 -0
- package/lib/commonjs/components/Calendar/CalendarYearList.js.map +1 -0
- package/lib/commonjs/components/Calendar/CalendarYearPickerGrid.js +58 -31
- package/lib/commonjs/components/Calendar/CalendarYearPickerGrid.js.map +1 -1
- package/lib/commonjs/components/Calendar/flashCalendarAdapter.js +22 -3
- package/lib/commonjs/components/Calendar/flashCalendarAdapter.js.map +1 -1
- package/lib/commonjs/components/Calendar/index.js +6 -2
- package/lib/commonjs/components/Calendar/index.js.map +1 -1
- package/lib/commonjs/components/Calendar/styles.js +16 -2
- package/lib/commonjs/components/Calendar/styles.js.map +1 -1
- package/lib/commonjs/components/MonthCalendar/index.js +39 -13
- package/lib/commonjs/components/MonthCalendar/index.js.map +1 -1
- package/lib/commonjs/components/RangeCalendar/index.js +6 -2
- package/lib/commonjs/components/RangeCalendar/index.js.map +1 -1
- package/lib/module/components/BottomSheet/index.js +27 -3
- package/lib/module/components/BottomSheet/index.js.map +1 -1
- package/lib/module/components/BottomSheet/styles.js +8 -2
- package/lib/module/components/BottomSheet/styles.js.map +1 -1
- package/lib/module/components/Calendar/CalendarMonthHeader.js +15 -5
- package/lib/module/components/Calendar/CalendarMonthHeader.js.map +1 -1
- package/lib/module/components/Calendar/CalendarYearList.js +112 -0
- package/lib/module/components/Calendar/CalendarYearList.js.map +1 -0
- package/lib/module/components/Calendar/CalendarYearPickerGrid.js +61 -34
- package/lib/module/components/Calendar/CalendarYearPickerGrid.js.map +1 -1
- package/lib/module/components/Calendar/flashCalendarAdapter.js +20 -2
- package/lib/module/components/Calendar/flashCalendarAdapter.js.map +1 -1
- package/lib/module/components/Calendar/index.js +7 -3
- package/lib/module/components/Calendar/index.js.map +1 -1
- package/lib/module/components/Calendar/styles.js +15 -1
- package/lib/module/components/Calendar/styles.js.map +1 -1
- package/lib/module/components/MonthCalendar/index.js +42 -15
- package/lib/module/components/MonthCalendar/index.js.map +1 -1
- package/lib/module/components/RangeCalendar/index.js +7 -3
- package/lib/module/components/RangeCalendar/index.js.map +1 -1
- package/lib/typescript/components/BottomSheet/index.d.ts +8 -0
- package/lib/typescript/components/BottomSheet/index.d.ts.map +1 -1
- package/lib/typescript/components/BottomSheet/styles.d.ts +5 -1
- package/lib/typescript/components/BottomSheet/styles.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/CalendarMonthHeader.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/CalendarYearList.d.ts +23 -0
- package/lib/typescript/components/Calendar/CalendarYearList.d.ts.map +1 -0
- package/lib/typescript/components/Calendar/CalendarYearPickerGrid.d.ts +5 -1
- package/lib/typescript/components/Calendar/CalendarYearPickerGrid.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/flashCalendarAdapter.d.ts +8 -0
- package/lib/typescript/components/Calendar/flashCalendarAdapter.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/index.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/styles.d.ts +7 -1
- package/lib/typescript/components/Calendar/styles.d.ts.map +1 -1
- package/lib/typescript/components/MonthCalendar/index.d.ts.map +1 -1
- package/lib/typescript/components/RangeCalendar/index.d.ts.map +1 -1
- package/lib/typescript/components/ToggleButton/styles.d.ts +1 -1
- package/package.json +5 -5
- package/src/components/BottomSheet/index.tsx +174 -138
- package/src/components/BottomSheet/styles.ts +11 -3
- package/src/components/Calendar/CalendarMonthHeader.tsx +29 -22
- package/src/components/Calendar/CalendarYearList.tsx +136 -0
- package/src/components/Calendar/CalendarYearPickerGrid.tsx +60 -33
- package/src/components/Calendar/flashCalendarAdapter.ts +27 -2
- package/src/components/Calendar/index.tsx +8 -3
- package/src/components/Calendar/styles.ts +17 -1
- package/src/components/MonthCalendar/index.tsx +55 -33
- package/src/components/RangeCalendar/index.tsx +8 -3
|
@@ -5,6 +5,7 @@ import React, {
|
|
|
5
5
|
useEffect,
|
|
6
6
|
useId,
|
|
7
7
|
useImperativeHandle,
|
|
8
|
+
useMemo,
|
|
8
9
|
useRef,
|
|
9
10
|
useState,
|
|
10
11
|
type ReactNode,
|
|
@@ -25,15 +26,21 @@ import {
|
|
|
25
26
|
type BottomSheetModalProps as GorhomBottomSheetModalProps,
|
|
26
27
|
} from '@gorhom/bottom-sheet';
|
|
27
28
|
import { useEscapeKey, useOverlayStackRegistration } from '@cdx-ui/primitives';
|
|
28
|
-
import { cn } from '@cdx-ui/utils';
|
|
29
|
+
import { cn, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
|
|
29
30
|
import { SheetHeader } from '../sheet-header';
|
|
30
31
|
import {
|
|
31
32
|
bottomSheetBackgroundClassName,
|
|
32
33
|
bottomSheetContentVariants,
|
|
33
34
|
bottomSheetFooterVariants,
|
|
34
35
|
bottomSheetModalContainerClassName,
|
|
36
|
+
type BottomSheetContentVariantProps,
|
|
35
37
|
} from './styles';
|
|
36
38
|
|
|
39
|
+
const SCOPE = 'BOTTOM_SHEET';
|
|
40
|
+
|
|
41
|
+
const useBottomSheetStyleContext = () =>
|
|
42
|
+
(useStyleContext(SCOPE) ?? {}) as BottomSheetContentVariantProps;
|
|
43
|
+
|
|
37
44
|
const ZERO_INSETS: EdgeInsets = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
38
45
|
|
|
39
46
|
function useSafeInsets(): EdgeInsets {
|
|
@@ -100,13 +107,19 @@ export interface BottomSheetContentProps {
|
|
|
100
107
|
className?: string;
|
|
101
108
|
/** Adds bottom padding equal to the safe-area inset. */
|
|
102
109
|
hasSafeAreaInset?: boolean;
|
|
110
|
+
/** Removes horizontal padding. Inherits the parent `BottomSheet.Modal`'s `fullBleed` when omitted. */
|
|
111
|
+
fullBleed?: BottomSheetContentVariantProps['fullBleed'];
|
|
103
112
|
style?: ViewStyle;
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
const BottomSheetContent = forwardRef<View, BottomSheetContentProps>(
|
|
107
|
-
({ children, className, hasSafeAreaInset, style }, ref) => {
|
|
116
|
+
({ children, className, hasSafeAreaInset, fullBleed, style }, ref) => {
|
|
108
117
|
const insets = useSafeInsets();
|
|
109
|
-
const
|
|
118
|
+
const { fullBleed: fullBleedFromContext } = useBottomSheetStyleContext();
|
|
119
|
+
const computedClassName = cn(
|
|
120
|
+
bottomSheetContentVariants({ fullBleed: fullBleed ?? fullBleedFromContext }),
|
|
121
|
+
className,
|
|
122
|
+
);
|
|
110
123
|
return (
|
|
111
124
|
<View
|
|
112
125
|
ref={ref}
|
|
@@ -162,6 +175,11 @@ export interface BottomSheetModalProps extends GorhomBottomSheetModalProps {
|
|
|
162
175
|
* Pass it through this prop; a footer placed as a child does not animate with the sheet.
|
|
163
176
|
*/
|
|
164
177
|
footer?: ReactNode;
|
|
178
|
+
/**
|
|
179
|
+
* Removes `BottomSheet.Content`'s horizontal padding so content can span the sheet edge to edge.
|
|
180
|
+
* `BottomSheet.Content` inherits this unless it sets its own `fullBleed`. The footer keeps its padding.
|
|
181
|
+
*/
|
|
182
|
+
fullBleed?: BottomSheetContentVariantProps['fullBleed'];
|
|
165
183
|
}
|
|
166
184
|
|
|
167
185
|
const MAX_DYNAMIC_CONTENT_SIZE = Dimensions.get('window').height * 0.9;
|
|
@@ -185,151 +203,169 @@ const CustomBackground = (props: React.ComponentPropsWithoutRef<typeof View>) =>
|
|
|
185
203
|
const BottomSheetModalStyled = forwardRef<
|
|
186
204
|
React.ComponentRef<typeof BottomSheetModal>,
|
|
187
205
|
BottomSheetModalProps
|
|
188
|
-
>(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Must stay a callback ref: gorhom hands this ref to its modal provider, which
|
|
204
|
-
// reads `.current` to minimize and restore stacked sheets. A ref object would
|
|
205
|
-
// switch that stacking on.
|
|
206
|
-
const setModalRef = useCallback((instance: BottomSheetModalRef | null) => {
|
|
207
|
-
modalRef.current = instance;
|
|
208
|
-
}, []);
|
|
209
|
-
|
|
210
|
-
// gorhom mounts the sheet a frame after `present()` and silently drops a
|
|
211
|
-
// `dismiss()` that arrives before then, never reporting `onDismiss`. Holding
|
|
212
|
-
// it until the sheet mounts keeps the early Escape/back claim releasable.
|
|
213
|
-
const dismissSheet = useCallback((...args: Parameters<BottomSheetModalRef['dismiss']>) => {
|
|
214
|
-
if (mountPendingRef.current) {
|
|
215
|
-
pendingDismissRef.current = args;
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
modalRef.current?.dismiss(...args);
|
|
219
|
-
}, []);
|
|
206
|
+
>(
|
|
207
|
+
(
|
|
208
|
+
{ handleVisible = true, footer, fullBleed, children, onDismiss, onChange, onAnimate, ...rest },
|
|
209
|
+
ref,
|
|
210
|
+
) => {
|
|
211
|
+
const parent = useParentContext();
|
|
212
|
+
const ctx = useMemo(() => ({ ...parent, [SCOPE]: { fullBleed } }), [parent, fullBleed]);
|
|
213
|
+
|
|
214
|
+
const renderFooter = useCallback(
|
|
215
|
+
(props: GorhomBottomSheetFooterProps) => (
|
|
216
|
+
<GorhomBottomSheetFooter {...props}>{footer}</GorhomBottomSheetFooter>
|
|
217
|
+
),
|
|
218
|
+
[footer],
|
|
219
|
+
);
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
mountPendingRef
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
const modalRef = useRef<BottomSheetModalRef | null>(null);
|
|
222
|
+
const dismissedRef = useRef(false);
|
|
223
|
+
const sheetMountedRef = useRef(false);
|
|
224
|
+
const mountPendingRef = useRef(false);
|
|
225
|
+
const pendingDismissRef = useRef<Parameters<BottomSheetModalRef['dismiss']> | null>(null);
|
|
226
|
+
const [presented, setPresented] = useState(false);
|
|
227
|
+
|
|
228
|
+
// Must stay a callback ref: gorhom hands this ref to its modal provider, which
|
|
229
|
+
// reads `.current` to minimize and restore stacked sheets. A ref object would
|
|
230
|
+
// switch that stacking on.
|
|
231
|
+
const setModalRef = useCallback((instance: BottomSheetModalRef | null) => {
|
|
232
|
+
modalRef.current = instance;
|
|
233
|
+
}, []);
|
|
234
|
+
|
|
235
|
+
// gorhom mounts the sheet a frame after `present()` and silently drops a
|
|
236
|
+
// `dismiss()` that arrives before then, never reporting `onDismiss`. Holding
|
|
237
|
+
// it until the sheet mounts keeps the early Escape/back claim releasable.
|
|
238
|
+
const dismissSheet = useCallback((...args: Parameters<BottomSheetModalRef['dismiss']>) => {
|
|
239
|
+
if (mountPendingRef.current) {
|
|
240
|
+
pendingDismissRef.current = args;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
modalRef.current?.dismiss(...args);
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
const markSheetMounted = useCallback(() => {
|
|
247
|
+
sheetMountedRef.current = true;
|
|
248
|
+
if (!mountPendingRef.current) return;
|
|
249
|
+
mountPendingRef.current = false;
|
|
250
|
+
const pendingDismiss = pendingDismissRef.current;
|
|
251
|
+
pendingDismissRef.current = null;
|
|
252
|
+
if (pendingDismiss) modalRef.current?.dismiss(...pendingDismiss);
|
|
253
|
+
}, []);
|
|
254
|
+
|
|
255
|
+
// `present()` claims Escape and back immediately; waiting for the first
|
|
256
|
+
// `onChange` would leave the overlay beneath in charge for the whole open animation.
|
|
257
|
+
useImperativeHandle(
|
|
258
|
+
ref,
|
|
259
|
+
(): BottomSheetModalRef => ({
|
|
260
|
+
present: (...args) => {
|
|
261
|
+
dismissedRef.current = false;
|
|
262
|
+
if (!sheetMountedRef.current) mountPendingRef.current = true;
|
|
263
|
+
setPresented(true);
|
|
264
|
+
modalRef.current?.present(...args);
|
|
265
|
+
},
|
|
266
|
+
dismiss: dismissSheet,
|
|
267
|
+
snapToIndex: (...args) => modalRef.current?.snapToIndex(...args),
|
|
268
|
+
snapToPosition: (...args) => modalRef.current?.snapToPosition(...args),
|
|
269
|
+
expand: (...args) => modalRef.current?.expand(...args),
|
|
270
|
+
collapse: (...args) => modalRef.current?.collapse(...args),
|
|
271
|
+
close: (...args) => modalRef.current?.close(...args),
|
|
272
|
+
forceClose: (...args) => modalRef.current?.forceClose(...args),
|
|
273
|
+
}),
|
|
274
|
+
[dismissSheet],
|
|
275
|
+
);
|
|
229
276
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
277
|
+
const handleDismiss = useCallback(() => {
|
|
278
|
+
sheetMountedRef.current = false;
|
|
279
|
+
mountPendingRef.current = false;
|
|
280
|
+
pendingDismissRef.current = null;
|
|
281
|
+
setPresented(false);
|
|
282
|
+
if (dismissedRef.current) return;
|
|
283
|
+
dismissedRef.current = true;
|
|
284
|
+
onDismiss?.();
|
|
285
|
+
}, [onDismiss]);
|
|
286
|
+
|
|
287
|
+
const handleChange = useCallback(
|
|
288
|
+
(...args: Parameters<NonNullable<BottomSheetModalProps['onChange']>>) => {
|
|
236
289
|
dismissedRef.current = false;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
290
|
+
setPresented(args[0] >= 0);
|
|
291
|
+
if (args[0] >= 0) markSheetMounted();
|
|
292
|
+
onChange?.(...args);
|
|
240
293
|
},
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
snapToPosition: (...args) => modalRef.current?.snapToPosition(...args),
|
|
244
|
-
expand: (...args) => modalRef.current?.expand(...args),
|
|
245
|
-
collapse: (...args) => modalRef.current?.collapse(...args),
|
|
246
|
-
close: (...args) => modalRef.current?.close(...args),
|
|
247
|
-
forceClose: (...args) => modalRef.current?.forceClose(...args),
|
|
248
|
-
}),
|
|
249
|
-
[dismissSheet],
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
const handleDismiss = useCallback(() => {
|
|
253
|
-
sheetMountedRef.current = false;
|
|
254
|
-
mountPendingRef.current = false;
|
|
255
|
-
pendingDismissRef.current = null;
|
|
256
|
-
setPresented(false);
|
|
257
|
-
if (dismissedRef.current) return;
|
|
258
|
-
dismissedRef.current = true;
|
|
259
|
-
onDismiss?.();
|
|
260
|
-
}, [onDismiss]);
|
|
261
|
-
|
|
262
|
-
const handleChange = useCallback(
|
|
263
|
-
(...args: Parameters<NonNullable<BottomSheetModalProps['onChange']>>) => {
|
|
264
|
-
dismissedRef.current = false;
|
|
265
|
-
setPresented(args[0] >= 0);
|
|
266
|
-
if (args[0] >= 0) markSheetMounted();
|
|
267
|
-
onChange?.(...args);
|
|
268
|
-
},
|
|
269
|
-
[markSheetMounted, onChange],
|
|
270
|
-
);
|
|
294
|
+
[markSheetMounted, onChange],
|
|
295
|
+
);
|
|
271
296
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
297
|
+
const handleAnimate = useCallback(
|
|
298
|
+
(...args: Parameters<NonNullable<BottomSheetModalProps['onAnimate']>>) => {
|
|
299
|
+
if (args[1] >= 0) markSheetMounted();
|
|
300
|
+
onAnimate?.(...args);
|
|
301
|
+
},
|
|
302
|
+
[markSheetMounted, onAnimate],
|
|
303
|
+
);
|
|
279
304
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
useEffect(() => {
|
|
296
|
-
// BackHandler is native-only; react-native-web warns on every subscription.
|
|
297
|
-
if (!presented || Platform.OS === 'web') return;
|
|
298
|
-
|
|
299
|
-
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
300
|
-
if (!isTopmost('back')) return false;
|
|
301
|
-
dismissSheet();
|
|
302
|
-
return true;
|
|
305
|
+
const id = useId();
|
|
306
|
+
const overlayId = `bottom-sheet-${id}`;
|
|
307
|
+
useEscapeKey(presented, dismissSheet, {
|
|
308
|
+
id: overlayId,
|
|
309
|
+
groupId: overlayId,
|
|
310
|
+
layer: 'modal',
|
|
311
|
+
});
|
|
312
|
+
const { isTopmost } = useOverlayStackRegistration({
|
|
313
|
+
active: presented && Platform.OS !== 'web',
|
|
314
|
+
capabilities: ['back'],
|
|
315
|
+
id: overlayId,
|
|
316
|
+
groupId: overlayId,
|
|
317
|
+
layer: 'modal',
|
|
303
318
|
});
|
|
304
319
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}, [dismissSheet, isTopmost, presented]);
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
// BackHandler is native-only; react-native-web warns on every subscription.
|
|
322
|
+
if (!presented || Platform.OS === 'web') return;
|
|
309
323
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
324
|
+
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
325
|
+
if (!isTopmost('back')) return false;
|
|
326
|
+
dismissSheet();
|
|
327
|
+
return true;
|
|
328
|
+
});
|
|
314
329
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
330
|
+
return () => {
|
|
331
|
+
backHandler.remove();
|
|
332
|
+
};
|
|
333
|
+
}, [dismissSheet, isTopmost, presented]);
|
|
334
|
+
|
|
335
|
+
const sheetContainerStyle = useResolveClassNames(bottomSheetModalContainerClassName);
|
|
336
|
+
const handleIndicatorStyle = useResolveClassNames(
|
|
337
|
+
'bg-surface-neutral-subtle w-9 h-[5px] rounded-full',
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<BottomSheetModal
|
|
342
|
+
ref={setModalRef}
|
|
343
|
+
enableDynamicSizing
|
|
344
|
+
maxDynamicContentSize={MAX_DYNAMIC_CONTENT_SIZE}
|
|
345
|
+
style={sheetContainerStyle}
|
|
346
|
+
handleComponent={handleVisible ? undefined : null}
|
|
347
|
+
handleIndicatorStyle={handleVisible ? handleIndicatorStyle : undefined}
|
|
348
|
+
backdropComponent={CustomBackdrop}
|
|
349
|
+
backgroundComponent={CustomBackground}
|
|
350
|
+
{...rest}
|
|
351
|
+
onDismiss={handleDismiss}
|
|
352
|
+
onChange={handleChange}
|
|
353
|
+
onAnimate={handleAnimate}
|
|
354
|
+
{...(footer != null && { footerComponent: renderFooter })}
|
|
355
|
+
>
|
|
356
|
+
{typeof children === 'function' ? (
|
|
357
|
+
(props: { data?: unknown }) => (
|
|
358
|
+
<ParentContext.Provider value={ctx}>
|
|
359
|
+
{children(props) as ReactNode}
|
|
360
|
+
</ParentContext.Provider>
|
|
361
|
+
)
|
|
362
|
+
) : (
|
|
363
|
+
<ParentContext.Provider value={ctx}>{children}</ParentContext.Provider>
|
|
364
|
+
)}
|
|
365
|
+
</BottomSheetModal>
|
|
366
|
+
);
|
|
367
|
+
},
|
|
368
|
+
);
|
|
333
369
|
|
|
334
370
|
BottomSheetModalStyled.displayName = 'BottomSheet.Modal';
|
|
335
371
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cva } from '@cdx-ui/utils';
|
|
1
|
+
import { cva, type VariantProps } from '@cdx-ui/utils';
|
|
2
2
|
|
|
3
3
|
/** Keep gorhom's animated container unclipped so web transforms can settle correctly. */
|
|
4
4
|
export const bottomSheetModalContainerClassName = 'rounded-t-4xl';
|
|
@@ -6,6 +6,14 @@ export const bottomSheetModalContainerClassName = 'rounded-t-4xl';
|
|
|
6
6
|
/** Clip the static background for rounded corners without clipping the animated sheet. */
|
|
7
7
|
export const bottomSheetBackgroundClassName = `${bottomSheetModalContainerClassName} overflow-hidden bg-surface-primary`;
|
|
8
8
|
|
|
9
|
-
export const bottomSheetContentVariants = cva(['bg-surface-primary', 'px-
|
|
9
|
+
export const bottomSheetContentVariants = cva(['bg-surface-primary', 'px-4 pb-4 gap-3'], {
|
|
10
|
+
variants: {
|
|
11
|
+
fullBleed: {
|
|
12
|
+
true: 'px-0',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
10
16
|
|
|
11
|
-
export
|
|
17
|
+
export type BottomSheetContentVariantProps = VariantProps<typeof bottomSheetContentVariants>;
|
|
18
|
+
|
|
19
|
+
export const bottomSheetFooterVariants = cva(['px-4 pb-5 gap-2', 'bg-surface-primary']);
|
|
@@ -94,28 +94,35 @@ export const CalendarMonthHeader = forwardRef<ComponentRef<typeof View>, Calenda
|
|
|
94
94
|
/>
|
|
95
95
|
</Pressable>
|
|
96
96
|
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
97
|
+
{/* Stays mounted in the year view so the header keeps its height and the label does not move. */}
|
|
98
|
+
<View
|
|
99
|
+
className={calendarMonthHeaderNavVariants({ hidden: isYearView })}
|
|
100
|
+
pointerEvents={isYearView ? 'none' : 'auto'}
|
|
101
|
+
accessibilityElementsHidden={isYearView}
|
|
102
|
+
importantForAccessibility={isYearView ? 'no-hide-descendants' : 'auto'}
|
|
103
|
+
{...(isYearView ? { 'aria-hidden': true } : {})}
|
|
104
|
+
>
|
|
105
|
+
<IconButton
|
|
106
|
+
as={ChevronLeft}
|
|
107
|
+
variant="ghost"
|
|
108
|
+
color="neutral"
|
|
109
|
+
size="small"
|
|
110
|
+
accessibilityLabel={labels.previousMonth}
|
|
111
|
+
isDisabled={disabled || isYearView}
|
|
112
|
+
focusable={!isYearView}
|
|
113
|
+
onPress={() => shiftMonth(-1)}
|
|
114
|
+
/>
|
|
115
|
+
<IconButton
|
|
116
|
+
as={ChevronRight}
|
|
117
|
+
variant="ghost"
|
|
118
|
+
color="neutral"
|
|
119
|
+
size="small"
|
|
120
|
+
accessibilityLabel={labels.nextMonth}
|
|
121
|
+
isDisabled={disabled || isYearView}
|
|
122
|
+
focusable={!isYearView}
|
|
123
|
+
onPress={() => shiftMonth(1)}
|
|
124
|
+
/>
|
|
125
|
+
</View>
|
|
119
126
|
</View>
|
|
120
127
|
);
|
|
121
128
|
},
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Fragment, useCallback, useMemo, useRef, type ReactElement } from 'react';
|
|
2
|
+
import { ScrollView, View } from 'react-native';
|
|
3
|
+
import { ScrollShadow } from '../ScrollShadow';
|
|
4
|
+
import {
|
|
5
|
+
calendarYearPickerListContentVariants,
|
|
6
|
+
calendarYearPickerPlaceholderVariants,
|
|
7
|
+
calendarYearPickerRowVariants,
|
|
8
|
+
} from './styles';
|
|
9
|
+
|
|
10
|
+
/** Years listed before / after today's year on a side with no `minValue` / `maxValue`. */
|
|
11
|
+
const DEFAULT_YEAR_WINDOW = 100;
|
|
12
|
+
const YEAR_COLUMNS = 3;
|
|
13
|
+
/** Pill cell (40px) plus its vertical margins and row gap; fixed so the initial offset is exact. */
|
|
14
|
+
export const YEAR_ROW_HEIGHT = 52;
|
|
15
|
+
|
|
16
|
+
const ROW_STYLE = { height: YEAR_ROW_HEIGHT };
|
|
17
|
+
|
|
18
|
+
export interface YearRange {
|
|
19
|
+
first: number;
|
|
20
|
+
last: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getYearRange(
|
|
24
|
+
minYear: number | undefined,
|
|
25
|
+
maxYear: number | undefined,
|
|
26
|
+
visibleYear: number,
|
|
27
|
+
todayYear: number,
|
|
28
|
+
): YearRange {
|
|
29
|
+
const first =
|
|
30
|
+
minYear ?? Math.min(todayYear - DEFAULT_YEAR_WINDOW, visibleYear, maxYear ?? Infinity);
|
|
31
|
+
const last = maxYear ?? Math.max(todayYear + DEFAULT_YEAR_WINDOW, visibleYear, first);
|
|
32
|
+
return { first, last: Math.max(first, last) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clamp(value: number, min: number, max: number): number {
|
|
36
|
+
return Math.min(Math.max(value, min), max);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CalendarYearListProps extends YearRange {
|
|
40
|
+
selectedYear: number;
|
|
41
|
+
/** Fixed list height, matched to the sibling view so switching views does not resize. */
|
|
42
|
+
height: number;
|
|
43
|
+
renderYear: (year: number) => ReactElement;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Three-up year list shared by the Calendar, RangeCalendar, and MonthCalendar year views.
|
|
48
|
+
*
|
|
49
|
+
* A plain `ScrollView` rather than a `FlatList`: the pickers render this inside their popover
|
|
50
|
+
* `ScrollView`, and a nested `VirtualizedList` breaks windowing there. The list is a few hundred
|
|
51
|
+
* light cells at most, so virtualization buys little.
|
|
52
|
+
*/
|
|
53
|
+
export function CalendarYearList({
|
|
54
|
+
first,
|
|
55
|
+
last,
|
|
56
|
+
selectedYear,
|
|
57
|
+
height,
|
|
58
|
+
renderYear,
|
|
59
|
+
}: CalendarYearListProps) {
|
|
60
|
+
const scrollRef = useRef<ScrollView>(null);
|
|
61
|
+
const hasAppliedInitialOffset = useRef(false);
|
|
62
|
+
|
|
63
|
+
const rows = useMemo<(number | null)[][]>(() => {
|
|
64
|
+
const count = last - first + 1;
|
|
65
|
+
return Array.from({ length: Math.ceil(count / YEAR_COLUMNS) }, (_, row) =>
|
|
66
|
+
Array.from({ length: YEAR_COLUMNS }, (_, column) => {
|
|
67
|
+
const index = row * YEAR_COLUMNS + column;
|
|
68
|
+
return index < count ? first + index : null;
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}, [first, last]);
|
|
72
|
+
|
|
73
|
+
const visibleRows = height / YEAR_ROW_HEIGHT;
|
|
74
|
+
const selectedRow = Math.floor((clamp(selectedYear, first, last) - first) / YEAR_COLUMNS);
|
|
75
|
+
const firstVisibleRow = Math.max(0, selectedRow - Math.floor((visibleRows - 1) / 2));
|
|
76
|
+
const maxOffset = Math.max(0, rows.length * YEAR_ROW_HEIGHT - height);
|
|
77
|
+
const initialOffsetY = Math.min(firstVisibleRow * YEAR_ROW_HEIGHT, maxOffset);
|
|
78
|
+
|
|
79
|
+
// `contentOffset` is computed once so later re-renders (a new selection) never yank the
|
|
80
|
+
// scroll position back.
|
|
81
|
+
const initialOffset = useRef({ x: 0, y: initialOffsetY }).current;
|
|
82
|
+
const listStyle = useMemo(() => ({ height }), [height]);
|
|
83
|
+
|
|
84
|
+
// iOS and Android apply `contentOffset` before first paint; React Native Web ignores it, so
|
|
85
|
+
// the same offset is applied once content has a size.
|
|
86
|
+
const handleContentSizeChange = useCallback(() => {
|
|
87
|
+
if (hasAppliedInitialOffset.current) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
hasAppliedInitialOffset.current = true;
|
|
91
|
+
if (initialOffset.y > 0) {
|
|
92
|
+
scrollRef.current?.scrollTo({ x: 0, y: initialOffset.y, animated: false });
|
|
93
|
+
}
|
|
94
|
+
}, [initialOffset]);
|
|
95
|
+
|
|
96
|
+
const content = useMemo(
|
|
97
|
+
() =>
|
|
98
|
+
rows.map((cells, row) => (
|
|
99
|
+
<View
|
|
100
|
+
key={first + row * YEAR_COLUMNS}
|
|
101
|
+
className={calendarYearPickerRowVariants()}
|
|
102
|
+
style={ROW_STYLE}
|
|
103
|
+
>
|
|
104
|
+
{cells.map((year, column) =>
|
|
105
|
+
year === null ? (
|
|
106
|
+
<View
|
|
107
|
+
key={`placeholder-${String(column)}`}
|
|
108
|
+
className={calendarYearPickerPlaceholderVariants()}
|
|
109
|
+
accessibilityElementsHidden
|
|
110
|
+
importantForAccessibility="no-hide-descendants"
|
|
111
|
+
aria-hidden
|
|
112
|
+
/>
|
|
113
|
+
) : (
|
|
114
|
+
<Fragment key={year}>{renderYear(year)}</Fragment>
|
|
115
|
+
),
|
|
116
|
+
)}
|
|
117
|
+
</View>
|
|
118
|
+
)),
|
|
119
|
+
[first, rows, renderYear],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<ScrollShadow surface="primary" size="small">
|
|
124
|
+
<ScrollView
|
|
125
|
+
ref={scrollRef}
|
|
126
|
+
style={listStyle}
|
|
127
|
+
contentContainerClassName={calendarYearPickerListContentVariants()}
|
|
128
|
+
contentOffset={initialOffset}
|
|
129
|
+
onContentSizeChange={handleContentSizeChange}
|
|
130
|
+
nestedScrollEnabled
|
|
131
|
+
>
|
|
132
|
+
{content}
|
|
133
|
+
</ScrollView>
|
|
134
|
+
</ScrollShadow>
|
|
135
|
+
);
|
|
136
|
+
}
|