From 8ba519ef62e4e49fbe6078b8c9cded3a5d8f6642 Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Mon, 6 Feb 2023 22:17:53 +0100 Subject: [PATCH 01/10] Calendar: added files --- src/components/Calendar/Calendar.children.tsx | 0 src/components/Calendar/Calendar.stories.tsx | 25 +++++++++++++++++++ src/components/Calendar/Calendar.tsx | 12 +++++++++ src/components/Calendar/Calendar.types.ts | 1 + src/components/index.ts | 1 + 5 files changed, 39 insertions(+) create mode 100644 src/components/Calendar/Calendar.children.tsx create mode 100644 src/components/Calendar/Calendar.stories.tsx create mode 100644 src/components/Calendar/Calendar.tsx create mode 100644 src/components/Calendar/Calendar.types.ts diff --git a/src/components/Calendar/Calendar.children.tsx b/src/components/Calendar/Calendar.children.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Calendar/Calendar.stories.tsx b/src/components/Calendar/Calendar.stories.tsx new file mode 100644 index 0000000..3c213aa --- /dev/null +++ b/src/components/Calendar/Calendar.stories.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import type { Meta, StoryFn, StoryObj } from '@storybook/react' + +import Calendar from './Calendar' + +const meta: Meta = { + /* 👇 The title prop is optional. + * See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'Calendar', + component: Calendar, +} + +export default meta +type Story = StoryObj + +/* + *👇 Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/7.0/react/api/csf + * to learn how to use render functions. + */ +export const Default: StoryFn = () => { + return +} diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx new file mode 100644 index 0000000..ab7ec94 --- /dev/null +++ b/src/components/Calendar/Calendar.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { CalendarProps } from './Calendar.types' + +function Calendar(props: CalendarProps) { + const {} = props + + return
+} + +const CalendarNamespace = Object.assign(Calendar, {}) + +export default CalendarNamespace diff --git a/src/components/Calendar/Calendar.types.ts b/src/components/Calendar/Calendar.types.ts new file mode 100644 index 0000000..b78b61d --- /dev/null +++ b/src/components/Calendar/Calendar.types.ts @@ -0,0 +1 @@ +export interface CalendarProps {} diff --git a/src/components/index.ts b/src/components/index.ts index e69de29..393f24f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -0,0 +1 @@ +export { default } from '@/components/Calendar/Calendar' From 4f0227720aa5aee88425858deeb3a042512d6887 Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Wed, 8 Feb 2023 01:13:22 +0100 Subject: [PATCH 02/10] Calendar: implemented proof-of-concept --- src/components/Calendar/Calendar.children.tsx | 97 +++++++++++++++++++ src/components/Calendar/Calendar.stories.tsx | 17 +++- src/components/Calendar/Calendar.tsx | 59 ++++++++++- src/components/Calendar/Calendar.types.ts | 38 +++++++- src/utils/index.ts | 4 + 5 files changed, 208 insertions(+), 7 deletions(-) diff --git a/src/components/Calendar/Calendar.children.tsx b/src/components/Calendar/Calendar.children.tsx index e69de29..e9ca348 100644 --- a/src/components/Calendar/Calendar.children.tsx +++ b/src/components/Calendar/Calendar.children.tsx @@ -0,0 +1,97 @@ +import { getRangeIterator } from '@/utils' +import React, { useCallback, useContext, useMemo } from 'react' +import { CalendarContext } from './Calendar' +import { + DaysDataType, + DaysGridProps, + HandleSetCurrentDateType, + MonthSwitcherProps, +} from './Calendar.types' + +export function MonthSwitcherButton(props: MonthSwitcherProps) { + const { direction, children, onClick, ...restProps } = props + const { switchDisplayedDate } = useContext(CalendarContext) + + const handleOnClick = (event) => { + onClick && onClick(event) + switchDisplayedDate(direction) + } + + return ( + + ) +} + +export function DaysGrid(props: DaysGridProps) { + const { render, ...restProps } = props + const { setCurrentDate, displayedDate, switchDisplayedDate } = + useContext(CalendarContext) + + const daysData = useMemo(() => { + const monthLength = new Date( + displayedDate.getFullYear(), + displayedDate.getMonth() + 1, + 0 + ).getDate() + + const previousMonthLength = new Date( + displayedDate.getFullYear(), + displayedDate.getMonth(), + 0 + ).getDate() + + const weekdayIndex = + (displayedDate.getDay() === 0 ? 7 : displayedDate.getDay()) - 1 + + const nextMonthDaysCount = 7 - ((weekdayIndex + monthLength) % 7) + console.log(weekdayIndex) + return [ + ...getRangeIterator({ + start: previousMonthLength - weekdayIndex + 1, + end: previousMonthLength, + }), + ...getRangeIterator({ end: monthLength }), + ...getRangeIterator({ end: nextMonthDaysCount }), + ].map((day, index) => ({ + day, + monthOffset: + index < weekdayIndex + ? -1 + : index >= weekdayIndex && index < monthLength + weekdayIndex + ? 0 + : 1, + })) + }, [displayedDate]) + + const handleSetCurrentDate = useCallback( + ({ day, monthOffset }) => { + setCurrentDate( + new Date( + displayedDate.getFullYear(), + displayedDate.getMonth() + monthOffset, + day + ) + ) + if (Math.abs(monthOffset)) { + switchDisplayedDate(monthOffset === -1 ? 'previous' : 'next') + } + }, + [displayedDate] + ) + + return ( +
+ {daysData.map(({ day, monthOffset }) => + render({ + day, + onClick: () => handleSetCurrentDate({ day, monthOffset }), + }) + )} +
+ ) +} diff --git a/src/components/Calendar/Calendar.stories.tsx b/src/components/Calendar/Calendar.stories.tsx index 3c213aa..fbb3648 100644 --- a/src/components/Calendar/Calendar.stories.tsx +++ b/src/components/Calendar/Calendar.stories.tsx @@ -21,5 +21,20 @@ type Story = StoryObj * to learn how to use render functions. */ export const Default: StoryFn = () => { - return + return ( + + + Previous + + + Next + + } + /> + + ) } diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index ab7ec94..bd17e43 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -1,12 +1,61 @@ -import React from 'react' -import { CalendarProps } from './Calendar.types' +import React, { createContext, useState } from 'react' +import { DaysGrid, MonthSwitcherButton } from './Calendar.children' +import { + CalendarContextType, + CalendarProps, + SwitchDisplayedDateFunctionType, +} from './Calendar.types' + +export const CalendarContext = createContext({}) function Calendar(props: CalendarProps) { - const {} = props + const { children, weekdays, defaultDate } = props + const [currentDate, setCurrentDate] = useState(defaultDate) + const [displayedDate, setDisplayedDate] = useState( + new Date(currentDate.getFullYear(), currentDate.getMonth(), 1) + ) + + const switchDisplayedDate: SwitchDisplayedDateFunctionType = ( + direction, + exactDate + ) => { + setDisplayedDate((displayedDate) => { + const newYear = exactDate?.getFullYear() ?? displayedDate.getFullYear() + const newMonth = + exactDate?.getMonth() ?? + displayedDate.getMonth() + (direction === 'next' ? 1 : -1) + + return new Date(newYear, newMonth, 1) + }) + } - return
+ return ( + +
+
+ {weekdays.map((x) => ( + {x} + ))} +
+ {currentDate.toDateString()} + {displayedDate.toDateString()} + {children} +
+
+ ) } -const CalendarNamespace = Object.assign(Calendar, {}) +const CalendarNamespace = Object.assign(Calendar, { + DaysGrid, + MonthSwitcherButton, +}) export default CalendarNamespace diff --git a/src/components/Calendar/Calendar.types.ts b/src/components/Calendar/Calendar.types.ts index b78b61d..3155751 100644 --- a/src/components/Calendar/Calendar.types.ts +++ b/src/components/Calendar/Calendar.types.ts @@ -1 +1,37 @@ -export interface CalendarProps {} +export interface CalendarProps extends React.PropsWithChildren { + weekdays: string[] + defaultDate: Date +} + +export type CalendarContextType = { + weekdays?: string[] + currentDate?: Date + setCurrentDate?: React.Dispatch> + displayedDate?: Date + switchDisplayedDate?: SwitchDisplayedDateFunctionType +} + +export type SwitchDisplayedDateFunctionType = ( + direction: MonthDirectionType, + exactDate?: Date +) => void + +export type MonthDirectionType = 'next' | 'previous' + +export type MonthSwitcherProps = React.PropsWithChildren & { + direction: MonthDirectionType + onClick?: React.MouseEventHandler +} + +export type DaysGridProps = { + render: (renderProps: { day: number; onClick: () => void }) => React.ReactNode +} + +type DayType = { + day: number + monthOffset: -1 | 0 | 1 +} + +export type HandleSetCurrentDateType = (args: DayType) => void + +export type DaysDataType = DayType[] diff --git a/src/utils/index.ts b/src/utils/index.ts index e69de29..1eba4ae 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -0,0 +1,4 @@ +export function* getRangeIterator({ start = 1, end = Infinity, step = 1 }) { + let x = start - step + while (x <= end - step) yield (x += step) +} From dda3ef839b4f2c6053d29fc51e5f06ffca43a3e2 Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Wed, 8 Feb 2023 21:31:33 +0100 Subject: [PATCH 03/10] Calendar: transformed components to arrow functions --- src/components/Calendar/Calendar.children.tsx | 4 ++-- src/components/Calendar/Calendar.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Calendar/Calendar.children.tsx b/src/components/Calendar/Calendar.children.tsx index e9ca348..9f0cfb0 100644 --- a/src/components/Calendar/Calendar.children.tsx +++ b/src/components/Calendar/Calendar.children.tsx @@ -8,7 +8,7 @@ import { MonthSwitcherProps, } from './Calendar.types' -export function MonthSwitcherButton(props: MonthSwitcherProps) { +export const MonthSwitcherButton = (props: MonthSwitcherProps) => { const { direction, children, onClick, ...restProps } = props const { switchDisplayedDate } = useContext(CalendarContext) @@ -27,7 +27,7 @@ export function MonthSwitcherButton(props: MonthSwitcherProps) { ) } -export function DaysGrid(props: DaysGridProps) { +export const DaysGrid = (props: DaysGridProps) => { const { render, ...restProps } = props const { setCurrentDate, displayedDate, switchDisplayedDate } = useContext(CalendarContext) diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index bd17e43..835baa8 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -8,7 +8,7 @@ import { export const CalendarContext = createContext({}) -function Calendar(props: CalendarProps) { +const Calendar = (props: CalendarProps) => { const { children, weekdays, defaultDate } = props const [currentDate, setCurrentDate] = useState(defaultDate) const [displayedDate, setDisplayedDate] = useState( From 2ccd89efdc8735f325c34894934bac0473010dc0 Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Wed, 8 Feb 2023 23:50:44 +0100 Subject: [PATCH 04/10] Calendar: moved currentDate state to props --- src/components/Calendar/Calendar.stories.tsx | 48 ++++++++++++++------ src/components/Calendar/Calendar.tsx | 16 +++++-- src/components/Calendar/Calendar.types.ts | 4 +- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/components/Calendar/Calendar.stories.tsx b/src/components/Calendar/Calendar.stories.tsx index fbb3648..b314472 100644 --- a/src/components/Calendar/Calendar.stories.tsx +++ b/src/components/Calendar/Calendar.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo, useState } from 'react' import type { Meta, StoryFn, StoryObj } from '@storybook/react' import Calendar from './Calendar' @@ -21,20 +21,40 @@ type Story = StoryObj * to learn how to use render functions. */ export const Default: StoryFn = () => { + const [date, setDate] = useState(new Date()) + + const dateString = useMemo(() => { + const day = date.getDate().toString().padStart(2, '0') + const month = (date.getMonth() + 1).toString().padStart(2, '0') + const year = date.getFullYear().toString() + + return `${year}-${month}-${day}` + }, [date]) + return ( - - - Previous - - - Next - - } + <> + - + + + Previous + + + Next + + ( + + )} + /> + + ) } diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 835baa8..8ddee4a 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -9,10 +9,20 @@ import { export const CalendarContext = createContext({}) const Calendar = (props: CalendarProps) => { - const { children, weekdays, defaultDate } = props - const [currentDate, setCurrentDate] = useState(defaultDate) + const { + children, + weekdays, + defaultDate = new Date(), + value: currentDate, + onChange: setCurrentDate, + } = props + + if ((currentDate && !setCurrentDate) || (!currentDate && setCurrentDate)) { + throw new Error('xd') + } + const [displayedDate, setDisplayedDate] = useState( - new Date(currentDate.getFullYear(), currentDate.getMonth(), 1) + new Date(defaultDate.getFullYear(), defaultDate.getMonth(), 1) ) const switchDisplayedDate: SwitchDisplayedDateFunctionType = ( diff --git a/src/components/Calendar/Calendar.types.ts b/src/components/Calendar/Calendar.types.ts index 3155751..4f46eb6 100644 --- a/src/components/Calendar/Calendar.types.ts +++ b/src/components/Calendar/Calendar.types.ts @@ -1,6 +1,8 @@ export interface CalendarProps extends React.PropsWithChildren { weekdays: string[] - defaultDate: Date + defaultDate?: Date + value?: Date + onChange?: React.Dispatch> } export type CalendarContextType = { From ba6d3e20318b776476fa7591f0a01e28bbde82f1 Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Fri, 10 Feb 2023 01:44:42 +0100 Subject: [PATCH 05/10] Calendar: added SelectedDate component, expanded DaysGrid functionality, added stories styles --- src/components/Calendar/Calendar.children.tsx | 101 +++++++++++++----- .../Calendar/Calendar.stories.styles.css | 15 +++ src/components/Calendar/Calendar.stories.tsx | 24 +++-- src/components/Calendar/Calendar.tsx | 21 ++-- src/components/Calendar/Calendar.types.ts | 22 ++-- 5 files changed, 129 insertions(+), 54 deletions(-) create mode 100644 src/components/Calendar/Calendar.stories.styles.css diff --git a/src/components/Calendar/Calendar.children.tsx b/src/components/Calendar/Calendar.children.tsx index 9f0cfb0..8683a6a 100644 --- a/src/components/Calendar/Calendar.children.tsx +++ b/src/components/Calendar/Calendar.children.tsx @@ -2,10 +2,11 @@ import { getRangeIterator } from '@/utils' import React, { useCallback, useContext, useMemo } from 'react' import { CalendarContext } from './Calendar' import { - DaysDataType, + DaysArrayType, DaysGridProps, HandleSetCurrentDateType, MonthSwitcherProps, + SelectedDateProps, } from './Calendar.types' export const MonthSwitcherButton = (props: MonthSwitcherProps) => { @@ -27,45 +28,85 @@ export const MonthSwitcherButton = (props: MonthSwitcherProps) => { ) } +export const SelectedDate = (props: SelectedDateProps) => { + const { scope, render } = props + const { currentDate, displayedDate } = useContext(CalendarContext) + + if ( + (scope === 'currentDate' && !currentDate) || + (scope === 'displayedDate' && !displayedDate) + ) { + throw new Error( + `Requested selected date is out of scope: ${scope} value is not provided or avaliable.` + ) + } + + return <>{render(scope === 'currentDate' ? currentDate : displayedDate)} +} + export const DaysGrid = (props: DaysGridProps) => { - const { render, ...restProps } = props + const { render, completeWithExtraDays, ...restProps } = props const { setCurrentDate, displayedDate, switchDisplayedDate } = useContext(CalendarContext) - const daysData = useMemo(() => { + const daysData = useMemo(() => { const monthLength = new Date( displayedDate.getFullYear(), displayedDate.getMonth() + 1, 0 ).getDate() - const previousMonthLength = new Date( displayedDate.getFullYear(), displayedDate.getMonth(), 0 ).getDate() - - const weekdayIndex = + const previousMonthDaysCount = (displayedDate.getDay() === 0 ? 7 : displayedDate.getDay()) - 1 + const nextMonthDaysCount = 7 - ((previousMonthDaysCount + monthLength) % 7) - const nextMonthDaysCount = 7 - ((weekdayIndex + monthLength) % 7) - console.log(weekdayIndex) - return [ - ...getRangeIterator({ - start: previousMonthLength - weekdayIndex + 1, - end: previousMonthLength, - }), - ...getRangeIterator({ end: monthLength }), - ...getRangeIterator({ end: nextMonthDaysCount }), - ].map((day, index) => ({ - day, - monthOffset: - index < weekdayIndex - ? -1 - : index >= weekdayIndex && index < monthLength + weekdayIndex - ? 0 - : 1, - })) + return { + monthLength, + previousMonthLength, + previousMonthDaysCount, + nextMonthDaysCount, + } + }, [displayedDate]) + + const daysArray = useMemo(() => { + const { + monthLength, + previousMonthLength, + previousMonthDaysCount, + nextMonthDaysCount, + } = daysData + + const previousMonthDays = ['previous', 'both'].includes( + completeWithExtraDays + ) + ? [ + ...getRangeIterator({ + start: previousMonthLength - previousMonthDaysCount + 1, + end: previousMonthLength, + }), + ] + : [] + const currentMonthDays = [...getRangeIterator({ end: monthLength })] + const nextMonthDays = ['next', 'both'].includes(completeWithExtraDays) + ? [...getRangeIterator({ end: nextMonthDaysCount })] + : [] + + return [...previousMonthDays, ...currentMonthDays, ...nextMonthDays].map( + (day, index) => ({ + day, + monthOffset: + index < previousMonthDaysCount + ? -1 + : index >= previousMonthDaysCount && + index < monthLength + previousMonthDaysCount + ? 0 + : 1, + }) + ) }, [displayedDate]) const handleSetCurrentDate = useCallback( @@ -85,8 +126,16 @@ export const DaysGrid = (props: DaysGridProps) => { ) return ( -
- {daysData.map(({ day, monthOffset }) => +
+ {daysArray.map(({ day, monthOffset }) => render({ day, onClick: () => handleSetCurrentDate({ day, monthOffset }), diff --git a/src/components/Calendar/Calendar.stories.styles.css b/src/components/Calendar/Calendar.stories.styles.css new file mode 100644 index 0000000..30774ff --- /dev/null +++ b/src/components/Calendar/Calendar.stories.styles.css @@ -0,0 +1,15 @@ +.calendar__header { + display: flex; + justify-content: space-between; +} + +.calendar { + display: flex; + flex-flow: column; + max-width: 320px; +} + +.calendar__body { + display: grid; + grid-template-columns: repeat(7, 1fr); +} diff --git a/src/components/Calendar/Calendar.stories.tsx b/src/components/Calendar/Calendar.stories.tsx index b314472..65a1d25 100644 --- a/src/components/Calendar/Calendar.stories.tsx +++ b/src/components/Calendar/Calendar.stories.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useState } from 'react' import type { Meta, StoryFn, StoryObj } from '@storybook/react' +import './Calendar.stories.styles.css' import Calendar from './Calendar' @@ -37,19 +38,28 @@ export const Default: StoryFn = () => { type="date" value={dateString} readOnly + className="elo" /> - - Previous - - - Next - +
+ + Previous + + {date.toDateString()}} + /> + + Next + +
( )} diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 8ddee4a..0f754a3 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -1,5 +1,9 @@ import React, { createContext, useState } from 'react' -import { DaysGrid, MonthSwitcherButton } from './Calendar.children' +import { + DaysGrid, + MonthSwitcherButton, + SelectedDate, +} from './Calendar.children' import { CalendarContextType, CalendarProps, @@ -11,10 +15,10 @@ export const CalendarContext = createContext({}) const Calendar = (props: CalendarProps) => { const { children, - weekdays, defaultDate = new Date(), value: currentDate, onChange: setCurrentDate, + ...restProps } = props if ((currentDate && !setCurrentDate) || (!currentDate && setCurrentDate)) { @@ -42,23 +46,13 @@ const Calendar = (props: CalendarProps) => { return ( -
-
- {weekdays.map((x) => ( - {x} - ))} -
- {currentDate.toDateString()} - {displayedDate.toDateString()} - {children} -
+
{children}
) } @@ -66,6 +60,7 @@ const Calendar = (props: CalendarProps) => { const CalendarNamespace = Object.assign(Calendar, { DaysGrid, MonthSwitcherButton, + SelectedDate, }) export default CalendarNamespace diff --git a/src/components/Calendar/Calendar.types.ts b/src/components/Calendar/Calendar.types.ts index 4f46eb6..8f58388 100644 --- a/src/components/Calendar/Calendar.types.ts +++ b/src/components/Calendar/Calendar.types.ts @@ -1,12 +1,11 @@ export interface CalendarProps extends React.PropsWithChildren { - weekdays: string[] defaultDate?: Date value?: Date onChange?: React.Dispatch> + className?: string } export type CalendarContextType = { - weekdays?: string[] currentDate?: Date setCurrentDate?: React.Dispatch> displayedDate?: Date @@ -20,15 +19,22 @@ export type SwitchDisplayedDateFunctionType = ( export type MonthDirectionType = 'next' | 'previous' -export type MonthSwitcherProps = React.PropsWithChildren & { - direction: MonthDirectionType - onClick?: React.MouseEventHandler -} +export type MonthSwitcherProps = React.HTMLAttributes & + React.PropsWithChildren & { + direction: MonthDirectionType + onClick?: React.MouseEventHandler + } -export type DaysGridProps = { +export type DaysGridProps = React.HTMLAttributes & { + completeWithExtraDays?: MonthDirectionType | 'both' render: (renderProps: { day: number; onClick: () => void }) => React.ReactNode } +export type SelectedDateProps = React.HTMLAttributes & { + scope: 'currentDate' | 'displayedDate' + render: (scopedDateObject: Date) => React.ReactNode +} + type DayType = { day: number monthOffset: -1 | 0 | 1 @@ -36,4 +42,4 @@ type DayType = { export type HandleSetCurrentDateType = (args: DayType) => void -export type DaysDataType = DayType[] +export type DaysArrayType = DayType[] From a944f06245489f507f4bfea4d28d2ed8faf7903d Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Sat, 25 Feb 2023 18:20:41 +0100 Subject: [PATCH 06/10] Calendar: refactored logic to use date-fns library --- package-lock.json | 20 +++ package.json | 3 + src/components/Calendar/Calendar.children.tsx | 143 ++++++------------ src/components/Calendar/Calendar.stories.tsx | 9 +- src/components/Calendar/Calendar.tsx | 22 +-- src/components/Calendar/Calendar.types.ts | 25 +-- src/utils/index.ts | 4 - 7 files changed, 90 insertions(+), 136 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7dc2be0..a6dd170 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "nardo-primitives", "version": "0.1.0", "license": "MIT", + "dependencies": { + "date-fns": "^2.29.3" + }, "devDependencies": { "@babel/preset-react": "^7.18.6", "@rollup/plugin-babel": "^6.0.3", @@ -7558,6 +7561,18 @@ "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", "dev": true }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -21196,6 +21211,11 @@ "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", "dev": true }, + "date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index e53435f..c9f1a05 100644 --- a/package.json +++ b/package.json @@ -58,5 +58,8 @@ "peerDependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" + }, + "dependencies": { + "date-fns": "^2.29.3" } } diff --git a/src/components/Calendar/Calendar.children.tsx b/src/components/Calendar/Calendar.children.tsx index 8683a6a..93c5b9b 100644 --- a/src/components/Calendar/Calendar.children.tsx +++ b/src/components/Calendar/Calendar.children.tsx @@ -1,21 +1,34 @@ -import { getRangeIterator } from '@/utils' -import React, { useCallback, useContext, useMemo } from 'react' +import React, { useContext, useMemo } from 'react' import { CalendarContext } from './Calendar' import { - DaysArrayType, + DatesGridItemType, DaysGridProps, - HandleSetCurrentDateType, MonthSwitcherProps, SelectedDateProps, } from './Calendar.types' +import { + endOfWeek, + eachDayOfInterval, + startOfWeek, + lastDayOfMonth, + isSameMonth, + addMonths, + differenceInDays, + getDaysInMonth, + setDate, + startOfMonth, + endOfMonth, +} from 'date-fns' export const MonthSwitcherButton = (props: MonthSwitcherProps) => { const { direction, children, onClick, ...restProps } = props - const { switchDisplayedDate } = useContext(CalendarContext) + const { setDisplayedDate } = useContext(CalendarContext) const handleOnClick = (event) => { onClick && onClick(event) - switchDisplayedDate(direction) + setDisplayedDate((prevState) => + addMonths(prevState, direction === 'next' ? 1 : -1) + ) } return ( @@ -41,106 +54,50 @@ export const SelectedDate = (props: SelectedDateProps) => { ) } - return <>{render(scope === 'currentDate' ? currentDate : displayedDate)} + return render(scope === 'currentDate' ? currentDate : displayedDate) } export const DaysGrid = (props: DaysGridProps) => { const { render, completeWithExtraDays, ...restProps } = props - const { setCurrentDate, displayedDate, switchDisplayedDate } = + const { setCurrentDate, displayedDate, setDisplayedDate } = useContext(CalendarContext) - const daysData = useMemo(() => { - const monthLength = new Date( - displayedDate.getFullYear(), - displayedDate.getMonth() + 1, - 0 - ).getDate() - const previousMonthLength = new Date( - displayedDate.getFullYear(), - displayedDate.getMonth(), - 0 - ).getDate() - const previousMonthDaysCount = - (displayedDate.getDay() === 0 ? 7 : displayedDate.getDay()) - 1 - const nextMonthDaysCount = 7 - ((previousMonthDaysCount + monthLength) % 7) - - return { - monthLength, - previousMonthLength, - previousMonthDaysCount, - nextMonthDaysCount, - } - }, [displayedDate]) - - const daysArray = useMemo(() => { - const { - monthLength, - previousMonthLength, - previousMonthDaysCount, - nextMonthDaysCount, - } = daysData - - const previousMonthDays = ['previous', 'both'].includes( - completeWithExtraDays - ) - ? [ - ...getRangeIterator({ - start: previousMonthLength - previousMonthDaysCount + 1, - end: previousMonthLength, - }), - ] - : [] - const currentMonthDays = [...getRangeIterator({ end: monthLength })] - const nextMonthDays = ['next', 'both'].includes(completeWithExtraDays) - ? [...getRangeIterator({ end: nextMonthDaysCount })] - : [] - - return [...previousMonthDays, ...currentMonthDays, ...nextMonthDays].map( - (day, index) => ({ - day, - monthOffset: - index < previousMonthDaysCount - ? -1 - : index >= previousMonthDaysCount && - index < monthLength + previousMonthDaysCount - ? 0 - : 1, - }) - ) - }, [displayedDate]) - - const handleSetCurrentDate = useCallback( - ({ day, monthOffset }) => { - setCurrentDate( - new Date( - displayedDate.getFullYear(), - displayedDate.getMonth() + monthOffset, - day - ) - ) - if (Math.abs(monthOffset)) { - switchDisplayedDate(monthOffset === -1 ? 'previous' : 'next') - } - }, + const datesGrid = useMemo( + () => + eachDayOfInterval({ + start: startOfWeek(startOfMonth(displayedDate), { weekStartsOn: 1 }), + end: endOfWeek(endOfMonth(displayedDate), { weekStartsOn: 1 }), + }).map((date) => ({ + date, + onClick: () => { + setCurrentDate(date) + !isSameMonth(date, displayedDate) && setDisplayedDate(date) + }, + })), [displayedDate] ) + const containerDataAttributes = useMemo( + () => ({ + 'data-calendar-previous-month-days': differenceInDays( + setDate(displayedDate, 1), + datesGrid.at(0).date + ), + 'data-calendar-next-month-days': differenceInDays( + datesGrid.at(-1).date, + lastDayOfMonth(displayedDate) + ), + 'data-calendar-days': getDaysInMonth(displayedDate), + }), + [datesGrid] + ) + return (
- {daysArray.map(({ day, monthOffset }) => - render({ - day, - onClick: () => handleSetCurrentDate({ day, monthOffset }), - }) - )} + {datesGrid.map((datesGridItem) => render(datesGridItem))}
) } diff --git a/src/components/Calendar/Calendar.stories.tsx b/src/components/Calendar/Calendar.stories.tsx index 65a1d25..004cd31 100644 --- a/src/components/Calendar/Calendar.stories.tsx +++ b/src/components/Calendar/Calendar.stories.tsx @@ -60,8 +60,13 @@ export const Default: StoryFn = () => { ( - + render={({ date, onClick }) => ( + )} />
diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 0f754a3..02de612 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -4,11 +4,7 @@ import { MonthSwitcherButton, SelectedDate, } from './Calendar.children' -import { - CalendarContextType, - CalendarProps, - SwitchDisplayedDateFunctionType, -} from './Calendar.types' +import { CalendarContextType, CalendarProps } from './Calendar.types' export const CalendarContext = createContext({}) @@ -29,27 +25,13 @@ const Calendar = (props: CalendarProps) => { new Date(defaultDate.getFullYear(), defaultDate.getMonth(), 1) ) - const switchDisplayedDate: SwitchDisplayedDateFunctionType = ( - direction, - exactDate - ) => { - setDisplayedDate((displayedDate) => { - const newYear = exactDate?.getFullYear() ?? displayedDate.getFullYear() - const newMonth = - exactDate?.getMonth() ?? - displayedDate.getMonth() + (direction === 'next' ? 1 : -1) - - return new Date(newYear, newMonth, 1) - }) - } - return (
{children}
diff --git a/src/components/Calendar/Calendar.types.ts b/src/components/Calendar/Calendar.types.ts index 8f58388..6d9e565 100644 --- a/src/components/Calendar/Calendar.types.ts +++ b/src/components/Calendar/Calendar.types.ts @@ -9,14 +9,9 @@ export type CalendarContextType = { currentDate?: Date setCurrentDate?: React.Dispatch> displayedDate?: Date - switchDisplayedDate?: SwitchDisplayedDateFunctionType + setDisplayedDate?: React.Dispatch> } -export type SwitchDisplayedDateFunctionType = ( - direction: MonthDirectionType, - exactDate?: Date -) => void - export type MonthDirectionType = 'next' | 'previous' export type MonthSwitcherProps = React.HTMLAttributes & @@ -25,21 +20,17 @@ export type MonthSwitcherProps = React.HTMLAttributes & onClick?: React.MouseEventHandler } +export type DatesGridItemType = { + date: Date + onClick: () => void +} + export type DaysGridProps = React.HTMLAttributes & { completeWithExtraDays?: MonthDirectionType | 'both' - render: (renderProps: { day: number; onClick: () => void }) => React.ReactNode + render: (args: DatesGridItemType) => React.ReactNode } export type SelectedDateProps = React.HTMLAttributes & { scope: 'currentDate' | 'displayedDate' - render: (scopedDateObject: Date) => React.ReactNode + render: (scopedDateObject: Date) => JSX.Element } - -type DayType = { - day: number - monthOffset: -1 | 0 | 1 -} - -export type HandleSetCurrentDateType = (args: DayType) => void - -export type DaysArrayType = DayType[] diff --git a/src/utils/index.ts b/src/utils/index.ts index 1eba4ae..e69de29 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +0,0 @@ -export function* getRangeIterator({ start = 1, end = Infinity, step = 1 }) { - let x = start - step - while (x <= end - step) yield (x += step) -} From 8014360bcd1bb059de7d42d4737a5f44efdb28ad Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Sat, 25 Feb 2023 18:42:22 +0100 Subject: [PATCH 07/10] Calendar: updated story --- src/components/Calendar/Calendar.stories.tsx | 96 +++++++++++++------- 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/src/components/Calendar/Calendar.stories.tsx b/src/components/Calendar/Calendar.stories.tsx index 004cd31..8e360b7 100644 --- a/src/components/Calendar/Calendar.stories.tsx +++ b/src/components/Calendar/Calendar.stories.tsx @@ -22,7 +22,39 @@ type Story = StoryObj * to learn how to use render functions. */ export const Default: StoryFn = () => { + return ( + +
+ + Previous + + {date.toDateString()}} + /> + + Next + +
+ ( + + )} + /> +
+ ) +} + +export const AsDatePicker: StoryFn = () => { const [date, setDate] = useState(new Date()) + const [isCalendarVisible, setIsCalendarVisible] = useState(false) const dateString = useMemo(() => { const day = date.getDate().toString().padStart(2, '0') @@ -35,41 +67,43 @@ export const Default: StoryFn = () => { return ( <> setIsCalendarVisible(true)} /> - -
- - Previous - - {date.toDateString()}} + {isCalendarVisible && ( + +
+ + Previous + + {date.toDateString()}} + /> + + Next + +
+ ( + + )} /> - - Next - -
- ( - - )} - /> -
+ + )} ) } From 6615fb33d87f298145a86756f82f4225f7672f29 Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Sun, 26 Feb 2023 02:06:58 +0100 Subject: [PATCH 08/10] Calendar: implemented keyboard controls (wip) --- src/components/Calendar/Calendar.children.tsx | 1 + src/components/Calendar/Calendar.tsx | 69 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/components/Calendar/Calendar.children.tsx b/src/components/Calendar/Calendar.children.tsx index 93c5b9b..5f101d3 100644 --- a/src/components/Calendar/Calendar.children.tsx +++ b/src/components/Calendar/Calendar.children.tsx @@ -88,6 +88,7 @@ export const DaysGrid = (props: DaysGridProps) => { lastDayOfMonth(displayedDate) ), 'data-calendar-days': getDaysInMonth(displayedDate), + 'data-days-grid': '', }), [datesGrid] ) diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 02de612..695af5e 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -1,4 +1,6 @@ -import React, { createContext, useState } from 'react' +// @ts-nocheck +import { addMonths } from 'date-fns' +import React, { createContext, useState, useRef, useCallback } from 'react' import { DaysGrid, MonthSwitcherButton, @@ -25,6 +27,63 @@ const Calendar = (props: CalendarProps) => { new Date(defaultDate.getFullYear(), defaultDate.getMonth(), 1) ) + const calendarRef = useRef(null) + + const getElementWithinOffset = (targetElement, focusOffset) => { + const elementsRoot = calendarRef.current.querySelector('[data-days-grid]') + const elements = [...elementsRoot.children] + + return elements[elements.indexOf(targetElement) + focusOffset] + } + + const getBoundaryElement = useCallback( + (bound: 'first' | 'last') => { + const elementsRoot = calendarRef.current.querySelector('[data-days-grid]') + console.log({ + first: elementsRoot.firstChild, + last: elementsRoot.lastChild, + }) + return elementsRoot[`${bound}Child`] + }, + [displayedDate] + ) + + const onKeyDown: React.KeyboardEventHandler = (event) => { + const { key, target } = event + + const offsetsMap = { + ArrowUp: -7, + ArrowDown: 7, + ArrowLeft: -1, + ArrowRight: 1, + } + + // temporary solution to skip handler for buttons other than arrows + if (!Object.keys(offsetsMap).includes(key)) { + return + } + + let focusableElement = getElementWithinOffset(target, offsetsMap[key]) + console.log('within offset: ', focusableElement) + + if (!focusableElement) { + const shouldIncrementMonth = offsetsMap[key] < 0 + setDisplayedDate((prevState) => + addMonths(prevState, shouldIncrementMonth ? -1 : 1) + ) + + focusableElement = getBoundaryElement( + shouldIncrementMonth ? 'first' : 'last' + ) + } + + console.log('boundary after state change', focusableElement) + + if (focusableElement.focus) { + focusableElement.focus() + } + } + return ( { setDisplayedDate, }} > -
{children}
+
+ {children} +
) } From 59a55186283236d3b88d7e9ef6797146fd960c6b Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Mon, 27 Feb 2023 23:49:58 +0100 Subject: [PATCH 09/10] Calenndar: fixed undefined function call --- src/components/Calendar/Calendar.children.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Calendar/Calendar.children.tsx b/src/components/Calendar/Calendar.children.tsx index 5f101d3..cae00d3 100644 --- a/src/components/Calendar/Calendar.children.tsx +++ b/src/components/Calendar/Calendar.children.tsx @@ -70,7 +70,7 @@ export const DaysGrid = (props: DaysGridProps) => { }).map((date) => ({ date, onClick: () => { - setCurrentDate(date) + ;(setCurrentDate ?? setDisplayedDate)(date) !isSameMonth(date, displayedDate) && setDisplayedDate(date) }, })), From ba1efd40d964e53196d670522150513a92a0b48e Mon Sep 17 00:00:00 2001 From: Kamil Pyszkowski Date: Tue, 28 Feb 2023 03:32:40 +0100 Subject: [PATCH 10/10] Calendar: fixed focus management between rerenders --- src/components/Calendar/Calendar.tsx | 48 +++++++++++++++------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 695af5e..c94d6cc 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import { addMonths } from 'date-fns' +import { addMonths, isAfter } from 'date-fns' import React, { createContext, useState, useRef, useCallback } from 'react' import { DaysGrid, @@ -26,8 +26,7 @@ const Calendar = (props: CalendarProps) => { const [displayedDate, setDisplayedDate] = useState( new Date(defaultDate.getFullYear(), defaultDate.getMonth(), 1) ) - - const calendarRef = useRef(null) + const focusableElementPosition = useRef(null) const getElementWithinOffset = (targetElement, focusOffset) => { const elementsRoot = calendarRef.current.querySelector('[data-days-grid]') @@ -36,17 +35,26 @@ const Calendar = (props: CalendarProps) => { return elements[elements.indexOf(targetElement) + focusOffset] } - const getBoundaryElement = useCallback( - (bound: 'first' | 'last') => { - const elementsRoot = calendarRef.current.querySelector('[data-days-grid]') - console.log({ - first: elementsRoot.firstChild, - last: elementsRoot.lastChild, - }) - return elementsRoot[`${bound}Child`] - }, - [displayedDate] - ) + const getBoundaryElement = (bound: 'first' | 'last') => { + const elementsRoot = calendarRef.current.querySelector('[data-days-grid]') + return elementsRoot[`${bound}Child`] + } + + const calendarRef = useRef() + const setCalendarRef = useCallback((node) => { + if (!node) { + return + } + + calendarRef.current = node + + if (focusableElementPosition.current) { + const focusableElement = getBoundaryElement( + focusableElementPosition.current + ) + focusableElement.focus() + } + }, []) const onKeyDown: React.KeyboardEventHandler = (event) => { const { key, target } = event @@ -64,21 +72,17 @@ const Calendar = (props: CalendarProps) => { } let focusableElement = getElementWithinOffset(target, offsetsMap[key]) - console.log('within offset: ', focusableElement) if (!focusableElement) { const shouldIncrementMonth = offsetsMap[key] < 0 + + focusableElementPosition.current = shouldIncrementMonth ? 'last' : 'first' setDisplayedDate((prevState) => addMonths(prevState, shouldIncrementMonth ? -1 : 1) ) - - focusableElement = getBoundaryElement( - shouldIncrementMonth ? 'first' : 'last' - ) + return } - console.log('boundary after state change', focusableElement) - if (focusableElement.focus) { focusableElement.focus() } @@ -94,7 +98,7 @@ const Calendar = (props: CalendarProps) => { }} >
setCalendarRef(node)} {...restProps} onKeyDown={onKeyDown} >