Skip to content

Commit 349a716

Browse files
committed
feat(date-picker): replace moment with vanilla JS
1 parent 52d4732 commit 349a716

24 files changed

+561
-67
lines changed

CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- `@lumx/react` no long depend on `moment` or `moment-range` to generate the date picker.
13+
1014
## [3.5.3][] - 2023-08-30
1115

1216
### Changed
@@ -1807,8 +1811,6 @@ _Failed released_
18071811
[3.5.0]: https://github.com/lumapps/design-system/tree/v3.5.0
18081812
[unreleased]: https://github.com/lumapps/design-system/compare/v3.5.1...HEAD
18091813
[3.5.1]: https://github.com/lumapps/design-system/tree/v3.5.1
1810-
1811-
1812-
[Unreleased]: https://github.com/lumapps/design-system/compare/v3.5.3...HEAD
1814+
[unreleased]: https://github.com/lumapps/design-system/compare/v3.5.3...HEAD
18131815
[3.5.3]: https://github.com/lumapps/design-system/compare/v3.5.2...v3.5.3
18141816
[3.5.2]: https://github.com/lumapps/design-system/tree/v3.5.2

packages/lumx-react/.storybook/preview.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import type { Preview } from '@storybook/react';
22
import { withStoryBlockDecorator } from './story-block/decorator';
33
import { Theme } from '@lumx/react';
44

5-
/**
6-
* Import non default language to test moment local change.
7-
*/
8-
import 'moment/dist/locale/fr';
9-
105
const preview: Preview = {
116
globalTypes: {
127
/** Add Theme switcher in the toolbar */

packages/lumx-react/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@
7979
},
8080
"peerDependencies": {
8181
"lodash": "4.17.21",
82-
"moment": ">= 2",
83-
"moment-range": "^4.0.2",
8482
"react": ">= 16.13.0",
8583
"react-dom": ">= 16.13.0"
8684
},
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};

packages/lumx-react/src/components/date-picker/DatePicker.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import moment from 'moment';
21
import React, { forwardRef, useState } from 'react';
32
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';
45
import { CLASSNAME, COMPONENT_NAME } from './constants';
56
import { DatePickerControlled } from './DatePickerControlled';
67
import { DatePickerProps } from './types';
@@ -14,17 +15,13 @@ import { DatePickerProps } from './types';
1415
*/
1516
export const DatePicker: Comp<DatePickerProps, HTMLDivElement> = forwardRef((props, ref) => {
1617
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)) {
2421
// 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();
2624
}
27-
const selectedDay = castedValue && castedValue.isValid() ? castedValue : moment();
2825

2926
const [monthOffset, setMonthOffset] = useState(0);
3027

@@ -36,7 +33,7 @@ export const DatePicker: Comp<DatePickerProps, HTMLDivElement> = forwardRef((pro
3633
setMonthOffset(0);
3734
};
3835

39-
const selectedMonth = moment(selectedDay).locale(locale).add(monthOffset, 'months').toDate();
36+
const selectedMonth = addMonthResetDay(referenceDate, monthOffset);
4037

4138
return (
4239
<DatePickerControlled

packages/lumx-react/src/components/date-picker/DatePickerControlled.tsx

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React, { forwardRef } from 'react';
2-
import moment from 'moment';
32
import classNames from 'classnames';
43
import { DatePickerProps, Emphasis, IconButton, Toolbar } from '@lumx/react';
54
import { mdiChevronLeft, mdiChevronRight } from '@lumx/icons';
6-
import { getAnnotatedMonthCalendar, getWeekDays } from '@lumx/core/js/date-picker';
75
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 { parseLocale } from '@lumx/react/utils/locale/parseLocale';
9+
import { Locale } from '@lumx/react/utils/locale/types';
810
import { CLASSNAME } from './constants';
911

1012
/**
@@ -45,14 +47,11 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
4547
todayOrSelectedDateRef,
4648
value,
4749
} = props;
48-
const days = React.useMemo(() => {
49-
return getAnnotatedMonthCalendar(locale, minDate, maxDate, moment(selectedMonth));
50+
const { weeks, weekDays } = React.useMemo(() => {
51+
const localeObj = parseLocale(locale) as Locale;
52+
return getMonthCalendar(localeObj, selectedMonth, minDate, maxDate);
5053
}, [locale, minDate, maxDate, selectedMonth]);
5154

52-
const weekDays = React.useMemo(() => {
53-
return getWeekDays(locale);
54-
}, [locale]);
55-
5655
return (
5756
<div ref={ref} className={`${CLASSNAME}`}>
5857
<Toolbar
@@ -75,49 +74,46 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
7574
}
7675
label={
7776
<span className={`${CLASSNAME}__month`}>
78-
{moment(selectedMonth).locale(locale).format('MMMM YYYY')}
77+
{selectedMonth.toLocaleDateString(locale, { year: 'numeric', month: 'long' })}
7978
</span>
8079
}
8180
/>
8281
<div className={`${CLASSNAME}__calendar`}>
8382
<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>
83+
{weekDays.map(({ letter, number }) => (
84+
<div key={number} className={`${CLASSNAME}__day-wrapper`}>
85+
<span className={`${CLASSNAME}__week-day`}>{letter.toLocaleUpperCase()}</span>
8986
</div>
9087
))}
9188
</div>
9289

9390
<div className={`${CLASSNAME}__month-days ${CLASSNAME}__days-wrapper`}>
94-
{days.map((annotatedDate) => {
95-
if (annotatedDate.isDisplayed) {
91+
{weeks.flatMap((week, weekIndex) => {
92+
return weekDays.map((weekDay, dayIndex) => {
93+
const { date, isOutOfRange } = week[weekDay.number] || {};
94+
const key = `${weekIndex}-${dayIndex}`;
95+
const isToday = !isOutOfRange && date && isSameDay(date, new Date());
96+
const isSelected = date && value && isSameDay(value, date);
97+
9698
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>
99+
<div key={key} className={`${CLASSNAME}__day-wrapper`}>
100+
{date && (
101+
<button
102+
ref={isSelected || (!value && isToday) ? todayOrSelectedDateRef : null}
103+
className={classNames(`${CLASSNAME}__month-day`, {
104+
[`${CLASSNAME}__month-day--is-selected`]: isSelected,
105+
[`${CLASSNAME}__month-day--is-today`]: isToday,
106+
})}
107+
disabled={isOutOfRange}
108+
type="button"
109+
onClick={() => onChange(date)}
110+
>
111+
<span>{date.toLocaleDateString(locale, { day: 'numeric' })}</span>
112+
</button>
113+
)}
117114
</div>
118115
);
119-
}
120-
return <div key={annotatedDate.date.unix()} className={`${CLASSNAME}__day-wrapper`} />;
116+
});
121117
})}
122118
</div>
123119
</div>

packages/lumx-react/src/components/date-picker/DatePickerField.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
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-
61
import React, { forwardRef, SyntheticEvent, useCallback, useRef, useState } from 'react';
72

3+
import { DatePicker, IconButtonProps, Placement, Popover, TextField } from '@lumx/react';
4+
import { useFocusTrap } from '@lumx/react/hooks/useFocusTrap';
85
import { useFocus } from '@lumx/react/hooks/useFocus';
96
import { Comp, GenericProps } from '@lumx/react/utils/type';
107

@@ -97,6 +94,9 @@ export const DatePickerField: Comp<DatePickerFieldProps, HTMLDivElement> = forwa
9794
onClose();
9895
};
9996

97+
// Format date for text field
98+
const textFieldValue = value?.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' }) || '';
99+
100100
return (
101101
<>
102102
<TextField
@@ -105,7 +105,7 @@ export const DatePickerField: Comp<DatePickerFieldProps, HTMLDivElement> = forwa
105105
name={name}
106106
forceFocusStyle={isOpen}
107107
textFieldRef={anchorRef}
108-
value={value ? moment(value).locale(locale).format('LL') : ''}
108+
value={textFieldValue}
109109
onClick={toggleSimpleMenu}
110110
onChange={onTextFieldChange}
111111
onKeyPress={handleKeyboardNav}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)