Skip to content

Commit

Permalink
feat: implement date picker component
Browse files Browse the repository at this point in the history
  • Loading branch information
jinhojang6 committed Mar 16, 2023
1 parent b181444 commit 4da2ec7
Show file tree
Hide file tree
Showing 25 changed files with 827 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/lsd-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"preview": "vite preview"
},
"dependencies": {
"@datepicker-react/hooks": "^2.8.4",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"clsx": "^1.2.1",
Expand Down
6 changes: 6 additions & 0 deletions packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { AutocompleteStyles } from '../Autocomplete/Autocomplete.styles'
import { BreadcrumbStyles } from '../Breadcrumb/Breadcrumb.styles'
import { BreadcrumbItemStyles } from '../BreadcrumbItem/BreadcrumbItem.styles'
import { ButtonStyles } from '../Button/Button.styles'
import { CalendarStyles } from '../Calendar/Calendar.styles'
import { CardStyles } from '../Card/Card.styles'
import { CardBodyStyles } from '../CardBody/CardBody.styles'
import { CardHeaderStyles } from '../CardHeader/CardHeader.styles'
import { CheckboxStyles } from '../Checkbox/Checkbox.styles'
import { CheckboxGroupStyles } from '../CheckboxGroup/CheckboxGroup.styles'
import { CollapseStyles } from '../Collapse/Collapse.styles'
import { CollapseHeaderStyles } from '../CollapseHeader/CollapseHeader.styles'
import { DateFieldStyles } from '../DateField/DateField.styles'
import { DatePickerStyles } from '../DatePicker/DatePicker.styles'
import { DropdownStyles } from '../Dropdown/Dropdown.styles'
import { DropdownItemStyles } from '../DropdownItem/DropdownItem.styles'
import { IconButtonStyles } from '../IconButton/IconButton.styles'
Expand Down Expand Up @@ -48,6 +51,9 @@ const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
CollapseStyles,
CollapseHeaderStyles,
CheckboxGroupStyles,
DateFieldStyles,
DatePickerStyles,
CalendarStyles,
]

export const CSSBaseline: React.FC<{ theme?: Theme }> = ({
Expand Down
14 changes: 14 additions & 0 deletions packages/lsd-react/src/components/Calendar/Calendar.classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const calendarClasses = {
root: `lsd-calendar`,
container: 'lsd-calendar-container',

open: 'lsd-calendar--open',

header: 'lsd-calendar-header',
grid: 'lsd-calendar-body',
button: 'lsd-calendar-button',

day: 'lsd-calendar-day',
daySelected: 'lsd-calendar-day--selected',
dayDisabled: 'lsd-calendar-day--diabled',
}
19 changes: 19 additions & 0 deletions packages/lsd-react/src/components/Calendar/Calendar.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'

export type CalendarContextType = {
focusedDate: Date | null
isDateFocused: (date: Date) => boolean
isDateSelected: (date: Date) => boolean
isDateHovered: (date: Date) => boolean
isDateBlocked: (date: Date) => boolean
isFirstOrLastSelectedDate: (date: Date) => boolean
onDateFocus: (date: Date) => void
onDateHover: (date: Date) => void
onDateSelect: (date: Date) => void
}

export const CalendarContext = React.createContext<CalendarContextType>(
null as any,
)

export const useCalendarContext = () => React.useContext(CalendarContext)
29 changes: 29 additions & 0 deletions packages/lsd-react/src/components/Calendar/Calendar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Meta, Story } from '@storybook/react'
import { useRef, useState } from 'react'
import { Calendar, CalendarProps } from './Calendar'

export default {
title: 'Calendar',
component: Calendar,
} as Meta

export const Root: Story<CalendarProps> = (arg) => {
const ref = useRef<HTMLDivElement>(null)

return (
<div ref={ref} style={{ width: '300px' }}>
<Calendar
{...arg}
handleDateFieldChange={(date) => console.log(date?.toDateString())}
open={true}
handleRef={ref}
>
Calendar
</Calendar>
</div>
)
}

Root.args = {
value: '',
}
70 changes: 70 additions & 0 deletions packages/lsd-react/src/components/Calendar/Calendar.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { css } from '@emotion/react'
import { calendarClasses } from './Calendar.classes'

export const CalendarStyles = css`
.${calendarClasses.root} {
border: 1px solid rgb(var(--lsd-border-primary));
visibility: hidden;
position: absolute;
top: 0;
left: 0;
opacity: 0;
visibility: hidden;
margin: 0;
padding: 0;
box-sizing: border-box;
background: rgb(var(--lsd-surface-primary));
}
.${calendarClasses.container} {
display: grid;
margin: 8px 2px 2px;
grid-gap: 0 64px;
}
.${calendarClasses.open} {
opacity: 1;
visibility: visible;
}
.${calendarClasses.header} {
display: flex;
justify-content: space-between;
align-items: center;
padding-inline: 10px;
padding-bottom: 10px;
}
.${calendarClasses.grid} {
display: grid;
grid-template-columns: repeat(7, 1fr);
justify-content: center;
}
.${calendarClasses.day} {
cursor: pointer;
border: none;
background: transparent;
aspect-ratio: 1 / 1;
}
.${calendarClasses.daySelected} {
border: 1px solid rgb(var(--lsd-border-primary));
}
.${calendarClasses.dayDisabled} {
opacity: 0.3;
cursor: default;
}
.${calendarClasses.button} {
border: 1px solid rgb(var(--lsd-border-primary));
cursor: pointer;
background: transparent;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
`
124 changes: 124 additions & 0 deletions packages/lsd-react/src/components/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
OnDatesChangeProps,
START_DATE,
useDatepicker,
} from '@datepicker-react/hooks'
import clsx from 'clsx'
import React, { useEffect, useRef, useState } from 'react'
import { useClickAway } from 'react-use'
import { calendarClasses } from './Calendar.classes'
import { CalendarContext } from './Calendar.context'
import { Month } from './Month'

export type CalendarProps = Omit<
React.HTMLAttributes<HTMLUListElement>,
'label'
> & {
open?: boolean
value?: string
onClose?: () => void
handleDateFieldChange: (data: Date) => void
handleRef: React.RefObject<HTMLElement>
}

export const Calendar: React.FC<CalendarProps> & {
classes: typeof calendarClasses
} = ({
open,
handleRef,
value = null,
onClose,
handleDateFieldChange,
children,
...props
}) => {
const ref = useRef<HTMLUListElement>(null)
const [style, setStyle] = useState<React.CSSProperties>({})

const handleDateChange = (data: OnDatesChangeProps) => {
handleDateFieldChange(data.startDate ?? new Date())
onDateFocus(data.startDate ?? new Date())
}

const {
activeMonths,
isDateSelected,
isDateHovered,
isFirstOrLastSelectedDate,
isDateBlocked,
isDateFocused,
focusedDate,
onDateHover,
onDateSelect,
onDateFocus,
goToPreviousMonths,
goToNextMonths,
} = useDatepicker({
startDate: value ? new Date(value) : null,
endDate: null,
focusedInput: START_DATE,
onDatesChange: handleDateChange,
numberOfMonths: 1,
})

useClickAway(ref, (event) => {
if (!open || event.composedPath().includes(handleRef.current!)) return

onClose && onClose()
})

const updateStyle = () => {
const { width, height, top, left } =
handleRef.current!.getBoundingClientRect()

setStyle({
left,
width,
top: top + height,
})
}

useEffect(() => {
updateStyle()
}, [open])

return (
<CalendarContext.Provider
value={{
focusedDate,
isDateFocused,
isDateSelected,
isDateHovered,
isDateBlocked,
isFirstOrLastSelectedDate,
onDateSelect,
onDateFocus,
onDateHover,
}}
>
<div
className={clsx(
props.className,
calendarClasses.root,
open && calendarClasses.open,
)}
style={{ ...style, ...(props.style ?? {}) }}
>
<div className={clsx(calendarClasses.container)}>
{activeMonths.map((month) => (
<Month
key={`${month.year}-${month.month}`}
year={month.year}
month={month.month}
firstDayOfWeek={0}
goToPreviousMonths={goToPreviousMonths}
goToNextMonths={goToNextMonths}
/>
))}
</div>
</div>
</CalendarContext.Provider>
)
}

Calendar.classes = calendarClasses
61 changes: 61 additions & 0 deletions packages/lsd-react/src/components/Calendar/Day.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useRef, useContext } from 'react'
import { useDay } from '@datepicker-react/hooks'
import { CalendarContext } from './Calendar.context'
import clsx from 'clsx'
import { calendarClasses } from './Calendar.classes'
import { Typography } from '../Typography'

export type DayProps = {
day?: string
date: Date
}

export const Day = ({ day, date }: DayProps) => {
const dayRef = useRef(null)
const {
focusedDate,
isDateFocused,
isDateSelected,
isDateHovered,
isDateBlocked,
isFirstOrLastSelectedDate,
onDateSelect,
onDateFocus,
onDateHover,
} = useContext(CalendarContext)

const { onClick, onKeyDown, onMouseEnter, tabIndex } = useDay({
date,
focusedDate,
isDateFocused,
isDateSelected,
isDateHovered,
isDateBlocked,
isFirstOrLastSelectedDate,
onDateFocus,
onDateSelect,
onDateHover,
dayRef,
})

if (!day) {
return null
}

return (
<button
onClick={onClick}
onKeyDown={onKeyDown}
onMouseEnter={onMouseEnter}
tabIndex={tabIndex}
type="button"
ref={dayRef}
className={clsx(
calendarClasses.day,
isDateFocused(date) && calendarClasses.daySelected,
)}
>
<Typography variant="label2">{parseInt(day, 10)}</Typography>
</button>
)
}
Loading

0 comments on commit 4da2ec7

Please sign in to comment.