@momo-kits/calendar 0.169.4 → 0.169.6

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.
@@ -13,6 +13,7 @@ type SwitchReturnSelectionProps = {
13
13
  onChange: () => void;
14
14
  };
15
15
 
16
+ /** Round-trip switch row above the date tabs; renders nothing when `hidden`. */
16
17
  export const SwitchReturnSelection: React.FC<
17
18
  SwitchReturnSelectionProps
18
19
  > = props => {
@@ -48,6 +49,7 @@ type HeaderPanelProps = {
48
49
  selectedDate: moment.Moment;
49
50
  };
50
51
 
52
+ /** Departure and return tabs in double-date mode, else one departure tab. */
51
53
  export const HeaderPanel: React.FC<HeaderPanelProps> = props => {
52
54
  const { theme } = useContext(ApplicationContext);
53
55
  const {
package/CalendarPro.tsx CHANGED
@@ -28,6 +28,18 @@ import {
28
28
  import { ContainerContext } from './ContainerContext';
29
29
  import styles from './styles';
30
30
 
31
+ /** Holidays to list: lunar-only ones are hidden while lunar dates are off. */
32
+ const getVisibleHolidays = (holidays: Holidays[], showLunar: boolean) => {
33
+ if (showLunar) {
34
+ return holidays;
35
+ }
36
+ return holidays.filter(item => !item.lunar || item.mixedLabel);
37
+ };
38
+
39
+ /**
40
+ * Weekday row, month list, lunar toggle and holiday list of `Calendar`, which
41
+ * drives the selection through `setDoubleDateAndTabIndex`.
42
+ */
31
43
  const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
32
44
  (
33
45
  {
@@ -55,7 +67,7 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
55
67
  const headerControlRef = useRef<HeaderControlRef>(null);
56
68
  const monthListRef = useRef<MonthListRef>(null);
57
69
 
58
- // Compute min/max date range
70
+ // An invalid bound falls back to 12 months from the other one (or today).
59
71
  const [minDate, maxDate] = useMemo(() => {
60
72
  let max = moment(propMaxDate);
61
73
  let min = moment(propMinDate);
@@ -72,7 +84,6 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
72
84
  return [min, max];
73
85
  }, [propMinDate, propMaxDate]);
74
86
 
75
- // State
76
87
  const [startDate, setStartDate] = useState<Moment>(propStartDate);
77
88
  const [endDate, setEndDate] = useState<Moment | null>(propEndDate || null);
78
89
  const [showLunar, setShowLunar] = useState<boolean>(!!propIsShowLunar);
@@ -94,7 +105,7 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
94
105
  },
95
106
  }));
96
107
 
97
- // Sync showLunar prop
108
+ // Follow the isShowLunar prop, except right after the user toggled it.
98
109
  useEffect(() => {
99
110
  if (ownUpdate) {
100
111
  setOwnUpdate(false);
@@ -103,7 +114,6 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
103
114
  }
104
115
  }, [ownUpdate, propIsShowLunar, showLunar]);
105
116
 
106
- // Scroll to initial selected date on mount or when it changes
107
117
  useEffect(() => {
108
118
  const timer = setTimeout(() => {
109
119
  monthListRef.current?.scrollToMonth(propSelectedDate);
@@ -111,7 +121,6 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
111
121
  return () => clearTimeout(timer);
112
122
  }, [propSelectedDate]);
113
123
 
114
- // Handlers
115
124
  const onChooseDay = (day: Moment) => {
116
125
  if (isDoubleDateMode) {
117
126
  if (tabSelected === 1) {
@@ -132,11 +141,8 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
132
141
  };
133
142
 
134
143
  const executeProcessAfterScrollCalendar = (date: Moment, key: string) => {
135
- // get sorted array of holidays directly
136
144
  const allHolidays = Util.getHolidaysInMonth(moment(date));
137
- const filtered = showLunar
138
- ? allHolidays
139
- : allHolidays.filter(item => !item.lunar || item.mixedLabel);
145
+ const filtered = getVisibleHolidays(allHolidays, showLunar);
140
146
  headerControlRef.current?.onUpdateInfo({ date });
141
147
  headerRef.current = date.clone().startOf('month');
142
148
  setHolidays(allHolidays);
@@ -157,9 +163,7 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
157
163
  const onToggleLunar = () => {
158
164
  setOwnUpdate(true);
159
165
  const next = !showLunar;
160
- const filtered = next
161
- ? holidays
162
- : holidays.filter(item => !item.lunar || item.mixedLabel);
166
+ const filtered = getVisibleHolidays(holidays, next);
163
167
  onCallbackCalendar?.('lunar', next);
164
168
  setShowLunar(next);
165
169
  setTemp(filtered);
@@ -186,7 +190,6 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
186
190
  }
187
191
  };
188
192
 
189
- // Determine price list format
190
193
  let priceListFormat = priceList?.outbound;
191
194
  if (isDoubleDateMode && tabSelected !== 0) {
192
195
  priceListFormat = priceList?.inbound;
@@ -208,23 +211,25 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
208
211
  <View style={styles.blueSeperator} />
209
212
  <View>
210
213
  <View style={styles.viewDay}>
211
- {[1, 2, 3, 4, 5, 6, 7].map(dayIdx => (
212
- <Text
213
- key={dayIdx}
214
- color={
215
- dayIdx >= 6
216
- ? theme.colors.error.primary
217
- : theme.colors.text.default
218
- }
219
- typography="label_s_medium"
220
- style={[
221
- styles.textDay,
222
- { width: (size.width - Spacing.S * 2) / 7 },
223
- ]}
224
- >
225
- {translate?.(Util.mapWeeKDate(dayIdx))}
226
- </Text>
227
- ))}
214
+ {[1, 2, 3, 4, 5, 6, 7].map(dayIdx => {
215
+ let dayColor = theme.colors.text.default;
216
+ if (dayIdx >= 6) {
217
+ dayColor = theme.colors.error.primary;
218
+ }
219
+ return (
220
+ <Text
221
+ key={dayIdx}
222
+ color={dayColor}
223
+ typography="label_s_medium"
224
+ style={[
225
+ styles.textDay,
226
+ { width: (size.width - Spacing.S * 2) / 7 },
227
+ ]}
228
+ >
229
+ {translate?.(Util.mapWeeKDate(dayIdx))}
230
+ </Text>
231
+ );
232
+ })}
228
233
  </View>
229
234
  <MonthList
230
235
  ref={monthListRef}
@@ -265,9 +270,9 @@ const CalendarPro = forwardRef<CalendarProRef, CalendarProProps>(
265
270
  nestedScrollEnabled
266
271
  >
267
272
  {temp.map((item, idx) => {
268
- const labelDate = `${item.day > 9 ? item.day : `0${item.day}`}/${
269
- item.month > 9 ? item.month : `0${item.month}`
270
- }`;
273
+ const day = item.day > 9 ? item.day : `0${item.day}`;
274
+ const month = item.month > 9 ? item.month : `0${item.month}`;
275
+ const labelDate = `${day}/${month}`;
271
276
  const labelHoliday = (showLunar && item.mixedLabel) || item.label;
272
277
  const labelHighlight = showLunar ? item.highlight || '' : '';
273
278
  return (
@@ -1,3 +1,4 @@
1
1
  import { createContext } from 'react';
2
2
 
3
+ /** Calendar width and day cell height, shared by every month and day cell. */
3
4
  export const ContainerContext = createContext({ width: 0, height: 0 });
package/Day.tsx CHANGED
@@ -12,6 +12,10 @@ import { DayProps } from './types';
12
12
  import { ContainerContext } from './ContainerContext';
13
13
  import styles from './styles';
14
14
 
15
+ /**
16
+ * One day cell: solar date with optional lunar date and price, highlighted as
17
+ * the start, end or inside of the selected range.
18
+ */
15
19
  const Day: React.FC<DayProps> = props => {
16
20
  const {
17
21
  startDate,
@@ -46,15 +50,13 @@ const Day: React.FC<DayProps> = props => {
46
50
  return <View style={[styles.dayContainer, { width: itemWidth }]} />;
47
51
  }
48
52
 
49
- const isValid =
50
- !isDisabled &&
51
- (date.isAfter(minDate, 'day') || date.isSame(minDate, 'day')) &&
52
- (date.isBefore(maxDate, 'day') || date.isSame(maxDate, 'day'));
53
+ const isAfterMin = date.isSameOrAfter(minDate, 'day');
54
+ const isBeforeMax = date.isSameOrBefore(maxDate, 'day');
55
+ const isValid = !isDisabled && isAfterMin && isBeforeMax;
53
56
 
54
- const isMid =
55
- isDoubleDateMode &&
56
- date.isAfter(startDate, 'day') &&
57
- date.isBefore(endDate, 'day');
57
+ const isAfterStart = date.isAfter(startDate, 'day');
58
+ const isBeforeEnd = date.isBefore(endDate, 'day');
59
+ const isMid = isDoubleDateMode && isAfterStart && isBeforeEnd;
58
60
 
59
61
  const isStart = date.isSame(startDate, 'day');
60
62
  const isStartPart = !!(isStart && endDate);
package/HeaderControl.tsx CHANGED
@@ -5,20 +5,21 @@ import React, {
5
5
  useImperativeHandle,
6
6
  useState,
7
7
  } from 'react';
8
- import {TouchableOpacity, View} from 'react-native';
8
+ import { TouchableOpacity, View } from 'react-native';
9
9
  import moment from 'moment';
10
- import {ApplicationContext, Icon, Text} from '@momo-kits/foundation';
10
+ import { ApplicationContext, Icon, Text } from '@momo-kits/foundation';
11
11
 
12
12
  import Util from './Util';
13
- import {HeaderControlProps, HeaderControlRef} from './types';
13
+ import { HeaderControlProps, HeaderControlRef } from './types';
14
14
  import styles from './styles';
15
15
 
16
+ /** Month title between previous/next arrows; `onUpdateInfo` sets the month. */
16
17
  const HeaderControl: ForwardRefRenderFunction<
17
18
  HeaderControlRef,
18
19
  HeaderControlProps
19
- > = ({onPressBackArrow, onPressNextArrow, selectedDate}, ref) => {
20
- const {translate} = useContext(ApplicationContext);
21
- const [info, setInfo] = useState<{date: moment.Moment}>({
20
+ > = ({ onPressBackArrow, onPressNextArrow, selectedDate }, ref) => {
21
+ const { translate } = useContext(ApplicationContext);
22
+ const [info, setInfo] = useState<{ date: moment.Moment }>({
22
23
  date: moment(selectedDate || new Date()),
23
24
  });
24
25
 
@@ -26,14 +27,15 @@ const HeaderControl: ForwardRefRenderFunction<
26
27
  onUpdateInfo,
27
28
  }));
28
29
 
29
- const onUpdateInfo = (date: React.SetStateAction<{date: moment.Moment}>) => {
30
+ const onUpdateInfo = (
31
+ date: React.SetStateAction<{ date: moment.Moment }>,
32
+ ) => {
30
33
  setInfo(date);
31
34
  };
32
35
 
33
36
  if (info && info.date) {
34
- const headerFormat = `${translate?.(
35
- Util.mapMonth(info.date.month() + 1) || ''
36
- )}/${info.date.year()}`;
37
+ const monthKey = Util.mapMonth(info.date.month() + 1) || '';
38
+ const headerFormat = `${translate?.(monthKey)}/${info.date.year()}`;
37
39
  return (
38
40
  <View style={styles.headerControlContainer}>
39
41
  <TouchableOpacity onPress={onPressBackArrow}>
@@ -11,6 +11,20 @@ function Solar(this: any) {
11
11
  this.solarYear = 0;
12
12
  }
13
13
 
14
+ /** Day number of March 1 of `year` in the March-based count of `SolarToInt`. */
15
+ const daysBeforeYear = (year: number) => {
16
+ return (
17
+ 365 * year +
18
+ parseInt(String(year / 4), 10) -
19
+ parseInt(String(year / 100), 10) +
20
+ parseInt(String(year / 400), 10)
21
+ );
22
+ };
23
+
24
+ /**
25
+ * Table-based solar ↔ lunar date converter, used with `new`.
26
+ * Supports years from 1900 to the end of its tables and throws outside them.
27
+ */
14
28
  function LunarSolarConverter(this: any) {
15
29
  this.lunar_month_days = [
16
30
  1887, 0x1694, 0x16aa, 0x4ad5, 0xab6, 0xc4b7, 0x4ae, 0xa56, 0xb52a, 0x1d2a,
@@ -73,18 +87,12 @@ function LunarSolarConverter(this: any) {
73
87
  0x107c53, 0x107e48,
74
88
  ];
75
89
 
76
- /**
77
- * @return {number}
78
- */
79
90
  this.GetBitInt = function (data: number, length: number, shift: number) {
80
91
  // eslint-disable-next-line no-bitwise -- unpacks bit-packed lunar table data
81
92
  return (data & (((1 << length) - 1) << shift)) >> shift;
82
93
  };
83
94
 
84
- // WARNING: Dates before Oct. 1582 are inaccurate
85
- /**
86
- * @return {number}
87
- */
95
+ /** Day number of a Gregorian date; inaccurate before Oct 1582. */
88
96
  this.SolarToInt = function (y: number, m: number, d: number) {
89
97
  m = (m + 9) % 12;
90
98
  y = parseInt(String(y), 10) - parseInt(String(m / 10), 10);
@@ -98,22 +106,13 @@ function LunarSolarConverter(this: any) {
98
106
  );
99
107
  };
100
108
 
109
+ /** Gregorian date of a day number from `SolarToInt`. */
101
110
  this.SolarFromInt = function (g: number) {
102
111
  let y = parseInt(String((10000 * g + 14780) / 3652425), 10);
103
- let ddd =
104
- g -
105
- (365 * y +
106
- parseInt(String(y / 4), 10) -
107
- parseInt(String(y / 100), 10) +
108
- parseInt(String(y / 400), 10));
112
+ let ddd = g - daysBeforeYear(y);
109
113
  if (ddd < 0) {
110
114
  y--;
111
- ddd =
112
- g -
113
- (365 * y +
114
- parseInt(String(y / 4), 10) -
115
- parseInt(String(y / 100), 10) +
116
- parseInt(String(y / 400), 10));
115
+ ddd = g - daysBeforeYear(y);
117
116
  }
118
117
  const mi = parseInt(String((100 * ddd + 52) / 3060), 10);
119
118
  const mm = ((mi + 2) % 12) + 1;
@@ -133,15 +132,15 @@ function LunarSolarConverter(this: any) {
133
132
  lunarMonth: number;
134
133
  lunarDay: number;
135
134
  }) {
136
- // year range check: only support lunar calendar from 1900
135
+ const firstTableYear = this.lunar_month_days[0];
137
136
  const minLunarYear = 1900;
138
- // using converter data, maximum supported lunar year
139
- const maxLunarYear = this.lunar_month_days[0] + this.lunar_month_days.length - 1;
137
+ const maxLunarYear = firstTableYear + this.lunar_month_days.length - 1;
140
138
  if (lunar.lunarYear < minLunarYear || lunar.lunarYear > maxLunarYear) {
141
- throw new Error(`LunarToSolar: year ${lunar.lunarYear} out of supported range ${minLunarYear}-${maxLunarYear}`);
139
+ throw new Error(
140
+ `LunarToSolar: year ${lunar.lunarYear} out of supported range ${minLunarYear}-${maxLunarYear}`,
141
+ );
142
142
  }
143
- const days =
144
- this.lunar_month_days[lunar.lunarYear - this.lunar_month_days[0]];
143
+ const days = this.lunar_month_days[lunar.lunarYear - firstTableYear];
145
144
  const leap = this.GetBitInt(days, 4, 13);
146
145
  let offset = 0;
147
146
  let loopend = leap;
@@ -171,19 +170,19 @@ function LunarSolarConverter(this: any) {
171
170
  solarMonth: number;
172
171
  solarDay: number;
173
172
  }) {
174
- // year range check: only support solar dates from 1900
175
173
  const minSolarYear = 1900;
176
- // using converter data, maximum supported solar year
177
174
  const maxSolarYear = this.solar_1_1[0] + this.solar_1_1.length - 1;
178
175
  if (solar.solarYear < minSolarYear || solar.solarYear > maxSolarYear) {
179
- throw new Error(`SolarToLunar: year ${solar.solarYear} out of supported range ${minSolarYear}-${maxSolarYear}`);
176
+ throw new Error(
177
+ `SolarToLunar: year ${solar.solarYear} out of supported range ${minSolarYear}-${maxSolarYear}`,
178
+ );
180
179
  }
181
180
  // @ts-ignore
182
181
  const lunar = new Lunar();
183
- let index = solar.solarYear - this.solar_1_1[0];
184
- const data =
185
- // eslint-disable-next-line no-bitwise -- packs date to compare with solar_1_1 table
186
- (solar.solarYear << 9) | (solar.solarMonth << 5) | solar.solarDay;
182
+ const { solarYear, solarMonth, solarDay } = solar;
183
+ let index = solarYear - this.solar_1_1[0];
184
+ // eslint-disable-next-line no-bitwise -- packs date to compare with solar_1_1 table
185
+ const data = (solarYear << 9) | (solarMonth << 5) | solarDay;
187
186
  if (this.solar_1_1[index] > data) {
188
187
  index--;
189
188
  }
@@ -191,9 +190,8 @@ function LunarSolarConverter(this: any) {
191
190
  const y = this.GetBitInt(solar11, 12, 9);
192
191
  const m = this.GetBitInt(solar11, 4, 5);
193
192
  const d = this.GetBitInt(solar11, 5, 0);
194
- let offset =
195
- this.SolarToInt(solar.solarYear, solar.solarMonth, solar.solarDay) -
196
- this.SolarToInt(y, m, d);
193
+ const dayNumber = this.SolarToInt(solarYear, solarMonth, solarDay);
194
+ let offset = dayNumber - this.SolarToInt(y, m, d);
197
195
 
198
196
  const days = this.lunar_month_days[index];
199
197
  const leap = this.GetBitInt(days, 4, 13);
package/Month.tsx CHANGED
@@ -1,12 +1,13 @@
1
- import React, {useContext, useMemo} from 'react';
2
- import {View} from 'react-native';
3
- import moment, {Moment} from 'moment';
1
+ import React, { useContext, useMemo } from 'react';
2
+ import { View } from 'react-native';
3
+ import moment, { Moment } from 'moment';
4
4
  import Day from './Day';
5
- import {useScaleSize} from '@momo-kits/foundation';
6
- import {MonthProps} from './types';
7
- import {ContainerContext} from './ContainerContext';
5
+ import { useScaleSize } from '@momo-kits/foundation';
6
+ import { MonthProps } from './types';
7
+ import { ContainerContext } from './ContainerContext';
8
8
  import styles from './styles';
9
9
 
10
+ /** One month as rows of 7 `Day` cells; `dateList` is padded to whole weeks. */
10
11
  const Month: React.FC<MonthProps> = ({
11
12
  dateList,
12
13
  holidays,
@@ -49,26 +50,26 @@ const Month: React.FC<MonthProps> = ({
49
50
  const size = useContext(ContainerContext);
50
51
  const scaledHeight = useScaleSize(48);
51
52
 
52
- // Precompute disabled dates set for fast lookup (format: YYYY-MM-DD)
53
- const disabledSet = useMemo(
54
- () => new Set((disabledDays || []).map(d => moment(d).format('YYYY-MM-DD'))),
55
- [disabledDays]
56
- );
53
+ // YYYY-MM-DD set so each day cell is an O(1) lookup.
54
+ const disabledSet = useMemo(() => {
55
+ const keys = (disabledDays || []).map(d => moment(d).format('YYYY-MM-DD'));
56
+ return new Set(keys);
57
+ }, [disabledDays]);
57
58
 
58
59
  if (!month) {
59
60
  return <View />;
60
61
  }
61
62
 
62
63
  return (
63
- <View style={{width: size.width}}>
64
+ <View style={{ width: size.width }}>
64
65
  <View style={styles.monthRows}>
65
66
  {rows.map((dayList, rowIndex) => (
66
67
  <View
67
68
  key={`row${rowIndex}`}
68
- style={[styles.monthRow, {height: scaledHeight}]}>
69
+ style={[styles.monthRow, { height: scaledHeight }]}
70
+ >
69
71
  {dayList.map((item, i) => {
70
- const {isEmpty, date: dateMoment, lunarDate} = item;
71
- // compute key for price/disabled only if not empty
72
+ const { isEmpty, date: dateMoment, lunarDate } = item;
72
73
  const hasDate = !isEmpty && dateMoment;
73
74
  const keyDay = hasDate ? dateMoment.format('YYYY-MM-DD') : '';
74
75
  const priceInfo = keyDay ? priceListDate?.[keyDay] : undefined;
package/MonthList.tsx CHANGED
@@ -26,6 +26,14 @@ const MAX_RENDER_PER_BATCH = Platform.OS === 'android' ? 1 : 12;
26
26
  // @ts-ignore
27
27
  const converter = new LunarDateConverter();
28
28
 
29
+ const isSameMonth = (a: Moment, b: Moment) => {
30
+ return a.year() === b.year() && a.month() === b.month();
31
+ };
32
+
33
+ /**
34
+ * Horizontally paged months from `minDate` to `maxDate`; reports the visible
35
+ * month through `onScrollCalendar` and `scrollToMonth` pages to a month.
36
+ */
29
37
  const MonthList = forwardRef<MonthListRef, MonthListProps>((props, ref) => {
30
38
  const {
31
39
  selectedDate,
@@ -59,10 +67,7 @@ const MonthList = forwardRef<MonthListRef, MonthListProps>((props, ref) => {
59
67
  const months: { date: Moment; dateList: any[] }[] = [];
60
68
  const mMin = moment(minDate).date(1);
61
69
  const mMax = moment(maxDate);
62
- while (
63
- mMax > mMin ||
64
- (mMax.year() === mMin.year() && mMax.month() === mMin.month())
65
- ) {
70
+ while (mMax > mMin || isSameMonth(mMax, mMin)) {
66
71
  const d = moment(mMin);
67
72
  const dayList: any[] = [];
68
73
  const monthIdx = d.month();
@@ -109,8 +114,7 @@ const MonthList = forwardRef<MonthListRef, MonthListProps>((props, ref) => {
109
114
  [selectedDate, getIndexOfMonth],
110
115
  );
111
116
 
112
- // paging keeps the old pixel offset when the width changes (fold, unfold,
113
- // rotation), so remember the offset and move back onto the same month
117
+ // Paging keeps the pixel offset on resize (fold, rotation); re-snap it.
114
118
  const scrollOffset = useRef(Math.max(currentScrollIndex, 0) * size.width);
115
119
  const pageWidth = useRef(size.width);
116
120
 
@@ -128,20 +132,17 @@ const MonthList = forwardRef<MonthListRef, MonthListProps>((props, ref) => {
128
132
  });
129
133
  }, [size.width]);
130
134
 
131
- const onScroll = useCallback(
132
- (e: NativeSyntheticEvent<NativeScrollEvent>) => {
133
- scrollOffset.current = e.nativeEvent.contentOffset.x;
134
- },
135
- [],
136
- );
135
+ const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
136
+ scrollOffset.current = e.nativeEvent.contentOffset.x;
137
+ }, []);
137
138
 
138
139
  const scrollToMonth = useCallback(
139
140
  (month: Moment) => {
140
141
  const idx = getIndexOfMonth(month);
141
142
  if (listRef.current && idx !== -1) {
142
143
  const offset = idx * pageWidth.current;
143
- targetIndex.current =
144
- Math.abs(offset - scrollOffset.current) < 1 ? null : idx;
144
+ const isAtTarget = Math.abs(offset - scrollOffset.current) < 1;
145
+ targetIndex.current = isAtTarget ? null : idx;
145
146
  listRef.current.scrollToIndex({ index: idx, animated: true });
146
147
  }
147
148
  },
@@ -152,7 +153,9 @@ const MonthList = forwardRef<MonthListRef, MonthListProps>((props, ref) => {
152
153
 
153
154
  const onViewableItemsChanged = useRef(({ changed }: { changed: any[] }) => {
154
155
  const visible = changed.find(token => token.isViewable);
155
- if (targetIndex.current !== null && visible?.index !== targetIndex.current) {
156
+ // Ignore months passed while animating to a scrollToMonth target.
157
+ const isScrollingToTarget = targetIndex.current !== null;
158
+ if (isScrollingToTarget && visible?.index !== targetIndex.current) {
156
159
  return;
157
160
  }
158
161
  targetIndex.current = null;
@@ -161,12 +164,9 @@ const MonthList = forwardRef<MonthListRef, MonthListProps>((props, ref) => {
161
164
  if (onScrollCalendarRef.current && item && currentKey.current !== key) {
162
165
  currentKey.current = key;
163
166
  try {
164
- setHeightStyle({
165
- height:
166
- (scaledHeight48Ref.current * item.dateList.length) / 7 +
167
- Spacing.L * 2 +
168
- Spacing.XS,
169
- });
167
+ const rowHeight = scaledHeight48Ref.current;
168
+ const rowsHeight = (rowHeight * item.dateList.length) / 7;
169
+ setHeightStyle({ height: rowsHeight + Spacing.L * 2 + Spacing.XS });
170
170
  } catch {
171
171
  setHeightStyle({});
172
172
  }
package/TabHeader.tsx CHANGED
@@ -11,6 +11,7 @@ import styles from './styles';
11
11
  import { ApplicationContext, Text } from '@momo-kits/foundation';
12
12
  import { TabHeaderProps, TabHeaderRef } from './types';
13
13
 
14
+ /** Date tab with a label and the chosen date, updated through `updateView`. */
14
15
  const TabHeader = forwardRef<TabHeaderRef, TabHeaderProps>((props, ref) => {
15
16
  const { label, disabled, id, onChangeTab } = props;
16
17
  const { theme, translate } = useContext(ApplicationContext);
@@ -27,8 +28,9 @@ const TabHeader = forwardRef<TabHeaderRef, TabHeaderProps>((props, ref) => {
27
28
  setActiveTab: setActive,
28
29
  }));
29
30
 
30
- const weekday = (day: Moment.Moment) =>
31
- translate?.(Util.WEEKDAYSFROMMOMENT[day.day()]);
31
+ const weekday = (day: Moment.Moment) => {
32
+ return translate?.(Util.WEEKDAYSFROMMOMENT[day.day()]);
33
+ };
32
34
  const isHighlighted = active && !disabled;
33
35
 
34
36
  const formattedDate = date ? date.format('DD/MM/YYYY') : '';
package/Util.ts CHANGED
@@ -1,30 +1,41 @@
1
1
  import moment from 'moment';
2
2
  import LunarDateConverter from './LunarDateConverter';
3
3
  import holiday from './holidayData';
4
- import {Holidays} from './types';
4
+ import { Holidays } from './types';
5
5
 
6
6
  // @ts-ignore
7
7
  const converter = new LunarDateConverter();
8
8
 
9
+ /** Moment as the `{ solarDay, solarMonth, solarYear }` converter input. */
9
10
  const Solar = (momentDate: moment.Moment) => ({
10
11
  solarDay: momentDate.date(),
11
12
  solarMonth: momentDate.month() + 1,
12
13
  solarYear: momentDate.year(),
13
14
  });
14
15
 
15
- const formatYYYYMMDD = (dd: number, mm: number, yyyy: any) =>
16
- `${yyyy}-${mm < 10 ? `0${mm}` : mm}-${dd < 10 ? `0${dd}` : dd}`;
16
+ const padTwoDigits = (value: number) => {
17
+ return value < 10 ? `0${value}` : `${value}`;
18
+ };
19
+
20
+ const formatYYYYMMDD = (dd: number, mm: number, yyyy: any) => {
21
+ return `${yyyy}-${padTwoDigits(mm)}-${padTwoDigits(dd)}`;
22
+ };
17
23
 
18
- const formatDDMM = (dd: number, mm: number) =>
19
- `${dd < 10 ? `0${dd}` : dd}/${mm < 10 ? `0${mm}` : mm}`;
24
+ const formatDDMM = (dd: number, mm: number) => {
25
+ return `${padTwoDigits(dd)}/${padTwoDigits(mm)}`;
26
+ };
20
27
 
28
+ /**
29
+ * Groups holidays by DD/MM; a solar and a lunar holiday on the same day merge
30
+ * into one entry whose `mixedLabel` names both.
31
+ */
21
32
  const groupHolidaysByDate = (
22
- holidays: Holidays[]
23
- ): {[key: string]: Holidays} => {
24
- const groupedHolidays: {[key: string]: Holidays} = {};
33
+ holidays: Holidays[],
34
+ ): { [key: string]: Holidays } => {
35
+ const groupedHolidays: { [key: string]: Holidays } = {};
25
36
  if (holidays && holidays.length > 0) {
26
37
  holidays.forEach(item => {
27
- const {day, month, lunar, label} = item;
38
+ const { day, month, lunar, label } = item;
28
39
  const key = formatDDMM(day, month);
29
40
  let holidaysObj = { ...(groupedHolidays[key] || item) };
30
41
  if (groupedHolidays[key]) {
@@ -49,28 +60,32 @@ const groupHolidaysByDate = (
49
60
  return groupedHolidays;
50
61
  };
51
62
 
52
- /**
53
- * Sort and group holidays by date, return sorted array of unique Holidays.
54
- */
63
+ /** Sorts holidays by date (in place) and merges those on the same day. */
55
64
  const sortByDate = (arr: Holidays[]): Holidays[] => {
56
65
  if (arr && arr.length > 1) {
57
- arr.sort((a, b) =>
58
- a.month !== b.month ? a.month - b.month : a.day - b.day
59
- );
66
+ arr.sort((a, b) => {
67
+ if (a.month !== b.month) {
68
+ return a.month - b.month;
69
+ }
70
+ return a.day - b.day;
71
+ });
60
72
  }
61
- // group by date string and return unique values in sorted order
73
+ // "DD/MM" keys aren't integer-like, so Object.values keeps the sorted order.
62
74
  const grouped = groupHolidaysByDate(arr);
63
75
  return Object.values(grouped);
64
76
  };
65
77
 
78
+ /** Weekday/month i18n keys and holiday lookup for the calendar. */
66
79
  const Utils = {
67
80
  WEEKDAYSFROMMOMENT: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
68
81
 
82
+ /** i18n key of a weekday, 1 = Monday … 7 = Sunday. */
69
83
  mapWeeKDate(i: number) {
70
84
  const date = ['', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
71
85
  return date[i];
72
86
  },
73
87
 
88
+ /** i18n key of a month, 1 = January. */
74
89
  mapMonth(i: number) {
75
90
  const month = new Map([
76
91
  [1, 'jan'],
@@ -89,9 +104,7 @@ const Utils = {
89
104
  return month.get(i);
90
105
  },
91
106
 
92
- /**
93
- * Get sorted list of holidays within the given month.
94
- */
107
+ /** Solar and lunar holidays in the month of `headerInfo`, sorted by date. */
95
108
  getHolidaysInMonth(headerInfo: moment.Moment): Holidays[] {
96
109
  if (headerInfo) {
97
110
  const startDate = moment(headerInfo).startOf('month');
@@ -103,12 +116,11 @@ const Utils = {
103
116
  const maxYear = maxLunarDate.lunarYear;
104
117
  const currentYear = minYear !== maxYear ? [minYear, maxYear] : minYear;
105
118
 
106
- // Handle Solar holidays
107
119
  // @ts-ignore
108
120
  if (holiday.solar[headerInfo.month() + 1]) {
109
121
  // @ts-ignore
110
122
  holiday.solar[headerInfo.month() + 1].forEach(
111
- (date: {month: number; day: any}) => {
123
+ (date: { month: number; day: any }) => {
112
124
  const dateAsMoment = moment({
113
125
  year: headerInfo.year(),
114
126
  month: date.month - 1,
@@ -117,11 +129,11 @@ const Utils = {
117
129
  if (dateAsMoment.isBetween(startDate, endDate, 'day', '[]')) {
118
130
  holidays.push(date);
119
131
  }
120
- }
132
+ },
121
133
  );
122
134
  }
123
135
 
124
- holiday.lunar.forEach((item: {lunarDay: any; lunarMonth: any}) => {
136
+ holiday.lunar.forEach((item: { lunarDay: any; lunarMonth: any }) => {
125
137
  if (currentYear instanceof Array) {
126
138
  const solar1 = converter.LunarToSolar({
127
139
  lunarDay: item.lunarDay,
@@ -134,14 +146,20 @@ const Utils = {
134
146
  lunarYear: currentYear[1],
135
147
  });
136
148
  const solar1AsMoment = moment(
137
- formatYYYYMMDD(solar1.solarDay, solar1.solarMonth, solar1.solarYear)
149
+ formatYYYYMMDD(
150
+ solar1.solarDay,
151
+ solar1.solarMonth,
152
+ solar1.solarYear,
153
+ ),
138
154
  );
139
155
  const solar2AsMoment = moment(
140
- formatYYYYMMDD(solar2.solarDay, solar2.solarMonth, solar2.solarYear)
156
+ formatYYYYMMDD(
157
+ solar2.solarDay,
158
+ solar2.solarMonth,
159
+ solar2.solarYear,
160
+ ),
141
161
  );
142
- if (
143
- solar1AsMoment.isBetween(startDate, endDate, 'day', '[]')
144
- ) {
162
+ if (solar1AsMoment.isBetween(startDate, endDate, 'day', '[]')) {
145
163
  holidays.push({
146
164
  ...item,
147
165
  day: solar1.solarDay,
@@ -163,11 +181,9 @@ const Utils = {
163
181
  lunarYear: currentYear,
164
182
  });
165
183
  const solarAsMoment = moment(
166
- formatYYYYMMDD(solar.solarDay, solar.solarMonth, solar.solarYear)
184
+ formatYYYYMMDD(solar.solarDay, solar.solarMonth, solar.solarYear),
167
185
  );
168
- if (
169
- solarAsMoment.isBetween(startDate, endDate, 'day', '[]')
170
- ) {
186
+ if (solarAsMoment.isBetween(startDate, endDate, 'day', '[]')) {
171
187
  holidays.push({
172
188
  ...item,
173
189
  day: solar.solarDay,
package/index.tsx CHANGED
@@ -16,19 +16,21 @@ import styles from './styles';
16
16
 
17
17
  const DOUBLE = 'doubleDate';
18
18
 
19
+ /**
20
+ * Calendar with single or round-trip (`mode="doubleDate"`) selection, lunar
21
+ * dates, holidays and prices. Dates default to one year around today.
22
+ */
19
23
  const Calendar: React.FC<CalendarProps> = props => {
20
24
  const { theme } = useContext(ApplicationContext);
21
25
  const scaledHeight48 = useScaleSize(48);
22
26
  const scaledHeight34 = useScaleSize(34);
23
27
 
24
- // derive defaults for date bounds
25
28
  const currentDate = new Date();
26
29
  const minDateDefault = new Date(currentDate);
27
30
  const maxDateDefault = new Date(currentDate);
28
31
  minDateDefault.setFullYear(minDateDefault.getFullYear() - 1);
29
32
  maxDateDefault.setFullYear(maxDateDefault.getFullYear() + 1);
30
33
 
31
- // destructure all Calendar props
32
34
  const {
33
35
  mode,
34
36
  id,
@@ -72,37 +74,31 @@ const Calendar: React.FC<CalendarProps> = props => {
72
74
  const tabSelected = useRef<number>(id || 0);
73
75
 
74
76
  const calendarPicker = useRef<CalendarProRef>(null);
75
- // hold refs to TabHeader instances
76
77
  const cellHeader1 = useRef<TabHeaderRef>(null);
77
78
  const cellHeader2 = useRef<TabHeaderRef>(null);
78
79
  const cellHeaderSingle = useRef<TabHeaderRef>(null);
79
80
 
80
- // Initialize view on mount
81
81
  useEffect(() => {
82
82
  tabSelected.current = id || 0;
83
83
  if (mode === DOUBLE) {
84
- if (
85
- cellHeader1.current &&
86
- cellHeader2.current &&
87
- calendarPicker.current
88
- ) {
84
+ const header1 = cellHeader1.current;
85
+ const header2 = cellHeader2.current;
86
+ const picker = calendarPicker.current;
87
+ if (header1 && header2 && picker) {
89
88
  const start = doubleDate.current.first;
90
89
  const end = doubleDate.current.second;
91
- cellHeader1.current.updateView(start, tabSelected.current === 0);
92
- cellHeader2.current.updateView(end, tabSelected.current === 1);
93
- calendarPicker.current.setDoubleDateAndTabIndex(
94
- doubleDate.current.first,
95
- doubleDate.current.second,
96
- tabSelected.current,
97
- );
90
+ header1.updateView(start, tabSelected.current === 0);
91
+ header2.updateView(end, tabSelected.current === 1);
92
+ picker.setDoubleDateAndTabIndex(start, end, tabSelected.current);
98
93
  }
99
94
  } else {
100
95
  calendarPicker.current?.setDoubleDateAndTabIndex(selectedDate);
101
96
  }
102
97
  }, []);
103
98
 
104
- const onLayout = (e: LayoutChangeEvent) =>
99
+ const onLayout = (e: LayoutChangeEvent) => {
105
100
  setContainerWidth(e.nativeEvent.layout.width);
101
+ };
106
102
 
107
103
  const onChangeTab = (tabId: number) => {
108
104
  onChangeTabProp?.(tabId);
@@ -120,20 +116,16 @@ const Calendar: React.FC<CalendarProps> = props => {
120
116
  const { onDateChange, onCTAStateChange } = props;
121
117
  if (!cellHeader1.current || !cellHeader2.current || !calendarPicker.current)
122
118
  return;
123
- // set first date and reset second
124
119
  doubleDate.current.first = selectedDate;
125
120
  doubleDate.current.second = null;
126
121
  tabSelected.current = 1;
127
- // update headers
128
122
  cellHeader1.current.updateView(selectedDate, false);
129
123
  cellHeader2.current.updateView(null, true);
130
- // update calendar
131
124
  calendarPicker.current.setDoubleDateAndTabIndex(
132
125
  doubleDate.current.first,
133
126
  doubleDate.current.second,
134
127
  tabSelected.current,
135
128
  );
136
- // notify parent
137
129
  onDateChange?.({ first: doubleDate.current.first!, second: null });
138
130
  onCTAStateChange?.(false);
139
131
  };
@@ -142,7 +134,7 @@ const Calendar: React.FC<CalendarProps> = props => {
142
134
  const { onDateChange, onCTAStateChange } = props;
143
135
  if (!cellHeader1.current || !cellHeader2.current || !calendarPicker.current)
144
136
  return;
145
- // if selectedDate is before first, treat as new first date
137
+ // A date before the first one restarts the range.
146
138
  if (selectedDate.isBefore(doubleDate.current.first!, 'day')) {
147
139
  doubleDate.current.first = selectedDate;
148
140
  doubleDate.current.second = null;
@@ -157,7 +149,6 @@ const Calendar: React.FC<CalendarProps> = props => {
157
149
  onDateChange?.({ first: doubleDate.current.first!, second: null });
158
150
  onCTAStateChange?.(false);
159
151
  } else {
160
- // valid second date
161
152
  doubleDate.current.second = selectedDate;
162
153
  tabSelected.current = 0;
163
154
  cellHeader2.current.updateView(selectedDate, false);
@@ -189,7 +180,7 @@ const Calendar: React.FC<CalendarProps> = props => {
189
180
  }
190
181
  };
191
182
 
192
- // skip running updateView on initial mount to preserve initial doubleDate props
183
+ // Skip the mount run so the initial doubleDate props are kept.
193
184
  const didMount = useRef(false);
194
185
 
195
186
  useEffect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/calendar",
3
- "version": "0.169.4",
3
+ "version": "0.169.6",
4
4
  "private": false,
5
5
  "main": "index.tsx",
6
6
  "peerDependencies": {
package/styles.ts CHANGED
@@ -1,8 +1,7 @@
1
- import {StyleSheet} from 'react-native';
2
- import {Colors, Radius, Spacing} from '@momo-kits/foundation';
1
+ import { StyleSheet } from 'react-native';
2
+ import { Colors, Radius, Spacing } from '@momo-kits/foundation';
3
3
 
4
4
  export default StyleSheet.create({
5
- //calendar
6
5
  viewPanel: {
7
6
  flexDirection: 'row',
8
7
  marginHorizontal: Spacing.M,
@@ -30,8 +29,7 @@ export default StyleSheet.create({
30
29
  width: '100%',
31
30
  },
32
31
 
33
- //calendar pro
34
- row: {flexDirection: 'row'},
32
+ row: { flexDirection: 'row' },
35
33
  txtMonthLunar: {
36
34
  marginRight: Spacing.S,
37
35
  },
@@ -69,7 +67,6 @@ export default StyleSheet.create({
69
67
  backgroundColor: Colors.blue_05,
70
68
  },
71
69
 
72
- //headerControl
73
70
  txtHeader: {
74
71
  textAlign: 'center',
75
72
  },
@@ -82,7 +79,6 @@ export default StyleSheet.create({
82
79
  padding: Spacing.S,
83
80
  },
84
81
 
85
- //day
86
82
  dayContainer: {
87
83
  borderRadius: Radius.XS,
88
84
  alignItems: 'center',