@lumx/react 3.5.4-alpha-optimize-lumx-react-bundle.0 → 3.5.4-alpha-remove-moment.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@juggle/resize-observer": "^3.2.0",
10
- "@lumx/core": "^3.5.4-alpha-optimize-lumx-react-bundle.0",
11
- "@lumx/icons": "^3.5.4-alpha-optimize-lumx-react-bundle.0",
10
+ "@lumx/core": "^3.5.4-alpha-remove-moment.2",
11
+ "@lumx/icons": "^3.5.4-alpha-remove-moment.2",
12
12
  "@popperjs/core": "^2.5.4",
13
13
  "body-scroll-lock": "^3.1.5",
14
14
  "classnames": "^2.3.2",
@@ -79,8 +79,6 @@
79
79
  },
80
80
  "peerDependencies": {
81
81
  "lodash": "4.17.21",
82
- "moment": ">= 2",
83
- "moment-range": "^4.0.2",
84
82
  "react": ">= 16.13.0",
85
83
  "react-dom": ">= 16.13.0"
86
84
  },
@@ -117,5 +115,5 @@
117
115
  "build:storybook": "storybook build"
118
116
  },
119
117
  "sideEffects": false,
120
- "version": "3.5.4-alpha-optimize-lumx-react-bundle.0"
118
+ "version": "3.5.4-alpha-remove-moment.2"
121
119
  }
@@ -0,0 +1,38 @@
1
+ import { DatePicker, GridColumn } from '@lumx/react';
2
+ import { withValueOnChange } from '@lumx/react/stories/decorators/withValueOnChange';
3
+ import { withNestedProps } from '@lumx/react/stories/decorators/withNestedProps';
4
+ import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
5
+ import { withWrapper } from '@lumx/react/stories/decorators/withWrapper';
6
+
7
+ export default {
8
+ title: 'LumX components/date-picker/DatePicker',
9
+ component: DatePicker,
10
+ argTypes: {
11
+ onChange: { action: true },
12
+ },
13
+ decorators: [withValueOnChange(), withNestedProps()],
14
+ };
15
+
16
+ /**
17
+ * Default date picker
18
+ */
19
+ export const Default = {
20
+ args: {
21
+ defaultMonth: new Date('2023-02'),
22
+ 'nextButtonProps.label': 'Next month',
23
+ 'previousButtonProps.label': 'Previous month',
24
+ },
25
+ };
26
+
27
+ /**
28
+ * Demonstrate variations based on the given locale code
29
+ */
30
+ export const LocalesVariations = {
31
+ ...Default,
32
+ decorators: [
33
+ withCombinations({
34
+ combinations: { sections: { key: 'locale', options: ['fr', 'en-US', 'ar', 'zh-HK', 'ar-eg'] } },
35
+ }),
36
+ withWrapper({ maxColumns: 5, itemMinWidth: 300 }, GridColumn),
37
+ ],
38
+ };
@@ -1,6 +1,7 @@
1
- import moment from 'moment';
2
1
  import React, { forwardRef, useState } from 'react';
3
2
  import { Comp } from '@lumx/react/utils/type';
3
+ import { addMonthResetDay } from '@lumx/react/utils/date/addMonthResetDay';
4
+ import { isDateValid } from '@lumx/react/utils/date/isDateValid';
4
5
  import { CLASSNAME, COMPONENT_NAME } from './constants';
5
6
  import { DatePickerControlled } from './DatePickerControlled';
6
7
  import { DatePickerProps } from './types';
@@ -14,17 +15,13 @@ import { DatePickerProps } from './types';
14
15
  */
15
16
  export const DatePicker: Comp<DatePickerProps, HTMLDivElement> = forwardRef((props, ref) => {
16
17
  const { defaultMonth, locale, value, onChange, ...forwardedProps } = props;
17
- let castedValue;
18
- if (value) {
19
- castedValue = moment(value);
20
- } else if (defaultMonth) {
21
- castedValue = moment(defaultMonth);
22
- }
23
- if (castedValue && !castedValue.isValid()) {
18
+
19
+ let referenceDate = value || defaultMonth || new Date();
20
+ if (!isDateValid(referenceDate)) {
24
21
  // eslint-disable-next-line no-console
25
- console.warn(`[@lumx/react/DatePicker] Invalid date provided ${castedValue}`);
22
+ console.warn(`[@lumx/react/DatePicker] Invalid date provided ${referenceDate}`);
23
+ referenceDate = new Date();
26
24
  }
27
- const selectedDay = castedValue && castedValue.isValid() ? castedValue : moment();
28
25
 
29
26
  const [monthOffset, setMonthOffset] = useState(0);
30
27
 
@@ -36,7 +33,7 @@ export const DatePicker: Comp<DatePickerProps, HTMLDivElement> = forwardRef((pro
36
33
  setMonthOffset(0);
37
34
  };
38
35
 
39
- const selectedMonth = moment(selectedDay).locale(locale).add(monthOffset, 'months').toDate();
36
+ const selectedMonth = addMonthResetDay(referenceDate, monthOffset);
40
37
 
41
38
  return (
42
39
  <DatePickerControlled
@@ -1,10 +1,13 @@
1
1
  import React, { forwardRef } from 'react';
2
- import moment from 'moment';
3
2
  import classNames from 'classnames';
4
3
  import { DatePickerProps, Emphasis, IconButton, Toolbar } from '@lumx/react';
5
4
  import { mdiChevronLeft, mdiChevronRight } from '@lumx/icons';
6
- import { getAnnotatedMonthCalendar, getWeekDays } from '@lumx/core/js/date-picker';
7
5
  import { Comp } from '@lumx/react/utils/type';
6
+ import { getMonthCalendar } from '@lumx/react/utils/date/getMonthCalendar';
7
+ import { isSameDay } from '@lumx/react/utils/date/isSameDay';
8
+ import { getCurrentLocale } from '@lumx/react/utils/locale/getCurrentLocale';
9
+ import { parseLocale } from '@lumx/react/utils/locale/parseLocale';
10
+ import { Locale } from '@lumx/react/utils/locale/types';
8
11
  import { CLASSNAME } from './constants';
9
12
 
10
13
  /**
@@ -33,7 +36,7 @@ const COMPONENT_NAME = 'DatePickerControlled';
33
36
  */
34
37
  export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElement> = forwardRef((props, ref) => {
35
38
  const {
36
- locale,
39
+ locale = getCurrentLocale(),
37
40
  maxDate,
38
41
  minDate,
39
42
  nextButtonProps,
@@ -45,14 +48,11 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
45
48
  todayOrSelectedDateRef,
46
49
  value,
47
50
  } = props;
48
- const days = React.useMemo(() => {
49
- return getAnnotatedMonthCalendar(locale, minDate, maxDate, moment(selectedMonth));
51
+ const { weeks, weekDays } = React.useMemo(() => {
52
+ const localeObj = parseLocale(locale) as Locale;
53
+ return getMonthCalendar(localeObj, selectedMonth, minDate, maxDate);
50
54
  }, [locale, minDate, maxDate, selectedMonth]);
51
55
 
52
- const weekDays = React.useMemo(() => {
53
- return getWeekDays(locale);
54
- }, [locale]);
55
-
56
56
  return (
57
57
  <div ref={ref} className={`${CLASSNAME}`}>
58
58
  <Toolbar
@@ -75,49 +75,46 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
75
75
  }
76
76
  label={
77
77
  <span className={`${CLASSNAME}__month`}>
78
- {moment(selectedMonth).locale(locale).format('MMMM YYYY')}
78
+ {selectedMonth.toLocaleDateString(locale, { year: 'numeric', month: 'long' })}
79
79
  </span>
80
80
  }
81
81
  />
82
82
  <div className={`${CLASSNAME}__calendar`}>
83
83
  <div className={`${CLASSNAME}__week-days ${CLASSNAME}__days-wrapper`}>
84
- {weekDays.map((weekDay) => (
85
- <div key={weekDay.unix()} className={`${CLASSNAME}__day-wrapper`}>
86
- <span className={`${CLASSNAME}__week-day`}>
87
- {weekDay.format('dddd').slice(0, 1).toLocaleUpperCase()}
88
- </span>
84
+ {weekDays.map(({ letter, number }) => (
85
+ <div key={number} className={`${CLASSNAME}__day-wrapper`}>
86
+ <span className={`${CLASSNAME}__week-day`}>{letter.toLocaleUpperCase()}</span>
89
87
  </div>
90
88
  ))}
91
89
  </div>
92
90
 
93
91
  <div className={`${CLASSNAME}__month-days ${CLASSNAME}__days-wrapper`}>
94
- {days.map((annotatedDate) => {
95
- if (annotatedDate.isDisplayed) {
92
+ {weeks.flatMap((week, weekIndex) => {
93
+ return weekDays.map((weekDay, dayIndex) => {
94
+ const { date, isOutOfRange } = week[weekDay.number] || {};
95
+ const key = `${weekIndex}-${dayIndex}`;
96
+ const isToday = !isOutOfRange && date && isSameDay(date, new Date());
97
+ const isSelected = date && value && isSameDay(value, date);
98
+
96
99
  return (
97
- <div key={annotatedDate.date.unix()} className={`${CLASSNAME}__day-wrapper`}>
98
- <button
99
- ref={
100
- (value && annotatedDate.date.isSame(value, 'day')) ||
101
- (!value && annotatedDate.isToday)
102
- ? todayOrSelectedDateRef
103
- : null
104
- }
105
- className={classNames(`${CLASSNAME}__month-day`, {
106
- [`${CLASSNAME}__month-day--is-selected`]:
107
- value && annotatedDate.date.isSame(value, 'day'),
108
- [`${CLASSNAME}__month-day--is-today`]:
109
- annotatedDate.isClickable && annotatedDate.isToday,
110
- })}
111
- disabled={!annotatedDate.isClickable}
112
- type="button"
113
- onClick={() => onChange(moment(annotatedDate.date).toDate())}
114
- >
115
- <span>{annotatedDate.date.format('DD')}</span>
116
- </button>
100
+ <div key={key} className={`${CLASSNAME}__day-wrapper`}>
101
+ {date && (
102
+ <button
103
+ ref={isSelected || (!value && isToday) ? todayOrSelectedDateRef : null}
104
+ className={classNames(`${CLASSNAME}__month-day`, {
105
+ [`${CLASSNAME}__month-day--is-selected`]: isSelected,
106
+ [`${CLASSNAME}__month-day--is-today`]: isToday,
107
+ })}
108
+ disabled={isOutOfRange}
109
+ type="button"
110
+ onClick={() => onChange(date)}
111
+ >
112
+ <span>{date.toLocaleDateString(locale, { day: 'numeric' })}</span>
113
+ </button>
114
+ )}
117
115
  </div>
118
116
  );
119
- }
120
- return <div key={annotatedDate.date.unix()} className={`${CLASSNAME}__day-wrapper`} />;
117
+ });
121
118
  })}
122
119
  </div>
123
120
  </div>
@@ -8,7 +8,6 @@ export default {
8
8
  component: DatePickerField,
9
9
  args: {
10
10
  ...DatePickerField.defaultProps,
11
- locale: 'fr',
12
11
  'nextButtonProps.label': 'Next month',
13
12
  'previousButtonProps.label': 'Previous month',
14
13
  },
@@ -1,12 +1,10 @@
1
- import { DatePicker, Placement, Popover, TextField, IconButtonProps } from '@lumx/react';
2
- import { useFocusTrap } from '@lumx/react/hooks/useFocusTrap';
3
-
4
- import moment from 'moment';
5
-
6
1
  import React, { forwardRef, SyntheticEvent, useCallback, useRef, useState } from 'react';
7
2
 
3
+ import { DatePicker, IconButtonProps, Placement, Popover, TextField } from '@lumx/react';
4
+ import { useFocusTrap } from '@lumx/react/hooks/useFocusTrap';
8
5
  import { useFocus } from '@lumx/react/hooks/useFocus';
9
6
  import { Comp, GenericProps } from '@lumx/react/utils/type';
7
+ import { getCurrentLocale } from '@lumx/react/utils/locale/getCurrentLocale';
10
8
 
11
9
  /**
12
10
  * Defines the props of the component.
@@ -17,7 +15,7 @@ export interface DatePickerFieldProps extends GenericProps {
17
15
  /** Whether the component is disabled or not. */
18
16
  isDisabled?: boolean;
19
17
  /** Locale (language or region) to use. */
20
- locale: string;
18
+ locale?: string;
21
19
  /** Date after which dates can't be selected. */
22
20
  maxDate?: Date;
23
21
  /** Date before which dates can't be selected. */
@@ -52,7 +50,7 @@ export const DatePickerField: Comp<DatePickerFieldProps, HTMLDivElement> = forwa
52
50
  defaultMonth,
53
51
  disabled,
54
52
  isDisabled = disabled,
55
- locale,
53
+ locale = getCurrentLocale(),
56
54
  maxDate,
57
55
  minDate,
58
56
  name,
@@ -97,6 +95,9 @@ export const DatePickerField: Comp<DatePickerFieldProps, HTMLDivElement> = forwa
97
95
  onClose();
98
96
  };
99
97
 
98
+ // Format date for text field
99
+ const textFieldValue = value?.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' }) || '';
100
+
100
101
  return (
101
102
  <>
102
103
  <TextField
@@ -105,7 +106,7 @@ export const DatePickerField: Comp<DatePickerFieldProps, HTMLDivElement> = forwa
105
106
  name={name}
106
107
  forceFocusStyle={isOpen}
107
108
  textFieldRef={anchorRef}
108
- value={value ? moment(value).locale(locale).format('LL') : ''}
109
+ value={textFieldValue}
109
110
  onClick={toggleSimpleMenu}
110
111
  onChange={onTextFieldChange}
111
112
  onKeyPress={handleKeyboardNav}
@@ -9,7 +9,7 @@ export interface DatePickerProps extends GenericProps {
9
9
  /** Default month. */
10
10
  defaultMonth?: Date;
11
11
  /** Locale (language or region) to use. */
12
- locale: string;
12
+ locale?: string;
13
13
  /** Date after which dates can't be selected. */
14
14
  maxDate?: Date;
15
15
  /** Date before which dates can't be selected. */
@@ -0,0 +1,13 @@
1
+ import { addMonthResetDay } from '@lumx/react/utils/date/addMonthResetDay';
2
+
3
+ describe(addMonthResetDay.name, () => {
4
+ it('should add month to date', () => {
5
+ const actual = addMonthResetDay(new Date('2017-01-30'), 1);
6
+ expect(actual).toEqual(new Date('2017-02-01'));
7
+ });
8
+
9
+ it('should remove months to date', () => {
10
+ const actual = addMonthResetDay(new Date('2017-01-30'), -2);
11
+ expect(actual).toEqual(new Date('2016-11-01'));
12
+ });
13
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Add a number of months from a date while resetting the day to prevent month length mismatches.
3
+ */
4
+ export function addMonthResetDay(date: Date, monthOffset: number) {
5
+ const newDate = new Date(date.getTime());
6
+ newDate.setDate(1);
7
+ newDate.setMonth(date.getMonth() + monthOffset);
8
+ return newDate;
9
+ }
@@ -0,0 +1,20 @@
1
+ import { Locale } from '@lumx/react/utils/locale/types';
2
+ import { parseLocale } from '../locale/parseLocale';
3
+ import { getFirstDayOfWeek } from './getFirstDayOfWeek';
4
+
5
+ describe(getFirstDayOfWeek.name, () => {
6
+ it('should return for a valid locales', () => {
7
+ expect(getFirstDayOfWeek(parseLocale('fa-ir') as Locale)).toBe(6);
8
+ expect(getFirstDayOfWeek(parseLocale('ar-ma') as Locale)).toBe(1);
9
+ expect(getFirstDayOfWeek(parseLocale('ar') as Locale)).toBe(6);
10
+ expect(getFirstDayOfWeek(parseLocale('ar-eg') as Locale)).toBe(0);
11
+ });
12
+
13
+ it('should return for the lang locale if available', () => {
14
+ // Test for a specific locale and its root locale
15
+ const localeWithRoot = parseLocale('es-ES') as Locale; // Spanish (Spain) with root locale es
16
+ const expectedFirstDay = getFirstDayOfWeek(parseLocale('es') as Locale); // First day for root locale 'es'
17
+
18
+ expect(getFirstDayOfWeek(localeWithRoot)).toBe(expectedFirstDay);
19
+ });
20
+ });
@@ -0,0 +1,58 @@
1
+ import { Locale } from '@lumx/react/utils/locale/types';
2
+
3
+ /** Get first day of week for locale from the browser API */
4
+ export const getFromBrowser = (locale: Locale): number | undefined => {
5
+ try {
6
+ const localeMetadata = new Intl.Locale(locale.code) as any;
7
+ const { firstDay } = localeMetadata.getWeekInfo?.() || localeMetadata.weekInfo;
8
+ // Sunday is represented as `0` in Date.getDay()
9
+ if (firstDay === 7) return 0;
10
+ return firstDay;
11
+ } catch (e) {
12
+ return undefined;
13
+ }
14
+ };
15
+
16
+ /** List first day for each locale (could be removed when all browser implement Locale weekInfo) */
17
+ const FIRST_DAY_FOR_LOCALES = [
18
+ {
19
+ // Locales with Sunday as the first day of the week
20
+ localeRX: /^(af|ar-(dz|eg|sa)|bn|cy|en-(ca|us|za)|fr-ca|gd|he|hi|ja|km|ko|pt-br|te|th|ug|zh-hk)$/i,
21
+ firstDay: 0,
22
+ },
23
+ {
24
+ // Locales with Monday as the first day of the week
25
+ localeRX: /^(ar-(ma|tn)|az|be|bg|bs|ca|cs|da|de|el|en-(au|gb|ie|in|nz)|eo|es|et|eu|fi|fr|fy|gl|gu|hr|ht|hu|hy|id|is|it|ka|kk|kn|lb|lt|lv|mk|mn|ms|mt|nb|nl|nn|oc|pl|pt|ro|ru|sk|sl|sq|sr|sv|ta|tr|uk|uz|vi|zh-(cn|tw))$/i,
26
+ firstDay: 1,
27
+ },
28
+ {
29
+ // Locales with Saturday as the first day of the week
30
+ localeRX: /^(ar|fa-ir)$/i,
31
+ firstDay: 6,
32
+ },
33
+ ];
34
+
35
+ /** Find first day of week for locale from the constant */
36
+ const getFromConstant = (locale: Locale, searchBy: keyof Locale = 'code'): number | undefined => {
37
+ // Search for locale (lang + region)
38
+ for (const { localeRX, firstDay } of FIRST_DAY_FOR_LOCALES) {
39
+ if (localeRX.test(locale[searchBy] as string)) return firstDay;
40
+ }
41
+ // Fallback search for locale lang
42
+ if (locale.code !== locale.language) {
43
+ return getFromConstant(locale, 'language');
44
+ }
45
+ return undefined;
46
+ };
47
+
48
+ /**
49
+ * Get first day of the week for the given locale code (language + region).
50
+ */
51
+ export const getFirstDayOfWeek = (locale: Locale): number | undefined => {
52
+ // Get from browser API
53
+ const firstDay = getFromBrowser(locale);
54
+ if (firstDay !== undefined) return firstDay;
55
+
56
+ // Get from constant
57
+ return getFromConstant(locale);
58
+ };
@@ -0,0 +1,123 @@
1
+ import { parseLocale } from '@lumx/react/utils/locale/parseLocale';
2
+ import { Locale } from '@lumx/react/utils/locale/types';
3
+ import { getMonthCalendar } from './getMonthCalendar';
4
+
5
+ describe(getMonthCalendar.name, () => {
6
+ it('should generate calendar', () => {
7
+ const referenceDate = new Date('2017-02-03');
8
+ const french = parseLocale('fr') as Locale;
9
+ const month = getMonthCalendar(french, referenceDate);
10
+
11
+ expect(month).toEqual({
12
+ weekDays: [
13
+ { letter: 'L', number: 1 },
14
+ { letter: 'M', number: 2 },
15
+ { letter: 'M', number: 3 },
16
+ { letter: 'J', number: 4 },
17
+ { letter: 'V', number: 5 },
18
+ { letter: 'S', number: 6 },
19
+ { letter: 'D', number: 0 },
20
+ ],
21
+ weeks: [
22
+ {
23
+ '3': { date: new Date('2017-02-01') },
24
+ '4': { date: new Date('2017-02-02') },
25
+ '5': { date: new Date('2017-02-03') },
26
+ '6': { date: new Date('2017-02-04') },
27
+ '0': { date: new Date('2017-02-05') },
28
+ },
29
+ {
30
+ '1': { date: new Date('2017-02-06') },
31
+ '2': { date: new Date('2017-02-07') },
32
+ '3': { date: new Date('2017-02-08') },
33
+ '4': { date: new Date('2017-02-09') },
34
+ '5': { date: new Date('2017-02-10') },
35
+ '6': { date: new Date('2017-02-11') },
36
+ '0': { date: new Date('2017-02-12') },
37
+ },
38
+ {
39
+ '1': { date: new Date('2017-02-13') },
40
+ '2': { date: new Date('2017-02-14') },
41
+ '3': { date: new Date('2017-02-15') },
42
+ '4': { date: new Date('2017-02-16') },
43
+ '5': { date: new Date('2017-02-17') },
44
+ '6': { date: new Date('2017-02-18') },
45
+ '0': { date: new Date('2017-02-19') },
46
+ },
47
+ {
48
+ '1': { date: new Date('2017-02-20') },
49
+ '2': { date: new Date('2017-02-21') },
50
+ '3': { date: new Date('2017-02-22') },
51
+ '4': { date: new Date('2017-02-23') },
52
+ '5': { date: new Date('2017-02-24') },
53
+ '6': { date: new Date('2017-02-25') },
54
+ '0': { date: new Date('2017-02-26') },
55
+ },
56
+ {
57
+ '1': { date: new Date('2017-02-27') },
58
+ '2': { date: new Date('2017-02-28') },
59
+ },
60
+ ],
61
+ });
62
+ });
63
+
64
+ it('should generate calendar with sunday as start of week and mark dates in range', () => {
65
+ const referenceDate = new Date('2017-02-03');
66
+ const minDate = new Date('2017-02-06');
67
+ const maxDate = new Date('2017-02-10');
68
+ const englishUS = parseLocale('en-US') as Locale;
69
+ const month = getMonthCalendar(englishUS, referenceDate, minDate, maxDate);
70
+
71
+ expect(month).toEqual({
72
+ weekDays: [
73
+ { letter: 'S', number: 0 },
74
+ { letter: 'M', number: 1 },
75
+ { letter: 'T', number: 2 },
76
+ { letter: 'W', number: 3 },
77
+ { letter: 'T', number: 4 },
78
+ { letter: 'F', number: 5 },
79
+ { letter: 'S', number: 6 },
80
+ ],
81
+ weeks: [
82
+ {
83
+ '3': { date: new Date('2017-02-01'), isOutOfRange: true },
84
+ '4': { date: new Date('2017-02-02'), isOutOfRange: true },
85
+ '5': { date: new Date('2017-02-03'), isOutOfRange: true },
86
+ '6': { date: new Date('2017-02-04'), isOutOfRange: true },
87
+ },
88
+ {
89
+ '0': { date: new Date('2017-02-05'), isOutOfRange: true },
90
+ '1': { date: new Date('2017-02-06'), isOutOfRange: true },
91
+ '2': { date: new Date('2017-02-07') },
92
+ '3': { date: new Date('2017-02-08') },
93
+ '4': { date: new Date('2017-02-09') },
94
+ '5': { date: new Date('2017-02-10'), isOutOfRange: true },
95
+ '6': { date: new Date('2017-02-11'), isOutOfRange: true },
96
+ },
97
+ {
98
+ '0': { date: new Date('2017-02-12'), isOutOfRange: true },
99
+ '1': { date: new Date('2017-02-13'), isOutOfRange: true },
100
+ '2': { date: new Date('2017-02-14'), isOutOfRange: true },
101
+ '3': { date: new Date('2017-02-15'), isOutOfRange: true },
102
+ '4': { date: new Date('2017-02-16'), isOutOfRange: true },
103
+ '5': { date: new Date('2017-02-17'), isOutOfRange: true },
104
+ '6': { date: new Date('2017-02-18'), isOutOfRange: true },
105
+ },
106
+ {
107
+ '0': { date: new Date('2017-02-19'), isOutOfRange: true },
108
+ '1': { date: new Date('2017-02-20'), isOutOfRange: true },
109
+ '2': { date: new Date('2017-02-21'), isOutOfRange: true },
110
+ '3': { date: new Date('2017-02-22'), isOutOfRange: true },
111
+ '4': { date: new Date('2017-02-23'), isOutOfRange: true },
112
+ '5': { date: new Date('2017-02-24'), isOutOfRange: true },
113
+ '6': { date: new Date('2017-02-25'), isOutOfRange: true },
114
+ },
115
+ {
116
+ '0': { date: new Date('2017-02-26'), isOutOfRange: true },
117
+ '1': { date: new Date('2017-02-27'), isOutOfRange: true },
118
+ '2': { date: new Date('2017-02-28'), isOutOfRange: true },
119
+ },
120
+ ],
121
+ });
122
+ });
123
+ });
@@ -0,0 +1,52 @@
1
+ import last from 'lodash/last';
2
+
3
+ import { getWeekDays, WeekDayInfo } from '@lumx/react/utils/date/getWeekDays';
4
+ import { Locale } from '@lumx/react/utils/locale/types';
5
+
6
+ type AnnotatedDay = { date: Date; isOutOfRange?: boolean };
7
+ type AnnotatedWeek = Partial<Record<number, AnnotatedDay>>;
8
+
9
+ interface MonthCalendar {
10
+ weekDays: Array<WeekDayInfo>;
11
+ weeks: Array<AnnotatedWeek>;
12
+ }
13
+
14
+ /**
15
+ * Get month calendar.
16
+ * A list of weeks with days indexed by week day number
17
+ */
18
+ export const getMonthCalendar = (
19
+ locale: Locale,
20
+ referenceDate = new Date(),
21
+ rangeMinDate?: Date,
22
+ rangeMaxDate?: Date,
23
+ ): MonthCalendar => {
24
+ const month = referenceDate.getMonth();
25
+ const iterDate = new Date(referenceDate.getTime());
26
+ iterDate.setDate(1);
27
+
28
+ const weekDays = getWeekDays(locale);
29
+ const lastDayOfWeek = last(weekDays) as WeekDayInfo;
30
+
31
+ const weeks: Array<AnnotatedWeek> = [];
32
+ let week: AnnotatedWeek = {};
33
+ while (iterDate.getMonth() === month) {
34
+ const weekDayNumber = iterDate.getDay();
35
+ const day: AnnotatedDay = { date: new Date(iterDate.getTime()) };
36
+
37
+ // If a range is specified, check if the day is out of range.
38
+ if ((rangeMinDate && iterDate <= rangeMinDate) || (rangeMaxDate && iterDate >= rangeMaxDate)) {
39
+ day.isOutOfRange = true;
40
+ }
41
+
42
+ week[weekDayNumber] = day;
43
+ if (weekDayNumber === lastDayOfWeek.number) {
44
+ weeks.push(week);
45
+ week = {};
46
+ }
47
+ iterDate.setDate(iterDate.getDate() + 1);
48
+ }
49
+ if (Object.keys(week).length) weeks.push(week);
50
+
51
+ return { weeks, weekDays };
52
+ };
@@ -0,0 +1,48 @@
1
+ import { parseLocale } from '@lumx/react/utils/locale/parseLocale';
2
+ import { Locale } from '@lumx/react/utils/locale/types';
3
+ import { getWeekDays } from './getWeekDays';
4
+
5
+ describe(getWeekDays.name, () => {
6
+ const french = parseLocale('fr') as Locale;
7
+ const englishUS = parseLocale('en-us') as Locale;
8
+ const farsi = parseLocale('fa-ir') as Locale;
9
+
10
+ it('should list french week days', () => {
11
+ const weekDays = getWeekDays(french);
12
+ expect(weekDays).toEqual([
13
+ { letter: 'L', number: 1 },
14
+ { letter: 'M', number: 2 },
15
+ { letter: 'M', number: 3 },
16
+ { letter: 'J', number: 4 },
17
+ { letter: 'V', number: 5 },
18
+ { letter: 'S', number: 6 },
19
+ { letter: 'D', number: 0 },
20
+ ]);
21
+ });
22
+
23
+ it('should list US week days', () => {
24
+ const weekDays = getWeekDays(englishUS);
25
+ expect(weekDays).toEqual([
26
+ { letter: 'S', number: 0 },
27
+ { letter: 'M', number: 1 },
28
+ { letter: 'T', number: 2 },
29
+ { letter: 'W', number: 3 },
30
+ { letter: 'T', number: 4 },
31
+ { letter: 'F', number: 5 },
32
+ { letter: 'S', number: 6 },
33
+ ]);
34
+ });
35
+
36
+ it('should list fa-ir week days', () => {
37
+ const weekDays = getWeekDays(farsi);
38
+ expect(weekDays).toEqual([
39
+ { letter: 'ش', number: 6 },
40
+ { letter: 'ی', number: 0 },
41
+ { letter: 'د', number: 1 },
42
+ { letter: 'س', number: 2 },
43
+ { letter: 'چ', number: 3 },
44
+ { letter: 'پ', number: 4 },
45
+ { letter: 'ج', number: 5 },
46
+ ]);
47
+ });
48
+ });