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/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/tests/calendar/index.test.ts b/packages/neuron-ui/src/tests/calendar/index.test.ts new file mode 100644 index 0000000000..ca492f217f --- /dev/null +++ b/packages/neuron-ui/src/tests/calendar/index.test.ts @@ -0,0 +1,154 @@ +import { + isDayInRange, + isMonthInRange, + isYearInRange, + isDateEqual, + getMonthCalendar, + getLocalMonthNames, + getLocalMonthShortNames, + getLocalWeekNames, +} 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.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'], + ['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.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'], + ['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'], + ]) + }) +}) + +describe('Get Local Month Short Names', () => { + it('Chinese', () => { + const names = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] + expect(getLocalMonthShortNames('zh')).toEqual(names) + }) + + it('English', () => { + const names = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'] + expect(getLocalMonthShortNames('en')).toEqual(names) + }) +}) + +describe('Get Local Month Names', () => { + it('Chinese', () => { + const names = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'] + expect(getLocalMonthNames('zh')).toEqual(names) + }) + + it('English', () => { + const names = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ] + 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/calendar.module.scss b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss new file mode 100644 index 0000000000..0f51e13129 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Calendar/calendar.module.scss @@ -0,0 +1,130 @@ +@import '../../styles/mixin.scss'; + +.srOnly { + position: absolute; + top: -10000px; + left: -10000px; +} + +@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; + min-width: 0; + &:hover, &:focus { + @include semi-bold-text; + background-color: #efefef; + } + &[disabled] { + cursor: not-allowed; + opacity: 0.5; + box-shadow: none !important; + pointer-events: none; + &:hover { + background-color: transparent; + } + } +} + +.calendar { + .calOptions { + width: 100%; + display: flex; + flex-wrap: wrap; + 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; + } + } + } +} + +.calendarHeader { + display: flex; + justify-content: space-between; + border-bottom: 1px solid #eee; + margin-top: 10px; + margin-bottom: 10px; + + .calPrev, .calNext { + @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; + height: 100%; + min-width: 100px; + margin-right: 10px; + } + } +} + +.calendarTable { + width: 100%; + + td { + text-align: center; + } + + .calTableHeader { + @include semi-bold-text; + font-size: 13px; + } + + .calDateItem { + @include button; + width: 30px; + height: 30px; + border-radius: 50%; + &[aria-current="date"] { + color: var(--nervos-green); + } + &[aria-pressed="true"] { + background-color: var(--nervos-green); + color: white; + } + } +} 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..7b2eaae4cd --- /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 & { + isFocusable: boolean + isMoveFocus: boolean +} +export const ButtonHasFocus = ({ isFocusable, isMoveFocus, children, ...props }: ButtonHasFocusProps) => { + const ref = useRef(null) + useEffect(() => { + if (isFocusable && isMoveFocus && ref.current) { + ref.current.focus() + } else { + // ignore + } + }, [isFocusable, isMoveFocus]) + + 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, options]) + + 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, + calendarYear: number, + calendarMonth: number, + setYear: (month: number) => void, + setMonth: (month: number) => void, + onChange: (value: Date) => void +) => { + const [focusDate, setFocusDate] = useState(value || new Date()) + const curFocusYear = focusDate.getFullYear() + const curFocusMonth = focusDate.getMonth() + 1 + const curFocusDate = focusDate.getDate() + + function moveDate(year: number, month: number, date: number) { + if (!isMonthInRange(year, month, { minDate, maxDate })) { + return + } + setYear(year) + setMonth(month) + + const daysInMonth = new Date(year, month, 0).getDate() + const instance = new Date(year, month - 1, daysInMonth < date ? daysInMonth : date) + + if (!isDayInRange(instance, { minDate, maxDate })) { + if (isDayInRange(instance, { minDate })) { + setFocusDate(maxDate as Date) + } else { + setFocusDate(minDate as Date) + } + } else { + setFocusDate(instance) + } + } + + function moveDateDiff(diff: number) { + const instance = new Date(focusDate) + instance.setDate(instance.getDate() + diff) + moveDate(instance.getFullYear(), instance.getMonth() + 1, instance.getDate()) + } + + useEffect(() => { + const instance = value || new Date() + moveDate(instance.getFullYear(), instance.getMonth() + 1, instance.getDate()) + }, [value?.toDateString(), minDate?.toDateString(), maxDate?.toDateString()]) + + useEffect(() => { + moveDate(calendarYear, calendarMonth, focusDate.getDate()) + }, [calendarYear, calendarMonth, minDate?.toDateString(), maxDate?.toDateString()]) + + const onKeyDown = (e: KeyboardEvent) => { + const keyEventMap = { + Enter: () => onChange(focusDate), + ' ': () => onChange(focusDate), + + ArrowLeft: () => moveDateDiff(-1), + ArrowRight: () => moveDateDiff(1), + ArrowUp: () => moveDateDiff(-7), + ArrowDown: () => moveDateDiff(7), + + PageUp() { + if (curFocusMonth <= 1) { + moveDate(curFocusYear - 1, 12, curFocusDate) + } else { + moveDate(curFocusYear, curFocusMonth - 1, curFocusDate) + } + }, + PageDown() { + if (curFocusMonth >= 12) { + moveDate(curFocusYear + 1, 1, curFocusDate) + } else { + moveDate(curFocusYear, curFocusMonth + 1, curFocusDate) + } + }, + Home() { + moveDate(curFocusYear, curFocusMonth, 1) + }, + End() { + moveDate(curFocusYear, curFocusMonth, 100) + }, + } + + if (Object.keys(keyEventMap).includes(e.key)) { + e.preventDefault() + e.stopPropagation() + + keyEventMap[e.key as keyof typeof keyEventMap]() + } + } + + return { focusDate, onKeyDown } +} + +export const useFocusObserve = () => { + const [isComponetFocused, setIsComponetFocused] = useState(false) + + const onFocus = () => setIsComponetFocused(true) + const onBlur = () => setIsComponetFocused(false) + + return { isComponetFocused, onFocus, onBlur } +} 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..e7c6b71b2d --- /dev/null +++ b/packages/neuron-ui/src/widgets/Calendar/index.tsx @@ -0,0 +1,264 @@ +import React, { useState, useEffect, useMemo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { + getMonthCalendar, + getLocalMonthNames, + getLocalMonthShortNames, + getLocalWeekNames, + isMonthInRange, + isYearInRange, + isDateEqual, + isDayInRange, + WeekDayRange, +} from './utils' +import { ButtonHasFocus, useTableFocusControl, useSelectorFocusControl, useFocusObserve } from './focusControl' +import styles from './calendar.module.scss' + +interface Option { + value: number + title: string + label: string + selectable: boolean +} +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 + minDate?: Date + maxDate?: Date + firstDayOfWeek?: WeekDayRange + className?: string +} +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') + + useEffect(() => { + setYear(value?.getFullYear() ?? new Date().getFullYear()) + setMonth((value?.getMonth() ?? new Date().getMonth()) + 1) + }, [value?.toDateString()]) + + const [uId] = useState(() => (+new Date()).toString(16).slice(-4)) + + 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, 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, + setYear, + setMonth, + onChange + ) + const { isComponetFocused, ...focusListeners } = useFocusObserve() + const calendarTable = ( + + + + {weekTitle.map(weekname => ( + + ))} + + + + {calendar.map(week => ( + + {week.map(date => ( + + ))} + + ))} + +
+ onChange(date.instance)} + > + {date.date} + +
+ ) + + const calendarHeader = ( +
+

+ + +

+
+ ) + + const monthOptions: Option[] = Array.from({ length: 12 }, (_, index) => ({ + value: index + 1, + title: monthShortNames[index], + label: 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}`, + label: `${year - 6 + index}`, + selectable: isYearInRange(year - 6 + index, { minDate, maxDate }), + })) + const onChangeMonth = useCallback((monthOptionItem: Option) => { + setMonth(monthOptionItem.value) + setStatus('date') + }, []) + const onChangeYear = useCallback( + (yearOptionItem: Option) => { + setYear(yearOptionItem.value) + if (!isMonthInRange(yearOptionItem.value, month, { minDate, maxDate })) { + setMonth((minDate?.getMonth() || 0) + 1) + } + setStatus('month') + }, + [month, minDate?.toDateString(), maxDate?.toDateString()] + ) + + return ( +
+ {calendarHeader} + {status === 'date' && calendarTable} + {status === 'year' && } + {status === 'month' && } +
+ ) +} + +export default React.memo( + Calendar, + (prevProps, nextProps) => + prevProps.value?.toDateString() === nextProps.value?.toDateString() && + prevProps.minDate?.toDateString() === nextProps.minDate?.toDateString() && + prevProps.maxDate?.toDateString() === nextProps.maxDate?.toDateString() && + prevProps.className === nextProps.className && + 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 new file mode 100644 index 0000000000..15d8033c32 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Calendar/utils.ts @@ -0,0 +1,115 @@ +export interface Day { + instance: Date + year: number + month: number + date: number + weekday: number + isCurMonth: boolean + isToday: boolean + label: string +} + +interface DateRange { + minDate?: Date + maxDate?: Date +} + +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) + + if (range.minDate !== undefined && dayEnd <= range.minDate) { + return false + } + if (range.maxDate !== undefined && dayBegin > range.maxDate) { + return false + } + return true +} + +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 !== undefined && monthEnd <= range.minDate) { + return false + } + if (range.maxDate !== undefined && monthBegin > range.maxDate) { + return false + } + return true +} + +export function isYearInRange(year: number, range: DateRange): boolean { + if (range.minDate !== undefined && year < range.minDate.getFullYear()) { + return false + } + if (range.maxDate !== undefined && year > range.maxDate.getFullYear()) { + return false + } + return true +} + +export function isDateEqual(a: Date | undefined, b: Date | undefined): boolean { + 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, firstDayOfWeek: WeekDayRange = 0, lang = 'en'): Day[][] { + const today = new Date() + const weekdayOfFirstDay = new Date(year, month - 1, 1).getDay() + const DAYS_IN_WEEK = 7 + const ROWS_IN_CALENDAR = 6 + const numOfDaysInCalendar = DAYS_IN_WEEK * ROWS_IN_CALENDAR + + const dateList: Day[] = [] + const formater = new Intl.DateTimeFormat(lang, { dateStyle: 'full' }) + + for (let i = 1; i <= numOfDaysInCalendar; i++) { + const instance = new Date(year, month - 1, ((firstDayOfWeek - weekdayOfFirstDay - 7) % 7) + i) + const day: Day = { + instance, + year: instance.getFullYear(), + month: instance.getMonth() + 1, + date: instance.getDate(), + weekday: instance.getDay(), + isCurMonth: instance.getMonth() + 1 === month, + isToday: instance.toDateString() === today.toDateString(), + label: formater.format(instance), + } + 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 getLocalMonthShortNames = (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)))}${lang.startsWith('en') ? '.' : ''}` + ) +} + +export const getLocalMonthNames = (lang: string) => { + const formater = new Intl.DateTimeFormat(lang, { month: 'long' }) + return Array.from({ length: 12 }, (_, i) => formater.format(new Date(Date.UTC(2023, i, 1)))) +} + +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/datetimePicker.module.scss b/packages/neuron-ui/src/widgets/DatetimePicker/datetimePicker.module.scss index bf9be01d42..dc0ab87ccd 100644 --- a/packages/neuron-ui/src/widgets/DatetimePicker/datetimePicker.module.scss +++ b/packages/neuron-ui/src/widgets/DatetimePicker/datetimePicker.module.scss @@ -1,128 +1,8 @@ @import '../../styles/mixin.scss'; $width: 374px; -:global { - .p-calendar { - @include regular-text; - - width: $width; - padding: 20px 0; - border: 1px solid #eee; - border-color: #eee transparent; - display: flex; - justify-content: center; - align-items: center; - - .p-datepicker-today span { - color: var(--nervos-green); - pointer-events: none; - } - - .p-datepicker-header { - display: flex; - justify-content: space-between; - padding: 0 10px; - margin-bottom: 16px; - - .p-datepicker-next.p-link { - order: 1; - } - - @mixin chevron-line { - display: block; - content: ''; - width: 8px; - height: 2px; - background: #000; - position: absolute; - } - - button { - background-color: transparent; - border: none; - font-weight: bolder; - position: relative; - padding: 0; - width: 10px; - } - - .pi-chevron-left { - &::before { - @include chevron-line; - left: 0; - top: 50%; - transform-origin: left center; - position: absolute; - transform: rotate(-45deg) translateY(50%); - } - - &::after { - @include chevron-line; - left: 0; - top: 50%; - transform-origin: left center; - transform: rotate(45deg) translateY(-50%); - } - } - - .pi-chevron-right { - &::before { - @include chevron-line; - right: 0; - top: 50%; - transform-origin: right center; - position: absolute; - transform: rotate(-45deg) translateY(-50%); - } - - &::after { - @include chevron-line; - right: 0; - top: 50%; - transform-origin: right center; - transform: rotate(45deg) translateY(50%); - } - } - - .p-datepicker-month { - margin-right: 5px; - } - } - - .p-highlight { - background: var(--nervos-green); - color: #fff !important; - } - - .p-disabled { - opacity: 0.2; - } - - th { - color: rgba(0, 0, 0, 0.38); - font-size: 12px; - } - - td { - span { - display: flex; - justify-content: center; - align-items: center; - width: 30px; - height: 30px; - border-radius: 50%; - color: #000; - - font-size: 12px; - margin: auto 10px; - - &:not(.p-disabled):hover { - color: #fff; - background-color: var(--nervos-green); - } - } - } - } +.calendar { + width: $width; } .container { diff --git a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx index dc09f7e562..7833442376 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(date)) setStatus('done') }, [setDisplay, setStatus] @@ -155,14 +128,7 @@ const DatetimePicker = ({ onKeyPress={onKeyPress} /> )} - + {isSinceTomorrow ? null : {t('datetime.start-tomorrow')}} {notice ? (