react-native-molecules 0.5.0-beta.2 → 0.5.0-beta.20

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.
Files changed (157) hide show
  1. package/README.md +1 -1
  2. package/components/Accordion/Accordion.tsx +2 -6
  3. package/components/Accordion/AccordionItem.tsx +16 -12
  4. package/components/Accordion/AccordionItemContent.tsx +6 -1
  5. package/components/Accordion/AccordionItemHeader.tsx +1 -1
  6. package/components/Accordion/utils.ts +6 -0
  7. package/components/ActivityIndicator/ActivityIndicator.tsx +6 -15
  8. package/components/Appbar/AppbarBase.tsx +18 -13
  9. package/components/Button/Button.tsx +209 -264
  10. package/components/Button/index.tsx +9 -3
  11. package/components/Button/types.ts +16 -2
  12. package/components/Button/utils.ts +230 -208
  13. package/components/Checkbox/CheckboxBase.tsx +23 -128
  14. package/components/Checkbox/utils.ts +0 -25
  15. package/components/Chip/Chip.tsx +40 -52
  16. package/components/Chip/utils.ts +3 -7
  17. package/components/DateField/DateField.tsx +110 -0
  18. package/components/DateField/index.tsx +6 -0
  19. package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +17 -49
  20. package/components/DatePicker/DateCalendar.tsx +83 -0
  21. package/components/DatePicker/DatePickerActions.tsx +73 -0
  22. package/components/DatePicker/DatePickerModal.tsx +234 -0
  23. package/components/DatePicker/DatePickerPopover.tsx +79 -0
  24. package/components/DatePicker/DatePickerProvider.tsx +152 -0
  25. package/components/DatePicker/DatePickerTrigger.tsx +23 -0
  26. package/components/DatePicker/context.tsx +82 -0
  27. package/components/DatePicker/index.tsx +44 -0
  28. package/components/DatePicker/utils.ts +293 -0
  29. package/components/DatePickerInline/DatePickerContext.tsx +1 -0
  30. package/components/DatePickerInline/DatePickerDockedHeader.tsx +113 -0
  31. package/components/DatePickerInline/DatePickerInline.tsx +16 -15
  32. package/components/DatePickerInline/DatePickerInlineBase.tsx +7 -1
  33. package/components/DatePickerInline/Day.tsx +25 -1
  34. package/components/DatePickerInline/DayRange.tsx +2 -4
  35. package/components/DatePickerInline/HeaderItem.tsx +42 -27
  36. package/components/DatePickerInline/Month.tsx +45 -65
  37. package/components/DatePickerInline/MonthPicker.tsx +25 -41
  38. package/components/DatePickerInline/Swiper.native.tsx +21 -4
  39. package/components/DatePickerInline/Swiper.tsx +168 -13
  40. package/components/DatePickerInline/Week.tsx +6 -1
  41. package/components/DatePickerInline/YearPicker.tsx +206 -53
  42. package/components/DatePickerInline/dateUtils.tsx +17 -12
  43. package/components/DatePickerInline/types.ts +3 -0
  44. package/components/DatePickerInline/utils.ts +66 -29
  45. package/components/Drawer/Drawer.tsx +17 -6
  46. package/components/ElementGroup/ElementGroup.tsx +16 -14
  47. package/components/FilePicker/FilePicker.tsx +48 -78
  48. package/components/FilePicker/index.tsx +2 -1
  49. package/components/FilePicker/utils.ts +9 -0
  50. package/components/HelperText/HelperText.tsx +0 -35
  51. package/components/Icon/iconFactory.tsx +3 -3
  52. package/components/Icon/index.tsx +1 -1
  53. package/components/Icon/types.ts +17 -6
  54. package/components/IconButton/IconButton.tsx +42 -57
  55. package/components/IconButton/utils.ts +142 -33
  56. package/components/ListItem/ListItem.tsx +3 -1
  57. package/components/ListItem/utils.ts +1 -1
  58. package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
  59. package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
  60. package/components/LoadingIndicator/index.tsx +13 -0
  61. package/components/LoadingIndicator/utils.ts +117 -0
  62. package/components/Menu/Menu.tsx +3 -18
  63. package/components/NavigationRail/NavigationRail.tsx +15 -9
  64. package/components/Popover/Popover.tsx +122 -145
  65. package/components/Popover/PopoverRoot.tsx +74 -0
  66. package/components/Popover/common.ts +50 -34
  67. package/components/Popover/index.ts +18 -1
  68. package/components/Popover/usePlatformMeasure.native.ts +90 -0
  69. package/components/Popover/usePlatformMeasure.ts +118 -0
  70. package/components/Popover/utils.ts +34 -0
  71. package/components/Select/Select.tsx +368 -507
  72. package/components/Select/context.tsx +72 -0
  73. package/components/Select/index.ts +8 -14
  74. package/components/Select/types.ts +2 -4
  75. package/components/Select/utils.ts +144 -0
  76. package/components/Slot/Slot.tsx +244 -0
  77. package/components/Slot/compose-refs.tsx +62 -0
  78. package/components/Slot/index.tsx +8 -0
  79. package/components/Surface/Surface.android.tsx +34 -8
  80. package/components/Surface/Surface.ios.tsx +36 -29
  81. package/components/Surface/Surface.tsx +31 -4
  82. package/components/Surface/utils.ts +44 -30
  83. package/components/Switch/Switch.tsx +8 -2
  84. package/components/Tabs/TabItem.tsx +35 -58
  85. package/components/Tabs/TabLabel.tsx +5 -9
  86. package/components/Tabs/Tabs.tsx +154 -148
  87. package/components/Tabs/utils.ts +15 -2
  88. package/components/TextInput/TextInput.tsx +658 -575
  89. package/components/TextInput/index.tsx +19 -3
  90. package/components/TextInput/types.ts +76 -27
  91. package/components/TextInput/utils.ts +225 -145
  92. package/components/TimeField/TimeField.tsx +75 -0
  93. package/components/TimeField/index.tsx +6 -0
  94. package/components/TimeField/useTimeFieldState.ts +70 -0
  95. package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
  96. package/components/TimePicker/TimeInput.tsx +87 -37
  97. package/components/TimePicker/TimeInputs.tsx +137 -49
  98. package/components/TimePicker/TimePicker.tsx +73 -10
  99. package/components/TimePicker/TimePickerModal.tsx +186 -0
  100. package/components/TimePicker/context.tsx +17 -0
  101. package/components/TimePicker/index.tsx +15 -3
  102. package/components/TimePicker/utils.ts +93 -0
  103. package/components/Tooltip/Tooltip.tsx +42 -67
  104. package/components/Tooltip/TooltipContent.tsx +32 -5
  105. package/components/Tooltip/TooltipTrigger.tsx +20 -20
  106. package/components/Tooltip/index.tsx +1 -1
  107. package/components/TouchableRipple/TouchableRipple.native.tsx +50 -14
  108. package/components/TouchableRipple/TouchableRipple.tsx +137 -47
  109. package/hocs/withPortal.tsx +1 -1
  110. package/hooks/index.tsx +0 -6
  111. package/hooks/useActionState.tsx +19 -8
  112. package/hooks/useControlledValue.tsx +20 -4
  113. package/hooks/useFilePicker.tsx +6 -16
  114. package/hooks/useWhatHasUpdated.tsx +48 -0
  115. package/package.json +17 -13
  116. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +5 -2
  117. package/styles/shadow.ts +2 -1
  118. package/styles/themes/LightTheme.tsx +1 -1
  119. package/utils/DocumentPicker/documentPicker.ts +78 -27
  120. package/utils/DocumentPicker/types.ts +0 -1
  121. package/utils/extractPropertiesFromStyles.ts +25 -0
  122. package/utils/extractSubcomponents.ts +89 -0
  123. package/utils/lodash.ts +77 -5
  124. package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
  125. package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
  126. package/components/DatePickerDocked/index.tsx +0 -17
  127. package/components/DatePickerDocked/types.ts +0 -11
  128. package/components/DatePickerDocked/utils.ts +0 -157
  129. package/components/DatePickerInput/DatePickerInput.tsx +0 -139
  130. package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
  131. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -77
  132. package/components/DatePickerInput/DateRangeInput.tsx +0 -88
  133. package/components/DatePickerInput/index.tsx +0 -10
  134. package/components/DatePickerInput/types.ts +0 -28
  135. package/components/DatePickerInput/utils.ts +0 -15
  136. package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
  137. package/components/DatePickerModal/CalendarEdit.tsx +0 -139
  138. package/components/DatePickerModal/DatePickerModal.tsx +0 -85
  139. package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
  140. package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
  141. package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
  142. package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
  143. package/components/DatePickerModal/index.tsx +0 -16
  144. package/components/DatePickerModal/types.ts +0 -92
  145. package/components/DatePickerModal/utils.ts +0 -122
  146. package/components/DateTimePicker/DateTimePicker.tsx +0 -172
  147. package/components/DateTimePicker/index.tsx +0 -10
  148. package/components/DateTimePicker/utils.ts +0 -12
  149. package/components/Popover/Popover.native.tsx +0 -185
  150. package/components/TimePickerField/TimePickerField.tsx +0 -152
  151. package/components/TimePickerField/index.tsx +0 -10
  152. package/components/TimePickerField/utils.ts +0 -94
  153. package/components/TimePickerModal/TimePickerModal.tsx +0 -115
  154. package/components/TimePickerModal/index.tsx +0 -10
  155. package/components/TimePickerModal/utils.ts +0 -47
  156. package/hooks/useSearchable.tsx +0 -74
  157. package/hooks/useSubcomponents.tsx +0 -59
@@ -9,11 +9,12 @@ import {
9
9
  useRef,
10
10
  useState,
11
11
  } from 'react';
12
- import { StyleSheet, View } from 'react-native';
13
12
 
14
13
  import { useLatest } from '../../hooks';
15
14
  import AutoSizer from './AutoSizer';
16
- import { beginOffset, estimatedMonthHeight, totalMonths } from './dateUtils';
15
+ import { useDatePickerStore } from './DatePickerContext';
16
+ import { beginOffset, estimatedMonthHeight, getInitialIndex, totalMonths } from './dateUtils';
17
+ import { addMonths, getRealIndex } from './dateUtils';
17
18
  import { getIndexFromVerticalOffset, getMonthHeight, getVerticalMonthsOffset } from './Month';
18
19
  import type { SwiperProps } from './SwiperUtils';
19
20
  import { montHeaderHeight } from './utils';
@@ -25,7 +26,16 @@ function Swiper({ scrollMode, renderItem, renderHeader, renderFooter, initialInd
25
26
  <>
26
27
  {renderHeader && renderHeader()}
27
28
  {isHorizontal ? (
28
- <View style={styles.flex1}>{renderItem(initialIndex)}</View>
29
+ <AutoSizer>
30
+ {({ width, height }) => (
31
+ <HorizontalScroller
32
+ width={width}
33
+ height={height}
34
+ initialIndex={initialIndex}
35
+ renderItem={renderItem}
36
+ />
37
+ )}
38
+ </AutoSizer>
29
39
  ) : (
30
40
  <AutoSizer>
31
41
  {({ width, height }) => (
@@ -45,6 +55,142 @@ function Swiper({ scrollMode, renderItem, renderHeader, renderFooter, initialInd
45
55
  }
46
56
 
47
57
  const visibleArray = (i: number) => [i - 2, i - 1, i, i + 1, i + 2];
58
+ const visibleHorizontalArray = (i: number) => [i - 1, i, i + 1];
59
+
60
+ function HorizontalScroller({
61
+ width,
62
+ height,
63
+ initialIndex,
64
+ renderItem,
65
+ }: {
66
+ renderItem: (index: number) => any;
67
+ width: number;
68
+ height: number;
69
+ initialIndex: number;
70
+ }) {
71
+ const idx = useRef<number>(initialIndex);
72
+ const [visibleIndexes, setVisibleIndexes] = useState<number[]>(
73
+ visibleHorizontalArray(initialIndex),
74
+ );
75
+ const parentRef = useRef<HTMLDivElement | null>(null);
76
+ const [{ localDate }, setStore] = useDatePickerStore(state => state);
77
+ const settleTimerRef = useRef<number | null>(null);
78
+ const isRecenteringRef = useRef(false);
79
+
80
+ const syncIndex = useCallback(
81
+ (index: number) => {
82
+ idx.current = index;
83
+ setVisibleIndexes(visibleHorizontalArray(index));
84
+ setStore(prev => ({
85
+ ...prev,
86
+ localDate: addMonths(new Date(), getRealIndex(index)),
87
+ }));
88
+ },
89
+ [setStore],
90
+ );
91
+
92
+ const scrollToCenter = useCallback(
93
+ (behavior: ScrollBehavior = 'auto') => {
94
+ const element = parentRef.current;
95
+ if (!element) return;
96
+
97
+ isRecenteringRef.current = true;
98
+ element.scrollTo({
99
+ left: width,
100
+ top: 0,
101
+ behavior,
102
+ });
103
+ window.requestAnimationFrame(() => {
104
+ isRecenteringRef.current = false;
105
+ });
106
+ },
107
+ [width],
108
+ );
109
+
110
+ useIsomorphicLayoutEffect(() => {
111
+ scrollToCenter();
112
+ }, [scrollToCenter, visibleIndexes]);
113
+
114
+ useEffect(() => {
115
+ const targetIndex = getInitialIndex(localDate);
116
+ if (targetIndex === idx.current) return;
117
+ idx.current = targetIndex;
118
+ setVisibleIndexes(visibleHorizontalArray(targetIndex));
119
+ }, [localDate]);
120
+
121
+ useEffect(() => {
122
+ return () => {
123
+ if (settleTimerRef.current) {
124
+ window.clearTimeout(settleTimerRef.current);
125
+ }
126
+ };
127
+ }, []);
128
+
129
+ const onScroll = useCallback(
130
+ (e: UIEvent) => {
131
+ if (isRecenteringRef.current) {
132
+ return;
133
+ }
134
+
135
+ const left = e.currentTarget.scrollLeft;
136
+
137
+ if (settleTimerRef.current) {
138
+ window.clearTimeout(settleTimerRef.current);
139
+ }
140
+
141
+ settleTimerRef.current = window.setTimeout(() => {
142
+ const direction = left > width * 1.5 ? 1 : left < width * 0.5 ? -1 : 0;
143
+
144
+ if (direction !== 0) {
145
+ syncIndex(idx.current + direction);
146
+ }
147
+
148
+ scrollToCenter(direction === 0 ? 'smooth' : 'auto');
149
+ }, 120);
150
+ },
151
+ [scrollToCenter, syncIndex, width],
152
+ );
153
+
154
+ const { containerStyle, innerContainerStyle, itemContainerStyle } = useMemo(() => {
155
+ return {
156
+ containerStyle: {
157
+ height,
158
+ width,
159
+ overflowX: 'auto',
160
+ overflowY: 'hidden',
161
+ scrollSnapType: 'x mandatory',
162
+ WebkitOverflowScrolling: 'touch',
163
+ scrollbarWidth: 'none',
164
+ msOverflowStyle: 'none',
165
+ },
166
+ innerContainerStyle: {
167
+ width: width * 3,
168
+ height,
169
+ position: 'relative',
170
+ },
171
+ itemContainerStyle: (vi: number) => ({
172
+ left: width * vi,
173
+ top: 0,
174
+ bottom: 0,
175
+ position: 'absolute',
176
+ width,
177
+ scrollSnapAlign: 'start',
178
+ }),
179
+ };
180
+ }, [height, width]);
181
+
182
+ return (
183
+ <div ref={parentRef} style={containerStyle as CSSProperties} onScroll={onScroll}>
184
+ <div style={innerContainerStyle as CSSProperties}>
185
+ {[0, 1, 2].map(vi => (
186
+ <div key={vi} style={itemContainerStyle(vi) as CSSProperties}>
187
+ {renderItem(visibleIndexes[vi])}
188
+ </div>
189
+ ))}
190
+ </div>
191
+ </div>
192
+ );
193
+ }
48
194
 
49
195
  function VerticalScroller({
50
196
  width,
@@ -63,19 +209,30 @@ function VerticalScroller({
63
209
  const [visibleIndexes, setVisibleIndexes] = useState<number[]>(visibleArray(initialIndex));
64
210
 
65
211
  const parentRef = useRef<HTMLDivElement | null>(null);
212
+ const [{ localDate }, setStore] = useDatePickerStore(state => state);
66
213
 
67
214
  useIsomorphicLayoutEffect(() => {
68
215
  const element = parentRef.current;
69
216
  if (!element) {
70
217
  return;
71
218
  }
72
- const top = getVerticalMonthsOffset(idx.current) - montHeaderHeight;
73
-
74
219
  element.scrollTo({
75
- top,
220
+ top: getVerticalMonthsOffset(idx.current) - montHeaderHeight,
76
221
  });
77
222
  }, [parentRef, idx]);
78
223
 
224
+ useEffect(() => {
225
+ const targetIndex = getInitialIndex(localDate);
226
+ if (targetIndex === idx.current) return;
227
+ idx.current = targetIndex;
228
+ setVisibleIndexes(visibleArray(targetIndex));
229
+ const element = parentRef.current;
230
+ if (!element) return;
231
+ element.scrollTo({
232
+ top: getVerticalMonthsOffset(targetIndex) - montHeaderHeight,
233
+ });
234
+ }, [localDate]);
235
+
79
236
  const setVisibleIndexesThrottled = useDebouncedCallback(setVisibleIndexes);
80
237
 
81
238
  const onScroll = useCallback(
@@ -91,9 +248,13 @@ function VerticalScroller({
91
248
  if (idx.current !== index) {
92
249
  idx.current = index;
93
250
  setVisibleIndexesThrottled(visibleArray(index));
251
+ setStore(prev => ({
252
+ ...prev,
253
+ localDate: addMonths(new Date(), getRealIndex(index)),
254
+ }));
94
255
  }
95
256
  },
96
- [setVisibleIndexesThrottled],
257
+ [setStore, setVisibleIndexesThrottled],
97
258
  );
98
259
 
99
260
  const { containerStyle, innerContainerStyle, itemContainerStyle } = useMemo(() => {
@@ -132,12 +293,6 @@ function VerticalScroller({
132
293
  );
133
294
  }
134
295
 
135
- const styles = StyleSheet.create({
136
- flex1: {
137
- flex: 1,
138
- },
139
- });
140
-
141
296
  export function useDebouncedCallback(callback: any): any {
142
297
  const mounted = useRef<boolean>(true);
143
298
  const latest = useLatest(callback);
@@ -10,6 +10,7 @@ type Props = ViewProps & {
10
10
  generatedDays: {
11
11
  beforeWeekDay: boolean;
12
12
  afterWeekDay: boolean;
13
+ outside: boolean;
13
14
  year: number;
14
15
  month: number;
15
16
  dayOfMonth: number;
@@ -24,6 +25,7 @@ type Props = ViewProps & {
24
25
  }[];
25
26
  onPressDate: (date: Date) => any;
26
27
  disableWeekDays?: DisableWeekDaysType;
28
+ showOutsideDays?: boolean;
27
29
  };
28
30
 
29
31
  const Week = ({
@@ -31,6 +33,7 @@ const Week = ({
31
33
  generatedDays,
32
34
  onPressDate,
33
35
  disableWeekDays,
36
+ showOutsideDays,
34
37
  style,
35
38
  ...rest
36
39
  }: Props) => {
@@ -39,9 +42,10 @@ const Week = ({
39
42
  {generatedDays
40
43
  .filter(gd => showWeekDay(gd.dayIndex, disableWeekDays))
41
44
  .map(gd => {
45
+ const isOutside = gd.beforeWeekDay || gd.afterWeekDay;
42
46
  return (
43
47
  <Fragment key={gd.dayIndex + weekIndex}>
44
- {gd.beforeWeekDay || gd.afterWeekDay ? (
48
+ {isOutside && !showOutsideDays ? (
45
49
  <EmptyDay />
46
50
  ) : (
47
51
  <Day
@@ -55,6 +59,7 @@ const Week = ({
55
59
  onPressDate={onPressDate}
56
60
  isToday={gd.isToday}
57
61
  disabled={gd.disabled}
62
+ outside={isOutside}
58
63
  />
59
64
  )}
60
65
  </Fragment>
@@ -1,48 +1,70 @@
1
1
  import { setYear } from 'date-fns';
2
- import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
3
- import { FlatList, StyleSheet, View } from 'react-native';
2
+ import { memo, useCallback, useLayoutEffect, useMemo, useRef } from 'react';
3
+ import { FlatList, ScrollView, StyleSheet, View } from 'react-native';
4
4
 
5
5
  import { getYearRange, resolveStateVariant } from '../../utils';
6
+ import { datePickerMonthItemStyles, datePickerMonthPickerStyles } from '../DatePicker/utils';
6
7
  import { HorizontalDivider } from '../HorizontalDivider';
8
+ import { Icon } from '../Icon';
7
9
  import { ListItem } from '../ListItem';
10
+ import { Text } from '../Text';
8
11
  import { useDatePickerStore } from './DatePickerContext';
9
12
  import { datePickerYearItemStyles, datePickerYearPickerStyles } from './utils';
10
13
 
11
- const ITEM_HEIGHT = 62;
14
+ const GRID_ITEM_HEIGHT = 62;
15
+ const NUM_COLUMNS = 3;
16
+ const LIST_ITEM_HEIGHT = 46;
12
17
 
13
- export default function YearPicker() {
18
+ type YearPickerProps = {
19
+ layout?: 'grid' | 'list';
20
+ };
21
+
22
+ export default function YearPicker({ layout = 'grid' }: YearPickerProps) {
23
+ if (layout === 'list') return <YearPickerList />;
24
+ return <YearPickerGrid />;
25
+ }
26
+
27
+ function YearPickerGrid() {
14
28
  const [{ startDateYear, endDateYear, localDate, pickerType }, setStore] = useDatePickerStore(
15
29
  state => state,
16
30
  );
17
- const flatList = useRef<FlatList<number> | null>(null);
18
31
  const years = useMemo(
19
32
  () => getYearRange(startDateYear, endDateYear),
20
33
  [startDateYear, endDateYear],
21
34
  );
35
+ const rows = useMemo(() => {
36
+ const chunks: number[][] = [];
37
+ for (let i = 0; i < years.length; i += NUM_COLUMNS) {
38
+ chunks.push(years.slice(i, i + NUM_COLUMNS));
39
+ }
40
+ return chunks;
41
+ }, [years]);
22
42
  const selectingYear = pickerType === 'year';
23
43
  const selectedYear = localDate.getFullYear();
44
+ const scrollRef = useRef<ScrollView | null>(null);
24
45
 
25
- // scroll to selected year
26
- useEffect(() => {
27
- if (flatList.current && selectingYear && selectedYear) {
28
- const indexToGo = selectedYear - years[0];
29
- flatList.current.scrollToOffset({
30
- offset: (indexToGo / 3) * ITEM_HEIGHT - ITEM_HEIGHT,
31
- animated: false,
32
- });
33
- }
34
- }, [flatList, selectedYear, selectingYear, years]);
46
+ const initialScrollOffset = useMemo(() => {
47
+ if (years.length === 0) return 0;
48
+ const totalRows = Math.ceil(years.length / NUM_COLUMNS);
49
+ const unclampedRow = Math.floor((selectedYear - years[0]) / NUM_COLUMNS);
50
+ const rowIndex = Math.min(Math.max(0, unclampedRow), Math.max(0, totalRows - 1));
51
+ return rowIndex * GRID_ITEM_HEIGHT;
52
+ }, [selectedYear, years]);
53
+
54
+ useLayoutEffect(() => {
55
+ if (!selectingYear) return;
56
+ scrollRef.current?.scrollTo({ y: initialScrollOffset, animated: false });
57
+ }, [selectingYear, initialScrollOffset]);
35
58
 
36
59
  const { containerStyle, yearStyle } = useMemo(() => {
37
60
  const { backgroundColor, ...rest } = datePickerYearPickerStyles.root;
38
-
39
61
  return {
40
62
  containerStyle: [
41
63
  StyleSheet.absoluteFill,
42
- styles.root,
64
+ gridStyles.root,
43
65
  { backgroundColor },
44
66
  datePickerYearPickerStyles.yearContainer,
45
- selectingYear ? styles.opacity1 : styles.opacity0,
67
+ selectingYear ? gridStyles.opacity1 : gridStyles.opacity0,
46
68
  ],
47
69
  yearStyle: rest,
48
70
  };
@@ -59,32 +81,39 @@ export default function YearPicker() {
59
81
  );
60
82
 
61
83
  return (
62
- <>
63
- {selectingYear && (
64
- <View style={containerStyle} pointerEvents={selectingYear ? 'auto' : 'none'}>
65
- <HorizontalDivider />
66
- <FlatList<number>
67
- ref={flatList}
68
- style={styles.list}
69
- data={years}
70
- renderItem={({ item }) => (
71
- <Year
72
- year={item}
73
- selected={selectedYear === item}
74
- onPressYear={handleOnChange}
75
- yearStyles={yearStyle}
76
- />
77
- )}
78
- keyExtractor={item => `${item}`}
79
- numColumns={3}
80
- />
84
+ <View style={containerStyle} pointerEvents={selectingYear ? 'auto' : 'none'}>
85
+ <HorizontalDivider />
86
+ <ScrollView
87
+ ref={scrollRef}
88
+ style={gridStyles.list}
89
+ contentOffset={{ x: 0, y: initialScrollOffset }}
90
+ removeClippedSubviews>
91
+ <View style={gridStyles.grid}>
92
+ {rows.map((row, rowIdx) => (
93
+ <View key={rowIdx} style={gridStyles.row}>
94
+ {row.map(year => (
95
+ <View key={year} style={gridStyles.cell}>
96
+ <YearPill
97
+ year={year}
98
+ selected={selectedYear === year}
99
+ onPressYear={handleOnChange}
100
+ yearStyles={yearStyle}
101
+ />
102
+ </View>
103
+ ))}
104
+ {row.length < NUM_COLUMNS &&
105
+ Array.from({ length: NUM_COLUMNS - row.length }).map((_, i) => (
106
+ <View key={`pad-${i}`} style={gridStyles.cell} />
107
+ ))}
108
+ </View>
109
+ ))}
81
110
  </View>
82
- )}
83
- </>
111
+ </ScrollView>
112
+ </View>
84
113
  );
85
114
  }
86
115
 
87
- function YearPure({
116
+ function YearPillPure({
88
117
  year,
89
118
  selected,
90
119
  onPressYear,
@@ -96,9 +125,7 @@ function YearPure({
96
125
  yearStyles: Record<string, any>;
97
126
  }) {
98
127
  datePickerYearItemStyles.useVariants({
99
- state: resolveStateVariant({
100
- selected,
101
- }) as any,
128
+ state: resolveStateVariant({ selected }) as any,
102
129
  });
103
130
 
104
131
  const handlePressYear = useCallback(() => {
@@ -107,10 +134,11 @@ function YearPure({
107
134
 
108
135
  return (
109
136
  <ListItem
137
+ contentStyle={datePickerYearItemStyles.content}
110
138
  onPress={handlePressYear}
111
139
  accessibilityRole="button"
112
140
  accessibilityLabel={String(year)}
113
- style={[datePickerYearItemStyles.yearButton, yearStyles]}
141
+ style={[yearStyles, datePickerYearItemStyles.yearButton]}
114
142
  testID={`pick-year-${year}`}>
115
143
  <ListItem.Title style={datePickerYearItemStyles.yearLabel} selectable={false}>
116
144
  {year}
@@ -118,22 +146,147 @@ function YearPure({
118
146
  </ListItem>
119
147
  );
120
148
  }
121
- const Year = memo(YearPure);
149
+ const YearPill = memo(YearPillPure);
150
+
151
+ function YearPickerList() {
152
+ const [{ startDateYear, endDateYear, localDate, pickerType }, setStore] = useDatePickerStore(
153
+ state => state,
154
+ );
155
+ const flatList = useRef<FlatList<number> | null>(null);
156
+ const years = useMemo(
157
+ () => getYearRange(startDateYear, endDateYear),
158
+ [startDateYear, endDateYear],
159
+ );
160
+ const selectingYear = pickerType === 'year';
161
+ const selectedYear = localDate.getFullYear();
162
+
163
+ const initialScrollIndex = useMemo(() => {
164
+ if (years.length === 0) return 0;
165
+ const idx = years.indexOf(selectedYear);
166
+ return Math.max(0, idx);
167
+ }, [selectedYear, years]);
168
+
169
+ const handleOnChange = useCallback(
170
+ (year: number) => {
171
+ setStore(prev => ({
172
+ localDate: setYear(prev.localDate, year),
173
+ pickerType: undefined,
174
+ }));
175
+ },
176
+ [setStore],
177
+ );
178
+
179
+ const renderItem = useCallback(
180
+ ({ item }: { item: number }) => (
181
+ <YearRow year={item} selected={selectedYear === item} onPressYear={handleOnChange} />
182
+ ),
183
+ [selectedYear, handleOnChange],
184
+ );
122
185
 
123
- const styles = StyleSheet.create({
186
+ const getItemLayout = useCallback(
187
+ (_data: any, index: number) => ({
188
+ length: LIST_ITEM_HEIGHT,
189
+ offset: LIST_ITEM_HEIGHT * index,
190
+ index,
191
+ }),
192
+ [],
193
+ );
194
+
195
+ if (!selectingYear) return null;
196
+
197
+ return (
198
+ <View style={[StyleSheet.absoluteFill, listStyles.root]} pointerEvents="auto">
199
+ <HorizontalDivider />
200
+ <FlatList<number>
201
+ ref={flatList}
202
+ style={listStyles.list}
203
+ data={years}
204
+ renderItem={renderItem}
205
+ keyExtractor={item => `${item}`}
206
+ initialScrollIndex={initialScrollIndex}
207
+ getItemLayout={getItemLayout}
208
+ />
209
+ </View>
210
+ );
211
+ }
212
+
213
+ function YearRowPure({
214
+ year,
215
+ selected,
216
+ onPressYear,
217
+ }: {
218
+ year: number;
219
+ selected: boolean;
220
+ onPressYear: (newYear: number) => any;
221
+ }) {
222
+ datePickerMonthItemStyles.useVariants({
223
+ state: resolveStateVariant({ selected }) as any,
224
+ });
225
+
226
+ const handlePressYear = useCallback(() => {
227
+ onPressYear(year);
228
+ }, [year, onPressYear]);
229
+
230
+ return (
231
+ <ListItem
232
+ onPress={handlePressYear}
233
+ accessibilityRole="button"
234
+ accessibilityLabel={String(year)}
235
+ accessibilityState={{ selected }}
236
+ style={datePickerMonthItemStyles.monthButton}
237
+ testID={`pick-year-${year}`}
238
+ left={
239
+ selected ? (
240
+ <View style={listStyles.checkIconView}>
241
+ <Icon name="check" size={24} />
242
+ </View>
243
+ ) : (
244
+ <View style={listStyles.spacer} />
245
+ )
246
+ }>
247
+ <View style={datePickerMonthItemStyles.monthInner}>
248
+ <Text style={datePickerMonthItemStyles.monthLabel} selectable={false}>
249
+ {year}
250
+ </Text>
251
+ </View>
252
+ </ListItem>
253
+ );
254
+ }
255
+ const YearRow = memo(YearRowPure);
256
+
257
+ const gridStyles = StyleSheet.create({
124
258
  root: {
125
259
  flex: 1,
126
260
  top: 56,
127
261
  zIndex: 100,
128
262
  },
129
-
130
- list: {
131
- flex: 1,
263
+ list: { flex: 1 },
264
+ grid: { alignItems: 'center' },
265
+ row: {
266
+ flexDirection: 'row',
267
+ justifyContent: 'center',
268
+ alignSelf: 'stretch',
269
+ gap: 12,
132
270
  },
133
- opacity0: {
134
- opacity: 0,
271
+ cell: {
272
+ flex: 1,
273
+ maxWidth: 110,
274
+ minWidth: 90,
275
+ height: GRID_ITEM_HEIGHT,
276
+ justifyContent: 'center',
135
277
  },
136
- opacity1: {
137
- opacity: 1,
278
+ opacity0: { opacity: 0 },
279
+ opacity1: { opacity: 1 },
280
+ });
281
+
282
+ const listStyles = StyleSheet.create({
283
+ root: {
284
+ flex: 1,
285
+ top: 56,
286
+ zIndex: 100,
287
+ backgroundColor: datePickerMonthPickerStyles.root.backgroundColor,
138
288
  },
289
+ list: { flex: 1 },
290
+ checkIconView: { marginLeft: 16 },
291
+ spacer: { width: 44 },
139
292
  });
@@ -219,21 +219,25 @@ export const generateCalendarGrid = ({
219
219
  const isFirstWeek = weekGrid === 0;
220
220
  const realDayIndex = emptyDays - dayIndex;
221
221
  const beforeWeekDay = isFirstWeek && realDayIndex > 0;
222
- const dayOfMonth = weekGrid * 7 + dayIndex - emptyDays + 1;
223
- const afterWeekDay = dayOfMonth > daysInMonth;
224
-
225
- const day = new Date(year, month, dayOfMonth);
222
+ const rawDayOfMonth = weekGrid * 7 + dayIndex - emptyDays + 1;
223
+ const afterWeekDay = rawDayOfMonth > daysInMonth;
224
+ const outside = beforeWeekDay || afterWeekDay;
225
+
226
+ const day = new Date(year, month, rawDayOfMonth);
227
+ const dayOfMonth = day.getDate();
228
+ const displayYear = day.getFullYear();
229
+ const displayMonth = day.getMonth();
226
230
  const isToday = areDatesOnSameDay(day, today);
227
231
 
228
232
  let inRange = false;
229
233
  let disabled = isDisabled(day);
230
234
  let selected = false;
231
235
 
232
- let leftCrop = dayOfMonth === 1;
233
- let rightCrop = dayOfMonth === daysInMonth;
236
+ let leftCrop = rawDayOfMonth === 1;
237
+ let rightCrop = rawDayOfMonth === daysInMonth;
234
238
 
235
- const isFirstDayOfMonth = dayOfMonth === 1;
236
- const isLastDayOfMonth = dayOfMonth === daysInMonth;
239
+ const isFirstDayOfMonth = rawDayOfMonth === 1;
240
+ const isLastDayOfMonth = rawDayOfMonth === daysInMonth;
237
241
 
238
242
  if (mode === 'range') {
239
243
  const selectedStartDay = areDatesOnSameDay(day, startDate);
@@ -267,8 +271,8 @@ export const generateCalendarGrid = ({
267
271
  const safeDates = dates || [];
268
272
  selected = safeDates.some(d => areDatesOnSameDay(day, d));
269
273
 
270
- const yesterday = new Date(year, month, dayOfMonth - 1);
271
- const tomorrow = new Date(year, month, dayOfMonth + 1);
274
+ const yesterday = new Date(year, month, rawDayOfMonth - 1);
275
+ const tomorrow = new Date(year, month, rawDayOfMonth + 1);
272
276
 
273
277
  const yesterdaySelected = safeDates.some(d => areDatesOnSameDay(d, yesterday));
274
278
  const tomorrowSelected = safeDates.some(d => areDatesOnSameDay(d, tomorrow));
@@ -316,8 +320,9 @@ export const generateCalendarGrid = ({
316
320
  return {
317
321
  beforeWeekDay,
318
322
  afterWeekDay,
319
- year,
320
- month,
323
+ outside,
324
+ year: displayYear,
325
+ month: displayMonth,
321
326
  dayOfMonth,
322
327
  dayIndex,
323
328
  mode,
@@ -13,6 +13,7 @@ export interface BaseMonthProps {
13
13
  onPressDate: (date: Date) => any;
14
14
  validRange?: ValidRangeType;
15
15
  customMonthStyles?: Record<string, any>;
16
+ showOutsideDays?: boolean;
16
17
 
17
18
  // some of these should be required in final implementation
18
19
  date?: CalendarDate;
@@ -52,7 +53,9 @@ export type BaseDatePickerProps = {
52
53
  startYear?: number;
53
54
  endYear?: number;
54
55
  HeaderComponent?: MemoExoticComponent<CalendarHeaderProps | any>;
56
+ headerLayout?: 'inline' | 'docked';
55
57
  monthStyle?: Record<string, any>;
58
+ showOutsideDays?: boolean;
56
59
 
57
60
  // here they are optional but in final implemenation they are required
58
61
  date?: CalendarDate;