Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore: Calendarの内部処理を整理する #5346

Open
wants to merge 50 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3218dc1
chore: Calendar/YearPickerのthisYear,yearArray変数の生成をmemo化
AtsushiM Jan 28, 2025
e275b9e
chore: Calendar/YearPickerのyearArrayの生成ロジックを最適化
AtsushiM Jan 28, 2025
df5c1f0
chore: Calendar/YearPickerのonClickYearをuseCallbackする
AtsushiM Jan 28, 2025
c439a55
chore: Calendar/YearPickerのstyle生成ロジックを調整
AtsushiM Jan 28, 2025
b635bd9
chore: Calendar/YearPicker/YearButtonをmemo化
AtsushiM Jan 28, 2025
f0b9692
chore: CalendarのonSelectYearに関する処理を最適化
AtsushiM Jan 28, 2025
2b8330e
Merge branch 'master' of https://github.com/kufu/smarthr-ui into chor…
AtsushiM Jan 28, 2025
25dc445
chore: CalendarTableのstyle生成を適切にmemo化
AtsushiM Jan 28, 2025
f37cecd
chore: CalendarTableのdateCell styleをあらかじめ生成しきっておく方法に修正
AtsushiM Jan 28, 2025
c16beff
chore: CalendarTableのcurrentDayをmemo化
AtsushiM Jan 28, 2025
28abeb3
chore: CalendarTableのnowの計算を必要になるまで実行しないように修正
AtsushiM Jan 28, 2025
caeb14f
chore: CalendarTableのfromDate,toDateをmemo化
AtsushiM Jan 28, 2025
a5c66eb
chore: CalendarTableのmonthsをmemo化
AtsushiM Jan 28, 2025
9a81dd4
chore: CalendarTableのisSelectedDateの生成を最適化
AtsushiM Jan 28, 2025
11bd9b7
chore: CalendarTableのisOutRange. isSelectedDateの生成を最適化
AtsushiM Jan 28, 2025
260edfa
chore: CalendarTableのtdレンダリング判定を整理
AtsushiM Jan 28, 2025
ee3a967
chore: CalendarTableのcompareDay, compareDateの概念を整理
AtsushiM Jan 28, 2025
e48fe85
chore: CalendarTableのonClick内のdisabled判定は無駄なので削除
AtsushiM Jan 28, 2025
1e2b234
chore: CalendarTableのSelectButtonTdを切り出す
AtsushiM Jan 28, 2025
a94ea9f
chore: CalendarTableのtarget.day, target.dateをmemo化
AtsushiM Jan 29, 2025
67fd615
chore: CalendarTable/SelectButtonTdのdisabledの生成をmemo化
AtsushiM Jan 29, 2025
8bb5131
chore: CalendarTable/SelectButtonTdのariaPressedの生成をmemo化
AtsushiM Jan 29, 2025
ac721b9
chore: CalendarTable/SelectButtonTdのdataIsTodayの生成をmemo化
AtsushiM Jan 29, 2025
b2c0c6e
chore: CalendarTable/SelectButtonTdのonClickの生成をmemo化
AtsushiM Jan 29, 2025
f9be950
chore: CalendarTable/SelectButtonTd/SelectButtonTdDateCellを切り出す
AtsushiM Jan 29, 2025
8e30ef4
chore: CalendarTable/NullTdを切り出す
AtsushiM Jan 29, 2025
384b570
chore: CalendarTable/MemoizedTheadを切り出す
AtsushiM Jan 29, 2025
c8dfab7
chore: calendarHelperのgetMonthArray内のdayjs生成を整理
AtsushiM Jan 29, 2025
03dbfbf
chore: calendarHelperのgetMonthArray内のループをforに変更
AtsushiM Jan 29, 2025
c8af16d
chore: Calendarのstyles生成ロジックを整理
AtsushiM Jan 29, 2025
1a13320
chore: Calendarのfroms変数の生成ロジックを整理
AtsushiM Jan 29, 2025
8379269
chore: CalendarのcurrentMonth生成ロジックを整理
AtsushiM Jan 29, 2025
803a08d
chore: CalendarのisValidValueをmemo化
AtsushiM Jan 29, 2025
b2f9e8c
chore: CalendarのdirectionMonthをmemo化
AtsushiM Jan 29, 2025
07bd687
chore: CalendarのcalculatedCurrentMonthを定義
AtsushiM Jan 29, 2025
60c0402
chore: CalendarのonClickMonthPrev,onClickMonthNextをmemo化
AtsushiM Jan 29, 2025
21f2921
chore: CalendarのonClickSelectYearをmemo化
AtsushiM Jan 29, 2025
b12214e
chore: CalendarのMonthDirectionClusterを切り出す
AtsushiM Jan 29, 2025
e33e11b
chore: CalendarのMonthDirectionClusterを切り出す
AtsushiM Jan 29, 2025
3aab919
chore: CalendarのYearSelectButtonを切り出す
AtsushiM Jan 29, 2025
058d87b
chore: CalendarのYearSelectButtonを切り出す
AtsushiM Jan 29, 2025
46cea3f
chore: Calendar内にHeadingが存在しないため、Sectionでのマークアップを辞める
AtsushiM Jan 29, 2025
b05fa3d
chore: fix ci
AtsushiM Jan 29, 2025
6d711ba
chore: CalendarのselectedDayStrの精製方法を整理
AtsushiM Jan 29, 2025
6cc7932
chore: CalendarTableのcurrentに関する処理を最適化
AtsushiM Jan 29, 2025
177113b
chore: CalendarTableのfrom, toに関する処理を最適化
AtsushiM Jan 29, 2025
1c2384a
chore: CalendarTableのcurrentに対する処理を整理
AtsushiM Jan 29, 2025
5a054a4
chore: Calendarをリファクタリング
AtsushiM Jan 29, 2025
b1d5e31
chore: style -> className
AtsushiM Feb 6, 2025
96410ea
Merge branch 'master' of https://github.com/kufu/smarthr-ui into chor…
AtsushiM Feb 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 166 additions & 77 deletions packages/smarthr-ui/src/components/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import dayjs from 'dayjs'
import React, {
ComponentProps,
MouseEvent,
PropsWithChildren,
forwardRef,
useCallback,
useEffect,
useId,
useMemo,
Expand All @@ -13,13 +15,12 @@ import React, {
import { tv } from 'tailwind-variants'

import { Button } from '../Button'
import { FaCaretDownIcon, FaCaretUpIcon, FaChevronLeftIcon, FaChevronRightIcon } from '../Icon'
import { FaCaretDownIcon, FaChevronLeftIcon, FaChevronRightIcon } from '../Icon'
import { Cluster } from '../Layout'
import { Section } from '../SectioningContent'

import { CalendarTable } from './CalendarTable'
import { YearPicker } from './YearPicker'
import { getFromDate, getToDate, isBetween, minDate } from './calendarHelper'
import { getFromDate, getMonthArray, getToDate, isBetween, minDate } from './calendarHelper'

type Props = {
/** 選択可能な開始日 */
Expand All @@ -31,7 +32,8 @@ type Props = {
/** 選択された日付 */
value?: Date
}
type ElementProps = Omit<ComponentProps<'section'>, keyof Props>
type ElementProps = Omit<ComponentProps<'div'>, keyof Props>
type DayJsType = ReturnType<typeof dayjs>

const calendar = tv({
slots: {
Expand All @@ -41,108 +43,195 @@ const calendar = tv({
yearMonth: 'smarthr-ui-Calendar-yearMonth shr-me-0.5 shr-text-base shr-font-bold',
monthButtons: 'smarthr-ui-Calendar-monthButtons shr-ms-auto shr-flex',
tableLayout: 'shr-relative',
yearSelectButton:
'smarthr-ui-Calendar-selectingYear [&[aria-expanded="true"]_.smarthr-ui-Icon]:shr-rotate-180',
},
})

export const Calendar = forwardRef<HTMLDivElement, Props & ElementProps>(
({ from = minDate, to, onSelectDate, value, className, ...props }, ref) => {
const { containerStyle, yearMonthStyle, headerStyle, monthButtonsStyle, tableLayoutStyle } =
useMemo(() => {
const { container, yearMonth, header, monthButtons, tableLayout } = calendar()
return {
containerStyle: container({ className }),
headerStyle: header(),
yearMonthStyle: yearMonth(),
monthButtonsStyle: monthButtons(),
tableLayoutStyle: tableLayout(),
const classNames = useMemo(() => {
const { container, yearMonth, header, monthButtons, tableLayout, yearSelectButton } =
calendar()

return {
container: container({ className }),
header: header(),
yearMonth: yearMonth(),
monthButtons: monthButtons(),
tableLayout: tableLayout(),
yearSelectButton: yearSelectButton(),
}
}, [className])

const formattedFrom = useMemo(() => {
const date = getFromDate(from)
const day = dayjs(date)

return {
day,
date,
year: day.year(),
}
}, [from])
const formattedTo = useMemo(() => {
const date = getToDate(to)
const day = dayjs(date)

return {
day,
date,
year: day.year(),
}
}, [to])
Comment on lines +67 to +86
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from, to はほぼ固定で変わる可能性は低いため、memo化しています
またこれらをベースに計算される値も同様です


const isValidValue = useMemo(
() => value && isBetween(value, formattedFrom.date, formattedTo.date),
[value, formattedFrom.date, formattedTo.date],
)

const [currentMonth, setCurrentMonth] = useState(
(() => {
if (isValidValue) {
return dayjs(value)
}
}, [className])
const fromDate = dayjs(getFromDate(from))
const toDate = dayjs(getToDate(to))
const today = dayjs()
const currentDay = toDate.isBefore(today) ? toDate : fromDate.isAfter(today) ? fromDate : today
const isValidValue = value && isBetween(value, fromDate.toDate(), toDate.toDate())

const [currentMonth, setCurrentMonth] = useState(isValidValue ? dayjs(value) : currentDay)

const today = dayjs()

return formattedTo.day.isBefore(today)
? formattedTo.day
: formattedFrom.day.isAfter(today)
? formattedFrom.day
: today
})(),
)
const [isSelectingYear, setIsSelectingYear] = useState(false)

const yearPickerId = useId()

useEffect(() => {
if (value && isValidValue) {
if (isValidValue) {
setCurrentMonth(dayjs(value))
}
}, [value, isValidValue])

const prevMonth = currentMonth.subtract(1, 'month')
const nextMonth = currentMonth.add(1, 'month')
const calculatedCurrentMonth = useMemo(
() => ({
prev: currentMonth.subtract(1, 'month'),
next: currentMonth.add(1, 'month'),
day: currentMonth,
months: getMonthArray(currentMonth.toDate()),
yearMonthStr: `${currentMonth.year()}年${currentMonth.month() + 1}月`,
selectedStr: currentMonth.toString(),
}),
[currentMonth],
)

const onSelectYear = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
setCurrentMonth(currentMonth.year(parseInt(e.currentTarget.value, 10)))
setIsSelectingYear(false)
},
[currentMonth],
)

const onClickSelectYear = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
setIsSelectingYear((current) => !current)
}, [])

return (
// eslint-disable-next-line smarthr/a11y-heading-in-sectioning-content
<Section {...props} ref={ref} className={containerStyle}>
<header className={headerStyle}>
<div className={yearMonthStyle}>
{currentMonth.year()}年{currentMonth.month() + 1}月
</div>
<Button
onClick={(e) => {
e.stopPropagation()
setIsSelectingYear(!isSelectingYear)
}}
size="s"
square
<div {...props} ref={ref} className={classNames.container}>
<header className={classNames.header}>
<YearMonthRender className={classNames.yearMonth}>
{calculatedCurrentMonth.yearMonthStr}
</YearMonthRender>
<YearSelectButton
aria-expanded={isSelectingYear}
aria-controls={yearPickerId}
className="smarthr-ui-Calendar-selectingYear"
>
{isSelectingYear ? (
<FaCaretUpIcon alt="年を選択する" />
) : (
<FaCaretDownIcon alt="年を選択する" />
)}
</Button>
<Cluster gap={0.5} className={monthButtonsStyle}>
<Button
disabled={isSelectingYear || prevMonth.isBefore(fromDate, 'month')}
onClick={() => setCurrentMonth(prevMonth)}
size="s"
square
className="smarthr-ui-Calendar-monthButtonPrev"
>
<FaChevronLeftIcon alt="前の月へ" />
</Button>
<Button
disabled={isSelectingYear || nextMonth.isAfter(toDate, 'month')}
onClick={() => setCurrentMonth(nextMonth)}
size="s"
square
className="smarthr-ui-Calendar-monthButtonNext"
>
<FaChevronRightIcon alt="次の月へ" />
</Button>
</Cluster>
onClick={onClickSelectYear}
className={classNames.yearSelectButton}
/>
<MonthDirectionCluster
isSelectingYear={isSelectingYear}
directionMonth={calculatedCurrentMonth}
from={formattedFrom.day}
to={formattedTo.day}
setCurrentMonth={setCurrentMonth}
className={classNames.monthButtons}
/>
</header>
<div className={tableLayoutStyle}>
<div className={classNames.tableLayout}>
<YearPicker
fromYear={fromDate.year()}
toYear={toDate.year()}
fromYear={formattedFrom.year}
toYear={formattedTo.year}
selectedYear={value?.getFullYear()}
onSelectYear={(year) => {
setCurrentMonth(currentMonth.year(year))
setIsSelectingYear(false)
}}
onSelectYear={onSelectYear}
isDisplayed={isSelectingYear}
id={yearPickerId}
/>
<CalendarTable
current={currentMonth.toDate()}
from={fromDate.toDate()}
to={toDate.toDate()}
current={calculatedCurrentMonth}
from={formattedFrom.date}
to={formattedTo.date}
onSelectDate={onSelectDate}
selected={isValidValue ? value : null}
selectedDayStr={isValidValue ? calculatedCurrentMonth.selectedStr : ''}
/>
</div>
</Section>
</div>
)
},
)

const YearMonthRender = React.memo<PropsWithChildren<{ className: string }>>(
({ children, className }) => <div className={className}>{children}</div>,
)

const YearSelectButton = React.memo<{
'aria-expanded': boolean
'aria-controls': string
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
className: string
}>((rest) => (
<Button {...rest} size="s" square>
<FaCaretDownIcon alt="年を選択する" />
</Button>
))

const MonthDirectionCluster = React.memo<{
isSelectingYear: boolean
directionMonth: {
prev: DayJsType
next: DayJsType
}
from: DayJsType
to: DayJsType
setCurrentMonth: (day: DayJsType) => void
className: string
}>(({ isSelectingYear, directionMonth: { prev, next }, from, to, setCurrentMonth, className }) => {
const onClickMonthPrev = useCallback(() => setCurrentMonth(prev), [prev, setCurrentMonth])
const onClickMonthNext = useCallback(() => setCurrentMonth(next), [next, setCurrentMonth])

return (
<Cluster gap={0.5} className={className}>
<Button
disabled={isSelectingYear || prev.isBefore(from, 'month')}
onClick={onClickMonthPrev}
size="s"
square
className="smarthr-ui-Calendar-monthButtonPrev"
>
<FaChevronLeftIcon alt="前の月へ" />
</Button>
<Button
disabled={isSelectingYear || next.isAfter(to, 'month')}
onClick={onClickMonthNext}
size="s"
square
className="smarthr-ui-Calendar-monthButtonNext"
>
<FaChevronRightIcon alt="次の月へ" />
</Button>
</Cluster>
)
})
Loading