Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(date-picker): replace moment with vanilla JS #1013

Merged
merged 4 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- `@lumx/react` no long depend on `moment` or `moment-range` to generate the date picker.
- Deprecated `@lumx/core/js/date-picker` functions that **will be removed in the next major version** along with `moment` and `moment-range`.
- DatePicker & DatePickerField: `locale` prop is now optional (uses browser locale by default)

## [3.5.3][] - 2023-08-30

### Changed
Expand Down Expand Up @@ -1807,8 +1813,6 @@ _Failed released_
[3.5.0]: https://github.com/lumapps/design-system/tree/v3.5.0
[unreleased]: https://github.com/lumapps/design-system/compare/v3.5.1...HEAD
[3.5.1]: https://github.com/lumapps/design-system/tree/v3.5.1


[Unreleased]: https://github.com/lumapps/design-system/compare/v3.5.3...HEAD
[unreleased]: https://github.com/lumapps/design-system/compare/v3.5.3...HEAD
[3.5.3]: https://github.com/lumapps/design-system/compare/v3.5.2...v3.5.3
[3.5.2]: https://github.com/lumapps/design-system/tree/v3.5.2
6 changes: 6 additions & 0 deletions packages/lumx-core/src/js/date-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ interface AnnotatedDate {
/**
* Get the list of days in a week based on locale.
*
* @deprecated will be removed in next major version along with the removal of moment (no replacement planned)
*
* @param locale The locale using to generate the order of days in a week.
* @return The list of days in a week based on locale.
*/
Expand All @@ -26,6 +28,8 @@ export function getWeekDays(locale: string): Moment[] {
/**
* Get month calendar based on locale and start date.
*
* @deprecated will be removed in next major version along with the removal of moment (no replacement planned)
*
* @param locale The locale using to generate the order of days in a week.
* @param selectedMonth The selected month.
* @return The list of days in a week based on locale.
Expand All @@ -44,6 +48,8 @@ export function getMonthCalendar(locale: string, selectedMonth?: Moment): Moment
* Get month calendar based on locale and start date.
* Each day is annotated to know if they are displayed and/or clickable.
*
* @deprecated will be removed in next major version along with the removal of moment (no replacement planned)
*
* @param locale The locale using to generate the order of days in a week.
* @param minDate The first selectable date.
* @param maxDate The last selectable date.
Expand Down
5 changes: 0 additions & 5 deletions packages/lumx-react/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ import type { Preview } from '@storybook/react';
import { withStoryBlockDecorator } from './story-block/decorator';
import { Theme } from '@lumx/react';

/**
* Import non default language to test moment local change.
*/
import 'moment/dist/locale/fr';

const preview: Preview = {
globalTypes: {
/** Add Theme switcher in the toolbar */
Expand Down
2 changes: 0 additions & 2 deletions packages/lumx-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@
},
"peerDependencies": {
"lodash": "4.17.21",
"moment": ">= 2",
"moment-range": "^4.0.2",
"react": ">= 16.13.0",
"react-dom": ">= 16.13.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { DatePicker, GridColumn } from '@lumx/react';
import { withValueOnChange } from '@lumx/react/stories/decorators/withValueOnChange';
import { withNestedProps } from '@lumx/react/stories/decorators/withNestedProps';
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
import { withWrapper } from '@lumx/react/stories/decorators/withWrapper';

export default {
title: 'LumX components/date-picker/DatePicker',
component: DatePicker,
argTypes: {
onChange: { action: true },
},
decorators: [withValueOnChange(), withNestedProps()],
};

/**
* Default date picker
*/
export const Default = {
args: {
defaultMonth: new Date('2023-02'),
'nextButtonProps.label': 'Next month',
'previousButtonProps.label': 'Previous month',
},
};

/**
* Demonstrate variations based on the given locale code
*/
export const LocalesVariations = {
...Default,
decorators: [
withCombinations({
combinations: { sections: { key: 'locale', options: ['fr', 'en-US', 'ar', 'zh-HK', 'ar-eg'] } },
}),
withWrapper({ maxColumns: 5, itemMinWidth: 300 }, GridColumn),
],
};
19 changes: 8 additions & 11 deletions packages/lumx-react/src/components/date-picker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import moment from 'moment';
import React, { forwardRef, useState } from 'react';
import { Comp } from '@lumx/react/utils/type';
import { addMonthResetDay } from '@lumx/react/utils/date/addMonthResetDay';
import { isDateValid } from '@lumx/react/utils/date/isDateValid';
import { CLASSNAME, COMPONENT_NAME } from './constants';
import { DatePickerControlled } from './DatePickerControlled';
import { DatePickerProps } from './types';
Expand All @@ -14,17 +15,13 @@ import { DatePickerProps } from './types';
*/
export const DatePicker: Comp<DatePickerProps, HTMLDivElement> = forwardRef((props, ref) => {
const { defaultMonth, locale, value, onChange, ...forwardedProps } = props;
let castedValue;
if (value) {
castedValue = moment(value);
} else if (defaultMonth) {
castedValue = moment(defaultMonth);
}
if (castedValue && !castedValue.isValid()) {

let referenceDate = value || defaultMonth || new Date();
if (!isDateValid(referenceDate)) {
// eslint-disable-next-line no-console
console.warn(`[@lumx/react/DatePicker] Invalid date provided ${castedValue}`);
console.warn(`[@lumx/react/DatePicker] Invalid date provided ${referenceDate}`);
referenceDate = new Date();
}
const selectedDay = castedValue && castedValue.isValid() ? castedValue : moment();

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

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

const selectedMonth = moment(selectedDay).locale(locale).add(monthOffset, 'months').toDate();
const selectedMonth = addMonthResetDay(referenceDate, monthOffset);

return (
<DatePickerControlled
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, { forwardRef } from 'react';
import moment from 'moment';
import classNames from 'classnames';
import { DatePickerProps, Emphasis, IconButton, Toolbar } from '@lumx/react';
import { mdiChevronLeft, mdiChevronRight } from '@lumx/icons';
import { getAnnotatedMonthCalendar, getWeekDays } from '@lumx/core/js/date-picker';
import { Comp } from '@lumx/react/utils/type';
import { getMonthCalendar } from '@lumx/react/utils/date/getMonthCalendar';
import { isSameDay } from '@lumx/react/utils/date/isSameDay';
import { getCurrentLocale } from '@lumx/react/utils/locale/getCurrentLocale';
import { parseLocale } from '@lumx/react/utils/locale/parseLocale';
import { Locale } from '@lumx/react/utils/locale/types';
import { CLASSNAME } from './constants';

/**
Expand Down Expand Up @@ -33,7 +36,7 @@ const COMPONENT_NAME = 'DatePickerControlled';
*/
export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElement> = forwardRef((props, ref) => {
const {
locale,
locale = getCurrentLocale(),
maxDate,
minDate,
nextButtonProps,
Expand All @@ -45,14 +48,11 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
todayOrSelectedDateRef,
value,
} = props;
const days = React.useMemo(() => {
return getAnnotatedMonthCalendar(locale, minDate, maxDate, moment(selectedMonth));
const { weeks, weekDays } = React.useMemo(() => {
const localeObj = parseLocale(locale) as Locale;
return getMonthCalendar(localeObj, selectedMonth, minDate, maxDate);
}, [locale, minDate, maxDate, selectedMonth]);

const weekDays = React.useMemo(() => {
return getWeekDays(locale);
}, [locale]);

return (
<div ref={ref} className={`${CLASSNAME}`}>
<Toolbar
Expand All @@ -75,49 +75,46 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
}
label={
<span className={`${CLASSNAME}__month`}>
{moment(selectedMonth).locale(locale).format('MMMM YYYY')}
{selectedMonth.toLocaleDateString(locale, { year: 'numeric', month: 'long' })}
</span>
}
/>
<div className={`${CLASSNAME}__calendar`}>
<div className={`${CLASSNAME}__week-days ${CLASSNAME}__days-wrapper`}>
{weekDays.map((weekDay) => (
<div key={weekDay.unix()} className={`${CLASSNAME}__day-wrapper`}>
<span className={`${CLASSNAME}__week-day`}>
{weekDay.format('dddd').slice(0, 1).toLocaleUpperCase()}
</span>
{weekDays.map(({ letter, number }) => (
<div key={number} className={`${CLASSNAME}__day-wrapper`}>
<span className={`${CLASSNAME}__week-day`}>{letter.toLocaleUpperCase()}</span>
</div>
))}
</div>

<div className={`${CLASSNAME}__month-days ${CLASSNAME}__days-wrapper`}>
{days.map((annotatedDate) => {
if (annotatedDate.isDisplayed) {
{weeks.flatMap((week, weekIndex) => {
return weekDays.map((weekDay, dayIndex) => {
const { date, isOutOfRange } = week[weekDay.number] || {};
const key = `${weekIndex}-${dayIndex}`;
const isToday = !isOutOfRange && date && isSameDay(date, new Date());
const isSelected = date && value && isSameDay(value, date);

return (
<div key={annotatedDate.date.unix()} className={`${CLASSNAME}__day-wrapper`}>
<button
ref={
(value && annotatedDate.date.isSame(value, 'day')) ||
(!value && annotatedDate.isToday)
? todayOrSelectedDateRef
: null
}
className={classNames(`${CLASSNAME}__month-day`, {
[`${CLASSNAME}__month-day--is-selected`]:
value && annotatedDate.date.isSame(value, 'day'),
[`${CLASSNAME}__month-day--is-today`]:
annotatedDate.isClickable && annotatedDate.isToday,
})}
disabled={!annotatedDate.isClickable}
type="button"
onClick={() => onChange(moment(annotatedDate.date).toDate())}
>
<span>{annotatedDate.date.format('DD')}</span>
</button>
<div key={key} className={`${CLASSNAME}__day-wrapper`}>
{date && (
<button
ref={isSelected || (!value && isToday) ? todayOrSelectedDateRef : null}
className={classNames(`${CLASSNAME}__month-day`, {
[`${CLASSNAME}__month-day--is-selected`]: isSelected,
[`${CLASSNAME}__month-day--is-today`]: isToday,
})}
disabled={isOutOfRange}
type="button"
onClick={() => onChange(date)}
>
<span>{date.toLocaleDateString(locale, { day: 'numeric' })}</span>
</button>
)}
</div>
);
}
return <div key={annotatedDate.date.unix()} className={`${CLASSNAME}__day-wrapper`} />;
});
})}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export default {
component: DatePickerField,
args: {
...DatePickerField.defaultProps,
locale: 'fr',
'nextButtonProps.label': 'Next month',
'previousButtonProps.label': 'Previous month',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { DatePicker, Placement, Popover, TextField, IconButtonProps } from '@lumx/react';
import { useFocusTrap } from '@lumx/react/hooks/useFocusTrap';

import moment from 'moment';

import React, { forwardRef, SyntheticEvent, useCallback, useRef, useState } from 'react';

import { DatePicker, IconButtonProps, Placement, Popover, TextField } from '@lumx/react';
import { useFocusTrap } from '@lumx/react/hooks/useFocusTrap';
import { useFocus } from '@lumx/react/hooks/useFocus';
import { Comp, GenericProps } from '@lumx/react/utils/type';
import { getCurrentLocale } from '@lumx/react/utils/locale/getCurrentLocale';

/**
* Defines the props of the component.
Expand All @@ -17,7 +15,7 @@ export interface DatePickerFieldProps extends GenericProps {
/** Whether the component is disabled or not. */
isDisabled?: boolean;
/** Locale (language or region) to use. */
locale: string;
locale?: string;
/** Date after which dates can't be selected. */
maxDate?: Date;
/** Date before which dates can't be selected. */
Expand Down Expand Up @@ -52,7 +50,7 @@ export const DatePickerField: Comp<DatePickerFieldProps, HTMLDivElement> = forwa
defaultMonth,
disabled,
isDisabled = disabled,
locale,
locale = getCurrentLocale(),
maxDate,
minDate,
name,
Expand Down Expand Up @@ -97,6 +95,9 @@ export const DatePickerField: Comp<DatePickerFieldProps, HTMLDivElement> = forwa
onClose();
};

// Format date for text field
const textFieldValue = value?.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' }) || '';

return (
<>
<TextField
Expand All @@ -105,7 +106,7 @@ export const DatePickerField: Comp<DatePickerFieldProps, HTMLDivElement> = forwa
name={name}
forceFocusStyle={isOpen}
textFieldRef={anchorRef}
value={value ? moment(value).locale(locale).format('LL') : ''}
value={textFieldValue}
onClick={toggleSimpleMenu}
onChange={onTextFieldChange}
onKeyPress={handleKeyboardNav}
Expand Down
2 changes: 1 addition & 1 deletion packages/lumx-react/src/components/date-picker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface DatePickerProps extends GenericProps {
/** Default month. */
defaultMonth?: Date;
/** Locale (language or region) to use. */
locale: string;
locale?: string;
/** Date after which dates can't be selected. */
maxDate?: Date;
/** Date before which dates can't be selected. */
Expand Down
13 changes: 13 additions & 0 deletions packages/lumx-react/src/utils/date/addMonthResetDay.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { addMonthResetDay } from '@lumx/react/utils/date/addMonthResetDay';

describe(addMonthResetDay.name, () => {
it('should add month to date', () => {
const actual = addMonthResetDay(new Date('2017-01-30'), 1);
expect(actual).toEqual(new Date('2017-02-01'));
});

it('should remove months to date', () => {
const actual = addMonthResetDay(new Date('2017-01-30'), -2);
expect(actual).toEqual(new Date('2016-11-01'));
});
});
9 changes: 9 additions & 0 deletions packages/lumx-react/src/utils/date/addMonthResetDay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Add a number of months from a date while resetting the day to prevent month length mismatches.
*/
export function addMonthResetDay(date: Date, monthOffset: number) {
const newDate = new Date(date.getTime());
newDate.setDate(1);
newDate.setMonth(date.getMonth() + monthOffset);
return newDate;
}
20 changes: 20 additions & 0 deletions packages/lumx-react/src/utils/date/getFirstDayOfWeek.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Locale } from '@lumx/react/utils/locale/types';
import { parseLocale } from '../locale/parseLocale';
import { getFirstDayOfWeek } from './getFirstDayOfWeek';

describe(getFirstDayOfWeek.name, () => {
it('should return for a valid locales', () => {
expect(getFirstDayOfWeek(parseLocale('fa-ir') as Locale)).toBe(6);
expect(getFirstDayOfWeek(parseLocale('ar-ma') as Locale)).toBe(1);
expect(getFirstDayOfWeek(parseLocale('ar') as Locale)).toBe(6);
expect(getFirstDayOfWeek(parseLocale('ar-eg') as Locale)).toBe(0);
});

it('should return for the lang locale if available', () => {
// Test for a specific locale and its root locale
const localeWithRoot = parseLocale('es-ES') as Locale; // Spanish (Spain) with root locale es
const expectedFirstDay = getFirstDayOfWeek(parseLocale('es') as Locale); // First day for root locale 'es'

expect(getFirstDayOfWeek(localeWithRoot)).toBe(expectedFirstDay);
});
});
Loading
Loading