cpk-ui 0.7.0 → 0.8.0-beta.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/README.md +14 -14
- package/components/uis/Calendar/Calendar.d.ts +6 -0
- package/components/uis/Calendar/Calendar.d.ts.map +1 -0
- package/components/uis/Calendar/Calendar.js +482 -0
- package/components/uis/Calendar/CalendarDay.d.ts +21 -0
- package/components/uis/Calendar/CalendarDay.d.ts.map +1 -0
- package/components/uis/Calendar/CalendarDay.js +172 -0
- package/components/uis/Calendar/CalendarHeader.d.ts +22 -0
- package/components/uis/Calendar/CalendarHeader.d.ts.map +1 -0
- package/components/uis/Calendar/CalendarHeader.js +40 -0
- package/components/uis/Calendar/CalendarPage.d.ts +46 -0
- package/components/uis/Calendar/CalendarPage.d.ts.map +1 -0
- package/components/uis/Calendar/CalendarPage.js +34 -0
- package/components/uis/Calendar/CalendarPager.d.ts +11 -0
- package/components/uis/Calendar/CalendarPager.d.ts.map +1 -0
- package/components/uis/Calendar/CalendarPager.js +115 -0
- package/components/uis/Calendar/CalendarPager.web.d.ts +11 -0
- package/components/uis/Calendar/CalendarPager.web.d.ts.map +1 -0
- package/components/uis/Calendar/CalendarPager.web.js +72 -0
- package/components/uis/Calendar/CalendarWeekRow.d.ts +20 -0
- package/components/uis/Calendar/CalendarWeekRow.d.ts.map +1 -0
- package/components/uis/Calendar/CalendarWeekRow.js +64 -0
- package/components/uis/Calendar/CalendarWeekdayRow.d.ts +24 -0
- package/components/uis/Calendar/CalendarWeekdayRow.d.ts.map +1 -0
- package/components/uis/Calendar/CalendarWeekdayRow.js +45 -0
- package/components/uis/Calendar/intl.d.ts +18 -0
- package/components/uis/Calendar/intl.d.ts.map +1 -0
- package/components/uis/Calendar/intl.js +146 -0
- package/components/uis/Calendar/palette.d.ts +10 -0
- package/components/uis/Calendar/palette.d.ts.map +1 -0
- package/components/uis/Calendar/palette.js +93 -0
- package/components/uis/Calendar/types.d.ts +286 -0
- package/components/uis/Calendar/types.d.ts.map +1 -0
- package/components/uis/Calendar/types.js +1 -0
- package/components/uis/Calendar/useCalendarState.d.ts +66 -0
- package/components/uis/Calendar/useCalendarState.d.ts.map +1 -0
- package/components/uis/Calendar/useCalendarState.js +211 -0
- package/components/uis/Calendar/utils.d.ts +114 -0
- package/components/uis/Calendar/utils.d.ts.map +1 -0
- package/components/uis/Calendar/utils.js +429 -0
- package/components/uis/ParallaxHeader/ParallaxHeader.d.ts +44 -0
- package/components/uis/ParallaxHeader/ParallaxHeader.d.ts.map +1 -0
- package/components/uis/ParallaxHeader/ParallaxHeader.js +142 -0
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ maintained by Cross Platform Korea and uses
|
|
|
10
10
|
[kstyled](https://crossplatformkorea.github.io/kstyled) for typed,
|
|
11
11
|
compile-time-aware styling.
|
|
12
12
|
|
|
13
|
-
`cpk-ui@0.
|
|
13
|
+
`cpk-ui@0.8.0-beta.0` pairs with `kstyled@0.4.1`. Keep the runtime and Babel plugin on
|
|
14
14
|
the same kstyled version.
|
|
15
15
|
|
|
16
16
|
## Install
|
|
@@ -18,9 +18,9 @@ the same kstyled version.
|
|
|
18
18
|
### Expo
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npx expo install cpk-ui@0.
|
|
21
|
+
npx expo install cpk-ui@0.8.0-beta.0 kstyled@0.4.1 @expo/vector-icons \
|
|
22
22
|
react-native-gesture-handler react-native-svg expo-screen-orientation \
|
|
23
|
-
@expo/match-media expo-haptics
|
|
23
|
+
react-native-reanimated @expo/match-media expo-haptics
|
|
24
24
|
bun add --dev babel-plugin-kstyled@0.4.1
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -30,9 +30,9 @@ Install Expo modules first, then add the same runtime dependencies:
|
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
npx install-expo-modules@latest
|
|
33
|
-
bun add cpk-ui@0.
|
|
33
|
+
bun add cpk-ui@0.8.0-beta.0 kstyled@0.4.1 @expo/vector-icons \
|
|
34
34
|
react-native-gesture-handler react-native-svg expo-screen-orientation \
|
|
35
|
-
@expo/match-media expo-haptics
|
|
35
|
+
react-native-reanimated @expo/match-media expo-haptics
|
|
36
36
|
bun add --dev babel-plugin-kstyled@0.4.1
|
|
37
37
|
```
|
|
38
38
|
|
|
@@ -77,13 +77,13 @@ dark theme tokens. Applications that provide product-specific tokens can pass
|
|
|
77
77
|
|
|
78
78
|
## Component Surface
|
|
79
79
|
|
|
80
|
-
| Area | Components
|
|
81
|
-
| ---------------- |
|
|
82
|
-
| Actions | Button, CustomPressable, Fab, IconButton
|
|
83
|
-
| Inputs | Checkbox, EditText, RadioGroup, Rating, SegmentedControl, SwitchToggle |
|
|
84
|
-
| Feedback | AlertDialog, ErrorBoundary, LoadingIndicator, Snackbar
|
|
85
|
-
| Display | Accordion, Card, Hr, Icon, Typography
|
|
86
|
-
| Media and system | PinchZoom, StatusBarBrightness
|
|
80
|
+
| Area | Components |
|
|
81
|
+
| ---------------- | -------------------------------------------------------------------------------- |
|
|
82
|
+
| Actions | Button, CustomPressable, Fab, IconButton |
|
|
83
|
+
| Inputs | Calendar, Checkbox, EditText, RadioGroup, Rating, SegmentedControl, SwitchToggle |
|
|
84
|
+
| Feedback | AlertDialog, ErrorBoundary, LoadingIndicator, Snackbar |
|
|
85
|
+
| Display | Accordion, Card, Hr, Icon, ParallaxHeader, Typography |
|
|
86
|
+
| Media and system | PinchZoom, StatusBarBrightness |
|
|
87
87
|
|
|
88
88
|
Each public component has automated tests and Storybook coverage for its main
|
|
89
89
|
states. Use the [live Storybook](https://ui.crossplatformkorea.com) to inspect
|
|
@@ -91,7 +91,7 @@ controls, responsive behavior, accessibility results, and source examples.
|
|
|
91
91
|
|
|
92
92
|
## Release Matrix
|
|
93
93
|
|
|
94
|
-
The 0.
|
|
94
|
+
The 0.8.0 beta is verified with Expo 54, React Native 0.81, React 19,
|
|
95
95
|
kstyled 0.4, and React Native Web 0.21. This is the release validation matrix,
|
|
96
96
|
not a claim that every consumer must use those exact React or React Native
|
|
97
97
|
versions.
|
|
@@ -112,7 +112,7 @@ main-branch GitHub Actions publish workflow with provenance.
|
|
|
112
112
|
## Documentation
|
|
113
113
|
|
|
114
114
|
- [Storybook](https://ui.crossplatformkorea.com)
|
|
115
|
-
- [0.
|
|
115
|
+
- [0.8.0 beta release notes](./docs/releases/0.8.0-beta.0.md)
|
|
116
116
|
- [0.7 engineering notes](./docs/blog/2026-07-11-cpk-ui-0.7.0.md)
|
|
117
117
|
- [Performance guide](./docs/PERFORMANCE.md)
|
|
118
118
|
- [Changelog](./CHANGELOG.md)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { CalendarProps, CalendarRef } from './types';
|
|
3
|
+
export declare const Calendar: React.NamedExoticComponent<CalendarProps & React.RefAttributes<CalendarRef>>;
|
|
4
|
+
export default Calendar;
|
|
5
|
+
export * from './types';
|
|
6
|
+
//# sourceMappingURL=Calendar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Calendar.d.ts","sourceRoot":"","sources":["../../../../src/components/uis/Calendar/Calendar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAWN,MAAM,OAAO,CAAC;AAuBf,OAAO,KAAK,EASV,aAAa,EACb,WAAW,EAIZ,MAAM,SAAS,CAAC;AAw3BjB,eAAO,MAAM,QAAQ,8EAA4C,CAAC;AAElE,eAAe,QAAQ,CAAC;AAExB,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { AccessibilityInfo, I18nManager, useWindowDimensions, View, } from 'react-native';
|
|
4
|
+
import { css } from 'kstyled';
|
|
5
|
+
import { useAnimatedReaction, useSharedValue, withTiming, runOnJS, } from 'react-native-reanimated';
|
|
6
|
+
import { useTheme } from '../../../providers/ThemeProvider';
|
|
7
|
+
import { CalendarHeader } from './CalendarHeader';
|
|
8
|
+
import { CalendarPage } from './CalendarPage';
|
|
9
|
+
import { CalendarPager } from './CalendarPager';
|
|
10
|
+
import { CalendarWeekdayRow } from './CalendarWeekdayRow';
|
|
11
|
+
import { DEFAULT_FORMATTERS, getFormattersId, resolveLocale } from './intl';
|
|
12
|
+
import { isSamePalette, resolveCalendarPalette } from './palette';
|
|
13
|
+
import { resolveKeyboardAction, useControllable, useDisabledState, useReduceMotion, useShallowStable, useToday, } from './useCalendarState';
|
|
14
|
+
import { addDays, addPages, getCalendarGrid, localNoon, normalizePageKey, pageFirstDate, pageLastDate, resolveMetrics, resolveWeekStart, startOfWeek, toDateKeyInput, toDayKey, } from './utils';
|
|
15
|
+
const CONTAINER = css `
|
|
16
|
+
align-self: center;
|
|
17
|
+
width: 100%;
|
|
18
|
+
`;
|
|
19
|
+
const TWO_UP_ROW = css `
|
|
20
|
+
flex-direction: row;
|
|
21
|
+
align-items: flex-start;
|
|
22
|
+
gap: 24px;
|
|
23
|
+
`;
|
|
24
|
+
const COLUMN = css `
|
|
25
|
+
flex: 1;
|
|
26
|
+
`;
|
|
27
|
+
const EMPTY_MARKERS = Object.freeze({});
|
|
28
|
+
const EMPTY_STYLES = Object.freeze({});
|
|
29
|
+
const DEFAULT_LABELS = Object.freeze({
|
|
30
|
+
previousMonth: 'Previous month',
|
|
31
|
+
nextMonth: 'Next month',
|
|
32
|
+
previousWeek: 'Previous week',
|
|
33
|
+
nextWeek: 'Next week',
|
|
34
|
+
today: 'Today',
|
|
35
|
+
selected: 'Selected',
|
|
36
|
+
weekNumber: 'Week',
|
|
37
|
+
pageAnnouncement: (pageTitle) => pageTitle,
|
|
38
|
+
});
|
|
39
|
+
/** A Sunday, used only to derive weekday header labels. Never rendered. */
|
|
40
|
+
const WEEKDAY_LABEL_ANCHOR = localNoon(2024, 0, 7);
|
|
41
|
+
const COLLAPSE_DURATION = 220;
|
|
42
|
+
const BADGE_FONT_SCALE_LIMIT = 1.3;
|
|
43
|
+
const SINGLE_DOT_FONT_SCALE_LIMIT = 1.5;
|
|
44
|
+
/**
|
|
45
|
+
* `onKeyDown` is forwarded by react-native-web and simply never fires on native,
|
|
46
|
+
* so one handler covers both platforms without a `.web.tsx` split.
|
|
47
|
+
*/
|
|
48
|
+
const KeyboardView = View;
|
|
49
|
+
function buildGeometry(metrics) {
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
row: { height: metrics.rowHeight },
|
|
52
|
+
cell: { height: metrics.rowHeight, paddingTop: metrics.pillOffset },
|
|
53
|
+
pill: {
|
|
54
|
+
width: metrics.numberSize,
|
|
55
|
+
height: metrics.numberSize,
|
|
56
|
+
borderRadius: metrics.numberSize / 2,
|
|
57
|
+
},
|
|
58
|
+
dayText: { fontSize: metrics.dayFontSize },
|
|
59
|
+
badgeText: { fontSize: metrics.badgeFontSize },
|
|
60
|
+
dot: {
|
|
61
|
+
width: metrics.dotSize,
|
|
62
|
+
height: metrics.dotSize,
|
|
63
|
+
borderRadius: metrics.dotSize / 2,
|
|
64
|
+
},
|
|
65
|
+
dotRow: { height: metrics.dotRowHeight },
|
|
66
|
+
weekNumber: { width: metrics.weekNumberWidth },
|
|
67
|
+
weekNumberText: { fontSize: metrics.weekdayFontSize },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function CalendarContainer(props, ref) {
|
|
71
|
+
const { value, defaultValue, onChange, month, defaultMonth, onMonthChange, mode: modeProp, defaultMode, onModeChange, collapseProgress, locale: localeProp, weekStart: weekStartProp, today: todayProp, fixedWeeks = true, markers = EMPTY_MARKERS, showDots = true, maxDotsPerDay = 3, showWeekNumbers = false, showOutsideDays = true, minDate, maxDate, disabledDates, isDateDisabled, showHeader = true, showNavigationButtons = true, showWeekdayRow = true, paging = 'swipe', monthsToShow = 1, size, maxFontSizeMultiplier = 1.6, colors, styles: stylesProp = EMPTY_STYLES, style, formatters: formattersProp, renderHeader, renderWeekdayLabel, renderDay, onDayLongPress, onDisabledDayPress, labels: labelsPropInput, accessibilityLabel, testID = 'calendar', } = props;
|
|
72
|
+
const { theme, media } = useTheme();
|
|
73
|
+
const { fontScale } = useWindowDimensions();
|
|
74
|
+
const reduceMotion = useReduceMotion();
|
|
75
|
+
const locale = useMemo(() => localeProp ?? resolveLocale(), [localeProp]);
|
|
76
|
+
const weekStart = useMemo(() => weekStartProp ?? resolveWeekStart(locale), [locale, weekStartProp]);
|
|
77
|
+
const todayNoon = useToday(todayProp);
|
|
78
|
+
const todayKey = useMemo(() => toDayKey(todayNoon), [todayNoon]);
|
|
79
|
+
// Value comparison, not identity, for the three props callers almost always
|
|
80
|
+
// write as inline object literals. Without it `chrome` is rebuilt on every
|
|
81
|
+
// parent render and all 42 day cells re-render — the exact regression the
|
|
82
|
+
// memo test suite pins for `colors` but never checked for these.
|
|
83
|
+
const labelsProp = useShallowStable(labelsPropInput);
|
|
84
|
+
const styles = useShallowStable(stylesProp);
|
|
85
|
+
const formattersInput = useShallowStable(formattersProp);
|
|
86
|
+
const formatters = useMemo(() => formattersInput
|
|
87
|
+
? Object.freeze({ ...DEFAULT_FORMATTERS, ...formattersInput })
|
|
88
|
+
: DEFAULT_FORMATTERS, [formattersInput]);
|
|
89
|
+
const formattersId = getFormattersId(formatters);
|
|
90
|
+
const [mode, setModeState] = useControllable(modeProp, defaultMode ?? 'month');
|
|
91
|
+
// ── selection ───────────────────────────────────────────────────────────────
|
|
92
|
+
const isValueControlled = value !== undefined;
|
|
93
|
+
const [selectedKeyState, setSelectedKeyState] = useState(() => defaultValue == null ? null : toDateKeyInput(defaultValue));
|
|
94
|
+
const selectedKey = isValueControlled
|
|
95
|
+
? value === null
|
|
96
|
+
? null
|
|
97
|
+
: toDateKeyInput(value)
|
|
98
|
+
: selectedKeyState;
|
|
99
|
+
// ── paging ──────────────────────────────────────────────────────────────────
|
|
100
|
+
const isMonthControlled = month !== undefined;
|
|
101
|
+
const [pageKeyState, setPageKeyState] = useState(() => normalizePageKey(defaultMonth ?? selectedKeyState ?? todayNoon, defaultMode ?? 'month', weekStartProp ?? resolveWeekStart(localeProp ?? resolveLocale()), monthsToShow));
|
|
102
|
+
const pageInput = month === undefined
|
|
103
|
+
? pageKeyState
|
|
104
|
+
: typeof month === 'string'
|
|
105
|
+
? month
|
|
106
|
+
: toDayKey(month);
|
|
107
|
+
const pageKey = useMemo(() => normalizePageKey(pageInput, mode, weekStart, monthsToShow), [mode, monthsToShow, pageInput, weekStart]);
|
|
108
|
+
const [focusedKey, setFocusedKey] = useState(null);
|
|
109
|
+
// ── grids ───────────────────────────────────────────────────────────────────
|
|
110
|
+
const gridFor = useCallback((key) => getCalendarGrid({
|
|
111
|
+
pageKey: key,
|
|
112
|
+
mode,
|
|
113
|
+
weekStart,
|
|
114
|
+
fixedWeeks,
|
|
115
|
+
showWeekNumbers,
|
|
116
|
+
locale,
|
|
117
|
+
formatters,
|
|
118
|
+
formattersId,
|
|
119
|
+
}), [
|
|
120
|
+
fixedWeeks,
|
|
121
|
+
formatters,
|
|
122
|
+
formattersId,
|
|
123
|
+
locale,
|
|
124
|
+
mode,
|
|
125
|
+
showWeekNumbers,
|
|
126
|
+
weekStart,
|
|
127
|
+
]);
|
|
128
|
+
const previousPageKey = useMemo(() => addPages(pageKey, -1, mode, weekStart, monthsToShow), [mode, monthsToShow, pageKey, weekStart]);
|
|
129
|
+
const nextPageKey = useMemo(() => addPages(pageKey, 1, mode, weekStart, monthsToShow), [mode, monthsToShow, pageKey, weekStart]);
|
|
130
|
+
const currentGrid = useMemo(() => gridFor(pageKey), [gridFor, pageKey]);
|
|
131
|
+
// ── theming and geometry ────────────────────────────────────────────────────
|
|
132
|
+
// Value comparison, not identity: `theme` and an inline `colors` object are both
|
|
133
|
+
// rebuilt on every parent render, and a new palette would re-render every cell.
|
|
134
|
+
const paletteRef = useRef(null);
|
|
135
|
+
const palette = useMemo(() => {
|
|
136
|
+
const next = resolveCalendarPalette(theme, colors);
|
|
137
|
+
const previous = paletteRef.current;
|
|
138
|
+
if (previous && isSamePalette(previous, next)) {
|
|
139
|
+
return previous;
|
|
140
|
+
}
|
|
141
|
+
paletteRef.current = next;
|
|
142
|
+
return next;
|
|
143
|
+
}, [colors, theme]);
|
|
144
|
+
const resolvedSize = size ?? (media.isDesktop || media.isTablet ? 'expanded' : 'regular');
|
|
145
|
+
const metrics = useMemo(() => resolveMetrics(resolvedSize, fontScale, maxFontSizeMultiplier), [fontScale, maxFontSizeMultiplier, resolvedSize]);
|
|
146
|
+
const geometry = useMemo(() => buildGeometry(metrics), [metrics]);
|
|
147
|
+
const labels = useMemo(() => labelsProp
|
|
148
|
+
? Object.freeze({ ...DEFAULT_LABELS, ...labelsProp })
|
|
149
|
+
: DEFAULT_LABELS, [labelsProp]);
|
|
150
|
+
// ── constraints ─────────────────────────────────────────────────────────────
|
|
151
|
+
const { resolver: isDisabled, version: disabledVersion, minKey, maxKey, } = useDisabledState({ minDate, maxDate, disabledDates, isDateDisabled });
|
|
152
|
+
// ── latest-value refs, so no handler identity depends on a changing callback ─
|
|
153
|
+
const callbacksRef = useRef({
|
|
154
|
+
onChange,
|
|
155
|
+
onMonthChange,
|
|
156
|
+
onModeChange,
|
|
157
|
+
onDayLongPress,
|
|
158
|
+
onDisabledDayPress,
|
|
159
|
+
});
|
|
160
|
+
const stateRef = useRef({
|
|
161
|
+
pageKey,
|
|
162
|
+
mode,
|
|
163
|
+
weekStart,
|
|
164
|
+
monthsToShow,
|
|
165
|
+
isMonthControlled,
|
|
166
|
+
isValueControlled,
|
|
167
|
+
isModeControlled: modeProp !== undefined,
|
|
168
|
+
});
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
callbacksRef.current = {
|
|
171
|
+
onChange,
|
|
172
|
+
onMonthChange,
|
|
173
|
+
onModeChange,
|
|
174
|
+
onDayLongPress,
|
|
175
|
+
onDisabledDayPress,
|
|
176
|
+
};
|
|
177
|
+
}, [
|
|
178
|
+
onChange,
|
|
179
|
+
onDayLongPress,
|
|
180
|
+
onDisabledDayPress,
|
|
181
|
+
onModeChange,
|
|
182
|
+
onMonthChange,
|
|
183
|
+
]);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
stateRef.current = {
|
|
186
|
+
pageKey,
|
|
187
|
+
mode,
|
|
188
|
+
weekStart,
|
|
189
|
+
monthsToShow,
|
|
190
|
+
isMonthControlled,
|
|
191
|
+
isValueControlled,
|
|
192
|
+
isModeControlled: modeProp !== undefined,
|
|
193
|
+
};
|
|
194
|
+
}, [
|
|
195
|
+
isMonthControlled,
|
|
196
|
+
isValueControlled,
|
|
197
|
+
mode,
|
|
198
|
+
modeProp,
|
|
199
|
+
monthsToShow,
|
|
200
|
+
pageKey,
|
|
201
|
+
weekStart,
|
|
202
|
+
]);
|
|
203
|
+
const goToPage = useCallback((nextKey, source) => {
|
|
204
|
+
const current = stateRef.current;
|
|
205
|
+
if (!current.isMonthControlled) {
|
|
206
|
+
setPageKeyState(nextKey);
|
|
207
|
+
}
|
|
208
|
+
const first = pageFirstDate(nextKey, current.mode, current.weekStart);
|
|
209
|
+
callbacksRef.current.onMonthChange?.(new Date(first.getFullYear(), first.getMonth(), first.getDate()), nextKey, source);
|
|
210
|
+
}, []);
|
|
211
|
+
const stepPage = useCallback((delta, source) => {
|
|
212
|
+
const current = stateRef.current;
|
|
213
|
+
goToPage(addPages(current.pageKey, delta, current.mode, current.weekStart, current.monthsToShow), source);
|
|
214
|
+
}, [goToPage]);
|
|
215
|
+
const handleSwipeCommit = useCallback((delta) => stepPage(delta, 'swipe'), [stepPage]);
|
|
216
|
+
const focusKey = useCallback((key, source) => {
|
|
217
|
+
setFocusedKey(key);
|
|
218
|
+
const current = stateRef.current;
|
|
219
|
+
const targetPage = normalizePageKey(key, current.mode, current.weekStart, current.monthsToShow);
|
|
220
|
+
if (targetPage !== current.pageKey) {
|
|
221
|
+
goToPage(targetPage, source);
|
|
222
|
+
}
|
|
223
|
+
}, [goToPage]);
|
|
224
|
+
const selectKey = useCallback((key, source) => {
|
|
225
|
+
const current = stateRef.current;
|
|
226
|
+
if (!current.isValueControlled) {
|
|
227
|
+
setSelectedKeyState(key);
|
|
228
|
+
}
|
|
229
|
+
setFocusedKey(key);
|
|
230
|
+
const date = new Date(+key.slice(0, 4), +key.slice(5, 7) - 1, +key.slice(8, 10));
|
|
231
|
+
callbacksRef.current.onChange?.(date, key, source);
|
|
232
|
+
}, []);
|
|
233
|
+
// ── stable cell callbacks. No inline arrow ever reaches a day cell ───────────
|
|
234
|
+
const handleDayPress = useCallback((key) => selectKey(key, 'press'), [selectKey]);
|
|
235
|
+
const handleDayLongPress = useCallback((key) => {
|
|
236
|
+
callbacksRef.current.onDayLongPress?.(new Date(+key.slice(0, 4), +key.slice(5, 7) - 1, +key.slice(8, 10)), key);
|
|
237
|
+
}, []);
|
|
238
|
+
const handleDisabledDayPress = useCallback((key) => {
|
|
239
|
+
callbacksRef.current.onDisabledDayPress?.(new Date(+key.slice(0, 4), +key.slice(5, 7) - 1, +key.slice(8, 10)), key);
|
|
240
|
+
}, []);
|
|
241
|
+
const chrome = useMemo(() => Object.freeze({
|
|
242
|
+
palette,
|
|
243
|
+
metrics,
|
|
244
|
+
geometry,
|
|
245
|
+
showDots,
|
|
246
|
+
maxDots: fontScale > SINGLE_DOT_FONT_SCALE_LIMIT
|
|
247
|
+
? 1
|
|
248
|
+
: Math.max(maxDotsPerDay, 0),
|
|
249
|
+
showBadges: fontScale <= BADGE_FONT_SCALE_LIMIT,
|
|
250
|
+
maxFontSizeMultiplier,
|
|
251
|
+
fontScale,
|
|
252
|
+
labels,
|
|
253
|
+
styles,
|
|
254
|
+
renderDay,
|
|
255
|
+
onPress: handleDayPress,
|
|
256
|
+
onLongPress: onDayLongPress ? handleDayLongPress : undefined,
|
|
257
|
+
onDisabledPress: onDisabledDayPress
|
|
258
|
+
? handleDisabledDayPress
|
|
259
|
+
: undefined,
|
|
260
|
+
}), [
|
|
261
|
+
fontScale,
|
|
262
|
+
geometry,
|
|
263
|
+
handleDayLongPress,
|
|
264
|
+
handleDayPress,
|
|
265
|
+
handleDisabledDayPress,
|
|
266
|
+
labels,
|
|
267
|
+
maxDotsPerDay,
|
|
268
|
+
maxFontSizeMultiplier,
|
|
269
|
+
metrics,
|
|
270
|
+
onDayLongPress,
|
|
271
|
+
onDisabledDayPress,
|
|
272
|
+
palette,
|
|
273
|
+
renderDay,
|
|
274
|
+
showDots,
|
|
275
|
+
styles,
|
|
276
|
+
]);
|
|
277
|
+
// ── collapse ────────────────────────────────────────────────────────────────
|
|
278
|
+
const internalCollapse = useSharedValue(mode === 'week' ? 1 : 0);
|
|
279
|
+
const collapse = collapseProgress ?? internalCollapse;
|
|
280
|
+
const activeRow = useSharedValue(0);
|
|
281
|
+
const anchorKey = selectedKey &&
|
|
282
|
+
selectedKey >= currentGrid.firstKey &&
|
|
283
|
+
selectedKey <= currentGrid.lastKey
|
|
284
|
+
? selectedKey
|
|
285
|
+
: todayKey >= currentGrid.firstKey && todayKey <= currentGrid.lastKey
|
|
286
|
+
? todayKey
|
|
287
|
+
: currentGrid.firstKey;
|
|
288
|
+
const activeRowIndex = useMemo(() => {
|
|
289
|
+
for (let i = 0; i < currentGrid.days.length; i++) {
|
|
290
|
+
if (currentGrid.days[i].key === anchorKey) {
|
|
291
|
+
return (i / 7) | 0;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return 0;
|
|
295
|
+
}, [anchorKey, currentGrid]);
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
activeRow.value = mode === 'week' ? 0 : activeRowIndex;
|
|
298
|
+
}, [activeRow, activeRowIndex, mode]);
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (collapseProgress) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
internalCollapse.value = withTiming(mode === 'week' ? 1 : 0, {
|
|
304
|
+
duration: reduceMotion ? 0 : COLLAPSE_DURATION,
|
|
305
|
+
});
|
|
306
|
+
}, [collapseProgress, internalCollapse, mode, reduceMotion]);
|
|
307
|
+
const commitMode = useCallback((nextMode) => {
|
|
308
|
+
if (stateRef.current.mode === nextMode) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
setModeState(nextMode);
|
|
312
|
+
callbacksRef.current.onModeChange?.(nextMode);
|
|
313
|
+
}, [setModeState]);
|
|
314
|
+
useAnimatedReaction(() => collapse.value, (progress, previous) => {
|
|
315
|
+
if (previous === null) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (progress >= 0.999 && previous < 0.999) {
|
|
319
|
+
runOnJS(commitMode)('week');
|
|
320
|
+
}
|
|
321
|
+
else if (progress <= 0.001 && previous > 0.001) {
|
|
322
|
+
runOnJS(commitMode)('month');
|
|
323
|
+
}
|
|
324
|
+
}, [collapse, commitMode]);
|
|
325
|
+
// ── derived chrome values ───────────────────────────────────────────────────
|
|
326
|
+
const weekdayLabels = useMemo(() => {
|
|
327
|
+
const anchor = startOfWeek(WEEKDAY_LABEL_ANCHOR, weekStart);
|
|
328
|
+
return Object.freeze(Array.from({ length: 7 }, (_, index) => formatters.weekdayLabel(addDays(anchor, index), locale)));
|
|
329
|
+
}, [formatters, locale, weekStart]);
|
|
330
|
+
const headerDate = useMemo(() => pageFirstDate(pageKey, mode, weekStart), [mode, pageKey, weekStart]);
|
|
331
|
+
const title = useMemo(() => {
|
|
332
|
+
if (mode === 'week') {
|
|
333
|
+
return formatters.weekTitle(headerDate, addDays(headerDate, 6), locale);
|
|
334
|
+
}
|
|
335
|
+
if (monthsToShow > 1) {
|
|
336
|
+
const secondMonth = pageFirstDate(nextPageKey, mode, weekStart);
|
|
337
|
+
return `${formatters.monthTitle(headerDate, locale)} – ${formatters.monthTitle(addDays(secondMonth, -1), locale)}`;
|
|
338
|
+
}
|
|
339
|
+
return formatters.monthTitle(headerDate, locale);
|
|
340
|
+
}, [
|
|
341
|
+
formatters,
|
|
342
|
+
headerDate,
|
|
343
|
+
locale,
|
|
344
|
+
mode,
|
|
345
|
+
monthsToShow,
|
|
346
|
+
nextPageKey,
|
|
347
|
+
weekStart,
|
|
348
|
+
]);
|
|
349
|
+
// Compared against each neighbouring page's own month/week range, not against the
|
|
350
|
+
// padded grid, so a min date on the 1st really does disable the previous button.
|
|
351
|
+
const canGoPrevious = minKey === null ||
|
|
352
|
+
toDayKey(pageLastDate(previousPageKey, mode, weekStart, monthsToShow)) >=
|
|
353
|
+
minKey;
|
|
354
|
+
const canGoNext = maxKey === null ||
|
|
355
|
+
toDayKey(pageFirstDate(nextPageKey, mode, weekStart)) <= maxKey;
|
|
356
|
+
const goPrevious = useCallback(() => stepPage(-1, 'button'), [stepPage]);
|
|
357
|
+
const goNext = useCallback(() => stepPage(1, 'button'), [stepPage]);
|
|
358
|
+
const headerContext = useMemo(() => ({
|
|
359
|
+
title,
|
|
360
|
+
date: headerDate,
|
|
361
|
+
mode,
|
|
362
|
+
canGoPrevious,
|
|
363
|
+
canGoNext,
|
|
364
|
+
goPrevious,
|
|
365
|
+
goNext,
|
|
366
|
+
}), [canGoNext, canGoPrevious, goNext, goPrevious, headerDate, mode, title]);
|
|
367
|
+
const announcedTitle = useRef(null);
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
if (announcedTitle.current === null) {
|
|
370
|
+
announcedTitle.current = title;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (announcedTitle.current === title) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
announcedTitle.current = title;
|
|
377
|
+
AccessibilityInfo.announceForAccessibility(labels.pageAnnouncement(title));
|
|
378
|
+
}, [labels, title]);
|
|
379
|
+
// ── roving focus target ─────────────────────────────────────────────────────
|
|
380
|
+
const tabbableKey = useMemo(() => {
|
|
381
|
+
const inPage = (key) => key >= currentGrid.firstKey && key <= currentGrid.lastKey;
|
|
382
|
+
if (focusedKey !== null && inPage(focusedKey)) {
|
|
383
|
+
return focusedKey;
|
|
384
|
+
}
|
|
385
|
+
if (selectedKey !== null && inPage(selectedKey)) {
|
|
386
|
+
return selectedKey;
|
|
387
|
+
}
|
|
388
|
+
if (inPage(todayKey)) {
|
|
389
|
+
return todayKey;
|
|
390
|
+
}
|
|
391
|
+
for (const day of currentGrid.days) {
|
|
392
|
+
if (!day.isOutside) {
|
|
393
|
+
return day.key;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return currentGrid.firstKey;
|
|
397
|
+
}, [currentGrid, focusedKey, selectedKey, todayKey]);
|
|
398
|
+
const handleKeyDown = useCallback((event) => {
|
|
399
|
+
const action = resolveKeyboardAction({
|
|
400
|
+
key: event.key,
|
|
401
|
+
shiftKey: event.shiftKey === true,
|
|
402
|
+
focusedKey: tabbableKey,
|
|
403
|
+
weekStart: stateRef.current.weekStart,
|
|
404
|
+
isRTL: I18nManager.isRTL,
|
|
405
|
+
mode: stateRef.current.mode,
|
|
406
|
+
});
|
|
407
|
+
if (!action) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
event.preventDefault?.();
|
|
411
|
+
if (action.type === 'select') {
|
|
412
|
+
selectKey(tabbableKey, 'keyboard');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
focusKey(action.key, 'keyboard');
|
|
416
|
+
}, [focusKey, selectKey, tabbableKey]);
|
|
417
|
+
// ── imperative API ──────────────────────────────────────────────────────────
|
|
418
|
+
useImperativeHandle(ref, () => ({
|
|
419
|
+
goToMonth: (target) => {
|
|
420
|
+
const current = stateRef.current;
|
|
421
|
+
goToPage(normalizePageKey(target, current.mode, current.weekStart, current.monthsToShow), 'imperative');
|
|
422
|
+
},
|
|
423
|
+
goToToday: () => {
|
|
424
|
+
const current = stateRef.current;
|
|
425
|
+
goToPage(normalizePageKey(todayKey, current.mode, current.weekStart, current.monthsToShow), 'imperative');
|
|
426
|
+
},
|
|
427
|
+
next: () => stepPage(1, 'imperative'),
|
|
428
|
+
previous: () => stepPage(-1, 'imperative'),
|
|
429
|
+
setMode: (nextMode) => commitMode(nextMode),
|
|
430
|
+
focusDay: (key) => focusKey(key, 'imperative'),
|
|
431
|
+
}), [commitMode, focusKey, goToPage, stepPage, todayKey]);
|
|
432
|
+
// ── page rendering ──────────────────────────────────────────────────────────
|
|
433
|
+
const rowCount = mode === 'week' ? 1 : currentGrid.rowCount;
|
|
434
|
+
const collapsible = mode === 'week' || (monthsToShow === 1 && rowCount > 1);
|
|
435
|
+
const renderGrid = useCallback((gridPageKey, gridTestID) => {
|
|
436
|
+
const grid = gridFor(gridPageKey);
|
|
437
|
+
return (_jsx(CalendarPage, { activeRow: activeRow, chrome: chrome, collapse: collapse, collapsible: collapsible, disabledVersion: disabledVersion, grid: grid, isDisabled: isDisabled, markers: markers, selectedKey: selectedKey, showOutsideDays: showOutsideDays, showWeekNumbers: showWeekNumbers, tabbableKey: tabbableKey, testID: gridTestID, todayKey: todayKey }));
|
|
438
|
+
}, [
|
|
439
|
+
activeRow,
|
|
440
|
+
chrome,
|
|
441
|
+
collapse,
|
|
442
|
+
collapsible,
|
|
443
|
+
disabledVersion,
|
|
444
|
+
gridFor,
|
|
445
|
+
isDisabled,
|
|
446
|
+
markers,
|
|
447
|
+
selectedKey,
|
|
448
|
+
showOutsideDays,
|
|
449
|
+
showWeekNumbers,
|
|
450
|
+
tabbableKey,
|
|
451
|
+
todayKey,
|
|
452
|
+
]);
|
|
453
|
+
const renderSlot = useCallback((slotPageKey) => {
|
|
454
|
+
if (monthsToShow === 1) {
|
|
455
|
+
return renderGrid(slotPageKey, `calendar-page-${slotPageKey}`);
|
|
456
|
+
}
|
|
457
|
+
const monthKeys = [slotPageKey];
|
|
458
|
+
for (let index = 1; index < monthsToShow; index++) {
|
|
459
|
+
monthKeys.push(addPages(slotPageKey, index, mode, weekStart, 1));
|
|
460
|
+
}
|
|
461
|
+
return (_jsx(View, { style: TWO_UP_ROW, children: monthKeys.map((monthKey) => (_jsx(View, { style: COLUMN, children: renderGrid(monthKey, `calendar-page-${monthKey}`) }, monthKey))) }));
|
|
462
|
+
}, [mode, monthsToShow, renderGrid, weekStart]);
|
|
463
|
+
const slots = useMemo(() => {
|
|
464
|
+
if (paging === 'none') {
|
|
465
|
+
return [{ key: pageKey, offset: 0, node: renderSlot(pageKey) }];
|
|
466
|
+
}
|
|
467
|
+
return [
|
|
468
|
+
{ key: previousPageKey, offset: -1, node: renderSlot(previousPageKey) },
|
|
469
|
+
{ key: pageKey, offset: 0, node: renderSlot(pageKey) },
|
|
470
|
+
{ key: nextPageKey, offset: 1, node: renderSlot(nextPageKey) },
|
|
471
|
+
];
|
|
472
|
+
}, [nextPageKey, pageKey, paging, previousPageKey, renderSlot]);
|
|
473
|
+
const containerStyle = useMemo(() => ({
|
|
474
|
+
backgroundColor: palette.background,
|
|
475
|
+
maxWidth: metrics.cellSize * 7 * 1.6 * monthsToShow,
|
|
476
|
+
}), [metrics.cellSize, monthsToShow, palette.background]);
|
|
477
|
+
const weekdayRow = showWeekdayRow ? (_jsx(View, { style: monthsToShow > 1 ? TWO_UP_ROW : undefined, children: Array.from({ length: monthsToShow }, (_, index) => (_jsx(View, { style: COLUMN, children: _jsx(CalendarWeekdayRow, { maxFontSizeMultiplier: maxFontSizeMultiplier, metrics: metrics, palette: palette, renderWeekdayLabel: renderWeekdayLabel, showWeekNumbers: showWeekNumbers, styles: styles, weekStart: weekStart, weekdayLabels: weekdayLabels }) }, `weekday-row-${index}`))) })) : null;
|
|
478
|
+
return (_jsxs(KeyboardView, { accessibilityLabel: accessibilityLabel ?? title, onKeyDown: handleKeyDown, style: [CONTAINER, containerStyle, styles.container, style], testID: testID, children: [showHeader ? (_jsx(CalendarHeader, { canGoNext: canGoNext, canGoPrevious: canGoPrevious, headerContext: headerContext, labels: labels, maxFontSizeMultiplier: maxFontSizeMultiplier, metrics: metrics, mode: mode, onNext: goNext, onPrevious: goPrevious, palette: palette, renderHeader: renderHeader, showNavigationButtons: showNavigationButtons, styles: styles, title: title })) : null, weekdayRow, _jsx(CalendarPager, { activeRow: activeRow, collapse: collapse, onCommit: handleSwipeCommit, reduceMotion: reduceMotion, rowCount: rowCount, rowHeight: metrics.rowHeight, slots: slots, swipeEnabled: paging === 'swipe', testID: "calendar-pager" })] }));
|
|
479
|
+
}
|
|
480
|
+
export const Calendar = React.memo(forwardRef(CalendarContainer));
|
|
481
|
+
export default Calendar;
|
|
482
|
+
export * from './types';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, { type ReactElement } from 'react';
|
|
2
|
+
import type { CalendarChrome, CalendarDayModel, CalendarMarker } from './types';
|
|
3
|
+
export type CalendarDayProps = {
|
|
4
|
+
day: CalendarDayModel;
|
|
5
|
+
/** Precomputed by the grid builder. A cell never formats a date itself. */
|
|
6
|
+
dayLabel: string;
|
|
7
|
+
accessibilityLabel: string;
|
|
8
|
+
isSelected: boolean;
|
|
9
|
+
isToday: boolean;
|
|
10
|
+
isDisabled: boolean;
|
|
11
|
+
/** Roving tab index: exactly one cell per page is tabbable. */
|
|
12
|
+
isTabbable: boolean;
|
|
13
|
+
/** Inert spacer for an out of month day when `showOutsideDays` is false. */
|
|
14
|
+
isHidden: boolean;
|
|
15
|
+
marker?: CalendarMarker;
|
|
16
|
+
chrome: CalendarChrome;
|
|
17
|
+
};
|
|
18
|
+
declare function CalendarDayContainer(props: CalendarDayProps): ReactElement;
|
|
19
|
+
export declare const CalendarDay: React.MemoExoticComponent<typeof CalendarDayContainer>;
|
|
20
|
+
export default CalendarDay;
|
|
21
|
+
//# sourceMappingURL=CalendarDay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CalendarDay.d.ts","sourceRoot":"","sources":["../../../../src/components/uis/Calendar/CalendarDay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAuB,KAAK,YAAY,EAAC,MAAM,OAAO,CAAC;AAKrE,OAAO,KAAK,EACV,cAAc,EACd,gBAAgB,EAChB,cAAc,EAEf,MAAM,SAAS,CAAC;AAyCjB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,gBAAgB,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACxB,CAAC;AAoDF,iBAAS,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAsMnE;AAcD,eAAO,MAAM,WAAW,wDAAiD,CAAC;AAE1E,eAAe,WAAW,CAAC"}
|