@momo-kits/calendar 0.169.4-beta.3 → 0.169.5

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,14 +1,4 @@
1
1
  import { createContext } from 'react';
2
2
 
3
- /** Style width of the probe the calendar measures to learn the rendered size scale. */
4
- export const SCALE_PROBE_SIZE = 100;
5
-
6
- /**
7
- * Calendar container size: `width`/`height` are style sizes for layout, `pageWidth` is
8
- * the rendered width that horizontal paging and scroll offsets are measured in.
9
- */
10
- export const ContainerContext = createContext({
11
- width: 0,
12
- height: 0,
13
- pageWidth: 0,
14
- });
3
+ /** Calendar width and day cell height, shared by every month and day cell. */
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,41 +114,35 @@ 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
114
- const scrollOffset = useRef(
115
- Math.max(currentScrollIndex, 0) * size.pageWidth,
116
- );
117
- const pageWidth = useRef(size.pageWidth);
117
+ // Paging keeps the pixel offset on resize (fold, rotation); re-snap it.
118
+ const scrollOffset = useRef(Math.max(currentScrollIndex, 0) * size.width);
119
+ const pageWidth = useRef(size.width);
118
120
 
119
121
  useLayoutEffect(() => {
120
122
  const previousWidth = pageWidth.current;
121
- pageWidth.current = size.pageWidth;
122
- if (previousWidth === size.pageWidth || previousWidth <= 0) {
123
+ pageWidth.current = size.width;
124
+ if (previousWidth === size.width || previousWidth <= 0) {
123
125
  return;
124
126
  }
125
127
  const page = Math.round(scrollOffset.current / previousWidth);
126
- scrollOffset.current = page * size.pageWidth;
128
+ scrollOffset.current = page * size.width;
127
129
  listRef.current?.scrollToOffset({
128
130
  offset: scrollOffset.current,
129
131
  animated: false,
130
132
  });
131
- }, [size.pageWidth]);
133
+ }, [size.width]);
132
134
 
133
- const onScroll = useCallback(
134
- (e: NativeSyntheticEvent<NativeScrollEvent>) => {
135
- scrollOffset.current = e.nativeEvent.contentOffset.x;
136
- },
137
- [],
138
- );
135
+ const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
136
+ scrollOffset.current = e.nativeEvent.contentOffset.x;
137
+ }, []);
139
138
 
140
139
  const scrollToMonth = useCallback(
141
140
  (month: Moment) => {
142
141
  const idx = getIndexOfMonth(month);
143
142
  if (listRef.current && idx !== -1) {
144
143
  const offset = idx * pageWidth.current;
145
- targetIndex.current =
146
- Math.abs(offset - scrollOffset.current) < 1 ? null : idx;
144
+ const isAtTarget = Math.abs(offset - scrollOffset.current) < 1;
145
+ targetIndex.current = isAtTarget ? null : idx;
147
146
  listRef.current.scrollToIndex({ index: idx, animated: true });
148
147
  }
149
148
  },
@@ -154,7 +153,9 @@ const MonthList = forwardRef<MonthListRef, MonthListProps>((props, ref) => {
154
153
 
155
154
  const onViewableItemsChanged = useRef(({ changed }: { changed: any[] }) => {
156
155
  const visible = changed.find(token => token.isViewable);
157
- 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) {
158
159
  return;
159
160
  }
160
161
  targetIndex.current = null;
@@ -163,12 +164,9 @@ const MonthList = forwardRef<MonthListRef, MonthListProps>((props, ref) => {
163
164
  if (onScrollCalendarRef.current && item && currentKey.current !== key) {
164
165
  currentKey.current = key;
165
166
  try {
166
- setHeightStyle({
167
- height:
168
- (scaledHeight48Ref.current * item.dateList.length) / 7 +
169
- Spacing.L * 2 +
170
- Spacing.XS,
171
- });
167
+ const rowHeight = scaledHeight48Ref.current;
168
+ const rowsHeight = (rowHeight * item.dateList.length) / 7;
169
+ setHeightStyle({ height: rowsHeight + Spacing.L * 2 + Spacing.XS });
172
170
  } catch {
173
171
  setHeightStyle({});
174
172
  }
@@ -190,11 +188,11 @@ const MonthList = forwardRef<MonthListRef, MonthListProps>((props, ref) => {
190
188
 
191
189
  const getItemLayout = useCallback(
192
190
  (_: any, index: number) => ({
193
- length: size.pageWidth,
194
- offset: size.pageWidth * index,
191
+ length: size.width,
192
+ offset: size.width * index,
195
193
  index,
196
194
  }),
197
- [size.pageWidth],
195
+ [size.width],
198
196
  );
199
197
 
200
198
  const renderItem = useCallback(
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
@@ -11,24 +11,26 @@ import {
11
11
  import CalendarPro from './CalendarPro';
12
12
  import { HeaderPanel, SwitchReturnSelection } from './CalendarHeader';
13
13
  import { CalendarProps, CalendarProRef, TabHeaderRef } from './types';
14
- import { ContainerContext, SCALE_PROBE_SIZE } from './ContainerContext';
14
+ import { ContainerContext } from './ContainerContext';
15
15
  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,
@@ -51,8 +53,6 @@ const Calendar: React.FC<CalendarProps> = props => {
51
53
  const [containerWidth, setContainerWidth] = useState<number>(
52
54
  Dimensions.get('window').width,
53
55
  );
54
- // rendered size per style unit, read from a probe of known style width
55
- const [layoutScale, setLayoutScale] = useState(1);
56
56
  const [isDoubleDateMode, setIsDoubleDateMode] = useState<boolean>(
57
57
  mode === DOUBLE,
58
58
  );
@@ -74,41 +74,30 @@ const Calendar: React.FC<CalendarProps> = props => {
74
74
  const tabSelected = useRef<number>(id || 0);
75
75
 
76
76
  const calendarPicker = useRef<CalendarProRef>(null);
77
- // hold refs to TabHeader instances
78
77
  const cellHeader1 = useRef<TabHeaderRef>(null);
79
78
  const cellHeader2 = useRef<TabHeaderRef>(null);
80
79
  const cellHeaderSingle = useRef<TabHeaderRef>(null);
81
80
 
82
- // Initialize view on mount
83
81
  useEffect(() => {
84
82
  tabSelected.current = id || 0;
85
83
  if (mode === DOUBLE) {
86
- if (
87
- cellHeader1.current &&
88
- cellHeader2.current &&
89
- calendarPicker.current
90
- ) {
84
+ const header1 = cellHeader1.current;
85
+ const header2 = cellHeader2.current;
86
+ const picker = calendarPicker.current;
87
+ if (header1 && header2 && picker) {
91
88
  const start = doubleDate.current.first;
92
89
  const end = doubleDate.current.second;
93
- cellHeader1.current.updateView(start, tabSelected.current === 0);
94
- cellHeader2.current.updateView(end, tabSelected.current === 1);
95
- calendarPicker.current.setDoubleDateAndTabIndex(
96
- doubleDate.current.first,
97
- doubleDate.current.second,
98
- tabSelected.current,
99
- );
90
+ header1.updateView(start, tabSelected.current === 0);
91
+ header2.updateView(end, tabSelected.current === 1);
92
+ picker.setDoubleDateAndTabIndex(start, end, tabSelected.current);
100
93
  }
101
94
  } else {
102
95
  calendarPicker.current?.setDoubleDateAndTabIndex(selectedDate);
103
96
  }
104
97
  }, []);
105
98
 
106
- const onLayout = (e: LayoutChangeEvent) =>
99
+ const onLayout = (e: LayoutChangeEvent) => {
107
100
  setContainerWidth(e.nativeEvent.layout.width);
108
-
109
- const handleProbeLayout = (e: LayoutChangeEvent) => {
110
- const ratio = e.nativeEvent.layout.width / SCALE_PROBE_SIZE;
111
- if (ratio > 0) setLayoutScale(ratio);
112
101
  };
113
102
 
114
103
  const onChangeTab = (tabId: number) => {
@@ -127,20 +116,16 @@ const Calendar: React.FC<CalendarProps> = props => {
127
116
  const { onDateChange, onCTAStateChange } = props;
128
117
  if (!cellHeader1.current || !cellHeader2.current || !calendarPicker.current)
129
118
  return;
130
- // set first date and reset second
131
119
  doubleDate.current.first = selectedDate;
132
120
  doubleDate.current.second = null;
133
121
  tabSelected.current = 1;
134
- // update headers
135
122
  cellHeader1.current.updateView(selectedDate, false);
136
123
  cellHeader2.current.updateView(null, true);
137
- // update calendar
138
124
  calendarPicker.current.setDoubleDateAndTabIndex(
139
125
  doubleDate.current.first,
140
126
  doubleDate.current.second,
141
127
  tabSelected.current,
142
128
  );
143
- // notify parent
144
129
  onDateChange?.({ first: doubleDate.current.first!, second: null });
145
130
  onCTAStateChange?.(false);
146
131
  };
@@ -149,7 +134,7 @@ const Calendar: React.FC<CalendarProps> = props => {
149
134
  const { onDateChange, onCTAStateChange } = props;
150
135
  if (!cellHeader1.current || !cellHeader2.current || !calendarPicker.current)
151
136
  return;
152
- // if selectedDate is before first, treat as new first date
137
+ // A date before the first one restarts the range.
153
138
  if (selectedDate.isBefore(doubleDate.current.first!, 'day')) {
154
139
  doubleDate.current.first = selectedDate;
155
140
  doubleDate.current.second = null;
@@ -164,7 +149,6 @@ const Calendar: React.FC<CalendarProps> = props => {
164
149
  onDateChange?.({ first: doubleDate.current.first!, second: null });
165
150
  onCTAStateChange?.(false);
166
151
  } else {
167
- // valid second date
168
152
  doubleDate.current.second = selectedDate;
169
153
  tabSelected.current = 0;
170
154
  cellHeader2.current.updateView(selectedDate, false);
@@ -196,7 +180,7 @@ const Calendar: React.FC<CalendarProps> = props => {
196
180
  }
197
181
  };
198
182
 
199
- // skip running updateView on initial mount to preserve initial doubleDate props
183
+ // Skip the mount run so the initial doubleDate props are kept.
200
184
  const didMount = useRef(false);
201
185
 
202
186
  useEffect(() => {
@@ -259,17 +243,11 @@ const Calendar: React.FC<CalendarProps> = props => {
259
243
  return (
260
244
  <ContainerContext.Provider
261
245
  value={{
262
- width: containerWidth / layoutScale,
246
+ width: containerWidth,
263
247
  height: priceList ? scaledHeight48 : scaledHeight34,
264
- pageWidth: containerWidth,
265
248
  }}
266
249
  >
267
250
  <View onLayout={onLayout} style={[style, styles.scrollView]}>
268
- <View
269
- pointerEvents="none"
270
- style={styles.scaleProbe}
271
- onLayout={handleProbeLayout}
272
- />
273
251
  <View
274
252
  style={[
275
253
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/calendar",
3
- "version": "0.169.4-beta.3",
3
+ "version": "0.169.5",
4
4
  "private": false,
5
5
  "main": "index.tsx",
6
6
  "peerDependencies": {
package/styles.ts CHANGED
@@ -1,9 +1,7 @@
1
- import {StyleSheet} from 'react-native';
2
- import {Colors, Radius, Spacing} from '@momo-kits/foundation';
3
- import {SCALE_PROBE_SIZE} from './ContainerContext';
1
+ import { StyleSheet } from 'react-native';
2
+ import { Colors, Radius, Spacing } from '@momo-kits/foundation';
4
3
 
5
4
  export default StyleSheet.create({
6
- //calendar
7
5
  viewPanel: {
8
6
  flexDirection: 'row',
9
7
  marginHorizontal: Spacing.M,
@@ -31,8 +29,7 @@ export default StyleSheet.create({
31
29
  width: '100%',
32
30
  },
33
31
 
34
- //calendar pro
35
- row: {flexDirection: 'row'},
32
+ row: { flexDirection: 'row' },
36
33
  txtMonthLunar: {
37
34
  marginRight: Spacing.S,
38
35
  },
@@ -70,7 +67,6 @@ export default StyleSheet.create({
70
67
  backgroundColor: Colors.blue_05,
71
68
  },
72
69
 
73
- //headerControl
74
70
  txtHeader: {
75
71
  textAlign: 'center',
76
72
  },
@@ -83,7 +79,6 @@ export default StyleSheet.create({
83
79
  padding: Spacing.S,
84
80
  },
85
81
 
86
- //day
87
82
  dayContainer: {
88
83
  borderRadius: Radius.XS,
89
84
  alignItems: 'center',
@@ -101,7 +96,6 @@ export default StyleSheet.create({
101
96
  borderTopRightRadius: Radius.XS,
102
97
  borderBottomRightRadius: Radius.XS,
103
98
  },
104
- scaleProbe: {position: 'absolute', width: SCALE_PROBE_SIZE, height: 0},
105
99
  invisible: {
106
100
  height: 0,
107
101
  width: 0,