-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f1dae04
commit 25eae8a
Showing
14 changed files
with
513 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import React from "react"; | ||
import { StoryObj } from "@storybook/react"; | ||
|
||
import Calendar, { CalendarProps } from "./Calendar"; | ||
import dayjs from "dayjs"; | ||
|
||
export default { | ||
title: "Components/Inputs/Calendar", | ||
components: Calendar, | ||
}; | ||
|
||
export const Default: StoryObj<CalendarProps> = { | ||
render: () => { | ||
const [date, setDate] = React.useState(dayjs()); | ||
return <Calendar date={date} onChange={setDate} />; | ||
}, | ||
}; | ||
|
||
export const WithActions: StoryObj<CalendarProps> = { | ||
render: () => { | ||
const [date, setDate] = React.useState(dayjs()); | ||
const actions = [ | ||
{ | ||
text: "今日", | ||
onClick: () => setDate(dayjs()), | ||
}, | ||
{ | ||
text: "来週", | ||
onClick: () => setDate(dayjs().add(1, "week")), | ||
}, | ||
{ | ||
text: "来月", | ||
onClick: () => setDate(dayjs().add(1, "month")), | ||
}, | ||
]; | ||
return <Calendar date={date} actions={actions} onChange={setDate} />; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import dayjs, { Dayjs } from "dayjs"; | ||
import { Card, ScrollArea, Typography } from ".."; | ||
import React, { forwardRef, memo, useRef } from "react"; | ||
import { Day } from "./internal/Day"; | ||
import { weekList, HEIGHT } from "./constants"; | ||
import { | ||
Container, | ||
CalendarContainer, | ||
DatePickerContainer, | ||
DayStyle, | ||
CalendarMonth, | ||
} from "./styled"; | ||
import { useScroll } from "./hooks/useScroll"; | ||
import { Action, Actions } from "./internal/Actions"; | ||
|
||
export type CalendarProps = { | ||
date: Dayjs; | ||
actions?: Action[]; | ||
onChange: (value: Dayjs) => void; | ||
}; | ||
|
||
const Calendar = forwardRef<HTMLDivElement, CalendarProps>(function Calendar({ | ||
date, | ||
actions, | ||
onChange, | ||
}) { | ||
const ref = useRef<HTMLDivElement>(null); | ||
const { monthList } = useScroll(date, ref); | ||
|
||
return ( | ||
<Card ref={ref} display="flex" style={{ width: "fit-content" }}> | ||
<Actions actions={actions} /> | ||
<Container> | ||
<ScrollArea | ||
ref={ref} | ||
minHeight={HEIGHT} | ||
maxHeight={HEIGHT} | ||
id="calendar" | ||
> | ||
<> | ||
{monthList.map((m) => ( | ||
<DatePickerContainer | ||
key={m.format("YYYY-MM")} | ||
id={m.format("YYYY-MM")} | ||
className={m.format("YYYY-MM")} | ||
> | ||
{/* 年月の表示 */} | ||
<CalendarMonth> | ||
<Typography weight="bold" size="xl"> | ||
{m.format("YYYY年MM月")} | ||
</Typography> | ||
</CalendarMonth> | ||
|
||
{/* カレンダーの表示 */} | ||
<CalendarContainer> | ||
{/* 曜日の表示 */} | ||
{weekList["ja"].map((week) => ( | ||
<DayStyle key={week}>{week}</DayStyle> | ||
))} | ||
|
||
{/* 開始曜日まで空白をセット */} | ||
{Array.from(new Array(m.startOf("month").day()), (_, i) => ( | ||
<DayStyle key={i} /> | ||
))} | ||
|
||
{/* 日付の表示 */} | ||
{Array.from(new Array(m.daysInMonth()), (_, i) => i + 1).map( | ||
(day) => ( | ||
<Day | ||
key={day} | ||
value={dayjs(new Date(m.year(), m.month(), day))} | ||
// ややこしいけど、ここでのselectedは、選択中の日付かどうかを判定している | ||
// つまり、選択中の日付の場合はtrueになり、style で色を変える | ||
selected={ | ||
date.format("YYYY-MM-DD") === | ||
dayjs(new Date(m.year(), m.month(), day)).format( | ||
"YYYY-MM-DD", | ||
) | ||
} | ||
onClickDate={onChange} | ||
> | ||
{day} | ||
</Day> | ||
), | ||
)} | ||
</CalendarContainer> | ||
</DatePickerContainer> | ||
))} | ||
</> | ||
</ScrollArea> | ||
</Container> | ||
</Card> | ||
); | ||
}); | ||
|
||
export default memo(Calendar); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// for `<ScrollArea />` | ||
export const HEIGHT = "400px"; | ||
|
||
export const weekList = { | ||
en: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], | ||
ja: ["日", "月", "火", "水", "木", "金", "土"], | ||
}; | ||
|
||
export const monthList = { | ||
en: [ | ||
"Jan", | ||
"Feb", | ||
"Mar", | ||
"Apr", | ||
"May", | ||
"June", | ||
"July", | ||
"Aug", | ||
"Sept", | ||
"Oct", | ||
"Nov", | ||
"Dec", | ||
], | ||
ja: [ | ||
"1月", | ||
"2月", | ||
"3月", | ||
"4月", | ||
"5月", | ||
"6月", | ||
"7月", | ||
"8月", | ||
"9月", | ||
"10月", | ||
"11月", | ||
"12月", | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { renderHook, act } from "@testing-library/react"; | ||
import dayjs, { Dayjs } from "dayjs"; | ||
import { useScroll, getNextMonthList, getPrevMonthList } from "../useScroll"; | ||
|
||
(global as any).IntersectionObserver = jest.fn().mockImplementation(() => ({ | ||
observe: () => jest.fn(), | ||
unobserve: () => jest.fn(), | ||
disconnect: () => jest.fn(), | ||
})); | ||
|
||
describe("useScroll hook", () => { | ||
let date: Dayjs; | ||
let ref: React.RefObject<HTMLDivElement>; | ||
|
||
beforeEach(() => { | ||
date = dayjs("2021-01-01"); | ||
ref = { current: document.createElement("div") }; | ||
}); | ||
|
||
test("loads next six months when reaching the bottom 10% of ScrollArea", () => { | ||
const { result } = renderHook(() => useScroll(date, ref)); | ||
|
||
act(() => { | ||
// const targets = document.getElementsByClassName(date.format("YYYY-MM")); | ||
}); | ||
|
||
const months = [...getPrevMonthList(date), ...getNextMonthList(date)].map( | ||
(d) => d.format("YYYY-MM"), | ||
); | ||
expect(result.current.monthList.map((m) => m.format("YYYY-MM"))).toEqual( | ||
expect.arrayContaining(months), | ||
); | ||
}); | ||
|
||
test("loads previous six months when reaching the top 10% of ScrollArea", () => { | ||
const { result } = renderHook(() => useScroll(date, ref)); | ||
|
||
act(() => { | ||
// const targets = document.getElementsByClassName(date.format("YYYY-MM")); | ||
}); | ||
|
||
const months = [...getPrevMonthList(date), ...getNextMonthList(date)].map( | ||
(d) => d.format("YYYY-MM"), | ||
); | ||
expect(result.current.monthList.map((m) => m.format("YYYY-MM"))).toEqual( | ||
expect.arrayContaining(months), | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// MEMO: 3ヶ月でもいいかも? | ||
export const MONTH_SIZE = 4; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { Dayjs } from "dayjs"; | ||
import { useEffect, useState } from "react"; | ||
import { MONTH_SIZE } from "./constants"; | ||
|
||
export const getNextMonthList = (date: Dayjs) => | ||
Array.from(new Array(MONTH_SIZE)).map((_, i) => date.add(i, "month")); | ||
|
||
export const getPrevMonthList = (date: Dayjs) => | ||
Array.from(new Array(MONTH_SIZE)).map((_, i) => | ||
date.subtract(MONTH_SIZE - i, "month"), | ||
); | ||
|
||
/** | ||
* @memo カレンダーを選択中の月にするときに、アニメーション等があるといいかもしれない | ||
* | ||
* @param date 選択中の日付 | ||
* @param ref カレンダーの親要素のref、IntersectionObserverのrootに使う | ||
* @return monthList 表示する月のリスト | ||
*/ | ||
export const useScroll = ( | ||
date: Dayjs, | ||
ref: React.RefObject<HTMLDivElement>, | ||
) => { | ||
// 読み込み済みの日付を保持する | ||
const [loaded, setLoaded] = useState<{ | ||
prev: Dayjs; | ||
next: Dayjs; | ||
}>({ | ||
prev: date.subtract(MONTH_SIZE, "month"), | ||
next: date.add(MONTH_SIZE, "month"), | ||
}); | ||
// 表示する月のリスト | ||
// この hooks の戻り値 | ||
const [monthList, setMonthList] = useState<Dayjs[]>([ | ||
...getPrevMonthList(date), | ||
...getNextMonthList(date), | ||
]); | ||
|
||
useEffect(() => { | ||
// 全てのカレンダーに 2023-06 のような名前の className を振ってある | ||
const targets = document.getElementsByClassName(date.format("YYYY-MM")); | ||
for (const target of Array.from(targets)) { | ||
target.scrollIntoView({ block: "center" }); | ||
} | ||
}, [date]); | ||
|
||
useEffect(() => { | ||
setLoaded({ | ||
prev: date.subtract(MONTH_SIZE, "month"), | ||
next: date.add(MONTH_SIZE, "month"), | ||
}); | ||
setMonthList([...getPrevMonthList(date), ...getNextMonthList(date)]); | ||
}, [date]); | ||
|
||
// next を読み込む | ||
useEffect(() => { | ||
const observer = new IntersectionObserver( | ||
(entries) => { | ||
entries.forEach((entry) => { | ||
if (entry.isIntersecting) { | ||
// next、prev をそれぞれ MONTH_SIZE 分ずらす | ||
const next = loaded.next.add(MONTH_SIZE, "month"); | ||
const prev = loaded.prev.add(MONTH_SIZE, "month"); | ||
|
||
// prev と next の月のリストを取得 | ||
const prevYearMonthList = getPrevMonthList(loaded.next); | ||
const nextYearMonthList = getNextMonthList(loaded.next); | ||
|
||
setLoaded({ next, prev }); | ||
setMonthList([...prevYearMonthList, ...nextYearMonthList]); | ||
} | ||
}); | ||
}, | ||
{ | ||
root: ref.current, | ||
threshold: 0.1, | ||
}, | ||
); | ||
|
||
const targets = document.getElementsByClassName( | ||
loaded.next.subtract(1, "month").format("YYYY-MM"), | ||
); | ||
|
||
for (const target of Array.from(targets)) { | ||
observer.observe(target); | ||
} | ||
|
||
return () => { | ||
for (const target of Array.from(targets)) { | ||
observer.unobserve(target); | ||
} | ||
}; | ||
}, [loaded, ref]); | ||
|
||
// prev を読み込む | ||
useEffect(() => { | ||
const observer = new IntersectionObserver( | ||
(entries) => { | ||
entries.forEach((entry) => { | ||
if (entry.isIntersecting) { | ||
// next、prev をそれぞれ MONTH_SIZE 分ずらす | ||
const next = loaded.next.subtract(MONTH_SIZE, "month"); | ||
const prev = loaded.prev.subtract(MONTH_SIZE, "month"); | ||
|
||
// prev と next の月のリストを取得 | ||
const prevYearMonthList = getPrevMonthList(loaded.prev); | ||
const nextYearMonthList = getNextMonthList(loaded.prev); | ||
|
||
setLoaded({ next, prev }); | ||
setMonthList([...prevYearMonthList, ...nextYearMonthList]); | ||
} | ||
}); | ||
}, | ||
{ | ||
root: ref.current, | ||
threshold: 0.1, | ||
}, | ||
); | ||
|
||
const targets = document.getElementsByClassName( | ||
loaded.prev.add(1, "month").format("YYYY-MM"), | ||
); | ||
|
||
for (const target of Array.from(targets)) { | ||
observer.observe(target); | ||
} | ||
|
||
return () => { | ||
for (const target of Array.from(targets)) { | ||
observer.unobserve(target); | ||
} | ||
}; | ||
}, [loaded, ref]); | ||
|
||
return { monthList }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { Calendar } from "./Calendar"; |
Oops, something went wrong.