From 8e46ec1c4fac263c8d831a2595c280c0a289e817 Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Thu, 23 Feb 2023 16:56:20 +0800 Subject: [PATCH 01/14] Refactored Calendar component to remove dependency of primereact. --- packages/neuron-ui/package.json | 1 - .../src/widgets/Calendar/calendar.module.scss | 90 +++++++++ .../neuron-ui/src/widgets/Calendar/index.tsx | 186 ++++++++++++++++++ .../neuron-ui/src/widgets/Calendar/utils.ts | 115 +++++++++++ .../src/widgets/DatetimePicker/index.tsx | 42 +--- 5 files changed, 395 insertions(+), 39 deletions(-) create mode 100644 packages/neuron-ui/src/widgets/Calendar/calendar.module.scss create mode 100644 packages/neuron-ui/src/widgets/Calendar/index.tsx create mode 100644 packages/neuron-ui/src/widgets/Calendar/utils.ts diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index feba52c688..dd34406457 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -49,7 +49,6 @@ "immer": "9.0.16", "jsqr": "1.4.0", "office-ui-fabric-react": "7.199.6", - "primereact": "8.7.1", "qr.js": "0.0.0", "react": "17.0.2", "react-dom": "17.0.2", diff --git a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss new file mode 100644 index 0000000000..1e4bc1cd7c --- /dev/null +++ b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss @@ -0,0 +1,90 @@ +@import '../../styles/mixin.scss'; + +@mixin button { + @include medium-text; + appearance: none; + cursor: pointer; + font-size: 0.75rem; + line-height: 1rem; + font-weight: 500; + padding: 7px 7px; + border: none; + border-radius: 2px; + margin: 0; + box-sizing: border-box; + border-radius: 2px; + background-color: transparent; + &:hover { + @include semi-bold-text; + background-color: #efefef; + } + &[disabled] { + cursor: none; + opacity: 0.5; + box-shadow: none !important; + pointer-events: none; + &:hover { + background-color: transparent; + } + } +} + +.calendar { + width: 374px; + .calOptions { + width: 100%; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + list-style-type: none; + list-style: none; + padding: 0; + button { + @include button; + width: 100px; + } + } +} + +.calendarHeader { + display: flex; + justify-content: space-between; + border-bottom: 1px solid #eee; + margin-top: 10px; + margin-bottom: 10px; + + .calPrev, .calNext { + @include button; + width: 30px; + } + .calTitle { + button { + @include button; + @include semi-bold-text; + height: 100%; + min-width: 100px; + margin-right: 10px; + } + } +} + +.calendarTable { + width: 100%; + + .calTableHeader { + @include semi-bold-text; + font-size: 13px; + } + + .calDateItem { + @include button; + width: 100%; + &:global(.today) { + border: 1px solid var(--nervos-green-light); + margin: -1px; + } + &:global(.active) { + background-color: var(--nervos-green); + } + } +} \ No newline at end of file diff --git a/packages/neuron-ui/src/widgets/Calendar/index.tsx b/packages/neuron-ui/src/widgets/Calendar/index.tsx new file mode 100644 index 0000000000..96e91013a0 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Calendar/index.tsx @@ -0,0 +1,186 @@ +import React, { useState } from 'react' +import { getMonthCalendar, useLocalNames, monthInRange, yearInRange, dateEqual } from './utils' +import styles from './calendar.module.scss' + +interface Option { + value: number + title: string + selectable: boolean +} +const Selector = ({ options, onChange }: { options: Option[]; onChange: (option: Option) => void }) => ( +
    + {options.map(option => ( +
  1. + +
  2. + ))} +
+) + +export interface CalendarProps { + value: Date | undefined + onChange: (value: Date) => void + minDate?: Date + maxDate?: Date +} +const Calendar: React.FC = ({ value, onChange, minDate = null, maxDate = null }) => { + const today = new Date() + + const [year, setYear] = useState(value === undefined ? today.getFullYear() : value.getFullYear()) + const [month, setMonth] = useState(value === undefined ? today.getMonth() + 1 : value.getMonth() + 1) + const [status, setStatus] = useState<'year' | 'month' | 'date'>('date') + + React.useEffect(() => { + setYear(value === undefined ? today.getFullYear() : value.getFullYear()) + setMonth(value === undefined ? today.getMonth() + 1 : value.getMonth() + 1) + }, [value]) + + const locale = useLocalNames() + const weeknames = [...Array(7).keys()].map(_ => locale.dayNamesMin[(_ + locale.firstDayOfWeek) % 7]) + const monthname = locale.monthNames[month - 1] + + const calendar = getMonthCalendar(year, month, { minDate, maxDate }) + const calendarTable = ( + + + + {weeknames.map(weekname => ( + + ))} + + + + {calendar.map(week => ( + + {week.map(date => ( + + ))} + + ))} + +
+ {weekname} +
+ +
+ ) + + const monthOptions: Option[] = [...Array(12).keys()].map(index => ({ + value: index + 1, + title: locale.monthNames[index], + selectable: monthInRange(year, index, { minDate, maxDate }), + })) + const yearOptions: Option[] = [...Array(12).keys()].map(index => ({ + value: year - 6 + index, + title: `${year - 6 + index}`, + selectable: yearInRange(year - 6 + index, { minDate, maxDate }), + })) + + const prevMonth = () => { + if (month > 1) { + setMonth(m => m - 1) + } else { + setYear(y => y - 1) + setMonth(12) + } + } + const nextMonth = () => { + if (month < 12) { + setMonth(m => m + 1) + } else { + setYear(y => y + 1) + setMonth(1) + } + } + + const calendarHeader = ( +
+ +
+ + +
+ +
+ ) + + const onChangeMonth = (monthOptionItem: Option) => { + setMonth(monthOptionItem.value) + setStatus('date') + } + const onChangeYear = (yearOptionItem: Option) => { + setYear(yearOptionItem.value) + setStatus('month') + } + + return ( +
+ {calendarHeader} + {status === 'date' && calendarTable} + {status === 'year' && } + {status === 'month' && } +
+ ) +} + +export default React.memo(Calendar, (prevProps, nextProps) => { + if (prevProps.value?.toDateString() !== nextProps.value?.toDateString()) { + return false + } + if (prevProps.minDate?.toDateString() !== nextProps.minDate?.toDateString()) { + return false + } + if (prevProps.maxDate?.toDateString() !== nextProps.maxDate?.toDateString()) { + return false + } + if (prevProps.onChange !== nextProps.onChange) { + return false + } + return true +}) diff --git a/packages/neuron-ui/src/widgets/Calendar/utils.ts b/packages/neuron-ui/src/widgets/Calendar/utils.ts new file mode 100644 index 0000000000..d17ae4e743 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Calendar/utils.ts @@ -0,0 +1,115 @@ +import { useTranslation } from 'react-i18next' + +export interface Day { + instance: Date + year: number + month: number + date: number + weekday: number + curMonth: boolean + isToday: boolean + label: string + selectable: boolean +} + +interface DateRange { + minDate: Date | null + maxDate: Date | null +} + +export function dayInRange(date: Date, range: DateRange) { + if (range.minDate !== null) { + range.minDate.setHours(0, 0, 0, 0) + if (date < range.minDate) { + return false + } + } + if (range.maxDate !== null) { + range.maxDate.setHours(0, 0, 0, 0) + if (date > range.maxDate) { + return false + } + } + return true +} + +export function monthInRange(year: number, month: number, range: DateRange) { + return dayInRange(new Date(year, month + 1, 1), range) +} + +export function yearInRange(year: number, range: DateRange) { + return dayInRange(new Date(year + 1, 1, 1), range) +} + +export function dateEqual(a: Date | undefined, b: Date | undefined) { + if (a === undefined || b === undefined) { + return false + } + return a?.toDateString() === b?.toDateString() +} + +/** + * @description Generate monthly calendar 2D table data + */ +export function getMonthCalendar(year: number, month: number, range: DateRange): Day[][] { + const today = new Date() + const weekdayOfFirstDay = new Date(year, month - 1, 1).getDay() + const numOfDaysInCalendar = 42 + const firstDayOfWeek = 0 + + const dateList: Day[] = [] + + for (let i = 1; i <= numOfDaysInCalendar; i++) { + const instance = new Date(year, month - 1, firstDayOfWeek - weekdayOfFirstDay + i) + const day: Day = { + instance, + year: instance.getFullYear(), + month: instance.getMonth() + 1, + date: instance.getDate(), + weekday: instance.getDay(), + curMonth: instance.getMonth() + 1 === month, + isToday: instance.toDateString() === today.toDateString(), + label: instance.toLocaleDateString(), + selectable: dayInRange(instance, range), + } + dateList.push(day) + } + + const calendarData: Day[][] = [] + + for (let i = 0; i < dateList.length; i += 7) { + calendarData.push(dateList.slice(i, i + 7)) + } + + return calendarData +} + +export const useLocalNames = () => { + const [t] = useTranslation() + + const locale = { + firstDayOfWeek: 0, + dayNames: ['sun', 'mon', 'tues', 'wed', 'thur', 'fri', 'sat'].map(dayname => t(`datetime.${dayname}.full`)), + dayNamesShort: ['sun', 'mon', 'tue', 'wed', 'thur', 'fri', 'sat'].map(dayname => t(`datetime.${dayname}.short`)), + dayNamesMin: ['sun', 'mon', 'tue', 'wed', 'thur', 'fri', 'sat'].map(dayname => t(`datetime.${dayname}.tag`)), + monthNames: ['jan', 'feb', 'mar', 'apr', 'may', 'june', 'july', 'aug', 'sept', 'oct', 'nov', 'dec'].map(monname => + t(`datetime.${monname}.short`) + ), + monthNamesShort: [ + 'jan', + 'feb', + 'mar', + 'apr', + 'may', + 'june', + 'july', + 'aug', + 'sept', + 'oct', + 'nov', + 'dec', + ].map(monname => t(`datetime.${monname}.short`)), + } + + return locale +} diff --git a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx index dc09f7e562..c9aad1924b 100644 --- a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx +++ b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx @@ -1,8 +1,7 @@ import React, { useState, useCallback, useRef, useEffect } from 'react' -import { Calendar, CalendarChangeParams } from 'primereact/calendar' +import Calendar from 'widgets/Calendar' import Button from 'widgets/Button' import { useTranslation } from 'react-i18next' -import { addLocale } from 'primereact/api' import styles from './datetimePicker.module.scss' const SECONDS_PER_DAY = 24 * 3600 * 1000 @@ -42,32 +41,6 @@ const DatetimePicker = ({ const [display, setDisplay] = useState(preset ? formatDate(new Date(+preset)) : '') const inputRef = useRef(null) - const locale: any = { - firstDayOfWeek: 0, - dayNames: ['sun', 'mon', 'tues', 'wed', 'thur', 'fri', 'sat'].map(dayname => t(`datetime.${dayname}.full`)), - dayNamesShort: ['sun', 'mon', 'tue', 'wed', 'thur', 'fri', 'sat'].map(dayname => t(`datetime.${dayname}.short`)), - dayNamesMin: ['sun', 'mon', 'tue', 'wed', 'thur', 'fri', 'sat'].map(dayname => t(`datetime.${dayname}.tag`)), - monthNames: ['jan', 'feb', 'mar', 'apr', 'may', 'june', 'july', 'aug', 'sept', 'oct', 'nov', 'dec'].map(monname => - t(`datetime.${monname}.short`) - ), - monthNamesShort: [ - 'jan', - 'feb', - 'mar', - 'apr', - 'may', - 'june', - 'july', - 'aug', - 'sept', - 'oct', - 'nov', - 'dec', - ].map(monname => t(`datetime.${monname}.short`)), - } - - addLocale('es', locale) - let selected: Date | undefined = display ? new Date(display) : undefined if (selected?.toString() === 'Invalid Date') { selected = undefined @@ -100,8 +73,8 @@ const DatetimePicker = ({ ) const onCalendarChange = useCallback( - (e: CalendarChangeParams) => { - setDisplay(formatDate(new Date(+e.value!))) + (date: Date) => { + setDisplay(formatDate(new Date(+date))) setStatus('done') }, [setDisplay, setStatus] @@ -155,14 +128,7 @@ const DatetimePicker = ({ onKeyPress={onKeyPress} /> )} - + {isSinceTomorrow ? null : {t('datetime.start-tomorrow')}} {notice ? (
From 4dd0da130af0907c0d907d47df1bb68f96d7229f Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Sat, 25 Feb 2023 18:03:16 +0800 Subject: [PATCH 02/14] Modified according to pr review --- .../src/widgets/Calendar/calendar.module.scss | 2 +- .../neuron-ui/src/widgets/Calendar/index.tsx | 50 ++++++------- .../neuron-ui/src/widgets/Calendar/utils.ts | 74 +++++++++---------- 3 files changed, 60 insertions(+), 66 deletions(-) diff --git a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss index 1e4bc1cd7c..6d22a9ee30 100644 --- a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss +++ b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss @@ -87,4 +87,4 @@ background-color: var(--nervos-green); } } -} \ No newline at end of file +} diff --git a/packages/neuron-ui/src/widgets/Calendar/index.tsx b/packages/neuron-ui/src/widgets/Calendar/index.tsx index 96e91013a0..bf74871a70 100644 --- a/packages/neuron-ui/src/widgets/Calendar/index.tsx +++ b/packages/neuron-ui/src/widgets/Calendar/index.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react' -import { getMonthCalendar, useLocalNames, monthInRange, yearInRange, dateEqual } from './utils' +import React, { useState, useEffect, useMemo } from 'react' +import { getMonthCalendar, useLocalNames, monthInRange, yearInRange, dateEqual, dayInRange } from './utils' import styles from './calendar.module.scss' interface Option { @@ -33,22 +33,25 @@ export interface CalendarProps { maxDate?: Date } const Calendar: React.FC = ({ value, onChange, minDate = null, maxDate = null }) => { - const today = new Date() - - const [year, setYear] = useState(value === undefined ? today.getFullYear() : value.getFullYear()) - const [month, setMonth] = useState(value === undefined ? today.getMonth() + 1 : value.getMonth() + 1) + const [year, setYear] = useState(new Date().getFullYear()) + const [month, setMonth] = useState(new Date().getMonth() + 1) const [status, setStatus] = useState<'year' | 'month' | 'date'>('date') - React.useEffect(() => { - setYear(value === undefined ? today.getFullYear() : value.getFullYear()) - setMonth(value === undefined ? today.getMonth() + 1 : value.getMonth() + 1) + useEffect(() => { + setYear(value?.getFullYear() ?? new Date().getFullYear()) + setMonth((value?.getMonth() ?? new Date().getMonth()) + 1) }, [value]) const locale = useLocalNames() - const weeknames = [...Array(7).keys()].map(_ => locale.dayNamesMin[(_ + locale.firstDayOfWeek) % 7]) + const weeknames = useMemo(() => [...Array(7).keys()].map(_ => locale.dayNamesMin[(_ + locale.firstDayOfWeek) % 7]), [ + locale, + ]) const monthname = locale.monthNames[month - 1] - const calendar = getMonthCalendar(year, month, { minDate, maxDate }) + const calendar = useMemo(() => getMonthCalendar(year, month), [year, month]) + function disabledTime(date: Date) { + return !dayInRange(date, { minDate, maxDate }) + } const calendarTable = ( @@ -76,7 +79,7 @@ const Calendar: React.FC = ({ value, onChange, minDate = null, ma ${date.isToday ? 'today' : ''} ${dateEqual(date.instance, value) ? 'active' : ''} `} - disabled={!date.curMonth || !date.selectable} + disabled={!date.isCurMonth || disabledTime(date.instance)} onClick={() => onChange(date.instance)} > {date.date} @@ -169,18 +172,11 @@ const Calendar: React.FC = ({ value, onChange, minDate = null, ma ) } -export default React.memo(Calendar, (prevProps, nextProps) => { - if (prevProps.value?.toDateString() !== nextProps.value?.toDateString()) { - return false - } - if (prevProps.minDate?.toDateString() !== nextProps.minDate?.toDateString()) { - return false - } - if (prevProps.maxDate?.toDateString() !== nextProps.maxDate?.toDateString()) { - return false - } - if (prevProps.onChange !== nextProps.onChange) { - return false - } - return true -}) +export default React.memo( + Calendar, + (prevProps, nextProps) => + dateEqual(prevProps.value, nextProps.value) && + dateEqual(prevProps.minDate, nextProps.minDate) && + dateEqual(prevProps.maxDate, nextProps.maxDate) && + prevProps.onChange === nextProps.onChange +) diff --git a/packages/neuron-ui/src/widgets/Calendar/utils.ts b/packages/neuron-ui/src/widgets/Calendar/utils.ts index d17ae4e743..0dbd186694 100644 --- a/packages/neuron-ui/src/widgets/Calendar/utils.ts +++ b/packages/neuron-ui/src/widgets/Calendar/utils.ts @@ -6,10 +6,9 @@ export interface Day { month: number date: number weekday: number - curMonth: boolean + isCurMonth: boolean isToday: boolean label: string - selectable: boolean } interface DateRange { @@ -18,27 +17,39 @@ interface DateRange { } export function dayInRange(date: Date, range: DateRange) { - if (range.minDate !== null) { - range.minDate.setHours(0, 0, 0, 0) - if (date < range.minDate) { - return false - } + const dayBegin = new Date(date.getFullYear(), date.getMonth(), date.getDate()) + const dayEnd = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1) + + if (range.minDate !== null && dayEnd <= range.minDate) { + return false } - if (range.maxDate !== null) { - range.maxDate.setHours(0, 0, 0, 0) - if (date > range.maxDate) { - return false - } + if (range.maxDate !== null && dayBegin > range.maxDate) { + return false } return true } -export function monthInRange(year: number, month: number, range: DateRange) { - return dayInRange(new Date(year, month + 1, 1), range) +export function monthInRange(year: number, monthIndex: number, range: DateRange) { + const monthBegin = new Date(year, monthIndex, 1) + const monthEnd = new Date(year, monthIndex + 1, 1) + + if (range.minDate !== null && monthEnd <= range.minDate) { + return false + } + if (range.maxDate !== null && monthBegin > range.maxDate) { + return false + } + return true } export function yearInRange(year: number, range: DateRange) { - return dayInRange(new Date(year + 1, 1, 1), range) + if (range.minDate !== null && year < range.minDate.getFullYear()) { + return false + } + if (range.maxDate !== null && year > range.maxDate.getFullYear()) { + return false + } + return true } export function dateEqual(a: Date | undefined, b: Date | undefined) { @@ -51,7 +62,7 @@ export function dateEqual(a: Date | undefined, b: Date | undefined) { /** * @description Generate monthly calendar 2D table data */ -export function getMonthCalendar(year: number, month: number, range: DateRange): Day[][] { +export function getMonthCalendar(year: number, month: number): Day[][] { const today = new Date() const weekdayOfFirstDay = new Date(year, month - 1, 1).getDay() const numOfDaysInCalendar = 42 @@ -67,10 +78,9 @@ export function getMonthCalendar(year: number, month: number, range: DateRange): month: instance.getMonth() + 1, date: instance.getDate(), weekday: instance.getDay(), - curMonth: instance.getMonth() + 1 === month, + isCurMonth: instance.getMonth() + 1 === month, isToday: instance.toDateString() === today.toDateString(), label: instance.toLocaleDateString(), - selectable: dayInRange(instance, range), } dateList.push(day) } @@ -87,28 +97,16 @@ export function getMonthCalendar(year: number, month: number, range: DateRange): export const useLocalNames = () => { const [t] = useTranslation() + const dayNames = ['sun', 'mon', 'tue', 'wed', 'thur', 'fri', 'sat'] + const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'june', 'july', 'aug', 'sept', 'oct', 'nov', 'dec'] + const locale = { firstDayOfWeek: 0, - dayNames: ['sun', 'mon', 'tues', 'wed', 'thur', 'fri', 'sat'].map(dayname => t(`datetime.${dayname}.full`)), - dayNamesShort: ['sun', 'mon', 'tue', 'wed', 'thur', 'fri', 'sat'].map(dayname => t(`datetime.${dayname}.short`)), - dayNamesMin: ['sun', 'mon', 'tue', 'wed', 'thur', 'fri', 'sat'].map(dayname => t(`datetime.${dayname}.tag`)), - monthNames: ['jan', 'feb', 'mar', 'apr', 'may', 'june', 'july', 'aug', 'sept', 'oct', 'nov', 'dec'].map(monname => - t(`datetime.${monname}.short`) - ), - monthNamesShort: [ - 'jan', - 'feb', - 'mar', - 'apr', - 'may', - 'june', - 'july', - 'aug', - 'sept', - 'oct', - 'nov', - 'dec', - ].map(monname => t(`datetime.${monname}.short`)), + dayNames: dayNames.map(dayname => t(`datetime.${dayname}.full`)), + dayNamesShort: dayNames.map(dayname => t(`datetime.${dayname}.short`)), + dayNamesMin: dayNames.map(dayname => t(`datetime.${dayname}.tag`)), + monthNames: monthNames.map(monname => t(`datetime.${monname}.short`)), + monthNamesShort: monthNames.map(monname => t(`datetime.${monname}.short`)), } return locale From 14e7e55f7671b75524e52f64f131dda3e179f54d Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Sun, 26 Feb 2023 18:53:58 +0800 Subject: [PATCH 03/14] Modified according to pr review --- .../src/widgets/Calendar/calendar.module.scss | 6 +- .../neuron-ui/src/widgets/Calendar/index.tsx | 79 +++++++++++-------- .../neuron-ui/src/widgets/Calendar/utils.ts | 23 +++--- .../src/widgets/DatetimePicker/index.tsx | 2 +- 4 files changed, 62 insertions(+), 48 deletions(-) diff --git a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss index 6d22a9ee30..e6c56c5285 100644 --- a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss +++ b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss @@ -19,7 +19,7 @@ background-color: #efefef; } &[disabled] { - cursor: none; + cursor: not-allowed; opacity: 0.5; box-shadow: none !important; pointer-events: none; @@ -79,11 +79,11 @@ .calDateItem { @include button; width: 100%; - &:global(.today) { + &[aria-current="date"] { border: 1px solid var(--nervos-green-light); margin: -1px; } - &:global(.active) { + &[aria-pressed="true"] { background-color: var(--nervos-green); } } diff --git a/packages/neuron-ui/src/widgets/Calendar/index.tsx b/packages/neuron-ui/src/widgets/Calendar/index.tsx index bf74871a70..68f0f910ce 100644 --- a/packages/neuron-ui/src/widgets/Calendar/index.tsx +++ b/packages/neuron-ui/src/widgets/Calendar/index.tsx @@ -1,5 +1,13 @@ -import React, { useState, useEffect, useMemo } from 'react' -import { getMonthCalendar, useLocalNames, monthInRange, yearInRange, dateEqual, dayInRange } from './utils' +import React, { useState, useEffect, useMemo, useCallback } from 'react' +import { + getMonthCalendar, + useLocalNames, + isMonthInRange, + isYearInRange, + isDateEqual, + isDayInRange, + WeekDayRange, +} from './utils' import styles from './calendar.module.scss' interface Option { @@ -29,10 +37,11 @@ const Selector = ({ options, onChange }: { options: Option[]; onChange: (option: export interface CalendarProps { value: Date | undefined onChange: (value: Date) => void + firstDayOfWeek?: WeekDayRange minDate?: Date maxDate?: Date } -const Calendar: React.FC = ({ value, onChange, minDate = null, maxDate = null }) => { +const Calendar: React.FC = ({ value, onChange, firstDayOfWeek = 0, minDate = null, maxDate = null }) => { const [year, setYear] = useState(new Date().getFullYear()) const [month, setMonth] = useState(new Date().getMonth() + 1) const [status, setStatus] = useState<'year' | 'month' | 'date'>('date') @@ -43,14 +52,14 @@ const Calendar: React.FC = ({ value, onChange, minDate = null, ma }, [value]) const locale = useLocalNames() - const weeknames = useMemo(() => [...Array(7).keys()].map(_ => locale.dayNamesMin[(_ + locale.firstDayOfWeek) % 7]), [ + const weeknames = useMemo(() => Array.from({ length: 7 }, (_, i) => locale.dayNamesMin[(i + firstDayOfWeek) % 7]), [ locale, ]) - const monthname = locale.monthNames[month - 1] + const monthName = locale.monthNames[month - 1] - const calendar = useMemo(() => getMonthCalendar(year, month), [year, month]) - function disabledTime(date: Date) { - return !dayInRange(date, { minDate, maxDate }) + const calendar = useMemo(() => getMonthCalendar(year, month, firstDayOfWeek), [year, month, firstDayOfWeek]) + function isDisabledTime(date: Date): boolean { + return !isDayInRange(date, { minDate, maxDate }) } const calendarTable = (
@@ -72,14 +81,11 @@ const Calendar: React.FC = ({ value, onChange, minDate = null, ma type="button" data-type="button" aria-label={date.label} - aria-pressed={dateEqual(date.instance, value)} + aria-pressed={isDateEqual(date.instance, value)} + aria-current={date.isToday ? 'date' : 'false'} title={date.label} - className={` - ${styles.calDateItem} - ${date.isToday ? 'today' : ''} - ${dateEqual(date.instance, value) ? 'active' : ''} - `} - disabled={!date.isCurMonth || disabledTime(date.instance)} + className={styles.calDateItem} + disabled={!date.isCurMonth || isDisabledTime(date.instance)} onClick={() => onChange(date.instance)} > {date.date} @@ -92,15 +98,15 @@ const Calendar: React.FC = ({ value, onChange, minDate = null, ma
) - const monthOptions: Option[] = [...Array(12).keys()].map(index => ({ + const monthOptions: Option[] = Array.from({ length: 12 }, (_, index) => ({ value: index + 1, title: locale.monthNames[index], - selectable: monthInRange(year, index, { minDate, maxDate }), + selectable: isMonthInRange(year, index + 1, { minDate, maxDate }), })) - const yearOptions: Option[] = [...Array(12).keys()].map(index => ({ + const yearOptions: Option[] = Array.from({ length: 12 }, (_, index) => ({ value: year - 6 + index, title: `${year - 6 + index}`, - selectable: yearInRange(year - 6 + index, { minDate, maxDate }), + selectable: isYearInRange(year - 6 + index, { minDate, maxDate }), })) const prevMonth = () => { @@ -128,13 +134,13 @@ const Calendar: React.FC = ({ value, onChange, minDate = null, ma
) - const onChangeMonth = (monthOptionItem: Option) => { - setMonth(monthOptionItem.value) - setStatus('date') - } - const onChangeYear = (yearOptionItem: Option) => { - setYear(yearOptionItem.value) - setStatus('month') - } + const onChangeMonth = useCallback( + (monthOptionItem: Option) => { + setMonth(monthOptionItem.value) + setStatus('date') + }, + [setStatus, setMonth] + ) + const onChangeYear = useCallback( + (yearOptionItem: Option) => { + setYear(yearOptionItem.value) + setStatus('month') + }, + [setStatus, setYear] + ) return (
@@ -175,8 +187,9 @@ const Calendar: React.FC = ({ value, onChange, minDate = null, ma export default React.memo( Calendar, (prevProps, nextProps) => - dateEqual(prevProps.value, nextProps.value) && - dateEqual(prevProps.minDate, nextProps.minDate) && - dateEqual(prevProps.maxDate, nextProps.maxDate) && + isDateEqual(prevProps.value, nextProps.value) && + isDateEqual(prevProps.minDate, nextProps.minDate) && + isDateEqual(prevProps.maxDate, nextProps.maxDate) && + prevProps.firstDayOfWeek === nextProps.firstDayOfWeek && prevProps.onChange === nextProps.onChange ) diff --git a/packages/neuron-ui/src/widgets/Calendar/utils.ts b/packages/neuron-ui/src/widgets/Calendar/utils.ts index 0dbd186694..2a9d0eb216 100644 --- a/packages/neuron-ui/src/widgets/Calendar/utils.ts +++ b/packages/neuron-ui/src/widgets/Calendar/utils.ts @@ -16,7 +16,9 @@ interface DateRange { maxDate: Date | null } -export function dayInRange(date: Date, range: DateRange) { +export type WeekDayRange = 0 | 1 | 2 | 3 | 4 | 5 | 6 + +export function isDayInRange(date: Date, range: DateRange): boolean { const dayBegin = new Date(date.getFullYear(), date.getMonth(), date.getDate()) const dayEnd = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1) @@ -29,9 +31,9 @@ export function dayInRange(date: Date, range: DateRange) { return true } -export function monthInRange(year: number, monthIndex: number, range: DateRange) { - const monthBegin = new Date(year, monthIndex, 1) - const monthEnd = new Date(year, monthIndex + 1, 1) +export function isMonthInRange(year: number, month: number, range: DateRange): boolean { + const monthBegin = new Date(year, month - 1, 1) + const monthEnd = new Date(year, month, 1) if (range.minDate !== null && monthEnd <= range.minDate) { return false @@ -42,7 +44,7 @@ export function monthInRange(year: number, monthIndex: number, range: DateRange) return true } -export function yearInRange(year: number, range: DateRange) { +export function isYearInRange(year: number, range: DateRange): boolean { if (range.minDate !== null && year < range.minDate.getFullYear()) { return false } @@ -52,7 +54,7 @@ export function yearInRange(year: number, range: DateRange) { return true } -export function dateEqual(a: Date | undefined, b: Date | undefined) { +export function isDateEqual(a: Date | undefined, b: Date | undefined): boolean { if (a === undefined || b === undefined) { return false } @@ -62,11 +64,12 @@ export function dateEqual(a: Date | undefined, b: Date | undefined) { /** * @description Generate monthly calendar 2D table data */ -export function getMonthCalendar(year: number, month: number): Day[][] { +export function getMonthCalendar(year: number, month: number, firstDayOfWeek: WeekDayRange = 0): Day[][] { const today = new Date() const weekdayOfFirstDay = new Date(year, month - 1, 1).getDay() - const numOfDaysInCalendar = 42 - const firstDayOfWeek = 0 + const DAYS_IN_WEEK = 7 + const ROWS_IN_CALENDAR = 6 + const numOfDaysInCalendar = DAYS_IN_WEEK * ROWS_IN_CALENDAR const dateList: Day[] = [] @@ -101,12 +104,10 @@ export const useLocalNames = () => { const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'june', 'july', 'aug', 'sept', 'oct', 'nov', 'dec'] const locale = { - firstDayOfWeek: 0, dayNames: dayNames.map(dayname => t(`datetime.${dayname}.full`)), dayNamesShort: dayNames.map(dayname => t(`datetime.${dayname}.short`)), dayNamesMin: dayNames.map(dayname => t(`datetime.${dayname}.tag`)), monthNames: monthNames.map(monname => t(`datetime.${monname}.short`)), - monthNamesShort: monthNames.map(monname => t(`datetime.${monname}.short`)), } return locale diff --git a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx index c9aad1924b..59389297d6 100644 --- a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx +++ b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx @@ -74,7 +74,7 @@ const DatetimePicker = ({ const onCalendarChange = useCallback( (date: Date) => { - setDisplay(formatDate(new Date(+date))) + setDisplay(formatDate(date)) setStatus('done') }, [setDisplay, setStatus] From fb8e50af30670aa526e0b749e7798629bf5dd1a6 Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Mon, 27 Feb 2023 00:08:01 +0800 Subject: [PATCH 04/14] Add unit tests --- .../src/tests/calendar/index.test.ts | 90 +++++++++++++++++++ .../neuron-ui/src/widgets/Calendar/index.tsx | 2 +- .../neuron-ui/src/widgets/Calendar/utils.ts | 18 ++-- 3 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 packages/neuron-ui/src/tests/calendar/index.test.ts diff --git a/packages/neuron-ui/src/tests/calendar/index.test.ts b/packages/neuron-ui/src/tests/calendar/index.test.ts new file mode 100644 index 0000000000..0374fd9719 --- /dev/null +++ b/packages/neuron-ui/src/tests/calendar/index.test.ts @@ -0,0 +1,90 @@ +import { + isDayInRange, + isMonthInRange, + isYearInRange, + isDateEqual, + getMonthCalendar, +} from '../../widgets/Calendar/utils' + +describe('Check day in range', () => { + it('check no restrictions', () => { + expect(isDayInRange(new Date(2023, 1, 1), {})).toBe(true) + }) + it('check minDate', () => { + expect(isDayInRange(new Date(2023, 1, 1), { minDate: new Date(2023, 1, 1) })).toBe(true) + expect(isDayInRange(new Date(2023, 1, 1), { minDate: new Date(2023, 1, 1, 12) })).toBe(true) + expect(isDayInRange(new Date(2023, 1, 1), { minDate: new Date(2023, 1, 2) })).toBe(false) + }) + it('check maxDate', () => { + expect(isDayInRange(new Date(2023, 1, 2), { maxDate: new Date(2023, 1, 2) })).toBe(true) + expect(isDayInRange(new Date(2023, 1, 2), { maxDate: new Date(2023, 1, 1) })).toBe(false) + expect(isDayInRange(new Date(2023, 1, 2), { maxDate: new Date(2023, 1, 1, 12) })).toBe(false) + }) +}) + +describe('Check month in range', () => { + it('check no restrictions', () => { + expect(isMonthInRange(2023, 1, {})).toBe(true) + }) + it('check minDate', () => { + expect(isMonthInRange(2023, 1, { minDate: new Date(2023, 1, 1) })).toBe(false) + expect(isMonthInRange(2023, 1, { minDate: new Date(2023, 0, 31) })).toBe(true) + }) + it('check maxDate', () => { + expect(isMonthInRange(2023, 2, { maxDate: new Date(2023, 1, 1) })).toBe(true) + expect(isMonthInRange(2023, 2, { maxDate: new Date(2023, 0, 31) })).toBe(false) + }) +}) + +describe('Check year in range', () => { + it('check no restrictions', () => { + expect(isYearInRange(2023, {})).toBe(true) + }) + it('check minDate', () => { + expect(isYearInRange(2023, { minDate: new Date(2023, 1, 1) })).toBe(true) + expect(isYearInRange(2023, { minDate: new Date(2024, 1, 1) })).toBe(false) + expect(isYearInRange(2023, { minDate: new Date(2022, 1, 1) })).toBe(true) + }) + it('check maxDate', () => { + expect(isYearInRange(2023, { maxDate: new Date(2023, 1, 1) })).toBe(true) + expect(isYearInRange(2023, { maxDate: new Date(2024, 1, 1) })).toBe(true) + expect(isYearInRange(2023, { maxDate: new Date(2022, 1, 1) })).toBe(false) + }) +}) + +describe('Check date equal', () => { + it('undefined in one side', () => { + expect(isDateEqual(new Date(2023, 2, 1), undefined)).toBe(false) + expect(isDateEqual(undefined, new Date(2023, 2, 1))).toBe(false) + }) + it('check date equal', () => { + expect(isDateEqual(new Date(2023, 2, 1), new Date(2023, 2, 1))).toBe(true) + }) + it('check date equal ignore time', () => { + expect(isDateEqual(new Date(2023, 2, 1, 12), new Date(2023, 2, 1, 18))).toBe(true) + }) +}) + +describe('Generate monthly calendar data', () => { + it('Test month calendar output', () => { + expect(getMonthCalendar(2023, 1).map(week => week.map(date => date.label))).toEqual([ + ['2023/1/1', '2023/1/2', '2023/1/3', '2023/1/4', '2023/1/5', '2023/1/6', '2023/1/7'], + ['2023/1/8', '2023/1/9', '2023/1/10', '2023/1/11', '2023/1/12', '2023/1/13', '2023/1/14'], + ['2023/1/15', '2023/1/16', '2023/1/17', '2023/1/18', '2023/1/19', '2023/1/20', '2023/1/21'], + ['2023/1/22', '2023/1/23', '2023/1/24', '2023/1/25', '2023/1/26', '2023/1/27', '2023/1/28'], + ['2023/1/29', '2023/1/30', '2023/1/31', '2023/2/1', '2023/2/2', '2023/2/3', '2023/2/4'], + ['2023/2/5', '2023/2/6', '2023/2/7', '2023/2/8', '2023/2/9', '2023/2/10', '2023/2/11'], + ]) + }) + + it('Test month canlendar with specified start weekday', () => { + expect(getMonthCalendar(2023, 1, 1).map(week => week.map(date => date.label))).toEqual([ + ['2022/12/26', '2022/12/27', '2022/12/28', '2022/12/29', '2022/12/30', '2022/12/31', '2023/1/1'], + ['2023/1/2', '2023/1/3', '2023/1/4', '2023/1/5', '2023/1/6', '2023/1/7', '2023/1/8'], + ['2023/1/9', '2023/1/10', '2023/1/11', '2023/1/12', '2023/1/13', '2023/1/14', '2023/1/15'], + ['2023/1/16', '2023/1/17', '2023/1/18', '2023/1/19', '2023/1/20', '2023/1/21', '2023/1/22'], + ['2023/1/23', '2023/1/24', '2023/1/25', '2023/1/26', '2023/1/27', '2023/1/28', '2023/1/29'], + ['2023/1/30', '2023/1/31', '2023/2/1', '2023/2/2', '2023/2/3', '2023/2/4', '2023/2/5'], + ]) + }) +}) diff --git a/packages/neuron-ui/src/widgets/Calendar/index.tsx b/packages/neuron-ui/src/widgets/Calendar/index.tsx index 68f0f910ce..dd10b1ece9 100644 --- a/packages/neuron-ui/src/widgets/Calendar/index.tsx +++ b/packages/neuron-ui/src/widgets/Calendar/index.tsx @@ -41,7 +41,7 @@ export interface CalendarProps { minDate?: Date maxDate?: Date } -const Calendar: React.FC = ({ value, onChange, firstDayOfWeek = 0, minDate = null, maxDate = null }) => { +const Calendar: React.FC = ({ value, onChange, minDate, maxDate, firstDayOfWeek = 0 }) => { const [year, setYear] = useState(new Date().getFullYear()) const [month, setMonth] = useState(new Date().getMonth() + 1) const [status, setStatus] = useState<'year' | 'month' | 'date'>('date') diff --git a/packages/neuron-ui/src/widgets/Calendar/utils.ts b/packages/neuron-ui/src/widgets/Calendar/utils.ts index 2a9d0eb216..6c699adfbc 100644 --- a/packages/neuron-ui/src/widgets/Calendar/utils.ts +++ b/packages/neuron-ui/src/widgets/Calendar/utils.ts @@ -12,8 +12,8 @@ export interface Day { } interface DateRange { - minDate: Date | null - maxDate: Date | null + minDate?: Date + maxDate?: Date } export type WeekDayRange = 0 | 1 | 2 | 3 | 4 | 5 | 6 @@ -22,10 +22,10 @@ export function isDayInRange(date: Date, range: DateRange): boolean { const dayBegin = new Date(date.getFullYear(), date.getMonth(), date.getDate()) const dayEnd = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1) - if (range.minDate !== null && dayEnd <= range.minDate) { + if (range.minDate !== undefined && dayEnd <= range.minDate) { return false } - if (range.maxDate !== null && dayBegin > range.maxDate) { + if (range.maxDate !== undefined && dayBegin > range.maxDate) { return false } return true @@ -35,20 +35,20 @@ export function isMonthInRange(year: number, month: number, range: DateRange): b const monthBegin = new Date(year, month - 1, 1) const monthEnd = new Date(year, month, 1) - if (range.minDate !== null && monthEnd <= range.minDate) { + if (range.minDate !== undefined && monthEnd <= range.minDate) { return false } - if (range.maxDate !== null && monthBegin > range.maxDate) { + if (range.maxDate !== undefined && monthBegin > range.maxDate) { return false } return true } export function isYearInRange(year: number, range: DateRange): boolean { - if (range.minDate !== null && year < range.minDate.getFullYear()) { + if (range.minDate !== undefined && year < range.minDate.getFullYear()) { return false } - if (range.maxDate !== null && year > range.maxDate.getFullYear()) { + if (range.maxDate !== undefined && year > range.maxDate.getFullYear()) { return false } return true @@ -74,7 +74,7 @@ export function getMonthCalendar(year: number, month: number, firstDayOfWeek: We const dateList: Day[] = [] for (let i = 1; i <= numOfDaysInCalendar; i++) { - const instance = new Date(year, month - 1, firstDayOfWeek - weekdayOfFirstDay + i) + const instance = new Date(year, month - 1, ((firstDayOfWeek - weekdayOfFirstDay - 7) % 7) + i) const day: Day = { instance, year: instance.getFullYear(), From 9c0d2f7bac916f65d44a9cced6f773a32e5c3ba6 Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Mon, 27 Feb 2023 15:43:18 +0800 Subject: [PATCH 05/14] Upgrade unit tests for node environment --- packages/neuron-ui/src/tests/calendar/index.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/neuron-ui/src/tests/calendar/index.test.ts b/packages/neuron-ui/src/tests/calendar/index.test.ts index 0374fd9719..0960a07fd4 100644 --- a/packages/neuron-ui/src/tests/calendar/index.test.ts +++ b/packages/neuron-ui/src/tests/calendar/index.test.ts @@ -67,7 +67,7 @@ describe('Check date equal', () => { describe('Generate monthly calendar data', () => { it('Test month calendar output', () => { - expect(getMonthCalendar(2023, 1).map(week => week.map(date => date.label))).toEqual([ + expect(getMonthCalendar(2023, 1).map(week => week.map(date => `${date.year}/${date.month}/${date.date}`))).toEqual([ ['2023/1/1', '2023/1/2', '2023/1/3', '2023/1/4', '2023/1/5', '2023/1/6', '2023/1/7'], ['2023/1/8', '2023/1/9', '2023/1/10', '2023/1/11', '2023/1/12', '2023/1/13', '2023/1/14'], ['2023/1/15', '2023/1/16', '2023/1/17', '2023/1/18', '2023/1/19', '2023/1/20', '2023/1/21'], @@ -78,7 +78,9 @@ describe('Generate monthly calendar data', () => { }) it('Test month canlendar with specified start weekday', () => { - expect(getMonthCalendar(2023, 1, 1).map(week => week.map(date => date.label))).toEqual([ + expect( + getMonthCalendar(2023, 1, 1).map(week => week.map(date => `${date.year}/${date.month}/${date.date}`)) + ).toEqual([ ['2022/12/26', '2022/12/27', '2022/12/28', '2022/12/29', '2022/12/30', '2022/12/31', '2023/1/1'], ['2023/1/2', '2023/1/3', '2023/1/4', '2023/1/5', '2023/1/6', '2023/1/7', '2023/1/8'], ['2023/1/9', '2023/1/10', '2023/1/11', '2023/1/12', '2023/1/13', '2023/1/14', '2023/1/15'], From 1377b9210af46fc6a1c8d0d1f44cf0e8d1b3fdb3 Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Tue, 28 Feb 2023 00:31:48 +0800 Subject: [PATCH 06/14] Support all languages --- .../src/tests/calendar/index.test.ts | 36 +++++++++++++++++++ .../neuron-ui/src/widgets/Calendar/index.tsx | 20 ++++++----- .../neuron-ui/src/widgets/Calendar/utils.ts | 22 ++++-------- .../src/widgets/DatetimePicker/index.tsx | 3 +- 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/packages/neuron-ui/src/tests/calendar/index.test.ts b/packages/neuron-ui/src/tests/calendar/index.test.ts index 0960a07fd4..6cfca09a14 100644 --- a/packages/neuron-ui/src/tests/calendar/index.test.ts +++ b/packages/neuron-ui/src/tests/calendar/index.test.ts @@ -4,6 +4,8 @@ import { isYearInRange, isDateEqual, getMonthCalendar, + getLocalMonthNames, + getLocalWeekNames, } from '../../widgets/Calendar/utils' describe('Check day in range', () => { @@ -90,3 +92,37 @@ describe('Generate monthly calendar data', () => { ]) }) }) + +describe('Get Local Month Names', () => { + it('Chinese', () => { + const names = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] + expect(getLocalMonthNames('zh')).toEqual(names) + }) + + it('English', () => { + const names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + expect(getLocalMonthNames('en')).toEqual(names) + }) +}) + +describe('Get Local Week Names', () => { + it('Chinese', () => { + const names = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] + expect(getLocalWeekNames('zh')).toEqual(names) + }) + + it('English', () => { + const names = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + expect(getLocalWeekNames('en')).toEqual(names) + }) + + it('Traditional Chinese', () => { + const names = ['週日', '週一', '週二', '週三', '週四', '週五', '週六'] + expect(getLocalWeekNames('zh-TW')).toEqual(names) + }) + + it('Japanese', () => { + const names = ['日', '月', '火', '水', '木', '金', '土'] + expect(getLocalWeekNames('ja')).toEqual(names) + }) +}) diff --git a/packages/neuron-ui/src/widgets/Calendar/index.tsx b/packages/neuron-ui/src/widgets/Calendar/index.tsx index dd10b1ece9..af6f04ce08 100644 --- a/packages/neuron-ui/src/widgets/Calendar/index.tsx +++ b/packages/neuron-ui/src/widgets/Calendar/index.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react' import { getMonthCalendar, - useLocalNames, + getLocalMonthNames, + getLocalWeekNames, isMonthInRange, isYearInRange, isDateEqual, @@ -37,11 +38,12 @@ const Selector = ({ options, onChange }: { options: Option[]; onChange: (option: export interface CalendarProps { value: Date | undefined onChange: (value: Date) => void + lang?: string firstDayOfWeek?: WeekDayRange minDate?: Date maxDate?: Date } -const Calendar: React.FC = ({ value, onChange, minDate, maxDate, firstDayOfWeek = 0 }) => { +const Calendar: React.FC = ({ value, onChange, minDate, maxDate, lang = 'en', firstDayOfWeek = 0 }) => { const [year, setYear] = useState(new Date().getFullYear()) const [month, setMonth] = useState(new Date().getMonth() + 1) const [status, setStatus] = useState<'year' | 'month' | 'date'>('date') @@ -51,11 +53,11 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate, setMonth((value?.getMonth() ?? new Date().getMonth()) + 1) }, [value]) - const locale = useLocalNames() - const weeknames = useMemo(() => Array.from({ length: 7 }, (_, i) => locale.dayNamesMin[(i + firstDayOfWeek) % 7]), [ - locale, - ]) - const monthName = locale.monthNames[month - 1] + const monthNames = useMemo(() => getLocalMonthNames(lang), [lang]) + const weekNames = useMemo(() => getLocalWeekNames(lang), [lang]) + + const weekTitle = useMemo(() => Array.from({ length: 7 }, (_, i) => weekNames[(i + firstDayOfWeek) % 7]), [weekNames]) + const monthName = monthNames[month - 1] const calendar = useMemo(() => getMonthCalendar(year, month, firstDayOfWeek), [year, month, firstDayOfWeek]) function isDisabledTime(date: Date): boolean { @@ -65,7 +67,7 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate, - {weeknames.map(weekname => ( + {weekTitle.map(weekname => ( @@ -100,7 +102,7 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate, const monthOptions: Option[] = Array.from({ length: 12 }, (_, index) => ({ value: index + 1, - title: locale.monthNames[index], + title: monthNames[index], selectable: isMonthInRange(year, index + 1, { minDate, maxDate }), })) const yearOptions: Option[] = Array.from({ length: 12 }, (_, index) => ({ diff --git a/packages/neuron-ui/src/widgets/Calendar/utils.ts b/packages/neuron-ui/src/widgets/Calendar/utils.ts index 6c699adfbc..6a25244c57 100644 --- a/packages/neuron-ui/src/widgets/Calendar/utils.ts +++ b/packages/neuron-ui/src/widgets/Calendar/utils.ts @@ -1,5 +1,3 @@ -import { useTranslation } from 'react-i18next' - export interface Day { instance: Date year: number @@ -97,18 +95,12 @@ export function getMonthCalendar(year: number, month: number, firstDayOfWeek: We return calendarData } -export const useLocalNames = () => { - const [t] = useTranslation() - - const dayNames = ['sun', 'mon', 'tue', 'wed', 'thur', 'fri', 'sat'] - const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'june', 'july', 'aug', 'sept', 'oct', 'nov', 'dec'] - - const locale = { - dayNames: dayNames.map(dayname => t(`datetime.${dayname}.full`)), - dayNamesShort: dayNames.map(dayname => t(`datetime.${dayname}.short`)), - dayNamesMin: dayNames.map(dayname => t(`datetime.${dayname}.tag`)), - monthNames: monthNames.map(monname => t(`datetime.${monname}.short`)), - } +export const getLocalMonthNames = (lang: string) => { + const formater = new Intl.DateTimeFormat(lang, { month: 'short' }) + return Array.from({ length: 12 }, (_, i) => formater.format(new Date(Date.UTC(2023, i, 1)))) +} - return locale +export const getLocalWeekNames = (lang: string) => { + const formater = new Intl.DateTimeFormat(lang, { weekday: 'short' }) + return Array.from({ length: 7 }, (_, i) => formater.format(new Date(Date.UTC(2023, 0, 1 + i)))) } diff --git a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx index 59389297d6..53ddba7904 100644 --- a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx +++ b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx @@ -2,6 +2,7 @@ import React, { useState, useCallback, useRef, useEffect } from 'react' import Calendar from 'widgets/Calendar' import Button from 'widgets/Button' import { useTranslation } from 'react-i18next' +import i18n from 'i18next' import styles from './datetimePicker.module.scss' const SECONDS_PER_DAY = 24 * 3600 * 1000 @@ -128,7 +129,7 @@ const DatetimePicker = ({ onKeyPress={onKeyPress} /> )} - + {isSinceTomorrow ? null : {t('datetime.start-tomorrow')}} {notice ? (
From 01a2814661ec7a37bbb71565a3a65080ba6b3768 Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Tue, 28 Feb 2023 17:21:03 +0800 Subject: [PATCH 07/14] use useTranslation get current language --- packages/neuron-ui/src/widgets/Calendar/index.tsx | 11 ++++++++--- .../neuron-ui/src/widgets/DatetimePicker/index.tsx | 3 +-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/neuron-ui/src/widgets/Calendar/index.tsx b/packages/neuron-ui/src/widgets/Calendar/index.tsx index af6f04ce08..a8a6d62b55 100644 --- a/packages/neuron-ui/src/widgets/Calendar/index.tsx +++ b/packages/neuron-ui/src/widgets/Calendar/index.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' import { getMonthCalendar, getLocalMonthNames, @@ -38,12 +39,13 @@ const Selector = ({ options, onChange }: { options: Option[]; onChange: (option: export interface CalendarProps { value: Date | undefined onChange: (value: Date) => void + // lang tags: https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry lang?: string firstDayOfWeek?: WeekDayRange minDate?: Date maxDate?: Date } -const Calendar: React.FC = ({ value, onChange, minDate, maxDate, lang = 'en', firstDayOfWeek = 0 }) => { +const Calendar: React.FC = ({ value, onChange, minDate, maxDate, lang, firstDayOfWeek = 0 }) => { const [year, setYear] = useState(new Date().getFullYear()) const [month, setMonth] = useState(new Date().getMonth() + 1) const [status, setStatus] = useState<'year' | 'month' | 'date'>('date') @@ -53,8 +55,11 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate, setMonth((value?.getMonth() ?? new Date().getMonth()) + 1) }, [value]) - const monthNames = useMemo(() => getLocalMonthNames(lang), [lang]) - const weekNames = useMemo(() => getLocalWeekNames(lang), [lang]) + const [, i18n] = useTranslation() + const language = lang === undefined ? i18n.language : lang + + const monthNames = useMemo(() => getLocalMonthNames(language), [language]) + const weekNames = useMemo(() => getLocalWeekNames(language), [language]) const weekTitle = useMemo(() => Array.from({ length: 7 }, (_, i) => weekNames[(i + firstDayOfWeek) % 7]), [weekNames]) const monthName = monthNames[month - 1] diff --git a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx index 53ddba7904..59389297d6 100644 --- a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx +++ b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx @@ -2,7 +2,6 @@ import React, { useState, useCallback, useRef, useEffect } from 'react' import Calendar from 'widgets/Calendar' import Button from 'widgets/Button' import { useTranslation } from 'react-i18next' -import i18n from 'i18next' import styles from './datetimePicker.module.scss' const SECONDS_PER_DAY = 24 * 3600 * 1000 @@ -129,7 +128,7 @@ const DatetimePicker = ({ onKeyPress={onKeyPress} /> )} - + {isSinceTomorrow ? null : {t('datetime.start-tomorrow')}} {notice ? (
From 60e11016aaaead5e28a7de93aeb91900ad6eef85 Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Mon, 6 Mar 2023 11:30:10 +0800 Subject: [PATCH 08/14] update UI & useMemo Strategy --- .../src/widgets/Calendar/calendar.module.scss | 7 ++-- .../neuron-ui/src/widgets/Calendar/index.tsx | 37 ++++++++----------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss index e6c56c5285..27a20b220b 100644 --- a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss +++ b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss @@ -78,10 +78,11 @@ .calDateItem { @include button; - width: 100%; + width: 30px; + height: 30px; + border-radius: 50%; &[aria-current="date"] { - border: 1px solid var(--nervos-green-light); - margin: -1px; + color: var(--nervos-green-light); } &[aria-pressed="true"] { background-color: var(--nervos-green); diff --git a/packages/neuron-ui/src/widgets/Calendar/index.tsx b/packages/neuron-ui/src/widgets/Calendar/index.tsx index a8a6d62b55..af1a574219 100644 --- a/packages/neuron-ui/src/widgets/Calendar/index.tsx +++ b/packages/neuron-ui/src/widgets/Calendar/index.tsx @@ -39,13 +39,11 @@ const Selector = ({ options, onChange }: { options: Option[]; onChange: (option: export interface CalendarProps { value: Date | undefined onChange: (value: Date) => void - // lang tags: https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry - lang?: string firstDayOfWeek?: WeekDayRange minDate?: Date maxDate?: Date } -const Calendar: React.FC = ({ value, onChange, minDate, maxDate, lang, firstDayOfWeek = 0 }) => { +const Calendar: React.FC = ({ value, onChange, minDate, maxDate, firstDayOfWeek = 0 }) => { const [year, setYear] = useState(new Date().getFullYear()) const [month, setMonth] = useState(new Date().getMonth() + 1) const [status, setStatus] = useState<'year' | 'month' | 'date'>('date') @@ -53,11 +51,9 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate, useEffect(() => { setYear(value?.getFullYear() ?? new Date().getFullYear()) setMonth((value?.getMonth() ?? new Date().getMonth()) + 1) - }, [value]) - - const [, i18n] = useTranslation() - const language = lang === undefined ? i18n.language : lang + }, [value?.toDateString()]) + const [, { language }] = useTranslation() const monthNames = useMemo(() => getLocalMonthNames(language), [language]) const weekNames = useMemo(() => getLocalWeekNames(language), [language]) @@ -105,17 +101,6 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate,
{weekname}
) - const monthOptions: Option[] = Array.from({ length: 12 }, (_, index) => ({ - value: index + 1, - title: monthNames[index], - selectable: isMonthInRange(year, index + 1, { minDate, maxDate }), - })) - const yearOptions: Option[] = Array.from({ length: 12 }, (_, index) => ({ - value: year - 6 + index, - title: `${year - 6 + index}`, - selectable: isYearInRange(year - 6 + index, { minDate, maxDate }), - })) - const prevMonth = () => { if (month > 1) { setMonth(m => m - 1) @@ -180,6 +165,16 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate, }, [setStatus, setYear] ) + const monthOptions: Option[] = Array.from({ length: 12 }, (_, index) => ({ + value: index + 1, + title: monthNames[index], + selectable: isMonthInRange(year, index + 1, { minDate, maxDate }), + })) + const yearOptions: Option[] = Array.from({ length: 12 }, (_, index) => ({ + value: year - 6 + index, + title: `${year - 6 + index}`, + selectable: isYearInRange(year - 6 + index, { minDate, maxDate }), + })) return (
@@ -194,9 +189,9 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate, export default React.memo( Calendar, (prevProps, nextProps) => - isDateEqual(prevProps.value, nextProps.value) && - isDateEqual(prevProps.minDate, nextProps.minDate) && - isDateEqual(prevProps.maxDate, nextProps.maxDate) && + prevProps.value?.toDateString() === nextProps.value?.toDateString() && + prevProps.minDate?.toDateString() === nextProps.minDate?.toDateString() && + prevProps.maxDate?.toDateString() === nextProps.maxDate?.toDateString() && prevProps.firstDayOfWeek === nextProps.firstDayOfWeek && prevProps.onChange === nextProps.onChange ) From 50304972fb901fde4d23b873a2e3bcceb001e2bf Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Mon, 6 Mar 2023 11:33:16 +0800 Subject: [PATCH 09/14] update UI --- packages/neuron-ui/src/widgets/Calendar/calendar.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss index 27a20b220b..140be36379 100644 --- a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss +++ b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss @@ -86,6 +86,7 @@ } &[aria-pressed="true"] { background-color: var(--nervos-green); + color: white; } } } From cc2913ca3201fd08d9616846ee0b84713bf295da Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Mon, 6 Mar 2023 11:58:31 +0800 Subject: [PATCH 10/14] table cell align center --- packages/neuron-ui/src/widgets/Calendar/calendar.module.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss index 140be36379..9ac703eea2 100644 --- a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss +++ b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss @@ -71,6 +71,10 @@ .calendarTable { width: 100%; + :global(td) { + text-align: center; + } + .calTableHeader { @include semi-bold-text; font-size: 13px; From 41bba67b1d3ed2eeb4573320397cef2a6b4bc46e Mon Sep 17 00:00:00 2001 From: Morty Lu <15620954611@163.com> Date: Mon, 13 Mar 2023 00:55:09 +0800 Subject: [PATCH 11/14] add focus control --- packages/neuron-ui/src/locales/en.json | 2 + packages/neuron-ui/src/locales/zh-tw.json | 2 + packages/neuron-ui/src/locales/zh.json | 2 + .../src/widgets/Calendar/calendar.module.scss | 44 +++- .../src/widgets/Calendar/focusControl.tsx | 174 ++++++++++++++++ .../neuron-ui/src/widgets/Calendar/index.tsx | 191 +++++++++++------- .../neuron-ui/src/widgets/Calendar/utils.ts | 15 +- .../DatetimePicker/datetimePicker.module.scss | 124 +----------- .../src/widgets/DatetimePicker/index.tsx | 2 +- 9 files changed, 357 insertions(+), 199 deletions(-) create mode 100644 packages/neuron-ui/src/widgets/Calendar/focusControl.tsx diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index a14ac6c820..1b5edfdd61 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -763,6 +763,8 @@ "tag": "D" }, "timezone": "Time Zone", + "previous-month": "previous month", + "next-month": "next month", "start-tomorrow": "Selected time should start from tomorrow." }, "sign-and-verify": { diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index f92e77e793..e980435550 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -756,6 +756,8 @@ "tag": "十二月" }, "timezone": "時區", + "previous-month": "上個月", + "next-month": "下個月", "start-tomorrow": "所選時間不能早於明天。" }, "sign-and-verify": { diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index ca76359d19..74bf4026a6 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -756,6 +756,8 @@ "tag": "十二月" }, "timezone": "时区", + "previous-month": "上个月", + "next-month": "下个月", "start-tomorrow": "所选时间不能早于明天。" }, "sign-and-verify": { diff --git a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss index 9ac703eea2..0f51e13129 100644 --- a/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss +++ b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss @@ -1,5 +1,11 @@ @import '../../styles/mixin.scss'; +.srOnly { + position: absolute; + top: -10000px; + left: -10000px; +} + @mixin button { @include medium-text; appearance: none; @@ -14,7 +20,8 @@ box-sizing: border-box; border-radius: 2px; background-color: transparent; - &:hover { + min-width: 0; + &:hover, &:focus { @include semi-bold-text; background-color: #efefef; } @@ -30,18 +37,26 @@ } .calendar { - width: 374px; .calOptions { width: 100%; display: flex; flex-wrap: wrap; - justify-content: space-between; + justify-content: flex-start; list-style-type: none; list-style: none; padding: 0; + + li { + flex-grow: 1; + } + button { @include button; width: 100px; + &[aria-checked="true"] { + background-color: var(--nervos-green); + color: white; + } } } } @@ -57,7 +72,26 @@ @include button; width: 30px; } + .calPrev { + order: 1; + + &::before { + content: '<'; + } + } + .calNext { + order: 3; + + &::before { + content: '>'; + } + } + .calTitle { + order: 2; + margin: 0; + font-size: 0; + button { @include button; @include semi-bold-text; @@ -71,7 +105,7 @@ .calendarTable { width: 100%; - :global(td) { + td { text-align: center; } @@ -86,7 +120,7 @@ height: 30px; border-radius: 50%; &[aria-current="date"] { - color: var(--nervos-green-light); + color: var(--nervos-green); } &[aria-pressed="true"] { background-color: var(--nervos-green); diff --git a/packages/neuron-ui/src/widgets/Calendar/focusControl.tsx b/packages/neuron-ui/src/widgets/Calendar/focusControl.tsx new file mode 100644 index 0000000000..23d1022689 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Calendar/focusControl.tsx @@ -0,0 +1,174 @@ +import React, { useState, useRef, useEffect, KeyboardEvent } from 'react' +import { isMonthInRange, isDayInRange } from './utils' + +type ButtonHasFocusProps = React.ButtonHTMLAttributes & { isFocus: boolean } +export const ButtonHasFocus = ({ isFocus, children, ...props }: ButtonHasFocusProps) => { + const ref = useRef(null) + useEffect(() => { + if (isFocus && ref.current) { + ref.current.focus() + } + }, [isFocus]) + + return ( + // eslint-disable-next-line react/button-has-type + + ) +} + +interface Option { + value: number + title: string + label: string + selectable: boolean +} +export const useSelectorFocusControl = (value: number, options: Option[], onChange: (option: Option) => void) => { + const [focusIndex, setFocusIndex] = useState(-1) + + useEffect(() => { + setFocusIndex(options.findIndex(option => option.value === value)) + }, [value]) + + function moveBackward() { + const index = focusIndex - 1 + if (options[index].selectable) { + setFocusIndex(index) + } + } + function moveForward() { + const index = focusIndex + 1 + if (options[index].selectable) { + setFocusIndex(index) + } + } + + const onKeyDown = (e: KeyboardEvent) => { + const keyEventMap = { + Enter: () => onChange(options[focusIndex]), + ' ': () => onChange(options[focusIndex]), + + ArrowLeft: () => moveBackward(), + ArrowRight: () => moveForward(), + ArrowUp: () => moveBackward(), + ArrowDown: () => moveForward(), + } + if (Object.keys(keyEventMap).includes(e.key)) { + e.preventDefault() + e.stopPropagation() + + keyEventMap[e.key as keyof typeof keyEventMap]() + } + } + + return { focusIndex, onKeyDown } +} + +export const useTableFocusControl = ( + value: Date | undefined, + minDate: Date | undefined, + maxDate: Date | undefined, + year: number, + month: number, + prevMonth: () => void, + nextMonth: () => void, + onChange: (value: Date) => void +) => { + const [focusDate, setFocusDate] = useState(value || new Date()) + + function moveNextMonth() { + if (isMonthInRange(focusDate.getFullYear(), focusDate.getMonth() + 2, { minDate, maxDate })) { + nextMonth() + } + } + function movePrevMonth() { + if (isMonthInRange(focusDate.getFullYear(), focusDate.getMonth(), { minDate, maxDate })) { + prevMonth() + } + } + function moveDate(diff: number) { + const date = new Date(focusDate) + date.setDate(date.getDate() + diff) + if (date.getMonth() !== focusDate.getMonth() && date > focusDate) { + moveNextMonth() + } + if (date.getMonth() !== focusDate.getMonth() && date < focusDate) { + movePrevMonth() + } + if (isDayInRange(date, { minDate, maxDate })) { + setFocusDate(date) + } + } + function moveBackward(date: Date) { + if (isDayInRange(date, { minDate, maxDate })) { + setFocusDate(date) + } else { + setFocusDate(minDate as Date) + } + } + function moveForward(date: Date) { + if (isDayInRange(date, { minDate, maxDate })) { + setFocusDate(date) + } else { + setFocusDate(maxDate as Date) + } + } + + useEffect(() => { + setFocusDate(value || new Date()) + }, [value?.toDateString()]) + + useEffect(() => { + if (focusDate.getFullYear() !== year || focusDate.getMonth() + 1 !== month) { + moveBackward(new Date(year, month - 1, 1)) + } + }, [year, month]) + + const onKeyDown = (e: KeyboardEvent) => { + const keyEventMap = { + Enter: () => onChange(focusDate), + ' ': () => onChange(focusDate), + + ArrowLeft: () => moveDate(-1), + ArrowRight: () => moveDate(1), + ArrowUp: () => moveDate(-7), + ArrowDown: () => moveDate(7), + + PageUp() { + movePrevMonth() + const date = new Date(focusDate) + date.setMonth(focusDate.getMonth() - 1) + moveBackward(date) + }, + PageDown() { + moveNextMonth() + const date = new Date(focusDate) + date.setMonth(focusDate.getMonth() + 1) + moveForward(date) + }, + Home() { + const date = new Date(focusDate) + date.setDate(1) + moveBackward(date) + }, + End() { + const date = new Date(focusDate) + date.setMonth(focusDate.getMonth() + 1) + date.setDate(0) + moveForward(date) + }, + } + + if (Object.keys(keyEventMap).includes(e.key)) { + e.preventDefault() + e.stopPropagation() + + keyEventMap[e.key as keyof typeof keyEventMap]() + } + } + + return { focusDate, onKeyDown } +} + +export default useTableFocusControl diff --git a/packages/neuron-ui/src/widgets/Calendar/index.tsx b/packages/neuron-ui/src/widgets/Calendar/index.tsx index af1a574219..6b556cc1e6 100644 --- a/packages/neuron-ui/src/widgets/Calendar/index.tsx +++ b/packages/neuron-ui/src/widgets/Calendar/index.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { getMonthCalendar, getLocalMonthNames, + getLocalMonthShortNames, getLocalWeekNames, isMonthInRange, isYearInRange, @@ -10,40 +11,61 @@ import { isDayInRange, WeekDayRange, } from './utils' +import { ButtonHasFocus, useTableFocusControl, useSelectorFocusControl } from './focusControl' import styles from './calendar.module.scss' interface Option { value: number title: string + label: string selectable: boolean } -const Selector = ({ options, onChange }: { options: Option[]; onChange: (option: Option) => void }) => ( -
    - {options.map(option => ( -
  1. - -
  2. - ))} -
-) +interface SelectorProps { + value: number + options: Option[] + onChange: (option: Option) => void +} +const Selector = ({ value, options, onChange }: SelectorProps) => { + const { focusIndex, onKeyDown } = useSelectorFocusControl(value, options, onChange) + return ( +
    + {options.map((option, idx) => ( +
  1. + onChange(option)} + disabled={!option.selectable} + > + {option.title} + +
  2. + ))} +
+ ) +} export interface CalendarProps { value: Date | undefined onChange: (value: Date) => void - firstDayOfWeek?: WeekDayRange minDate?: Date maxDate?: Date + firstDayOfWeek?: WeekDayRange + className?: string } -const Calendar: React.FC = ({ value, onChange, minDate, maxDate, firstDayOfWeek = 0 }) => { +const Calendar: React.FC = ({ + value, + onChange, + minDate, + maxDate, + firstDayOfWeek = 0, + className = '', +}) => { const [year, setYear] = useState(new Date().getFullYear()) const [month, setMonth] = useState(new Date().getMonth() + 1) const [status, setStatus] = useState<'year' | 'month' | 'date'>('date') @@ -53,20 +75,53 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate, setMonth((value?.getMonth() ?? new Date().getMonth()) + 1) }, [value?.toDateString()]) - const [, { language }] = useTranslation() + const [t, { language }] = useTranslation() const monthNames = useMemo(() => getLocalMonthNames(language), [language]) + const monthShortNames = useMemo(() => getLocalMonthShortNames(language), [language]) const weekNames = useMemo(() => getLocalWeekNames(language), [language]) const weekTitle = useMemo(() => Array.from({ length: 7 }, (_, i) => weekNames[(i + firstDayOfWeek) % 7]), [weekNames]) const monthName = monthNames[month - 1] + const monthShortName = monthShortNames[month - 1] - const calendar = useMemo(() => getMonthCalendar(year, month, firstDayOfWeek), [year, month, firstDayOfWeek]) + const calendar = useMemo(() => getMonthCalendar(year, month, firstDayOfWeek, language), [ + year, + month, + firstDayOfWeek, + language, + ]) function isDisabledTime(date: Date): boolean { return !isDayInRange(date, { minDate, maxDate }) } + const prevMonth = () => { + if (month > 1) { + setMonth(m => m - 1) + } else { + setYear(y => y - 1) + setMonth(12) + } + } + const nextMonth = () => { + if (month < 12) { + setMonth(m => m + 1) + } else { + setYear(y => y + 1) + setMonth(1) + } + } + const { focusDate, onKeyDown } = useTableFocusControl( + value, + minDate, + maxDate, + year, + month, + prevMonth, + nextMonth, + onChange + ) const calendarTable = ( - - +
+ {weekTitle.map(weekname => ( {week.map(date => ( - ))} @@ -101,29 +159,9 @@ const Calendar: React.FC = ({ value, onChange, minDate, maxDate,
) - const prevMonth = () => { - if (month > 1) { - setMonth(m => m - 1) - } else { - setYear(y => y - 1) - setMonth(12) - } - } - const nextMonth = () => { - if (month < 12) { - setMonth(m => m + 1) - } else { - setYear(y => y + 1) - setMonth(1) - } - } - const calendarHeader = ( -
- -
+
+

-

- -
+ +