@cdx-ui/components 0.2.4 → 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/accordion/content.md +2 -2
- package/docs/components/alert/content.md +1 -1
- package/docs/components/badge/content.md +5 -15
- package/docs/components/bottom-sheet/content.md +18 -14
- package/docs/components/box/content.md +141 -0
- package/docs/components/breadcrumbs/content.md +9 -9
- package/docs/components/button/content.md +19 -19
- package/docs/components/calendar/content.md +25 -1
- package/docs/components/card/content.md +2 -2
- package/docs/components/carousel/content.md +9 -8
- package/docs/components/chip/content.md +1 -1
- package/docs/components/color-area/content.md +3 -5
- package/docs/components/color-field/content.md +3 -5
- package/docs/components/color-picker/content.md +5 -4
- package/docs/components/color-slider/content.md +3 -5
- package/docs/components/color-swatch/content.md +3 -5
- package/docs/components/color-swatch-picker/content.md +3 -5
- package/docs/components/content.md +10 -9
- package/docs/components/data-grid/content.md +10 -11
- package/docs/components/drawer/content.md +17 -25
- package/docs/components/full-screen-dialog/content.md +2 -2
- package/docs/components/icon/content.md +1 -1
- package/docs/components/icon-button/content.md +3 -3
- package/docs/components/input/content.md +65 -0
- package/docs/components/list-item/content.md +9 -9
- package/docs/components/menu/content.md +57 -50
- package/docs/components/month-calendar/content.md +2 -0
- package/docs/components/range-calendar/content.md +3 -1
- package/docs/components/reorderable-list/content.md +5 -3
- package/docs/components/scoped-theme/content.md +1 -1
- package/docs/components/scroll-shadow/content.md +43 -36
- package/docs/components/select/content.md +80 -7
- package/docs/components/skeleton/content.md +7 -6
- package/docs/components/table/content.md +2 -2
- package/docs/components/tabs/content.md +14 -9
- package/docs/components/tile/content.md +9 -10
- package/docs/components/toggle-button/content.md +1 -1
- package/docs/components/tooltip/content.md +9 -9
- package/docs/components/virtualized-list/content.md +14 -14
- package/docs/getting-started/developers/content.md +2 -2
- package/docs/getting-started/developers/styling/content.md +20 -18
- package/docs/llms.txt +12 -9
- package/docs/patterns/authentication/content.md +4 -6
- package/docs/patterns/dashboard/content.md +8 -13
- package/docs/patterns/empty-states/content.md +7 -6
- package/docs/patterns/forms/content.md +6 -7
- package/docs/patterns/onboarding/content.md +4 -4
- package/docs/patterns/settings/content.md +5 -5
- package/docs/resources/release-notes/0.2.4/content.md +17 -0
- package/docs/resources/release-notes/0.2.5/content.md +25 -0
- package/docs/resources/release-notes/content.md +2 -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/Box/Box.js +9 -1
- package/lib/commonjs/components/Box/Box.js.map +1 -1
- package/lib/commonjs/components/Breadcrumbs/index.js +28 -3
- package/lib/commonjs/components/Breadcrumbs/index.js.map +1 -1
- package/lib/commonjs/components/Breadcrumbs/styles.js +27 -0
- package/lib/commonjs/components/Breadcrumbs/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/commonjs/components/Stack/HStack.js +6 -2
- package/lib/commonjs/components/Stack/HStack.js.map +1 -1
- package/lib/commonjs/components/Stack/VStack.js +6 -2
- package/lib/commonjs/components/Stack/VStack.js.map +1 -1
- package/lib/commonjs/index.js +10 -10
- package/lib/commonjs/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/Box/Box.js +9 -1
- package/lib/module/components/Box/Box.js.map +1 -1
- package/lib/module/components/Breadcrumbs/index.js +29 -4
- package/lib/module/components/Breadcrumbs/index.js.map +1 -1
- package/lib/module/components/Breadcrumbs/styles.js +26 -0
- package/lib/module/components/Breadcrumbs/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/module/components/Stack/HStack.js +6 -2
- package/lib/module/components/Stack/HStack.js.map +1 -1
- package/lib/module/components/Stack/VStack.js +6 -2
- package/lib/module/components/Stack/VStack.js.map +1 -1
- package/lib/module/index.js +6 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/animation/hooks/usePopupDialogContentAnimation.d.ts +2 -2
- package/lib/typescript/animation/hooks/usePopupOverlayAnimation.d.ts +2 -2
- 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/Box/Box.d.ts +4 -0
- package/lib/typescript/components/Box/Box.d.ts.map +1 -1
- package/lib/typescript/components/Breadcrumbs/index.d.ts +3 -1
- package/lib/typescript/components/Breadcrumbs/index.d.ts.map +1 -1
- package/lib/typescript/components/Breadcrumbs/styles.d.ts +2 -0
- package/lib/typescript/components/Breadcrumbs/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/Stack/HStack.d.ts +3 -2
- package/lib/typescript/components/Stack/HStack.d.ts.map +1 -1
- package/lib/typescript/components/Stack/VStack.d.ts +3 -2
- package/lib/typescript/components/Stack/VStack.d.ts.map +1 -1
- package/lib/typescript/components/Toast/animation.d.ts +3 -3
- package/lib/typescript/components/Toast/animation.d.ts.map +1 -1
- package/lib/typescript/components/ToggleButton/styles.d.ts +1 -1
- package/lib/typescript/index.d.ts +6 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/components/BottomSheet/index.tsx +174 -138
- package/src/components/BottomSheet/styles.ts +11 -3
- package/src/components/Box/Box.tsx +18 -4
- package/src/components/Breadcrumbs/index.tsx +28 -4
- package/src/components/Breadcrumbs/styles.ts +30 -0
- 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
- package/src/components/Stack/HStack.tsx +8 -4
- package/src/components/Stack/VStack.tsx +8 -4
- package/src/index.ts +8 -1
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
type ILinkProps,
|
|
20
20
|
} from '@cdx-ui/primitives';
|
|
21
21
|
import { cn } from '@cdx-ui/utils';
|
|
22
|
+
import { Icon } from '../Icon';
|
|
23
|
+
import { Text as ForgeText } from '../Text';
|
|
22
24
|
import {
|
|
25
|
+
breadcrumbContentColor,
|
|
23
26
|
breadcrumbItemVariants,
|
|
24
27
|
breadcrumbDisabledCursor,
|
|
25
28
|
breadcrumbItemRootVariants,
|
|
@@ -35,7 +38,8 @@ type BreadcrumbItemState = 'current' | 'ancestor' | 'disabled';
|
|
|
35
38
|
export interface BreadcrumbsProps extends IBreadcrumbsProps {
|
|
36
39
|
className?: string;
|
|
37
40
|
/**
|
|
38
|
-
* Separator rendered between breadcrumb items.
|
|
41
|
+
* Separator rendered between breadcrumb items. A direct `Icon` or `Text` child takes the
|
|
42
|
+
* separator color unless it sets `color`.
|
|
39
43
|
* @default `/`
|
|
40
44
|
*/
|
|
41
45
|
separator?: ReactNode;
|
|
@@ -48,6 +52,7 @@ interface BreadcrumbItemInternalProps {
|
|
|
48
52
|
|
|
49
53
|
export interface BreadcrumbsItemProps extends Omit<ILinkProps, 'children' | 'disabled'> {
|
|
50
54
|
className?: string;
|
|
55
|
+
/** Item content. A direct `Icon` or `Text` child takes the item's state color unless it sets `color`. */
|
|
51
56
|
children?: ReactNode;
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -65,6 +70,23 @@ function isTextOnlyChild(children: ReactNode): children is string | number {
|
|
|
65
70
|
return typeof children === 'string' || typeof children === 'number';
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
// `Icon` and `Text` default to `text-content-primary` and native has no color cascade, so
|
|
74
|
+
// direct children that leave `color` unset take the content color through `className`.
|
|
75
|
+
function withContentColor(children: ReactNode, colorClassName: string): ReactNode {
|
|
76
|
+
return Children.map(children, (child) => {
|
|
77
|
+
if (!isValidElement<{ className?: string; color?: unknown }>(child)) {
|
|
78
|
+
return child;
|
|
79
|
+
}
|
|
80
|
+
if (child.type !== Icon && child.type !== ForgeText) {
|
|
81
|
+
return child;
|
|
82
|
+
}
|
|
83
|
+
if (child.props.color !== undefined) {
|
|
84
|
+
return child;
|
|
85
|
+
}
|
|
86
|
+
return cloneElement(child, { className: cn(colorClassName, child.props.className) });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
function BreadcrumbItemContent({
|
|
69
91
|
children,
|
|
70
92
|
className,
|
|
@@ -74,6 +96,8 @@ function BreadcrumbItemContent({
|
|
|
74
96
|
className: string;
|
|
75
97
|
state: BreadcrumbItemState;
|
|
76
98
|
}) {
|
|
99
|
+
const { color } = useBreadcrumbsContext();
|
|
100
|
+
|
|
77
101
|
if (isTextOnlyChild(children)) {
|
|
78
102
|
return (
|
|
79
103
|
<Text className={className} {...dataAttributes({ state })}>
|
|
@@ -84,13 +108,13 @@ function BreadcrumbItemContent({
|
|
|
84
108
|
|
|
85
109
|
return (
|
|
86
110
|
<View className={cn('flex-row items-center gap-1', className)} {...dataAttributes({ state })}>
|
|
87
|
-
{children}
|
|
111
|
+
{withContentColor(children, breadcrumbContentColor(state, color))}
|
|
88
112
|
</View>
|
|
89
113
|
);
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
function BreadcrumbsSeparator({ separator }: { separator: ReactNode }) {
|
|
93
|
-
const { isDisabled } = useBreadcrumbsContext();
|
|
117
|
+
const { isDisabled, color } = useBreadcrumbsContext();
|
|
94
118
|
const disabledClassName = isDisabled ? breadcrumbDisabledCursor : undefined;
|
|
95
119
|
const slotProps = mergeDataAttributes({}, dataAttributes({ slot: 'breadcrumbs-separator' }));
|
|
96
120
|
const a11yProps = {
|
|
@@ -121,7 +145,7 @@ function BreadcrumbsSeparator({ separator }: { separator: ReactNode }) {
|
|
|
121
145
|
{...a11yProps}
|
|
122
146
|
{...slotProps}
|
|
123
147
|
>
|
|
124
|
-
{separator}
|
|
148
|
+
{withContentColor(separator, breadcrumbContentColor('separator', color))}
|
|
125
149
|
</View>
|
|
126
150
|
);
|
|
127
151
|
}
|
|
@@ -13,6 +13,36 @@ export const breadcrumbDisabledCursor = 'web:cursor-not-allowed';
|
|
|
13
13
|
|
|
14
14
|
export const breadcrumbSeparatorVariants = cva(['body-md text-content-tertiary select-none px-1']);
|
|
15
15
|
|
|
16
|
+
const breadcrumbAncestorContentColor = {
|
|
17
|
+
neutral: 'text-content-secondary',
|
|
18
|
+
action: 'text-content-action',
|
|
19
|
+
danger: 'text-content-danger',
|
|
20
|
+
warning: 'text-content-warning',
|
|
21
|
+
success: 'text-content-success',
|
|
22
|
+
info: 'text-content-info',
|
|
23
|
+
} satisfies Record<BreadcrumbColor, string>;
|
|
24
|
+
|
|
25
|
+
// Color for an `Icon` or `Text` placed directly in item or separator content. Web inherits
|
|
26
|
+
// the CSS color of the content element, so hover follows it; native has no color cascade,
|
|
27
|
+
// so it resolves the state's token.
|
|
28
|
+
export function breadcrumbContentColor(
|
|
29
|
+
state: 'current' | 'ancestor' | 'disabled' | 'separator',
|
|
30
|
+
color: BreadcrumbColor,
|
|
31
|
+
): string {
|
|
32
|
+
if (Platform.OS === 'web') {
|
|
33
|
+
return 'text-inherit';
|
|
34
|
+
}
|
|
35
|
+
switch (state) {
|
|
36
|
+
case 'current':
|
|
37
|
+
return 'text-content-primary';
|
|
38
|
+
case 'ancestor':
|
|
39
|
+
return breadcrumbAncestorContentColor[color];
|
|
40
|
+
case 'disabled':
|
|
41
|
+
case 'separator':
|
|
42
|
+
return 'text-content-tertiary';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
16
46
|
export const breadcrumbItemRootVariants = cva([
|
|
17
47
|
'self-start',
|
|
18
48
|
'web:outline-none web:focus:outline-none web:focus-visible:outline-none',
|
|
@@ -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
|
+
}
|
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
import { forwardRef, useCallback, useMemo, type ComponentRef } from 'react';
|
|
1
|
+
import { forwardRef, useCallback, useMemo, useState, type ComponentRef } from 'react';
|
|
2
2
|
import { Pressable, Text, View, type ViewProps } from 'react-native';
|
|
3
3
|
import {
|
|
4
4
|
formatMonthId,
|
|
5
5
|
formatSelectYear,
|
|
6
|
+
parseDateId,
|
|
6
7
|
parseMonthId,
|
|
7
8
|
resolveDateComponentLabels,
|
|
8
9
|
type DateComponentLabels,
|
|
10
|
+
type DateId,
|
|
9
11
|
type MonthId,
|
|
10
12
|
} from '@cdx-ui/formatters';
|
|
11
13
|
import { cn } from '@cdx-ui/utils';
|
|
14
|
+
import { CalendarYearList, getYearRange } from './CalendarYearList';
|
|
12
15
|
import type { CalendarNavigationView } from './CalendarMonthHeader';
|
|
16
|
+
import { getFlashCalendarGridHeight } from './flashCalendarAdapter';
|
|
13
17
|
import {
|
|
14
18
|
calendarPillCellTextVariants,
|
|
15
19
|
calendarPillCellVariants,
|
|
16
|
-
|
|
20
|
+
calendarYearPickerListVariants,
|
|
17
21
|
} from './styles';
|
|
18
22
|
|
|
19
|
-
const YEAR_RANGE_BEFORE = 9;
|
|
20
|
-
const YEAR_RANGE_AFTER = 11;
|
|
21
|
-
|
|
22
23
|
export interface CalendarYearPickerGridProps extends ViewProps {
|
|
23
24
|
monthId: MonthId;
|
|
24
25
|
onMonthIdChange: (monthId: MonthId) => void;
|
|
26
|
+
/** Earliest selectable date; its year is the first year listed. */
|
|
27
|
+
minValue?: DateId;
|
|
28
|
+
/** Latest selectable date; its year is the last year listed. */
|
|
29
|
+
maxValue?: DateId;
|
|
25
30
|
disabled?: boolean;
|
|
26
31
|
onViewChange: (view: CalendarNavigationView) => void;
|
|
27
32
|
labels?: Partial<DateComponentLabels>;
|
|
@@ -35,6 +40,8 @@ export const CalendarYearPickerGrid = forwardRef<
|
|
|
35
40
|
{
|
|
36
41
|
monthId,
|
|
37
42
|
onMonthIdChange,
|
|
43
|
+
minValue,
|
|
44
|
+
maxValue,
|
|
38
45
|
disabled = false,
|
|
39
46
|
onViewChange,
|
|
40
47
|
labels: labelsProp,
|
|
@@ -45,54 +52,74 @@ export const CalendarYearPickerGrid = forwardRef<
|
|
|
45
52
|
ref,
|
|
46
53
|
) => {
|
|
47
54
|
const labels = useMemo(() => resolveDateComponentLabels(labelsProp), [labelsProp]);
|
|
55
|
+
const [todayYear] = useState(() => new Date().getFullYear());
|
|
48
56
|
const parts = parseMonthId(monthId);
|
|
49
|
-
const currentYear = parts?.year ??
|
|
57
|
+
const currentYear = parts?.year ?? todayYear;
|
|
50
58
|
const currentMonth = parts?.month ?? 1;
|
|
59
|
+
const minParts = minValue ? parseDateId(minValue) : null;
|
|
60
|
+
const maxParts = maxValue ? parseDateId(maxValue) : null;
|
|
61
|
+
const minYear = minParts?.year;
|
|
62
|
+
const minMonth = minParts?.month;
|
|
63
|
+
const maxYear = maxParts?.year;
|
|
64
|
+
const maxMonth = maxParts?.month;
|
|
51
65
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
(_, i) => currentYear - YEAR_RANGE_BEFORE + i,
|
|
57
|
-
),
|
|
58
|
-
[currentYear],
|
|
59
|
-
);
|
|
66
|
+
const { first, last } = getYearRange(minYear, maxYear, currentYear, todayYear);
|
|
67
|
+
|
|
68
|
+
// Matches the day grid so switching views does not change the Calendar's height.
|
|
69
|
+
const height = getFlashCalendarGridHeight(monthId);
|
|
60
70
|
|
|
61
71
|
const selectYear = useCallback(
|
|
62
72
|
(year: number) => {
|
|
63
73
|
if (disabled) {
|
|
64
74
|
return;
|
|
65
75
|
}
|
|
66
|
-
|
|
76
|
+
let month = currentMonth;
|
|
77
|
+
if (minYear === year && minMonth !== undefined) {
|
|
78
|
+
month = Math.max(month, minMonth);
|
|
79
|
+
}
|
|
80
|
+
if (maxYear === year && maxMonth !== undefined) {
|
|
81
|
+
month = Math.min(month, maxMonth);
|
|
82
|
+
}
|
|
83
|
+
onMonthIdChange(formatMonthId({ year, month }));
|
|
67
84
|
onViewChange('month');
|
|
68
85
|
},
|
|
69
|
-
[currentMonth, disabled, onMonthIdChange, onViewChange],
|
|
86
|
+
[currentMonth, disabled, maxMonth, maxYear, minMonth, minYear, onMonthIdChange, onViewChange],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const renderYear = useCallback(
|
|
90
|
+
(year: number) => {
|
|
91
|
+
const selected = year === currentYear;
|
|
92
|
+
return (
|
|
93
|
+
<Pressable
|
|
94
|
+
accessibilityRole="button"
|
|
95
|
+
accessibilityState={{ selected, disabled }}
|
|
96
|
+
accessibilityLabel={formatSelectYear(labels, year)}
|
|
97
|
+
disabled={disabled}
|
|
98
|
+
onPress={() => selectYear(year)}
|
|
99
|
+
className={calendarPillCellVariants({ selected, disabled })}
|
|
100
|
+
>
|
|
101
|
+
<Text className={calendarPillCellTextVariants({ selected })}>{String(year)}</Text>
|
|
102
|
+
</Pressable>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
[currentYear, disabled, labels, selectYear],
|
|
70
106
|
);
|
|
71
107
|
|
|
72
108
|
return (
|
|
73
109
|
<View
|
|
74
110
|
ref={ref}
|
|
75
|
-
className={cn(
|
|
111
|
+
className={cn(calendarYearPickerListVariants(), className)}
|
|
76
112
|
style={style}
|
|
77
113
|
accessibilityRole="none"
|
|
78
114
|
{...props}
|
|
79
115
|
>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
accessibilityLabel={formatSelectYear(labels, year)}
|
|
88
|
-
disabled={disabled}
|
|
89
|
-
onPress={() => selectYear(year)}
|
|
90
|
-
className={calendarPillCellVariants({ selected, disabled })}
|
|
91
|
-
>
|
|
92
|
-
<Text className={calendarPillCellTextVariants({ selected })}>{String(year)}</Text>
|
|
93
|
-
</Pressable>
|
|
94
|
-
);
|
|
95
|
-
})}
|
|
116
|
+
<CalendarYearList
|
|
117
|
+
first={first}
|
|
118
|
+
last={last}
|
|
119
|
+
selectedYear={currentYear}
|
|
120
|
+
height={height}
|
|
121
|
+
renderYear={renderYear}
|
|
122
|
+
/>
|
|
96
123
|
</View>
|
|
97
124
|
);
|
|
98
125
|
},
|
|
@@ -6,6 +6,9 @@ import { formatDateId, parseMonthId, type DateId, type MonthId } from '@cdx-ui/f
|
|
|
6
6
|
const DAY_SIZE = 36;
|
|
7
7
|
const DAY_RADIUS = DAY_SIZE / 2;
|
|
8
8
|
|
|
9
|
+
/** Bottom padding the theme adds under the weekday header row (`rowWeek.container`). */
|
|
10
|
+
const WEEK_HEADER_PADDING_BOTTOM = 4;
|
|
11
|
+
|
|
9
12
|
const CALENDAR_COLOR_VARS = [
|
|
10
13
|
'--color-surface-action-strong',
|
|
11
14
|
'--color-content-action-on-strong',
|
|
@@ -159,7 +162,7 @@ export function useFlashCalendarTheme(selectedDateId?: DateId): CalendarTheme {
|
|
|
159
162
|
},
|
|
160
163
|
rowWeek: {
|
|
161
164
|
container: {
|
|
162
|
-
paddingBottom:
|
|
165
|
+
paddingBottom: WEEK_HEADER_PADDING_BOTTOM,
|
|
163
166
|
},
|
|
164
167
|
},
|
|
165
168
|
itemWeekName: {
|
|
@@ -249,7 +252,7 @@ export function useFlashCalendarRangeTheme(range?: {
|
|
|
249
252
|
},
|
|
250
253
|
rowWeek: {
|
|
251
254
|
container: {
|
|
252
|
-
paddingBottom:
|
|
255
|
+
paddingBottom: WEEK_HEADER_PADDING_BOTTOM,
|
|
253
256
|
},
|
|
254
257
|
},
|
|
255
258
|
itemWeekName: {
|
|
@@ -348,6 +351,28 @@ export const FLASH_CALENDAR_RANGE_DAY_SPACING = 0;
|
|
|
348
351
|
|
|
349
352
|
export const FLASH_CALENDAR_DAY_HEIGHT = DAY_SIZE;
|
|
350
353
|
|
|
354
|
+
export const FLASH_CALENDAR_WEEK_HEADER_HEIGHT = 28;
|
|
355
|
+
|
|
356
|
+
export const FLASH_CALENDAR_ROW_VERTICAL_SPACING = 4;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Rendered height of the Flash Calendar month grid for `monthId`, derived from the same layout
|
|
360
|
+
* values passed to Flash Calendar (zero-height month row, weekday header, one row per week with
|
|
361
|
+
* Sunday as the first day). Lets the year view match the day view without measuring.
|
|
362
|
+
*/
|
|
363
|
+
export function getFlashCalendarGridHeight(monthId: MonthId): number {
|
|
364
|
+
const parts = parseMonthId(monthId);
|
|
365
|
+
const firstWeekday = parts ? new Date(parts.year, parts.month - 1, 1).getDay() : 0;
|
|
366
|
+
const daysInMonth = parts ? new Date(parts.year, parts.month, 0).getDate() : 31;
|
|
367
|
+
const weekCount = Math.ceil((firstWeekday + daysInMonth) / 7);
|
|
368
|
+
return (
|
|
369
|
+
FLASH_CALENDAR_WEEK_HEADER_HEIGHT +
|
|
370
|
+
WEEK_HEADER_PADDING_BOTTOM +
|
|
371
|
+
weekCount * FLASH_CALENDAR_DAY_HEIGHT +
|
|
372
|
+
(weekCount + 1) * FLASH_CALENDAR_ROW_VERTICAL_SPACING
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
351
376
|
export function formatFlashCalendarWeekdayShort(date: Date, locale: string): string {
|
|
352
377
|
return new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(date);
|
|
353
378
|
}
|
|
@@ -15,6 +15,8 @@ import { CalendarMonthHeader } from './CalendarMonthHeader';
|
|
|
15
15
|
import { CalendarYearPickerGrid } from './CalendarYearPickerGrid';
|
|
16
16
|
import {
|
|
17
17
|
FLASH_CALENDAR_DAY_HEIGHT,
|
|
18
|
+
FLASH_CALENDAR_ROW_VERTICAL_SPACING,
|
|
19
|
+
FLASH_CALENDAR_WEEK_HEADER_HEIGHT,
|
|
18
20
|
formatFlashCalendarWeekdayShort,
|
|
19
21
|
getMonthDateIds,
|
|
20
22
|
toCalendarDateId,
|
|
@@ -143,8 +145,8 @@ const CalendarGrid = forwardRef<View, ViewProps>(({ className, style, ...props }
|
|
|
143
145
|
calendarDisabledDateIds={unavailableDateIds}
|
|
144
146
|
calendarMonthHeaderHeight={0}
|
|
145
147
|
calendarDayHeight={FLASH_CALENDAR_DAY_HEIGHT}
|
|
146
|
-
calendarWeekHeaderHeight={
|
|
147
|
-
calendarRowVerticalSpacing={
|
|
148
|
+
calendarWeekHeaderHeight={FLASH_CALENDAR_WEEK_HEADER_HEIGHT}
|
|
149
|
+
calendarRowVerticalSpacing={FLASH_CALENDAR_ROW_VERTICAL_SPACING}
|
|
148
150
|
calendarRowHorizontalSpacing={4}
|
|
149
151
|
getCalendarMonthFormat={() => ''}
|
|
150
152
|
getCalendarWeekDayFormat={formatWeekday}
|
|
@@ -158,12 +160,15 @@ const CalendarGrid = forwardRef<View, ViewProps>(({ className, style, ...props }
|
|
|
158
160
|
CalendarGrid.displayName = 'Calendar.Grid';
|
|
159
161
|
|
|
160
162
|
const CalendarYearPicker = forwardRef<View, ViewProps>(({ className, style, ...props }, ref) => {
|
|
161
|
-
const { monthId, onMonthIdChange, disabled, setView, labels } =
|
|
163
|
+
const { monthId, onMonthIdChange, minValue, maxValue, disabled, setView, labels } =
|
|
164
|
+
useCalendarContext();
|
|
162
165
|
return (
|
|
163
166
|
<CalendarYearPickerGrid
|
|
164
167
|
ref={ref}
|
|
165
168
|
monthId={monthId}
|
|
166
169
|
onMonthIdChange={onMonthIdChange}
|
|
170
|
+
minValue={minValue}
|
|
171
|
+
maxValue={maxValue}
|
|
167
172
|
disabled={disabled}
|
|
168
173
|
onViewChange={setView}
|
|
169
174
|
labels={labels}
|
|
@@ -31,12 +31,28 @@ export const calendarMonthHeaderIconVariants = cva('size-5 shrink-0', {
|
|
|
31
31
|
defaultVariants: { isYearView: false },
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
export const calendarMonthHeaderNavVariants = cva('flex-row items-center gap-1'
|
|
34
|
+
export const calendarMonthHeaderNavVariants = cva('flex-row items-center gap-1', {
|
|
35
|
+
variants: {
|
|
36
|
+
hidden: {
|
|
37
|
+
true: 'opacity-0',
|
|
38
|
+
false: '',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: { hidden: false },
|
|
42
|
+
});
|
|
35
43
|
|
|
36
44
|
export const calendarYearPickerGridVariants = cva(
|
|
37
45
|
'flex-row flex-wrap justify-between gap-y-2 px-1 py-1',
|
|
38
46
|
);
|
|
39
47
|
|
|
48
|
+
export const calendarYearPickerListVariants = cva('w-full');
|
|
49
|
+
|
|
50
|
+
export const calendarYearPickerListContentVariants = cva('px-1');
|
|
51
|
+
|
|
52
|
+
export const calendarYearPickerRowVariants = cva('flex-row items-center justify-between');
|
|
53
|
+
|
|
54
|
+
export const calendarYearPickerPlaceholderVariants = cva('w-[31%]');
|
|
55
|
+
|
|
40
56
|
export const calendarPillCellVariants = cva(
|
|
41
57
|
'my-0.5 w-[31%] items-center justify-center rounded-[var(--border-radius-round)] py-2.5',
|
|
42
58
|
{
|