From e3500d4d715bb2fe13c8f994374d80e153ca3a8d Mon Sep 17 00:00:00 2001 From: jinhojang6 Date: Tue, 28 Feb 2023 03:01:18 +0900 Subject: [PATCH 01/14] feat: implement date picker component --- packages/lsd-react/package.json | 1 + .../components/CSSBaseline/CSSBaseline.tsx | 3 + .../components/Calendar/Calendar.classes.ts | 14 +++ .../components/Calendar/Calendar.context.ts | 19 +++ .../components/Calendar/Calendar.stories.tsx | 29 +++++ .../components/Calendar/Calendar.styles.ts | 70 +++++++++++ .../src/components/Calendar/Calendar.tsx | 114 ++++++++++++++++++ .../lsd-react/src/components/Calendar/Day.tsx | 61 ++++++++++ .../src/components/Calendar/Month.tsx | 99 +++++++++++++++ .../src/components/Calendar/index.ts | 1 + .../components/DateField/DateField.classes.ts | 16 +++ .../DateField/DateField.stories.tsx | 30 +++++ .../components/DateField/DateField.styles.ts | 79 ++++++++++++ .../src/components/DateField/DateField.tsx | 110 +++++++++++++++++ .../src/components/DateField/index.ts | 1 + .../DatePicker/DatePicker.classes.ts | 4 + .../DatePicker/DatePicker.stories.tsx | 32 +++++ .../DatePicker/DatePicker.styles.ts | 13 ++ .../src/components/DatePicker/DatePicker.tsx | 76 ++++++++++++ .../src/components/DatePicker/index.ts | 1 + .../Icons/CalendarIcon/CalendarIcon.tsx | 24 ++++ .../components/Icons/CalendarIcon/index.ts | 1 + .../lsd-react/src/components/Icons/index.ts | 1 + yarn.lock | 12 ++ 24 files changed, 811 insertions(+) create mode 100644 packages/lsd-react/src/components/Calendar/Calendar.classes.ts create mode 100644 packages/lsd-react/src/components/Calendar/Calendar.context.ts create mode 100644 packages/lsd-react/src/components/Calendar/Calendar.stories.tsx create mode 100644 packages/lsd-react/src/components/Calendar/Calendar.styles.ts create mode 100644 packages/lsd-react/src/components/Calendar/Calendar.tsx create mode 100644 packages/lsd-react/src/components/Calendar/Day.tsx create mode 100644 packages/lsd-react/src/components/Calendar/Month.tsx create mode 100644 packages/lsd-react/src/components/Calendar/index.ts create mode 100644 packages/lsd-react/src/components/DateField/DateField.classes.ts create mode 100644 packages/lsd-react/src/components/DateField/DateField.stories.tsx create mode 100644 packages/lsd-react/src/components/DateField/DateField.styles.ts create mode 100644 packages/lsd-react/src/components/DateField/DateField.tsx create mode 100644 packages/lsd-react/src/components/DateField/index.ts create mode 100644 packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts create mode 100644 packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx create mode 100644 packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts create mode 100644 packages/lsd-react/src/components/DatePicker/DatePicker.tsx create mode 100644 packages/lsd-react/src/components/DatePicker/index.ts create mode 100644 packages/lsd-react/src/components/Icons/CalendarIcon/CalendarIcon.tsx create mode 100644 packages/lsd-react/src/components/Icons/CalendarIcon/index.ts diff --git a/packages/lsd-react/package.json b/packages/lsd-react/package.json index b6d2a70..5c943ec 100644 --- a/packages/lsd-react/package.json +++ b/packages/lsd-react/package.json @@ -14,6 +14,7 @@ "prepublish": "yarn build" }, "dependencies": { + "@datepicker-react/hooks": "^2.8.4", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "clsx": "^1.2.1", diff --git a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx index 8043bfc..1b4c35d 100644 --- a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx +++ b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx @@ -5,6 +5,7 @@ import { BadgeStyles } from '../Badge/Badge.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' @@ -12,6 +13,8 @@ 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' diff --git a/packages/lsd-react/src/components/Calendar/Calendar.classes.ts b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts new file mode 100644 index 0000000..026cf75 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts @@ -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', +} diff --git a/packages/lsd-react/src/components/Calendar/Calendar.context.ts b/packages/lsd-react/src/components/Calendar/Calendar.context.ts new file mode 100644 index 0000000..0a8cb3e --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.context.ts @@ -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( + null as any, +) + +export const useCalendarContext = () => React.useContext(CalendarContext) diff --git a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx new file mode 100644 index 0000000..a3b64b9 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx @@ -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 = (arg) => { + const ref = useRef(null) + + return ( +
+ console.log(date?.toDateString())} + open={true} + handleRef={ref} + > + Calendar + +
+ ) +} + +Root.args = { + value: '', +} diff --git a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts new file mode 100644 index 0000000..904d0c9 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts @@ -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; + } +` diff --git a/packages/lsd-react/src/components/Calendar/Calendar.tsx b/packages/lsd-react/src/components/Calendar/Calendar.tsx new file mode 100644 index 0000000..f6c782e --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.tsx @@ -0,0 +1,114 @@ +import { + OnDatesChangeProps, + START_DATE, + useDatepicker, +} from '@datepicker-react/hooks' +import clsx from 'clsx' +import React, { useEffect, useRef, useState } from 'react' +import { calendarClasses } from './Calendar.classes' +import { CalendarContext } from './Calendar.context' +import { Month } from './Month' + +export type CalendarProps = Omit< + React.HTMLAttributes, + 'label' +> & { + open?: boolean + value?: string + handleDateFieldChange: (data: Date) => void + handleRef: React.RefObject +} + +export const Calendar: React.FC & { + classes: typeof calendarClasses +} = ({ + open, + handleRef, + value = null, + handleDateFieldChange, + children, + ...props +}) => { + const [style, setStyle] = useState({}) + + 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, + }) + + const updateStyle = () => { + const { width, height, top, left } = + handleRef.current!.getBoundingClientRect() + + setStyle({ + left, + width, + top: top + height, + }) + } + + useEffect(() => { + updateStyle() + }, [open]) + + return ( + +
+
+ {activeMonths.map((month) => ( + + ))} +
+
+
+ ) +} + +Calendar.classes = calendarClasses diff --git a/packages/lsd-react/src/components/Calendar/Day.tsx b/packages/lsd-react/src/components/Calendar/Day.tsx new file mode 100644 index 0000000..64a1434 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Day.tsx @@ -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 ( + + ) +} diff --git a/packages/lsd-react/src/components/Calendar/Month.tsx b/packages/lsd-react/src/components/Calendar/Month.tsx new file mode 100644 index 0000000..922eed5 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Month.tsx @@ -0,0 +1,99 @@ +import { FirstDayOfWeek, useMonth } from '@datepicker-react/hooks' +import clsx from 'clsx' +import { NavigateBeforeIcon, NavigateNextIcon } from '../Icons' +import { Typography } from '../Typography' +import { calendarClasses } from './Calendar.classes' +import { Day } from './Day' + +export type MonthProps = { + year: number + month: number + firstDayOfWeek: FirstDayOfWeek + goToPreviousMonths: () => void + goToNextMonths: () => void +} + +export const Month = ({ + year, + month, + firstDayOfWeek, + goToPreviousMonths, + goToNextMonths, +}: MonthProps) => { + const { days, weekdayLabels, monthLabel } = useMonth({ + year, + month, + firstDayOfWeek, + }) + + const renderOtherDays = (idx: number, firstDate: Date) => { + const date = new Date(firstDate) + date.setDate(date.getDate() + idx) + return date.getDate() + } + + return ( +
+
+ + {monthLabel} + +
+
+ {weekdayLabels.map((dayLabel, idx) => ( + + {dayLabel[0]} + + ))} +
+
+ {days.map((ele, idx) => + typeof ele !== 'number' ? ( + + ) : ( + + ), + )} + {days.length % 7 !== 0 && + new Array(7 - (days.length % 7)).fill(null).map((ele, idx) => ( + + ))} +
+
+ ) +} diff --git a/packages/lsd-react/src/components/Calendar/index.ts b/packages/lsd-react/src/components/Calendar/index.ts new file mode 100644 index 0000000..8c50cf8 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/index.ts @@ -0,0 +1 @@ +export * from './Calendar' diff --git a/packages/lsd-react/src/components/DateField/DateField.classes.ts b/packages/lsd-react/src/components/DateField/DateField.classes.ts new file mode 100644 index 0000000..c39607e --- /dev/null +++ b/packages/lsd-react/src/components/DateField/DateField.classes.ts @@ -0,0 +1,16 @@ +export const dateFieldClasses = { + root: `lsd-date-field`, + + inputContainer: `lsd-date-field-input-container`, + input: `lsd-date-field-input-container__input`, + icon: `lsd-date-field-input-container__icon`, + iconButton: `lsd-date-field-input-container__icon-button`, + + supportingText: 'lsd-date-field__supporting-text', + + disabled: `lsd-date-field--disabled`, + error: 'lsd-date-field--error', + + large: `lsd-date-field--large`, + medium: `lsd-date-field--medium`, +} diff --git a/packages/lsd-react/src/components/DateField/DateField.stories.tsx b/packages/lsd-react/src/components/DateField/DateField.stories.tsx new file mode 100644 index 0000000..f88b2f7 --- /dev/null +++ b/packages/lsd-react/src/components/DateField/DateField.stories.tsx @@ -0,0 +1,30 @@ +import { Meta, Story } from '@storybook/react' +import { DateField, DateFieldProps } from './DateField' + +export default { + title: 'DateField', + component: DateField, + argTypes: { + size: { + type: { + name: 'enum', + value: ['medium', 'large'], + }, + defaultValue: 'large', + }, + }, +} as Meta + +export const Root: Story = ({ ...args }) => { + return +} + +Root.args = { + size: 'large', + supportingText: 'Supporting text', + disabled: false, + error: false, + errorIcon: false, + clearButton: true, + onChange: undefined, +} diff --git a/packages/lsd-react/src/components/DateField/DateField.styles.ts b/packages/lsd-react/src/components/DateField/DateField.styles.ts new file mode 100644 index 0000000..0fd6002 --- /dev/null +++ b/packages/lsd-react/src/components/DateField/DateField.styles.ts @@ -0,0 +1,79 @@ +import { css } from '@emotion/react' +import { dateFieldClasses } from './DateField.classes' + +export const DateFieldStyles = css` + .${dateFieldClasses.root} { + width: auto; + border-bottom: 1px solid rgb(var(--lsd-border-primary)); + box-sizing: border-box; + } + + .${dateFieldClasses.inputContainer} { + display: flex; + align-items: center; + justify-content: space-between; + } + + .${dateFieldClasses.disabled} { + opacity: 0.34; + } + + .${dateFieldClasses.input} { + border: none; + outline: none; + font-size: 14px; + color: rgb(var(--lsd-text-primary)); + background: none; + width: 100%; + } + + .${dateFieldClasses.input}::-webkit-inner-spin-button, + .${dateFieldClasses.input}::-webkit-calendar-picker-indicator { + display: none; + -webkit-appearance: none; + } + + .${dateFieldClasses.input}:hover { + outline: none; + } + + .${dateFieldClasses.input}:focus::-webkit-datetime-edit { + color: rgb(var(--lsd-text-primary)); + opacity: 0.3; + } + + .${dateFieldClasses.error} + .${dateFieldClasses.input}::-webkit-datetime-edit-fields-wrapper { + text-decoration: line-through; + } + + .${dateFieldClasses.supportingText} { + width: fit-content; + margin-top: 20px; + } + + .${dateFieldClasses.large} { + width: 208px; + height: 40px; + padding: 10px 14px; + } + + .${dateFieldClasses.medium} { + width: 188px; + height: 32px; + padding: 6px 12px; + } + + .${dateFieldClasses.iconButton} { + padding: 0; + width: auto; + height: auto; + margin: 0; + border: 0; + + svg { + width: 100%; + height: auto; + } + } +` diff --git a/packages/lsd-react/src/components/DateField/DateField.tsx b/packages/lsd-react/src/components/DateField/DateField.tsx new file mode 100644 index 0000000..7dde40f --- /dev/null +++ b/packages/lsd-react/src/components/DateField/DateField.tsx @@ -0,0 +1,110 @@ +import clsx from 'clsx' +import React, { useRef } from 'react' +import { useInput } from '../../utils/useInput' +import { IconButton } from '../IconButton' +import { CloseIcon, ErrorIcon } from '../Icons' +import { Typography } from '../Typography' +import { dateFieldClasses } from './DateField.classes' + +export type DateFieldProps = Omit< + React.HTMLAttributes, + 'onChange' | 'value' +> & + Pick, 'onChange'> & { + size?: 'large' | 'medium' + error?: boolean + errorIcon?: boolean + clearButton?: boolean + disabled?: boolean + supportingText?: string + value?: string + defaultValue?: string + placeholder?: string + icon?: React.ReactNode + onIconClick?: () => void + inputProps?: React.InputHTMLAttributes + } + +export const DateField: React.FC & { + classes: typeof dateFieldClasses +} = ({ + size = 'large', + error = false, + errorIcon = false, + clearButton, + supportingText, + children, + value, + placeholder, + defaultValue, + disabled, + onChange, + icon, + onIconClick, + inputProps = {}, + ...props +}) => { + const ref = useRef(null) + const input = useInput({ defaultValue, value, onChange, ref }) + + const onCancel = () => input.setValue('') + + return ( +
+
+ + {icon ? ( + !disabled && onIconClick && onIconClick()} + > + {icon} + + ) : error && errorIcon ? ( + + ) : clearButton && input.filled ? ( + !disabled && onCancel()} + aria-label="clear" + className={dateFieldClasses.iconButton} + > + + + ) : null} +
+ {supportingText && ( +
+ + {supportingText} + +
+ )} + {children} +
+ ) +} + +DateField.classes = dateFieldClasses diff --git a/packages/lsd-react/src/components/DateField/index.ts b/packages/lsd-react/src/components/DateField/index.ts new file mode 100644 index 0000000..f865b34 --- /dev/null +++ b/packages/lsd-react/src/components/DateField/index.ts @@ -0,0 +1 @@ +export * from './DateField' diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts b/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts new file mode 100644 index 0000000..8af429d --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts @@ -0,0 +1,4 @@ +export const datePickerClasses = { + root: `lsd-date-picker`, + withCalendar: `lsd-date-picker--with-calendar`, +} diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx new file mode 100644 index 0000000..758dd4e --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx @@ -0,0 +1,32 @@ +import { Meta, Story } from '@storybook/react' +import { DatePicker, DatePickerProps } from './DatePicker' + +export default { + title: 'DatePicker', + component: DatePicker, + argTypes: { + size: { + type: { + name: 'enum', + value: ['medium', 'large'], + }, + defaultValue: 'large', + }, + }, +} as Meta + +export const Root: Story = ({ ...args }) => { + return DatePicker +} + +Root.args = { + size: 'large', + supportingText: 'Supporting text', + disabled: false, + error: false, + value: '', + onChange: undefined, + errorIcon: false, + clearButton: true, + withCalendar: true, +} diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts b/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts new file mode 100644 index 0000000..1a37e7d --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts @@ -0,0 +1,13 @@ +import { css } from '@emotion/react' +import { calendarClasses } from '../Calendar/Calendar.classes' +import { datePickerClasses } from './DatePicker.classes' + +export const DatePickerStyles = css` + .${datePickerClasses.root} { + width: fit-content; + } + + #lsd-presentation .${calendarClasses.root} { + border-top: none; + } +` diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx new file mode 100644 index 0000000..447f609 --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx @@ -0,0 +1,76 @@ +import clsx from 'clsx' +import React, { useRef, useState } from 'react' +import { Calendar } from '../Calendar' +import { DateField } from '../DateField' +import { CalendarIcon } from '../Icons' +import { Portal } from '../PortalProvider/Portal' +import { datePickerClasses } from './DatePicker.classes' + +export type DatePickerProps = Omit< + React.HTMLAttributes, + 'onChange' | 'value' +> & + Pick, 'onChange'> & { + size?: 'large' | 'medium' + error?: boolean + errorIcon?: boolean + clearButton?: boolean + disabled?: boolean + withCalendar?: boolean + supportingText?: string + value?: string + onChange?: (value: string) => void + defaultValue?: string + placeholder?: string + inputProps?: React.InputHTMLAttributes + } + +export const DatePicker: React.FC & { + classes: typeof datePickerClasses +} = ({ value, onChange, withCalendar = true, ...props }) => { + const ref = useRef(null) + const [openCalendar, setOpenCalendar] = useState(false) + const [date, setDate] = useState(value || '') + + const handleDateFieldChange = (date: any) => { + const offset = new Date(date).getTimezoneOffset() + const formattedDate = new Date(date.getTime() - offset * 60 * 1000) + const value = formattedDate.toISOString().split('T')[0] + setDate(value) + setOpenCalendar(false) + onChange && onChange(value) + } + + return ( +
+ } + onIconClick={() => setOpenCalendar((prev) => !prev)} + value={date} + onChange={(data) => setDate(data.target.value)} + style={{ width: '310px' }} + {...props} + > + + {withCalendar && ( + + )} + + +
+ ) +} + +DatePicker.classes = datePickerClasses diff --git a/packages/lsd-react/src/components/DatePicker/index.ts b/packages/lsd-react/src/components/DatePicker/index.ts new file mode 100644 index 0000000..192c395 --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/index.ts @@ -0,0 +1 @@ +export * from './DatePicker' diff --git a/packages/lsd-react/src/components/Icons/CalendarIcon/CalendarIcon.tsx b/packages/lsd-react/src/components/Icons/CalendarIcon/CalendarIcon.tsx new file mode 100644 index 0000000..3550d57 --- /dev/null +++ b/packages/lsd-react/src/components/Icons/CalendarIcon/CalendarIcon.tsx @@ -0,0 +1,24 @@ +import { LsdIcon } from '../LsdIcon' + +export const CalendarIcon = LsdIcon( + (props) => ( + + + + ), + { + filled: true, + }, +) diff --git a/packages/lsd-react/src/components/Icons/CalendarIcon/index.ts b/packages/lsd-react/src/components/Icons/CalendarIcon/index.ts new file mode 100644 index 0000000..e7b74eb --- /dev/null +++ b/packages/lsd-react/src/components/Icons/CalendarIcon/index.ts @@ -0,0 +1 @@ +export * from './CalendarIcon' diff --git a/packages/lsd-react/src/components/Icons/index.ts b/packages/lsd-react/src/components/Icons/index.ts index 06fe2af..b4c3312 100644 --- a/packages/lsd-react/src/components/Icons/index.ts +++ b/packages/lsd-react/src/components/Icons/index.ts @@ -19,4 +19,5 @@ export * from './SearchIcon' export * from './PickIcon' export * from './RadioButtonIcon' export * from './RadioButtonFilledIcon' +export * from './CalendarIcon' export * from './RemoveIcon' diff --git a/yarn.lock b/yarn.lock index fd51bde..8a32f70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1344,6 +1344,13 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@datepicker-react/hooks@^2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@datepicker-react/hooks/-/hooks-2.8.4.tgz#6e07aa98bf21b90b7c88fb35919cca6eb08f2c31" + integrity sha512-qaYJKK5sOSdqcL/OnCtyv3/Q6fRRljfeAyl5ISTPgEO0CM5xZzkGmTx40+6wvqjH5lEZH4ysS95nPyLwZS2tlw== + dependencies: + date-fns "^2.14.0" + "@design-systems/utils@2.12.0": version "2.12.0" resolved "https://registry.npmjs.org/@design-systems/utils/-/utils-2.12.0.tgz" @@ -6789,6 +6796,11 @@ dargs@^7.0.0: resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz" integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== +date-fns@^2.14.0: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + dateformat@^3.0.0: version "3.0.3" resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz" From d574c3efbda4300488109f17ad94e34974888751 Mon Sep 17 00:00:00 2001 From: jinhojang6 Date: Fri, 17 Mar 2023 22:23:39 +0900 Subject: [PATCH 02/14] refactor: refactor datefield, calendar, date picker --- .../components/Calendar/Calendar.stories.tsx | 3 +- .../Calendar/ControlledCalendar.stories.tsx | 30 +++++++++++++++++ .../DateField/ControlledDateField.stories.tsx | 31 ++++++++++++++++++ .../DateField/DateField.stories.tsx | 3 +- .../ControlledDatePicker.stories.tsx | 32 +++++++++++++++++++ .../DatePicker/DatePicker.stories.tsx | 2 +- 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 packages/lsd-react/src/components/Calendar/ControlledCalendar.stories.tsx create mode 100644 packages/lsd-react/src/components/DateField/ControlledDateField.stories.tsx create mode 100644 packages/lsd-react/src/components/DatePicker/ControlledDatePicker.stories.tsx diff --git a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx index a3b64b9..8bf8f27 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx @@ -25,5 +25,6 @@ export const Root: Story = (arg) => { } Root.args = { - value: '', + value: undefined, + onChange: undefined, } diff --git a/packages/lsd-react/src/components/Calendar/ControlledCalendar.stories.tsx b/packages/lsd-react/src/components/Calendar/ControlledCalendar.stories.tsx new file mode 100644 index 0000000..34874fe --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/ControlledCalendar.stories.tsx @@ -0,0 +1,30 @@ +import { Meta, Story } from '@storybook/react' +import { useRef } from 'react' +import { Calendar, CalendarProps } from './Calendar' + +export default { + title: 'ControlledCalendar', + component: Calendar, +} as Meta + +export const Root: Story = (arg) => { + const ref = useRef(null) + + return ( +
+ console.log(date?.toDateString())} + open={true} + handleRef={ref} + > + Calendar + +
+ ) +} + +Root.args = { + value: '2023-01-01', + onChange: undefined, +} diff --git a/packages/lsd-react/src/components/DateField/ControlledDateField.stories.tsx b/packages/lsd-react/src/components/DateField/ControlledDateField.stories.tsx new file mode 100644 index 0000000..943160a --- /dev/null +++ b/packages/lsd-react/src/components/DateField/ControlledDateField.stories.tsx @@ -0,0 +1,31 @@ +import { Meta, Story } from '@storybook/react' +import { DateField, DateFieldProps } from './DateField' + +export default { + title: 'ControlledDateField', + component: DateField, + argTypes: { + size: { + type: { + name: 'enum', + value: ['medium', 'large'], + }, + defaultValue: 'large', + }, + }, +} as Meta + +export const Root: Story = ({ ...args }) => { + return +} + +Root.args = { + size: 'large', + supportingText: 'Supporting text', + disabled: false, + value: '2023-01-01', + onChange: undefined, + error: false, + errorIcon: false, + clearButton: true, +} diff --git a/packages/lsd-react/src/components/DateField/DateField.stories.tsx b/packages/lsd-react/src/components/DateField/DateField.stories.tsx index f88b2f7..15b82d1 100644 --- a/packages/lsd-react/src/components/DateField/DateField.stories.tsx +++ b/packages/lsd-react/src/components/DateField/DateField.stories.tsx @@ -23,8 +23,9 @@ Root.args = { size: 'large', supportingText: 'Supporting text', disabled: false, + value: undefined, + onChange: undefined, error: false, errorIcon: false, clearButton: true, - onChange: undefined, } diff --git a/packages/lsd-react/src/components/DatePicker/ControlledDatePicker.stories.tsx b/packages/lsd-react/src/components/DatePicker/ControlledDatePicker.stories.tsx new file mode 100644 index 0000000..91a7b80 --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/ControlledDatePicker.stories.tsx @@ -0,0 +1,32 @@ +import { Meta, Story } from '@storybook/react' +import { DatePicker, DatePickerProps } from './DatePicker' + +export default { + title: 'ControlledDatePicker', + component: DatePicker, + argTypes: { + size: { + type: { + name: 'enum', + value: ['medium', 'large'], + }, + defaultValue: 'large', + }, + }, +} as Meta + +export const Root: Story = ({ ...args }) => { + return DatePicker +} + +Root.args = { + size: 'large', + supportingText: 'Supporting text', + disabled: false, + error: false, + value: '2023-01-01', + onChange: undefined, + errorIcon: false, + clearButton: true, + withCalendar: true, +} diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx index 758dd4e..976c3a3 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx @@ -24,7 +24,7 @@ Root.args = { supportingText: 'Supporting text', disabled: false, error: false, - value: '', + value: undefined, onChange: undefined, errorIcon: false, clearButton: true, From f9087411cb61c87d92be91c7380a1c79326e24f5 Mon Sep 17 00:00:00 2001 From: jinhojang6 Date: Tue, 21 Mar 2023 01:13:30 +0900 Subject: [PATCH 03/14] refactor: refactor datepicker, calendar, datefield based on feedback --- .../components/Calendar/Calendar.classes.ts | 10 +- .../components/Calendar/Calendar.context.ts | 1 + .../components/Calendar/Calendar.stories.tsx | 39 +++++- .../components/Calendar/Calendar.styles.ts | 74 ++++++++++ .../src/components/Calendar/Calendar.tsx | 22 ++- .../Calendar/ControlledCalendar.stories.tsx | 30 ----- .../lsd-react/src/components/Calendar/Day.tsx | 12 +- .../src/components/Calendar/Month.tsx | 127 ++++++++++++++---- .../DateField/ControlledDateField.stories.tsx | 31 ----- .../DateField/DateField.stories.tsx | 19 ++- .../ControlledDatePicker.stories.tsx | 32 ----- .../DatePicker/DatePicker.stories.tsx | 22 ++- .../src/components/DatePicker/DatePicker.tsx | 13 +- 13 files changed, 294 insertions(+), 138 deletions(-) delete mode 100644 packages/lsd-react/src/components/Calendar/ControlledCalendar.stories.tsx delete mode 100644 packages/lsd-react/src/components/DateField/ControlledDateField.stories.tsx delete mode 100644 packages/lsd-react/src/components/DatePicker/ControlledDatePicker.stories.tsx diff --git a/packages/lsd-react/src/components/Calendar/Calendar.classes.ts b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts index 026cf75..ea33f38 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.classes.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts @@ -3,12 +3,20 @@ export const calendarClasses = { container: 'lsd-calendar-container', open: 'lsd-calendar--open', + disabled: 'lsd-calendar--disabled', header: 'lsd-calendar-header', grid: 'lsd-calendar-body', + weekDay: 'lsd-calendar__weekDay', button: 'lsd-calendar-button', + row: 'lsd-calendar__row', + changeYear: 'lsd-calendar__change-year', + changeYearButton: 'lsd-calendar__change-year__button', + year: 'lsd-calendar-year', + month: 'lsd-calendar-month', day: 'lsd-calendar-day', daySelected: 'lsd-calendar-day--selected', - dayDisabled: 'lsd-calendar-day--diabled', + dayDisabled: 'lsd-calendar-day--disabled', + today: 'lsd-calendar-today', } diff --git a/packages/lsd-react/src/components/Calendar/Calendar.context.ts b/packages/lsd-react/src/components/Calendar/Calendar.context.ts index 0a8cb3e..97a2163 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.context.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.context.ts @@ -2,6 +2,7 @@ import React from 'react' export type CalendarContextType = { focusedDate: Date | null + size?: 'large' | 'medium' isDateFocused: (date: Date) => boolean isDateSelected: (date: Date) => boolean isDateHovered: (date: Date) => boolean diff --git a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx index 8bf8f27..7e45d38 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx @@ -1,13 +1,22 @@ import { Meta, Story } from '@storybook/react' -import { useRef, useState } from 'react' +import { useRef } from 'react' import { Calendar, CalendarProps } from './Calendar' export default { title: 'Calendar', component: Calendar, + argTypes: { + size: { + type: { + name: 'enum', + value: ['medium', 'large'], + }, + defaultValue: 'large', + }, + }, } as Meta -export const Root: Story = (arg) => { +export const Uncontrolled: Story = (arg) => { const ref = useRef(null) return ( @@ -24,7 +33,31 @@ export const Root: Story = (arg) => { ) } -Root.args = { +export const Controlled: Story = (arg) => { + const ref = useRef(null) + + return ( +
+ console.log(date?.toDateString())} + open={true} + handleRef={ref} + > + Calendar + +
+ ) +} + +Uncontrolled.args = { value: undefined, onChange: undefined, + size: 'large', +} + +Controlled.args = { + value: '2023-01-01', + onChange: undefined, + size: 'large', } diff --git a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts index 904d0c9..f483f03 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts @@ -39,6 +39,48 @@ export const CalendarStyles = css` display: grid; grid-template-columns: repeat(7, 1fr); justify-content: center; + cursor: pointer; + } + + .${calendarClasses.weekDay} { + text-align: center; + aspect-ratio: 1 / 1; + display: flex; + justify-content: center; + align-items: center; + } + + .${calendarClasses.row} { + display: flex; + justify-content: center; + align-items: center; + } + + .${calendarClasses.changeYear} { + display: flex; + justify-content: center; + align-items: center; + border: 1px solid rgb(var(--lsd-border-primary)); + padding: 2px 6px; + gap: 6px; + } + + .${calendarClasses.changeYearButton} { + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + background: transparent; + } + + .${calendarClasses.month} { + margin-right: 8px; + } + + .${calendarClasses.year}:hover { + cursor: pointer; + text-decoration: underline; + text-decoration-color: rgb(var(--lsd-border-primary)); } .${calendarClasses.day} { @@ -46,6 +88,17 @@ export const CalendarStyles = css` border: none; background: transparent; aspect-ratio: 1 / 1; + position: relative; + } + + .${calendarClasses.day}:hover { + cursor: pointer; + text-decoration: underline; + text-decoration-color: rgb(var(--lsd-border-primary)); + } + + .${calendarClasses.day} label:hover { + cursor: pointer; } .${calendarClasses.daySelected} { @@ -57,6 +110,27 @@ export const CalendarStyles = css` cursor: default; } + .${calendarClasses.today} { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: 0; + } + + .${calendarClasses.disabled} { + pointer-events: none; + border: 1px solid rgba(var(--lsd-border-primary), 0.3); + label { + opacity: 0.3; + } + .${calendarClasses.button} { + opacity: 0.3; + } + .${calendarClasses.daySelected} { + opacity: 0.3; + } + } + .${calendarClasses.button} { border: 1px solid rgb(var(--lsd-border-primary)); cursor: pointer; diff --git a/packages/lsd-react/src/components/Calendar/Calendar.tsx b/packages/lsd-react/src/components/Calendar/Calendar.tsx index f6c782e..15d0acb 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.tsx @@ -8,15 +8,19 @@ import React, { useEffect, useRef, useState } from 'react' import { calendarClasses } from './Calendar.classes' import { CalendarContext } from './Calendar.context' import { Month } from './Month' +import { useClickAway } from 'react-use' export type CalendarProps = Omit< React.HTMLAttributes, 'label' > & { open?: boolean + disabled?: boolean value?: string handleDateFieldChange: (data: Date) => void handleRef: React.RefObject + size?: 'large' | 'medium' + onClose?: () => void } export const Calendar: React.FC & { @@ -25,12 +29,22 @@ export const Calendar: React.FC & { open, handleRef, value = null, + size = 'large', + disabled = false, handleDateFieldChange, + onClose, children, ...props }) => { + const ref = useRef(null) const [style, setStyle] = useState({}) + useClickAway(ref, (event) => { + if (!open || event.composedPath().includes(handleRef.current!)) return + + onClose && onClose() + }) + const handleDateChange = (data: OnDatesChangeProps) => { handleDateFieldChange(data.startDate ?? new Date()) onDateFocus(data.startDate ?? new Date()) @@ -75,6 +89,7 @@ export const Calendar: React.FC & { return ( & { props.className, calendarClasses.root, open && calendarClasses.open, + disabled && calendarClasses.disabled, )} + ref={ref} style={{ ...style, ...(props.style ?? {}) }} >
- {activeMonths.map((month) => ( + {activeMonths.map((month, idx) => ( diff --git a/packages/lsd-react/src/components/Calendar/ControlledCalendar.stories.tsx b/packages/lsd-react/src/components/Calendar/ControlledCalendar.stories.tsx deleted file mode 100644 index 34874fe..0000000 --- a/packages/lsd-react/src/components/Calendar/ControlledCalendar.stories.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Meta, Story } from '@storybook/react' -import { useRef } from 'react' -import { Calendar, CalendarProps } from './Calendar' - -export default { - title: 'ControlledCalendar', - component: Calendar, -} as Meta - -export const Root: Story = (arg) => { - const ref = useRef(null) - - return ( -
- console.log(date?.toDateString())} - open={true} - handleRef={ref} - > - Calendar - -
- ) -} - -Root.args = { - value: '2023-01-01', - onChange: undefined, -} diff --git a/packages/lsd-react/src/components/Calendar/Day.tsx b/packages/lsd-react/src/components/Calendar/Day.tsx index 64a1434..895ecbb 100644 --- a/packages/lsd-react/src/components/Calendar/Day.tsx +++ b/packages/lsd-react/src/components/Calendar/Day.tsx @@ -8,9 +8,10 @@ import { Typography } from '../Typography' export type DayProps = { day?: string date: Date + disabled?: boolean } -export const Day = ({ day, date }: DayProps) => { +export const Day = ({ day, date, disabled = false }: DayProps) => { const dayRef = useRef(null) const { focusedDate, @@ -52,10 +53,17 @@ export const Day = ({ day, date }: DayProps) => { ref={dayRef} className={clsx( calendarClasses.day, - isDateFocused(date) && calendarClasses.daySelected, + !disabled && isDateFocused(date) && calendarClasses.daySelected, + disabled && calendarClasses.dayDisabled, )} > {parseInt(day, 10)} + {new Date(date).setHours(0, 0, 0, 0) === + new Date().setHours(0, 0, 0, 0) && ( + + ■ + + )} ) } diff --git a/packages/lsd-react/src/components/Calendar/Month.tsx b/packages/lsd-react/src/components/Calendar/Month.tsx index 922eed5..93cb99d 100644 --- a/packages/lsd-react/src/components/Calendar/Month.tsx +++ b/packages/lsd-react/src/components/Calendar/Month.tsx @@ -1,8 +1,16 @@ import { FirstDayOfWeek, useMonth } from '@datepicker-react/hooks' import clsx from 'clsx' -import { NavigateBeforeIcon, NavigateNextIcon } from '../Icons' +import { useRef, useState } from 'react' +import { useClickAway } from 'react-use' +import { + ArrowDownIcon, + ArrowUpIcon, + NavigateBeforeIcon, + NavigateNextIcon, +} from '../Icons' import { Typography } from '../Typography' import { calendarClasses } from './Calendar.classes' +import { useCalendarContext } from './Calendar.context' import { Day } from './Day' export type MonthProps = { @@ -11,27 +19,40 @@ export type MonthProps = { firstDayOfWeek: FirstDayOfWeek goToPreviousMonths: () => void goToNextMonths: () => void + size?: 'large' | 'medium' | 'small' } export const Month = ({ - year, + size: _size = 'large', + year: _year, month, firstDayOfWeek, goToPreviousMonths, goToNextMonths, }: MonthProps) => { + const sizeContext = useCalendarContext() + const size = sizeContext?.size ?? _size + const [year, setYear] = useState(_year) const { days, weekdayLabels, monthLabel } = useMonth({ year, month, firstDayOfWeek, }) + const [changeYear, setChangeYear] = useState(false) + const ref = useRef(null) - const renderOtherDays = (idx: number, firstDate: Date) => { - const date = new Date(firstDate) + const renderOtherDays = (idx: number, referenceDate: Date) => { + const date = new Date(referenceDate) date.setDate(date.getDate() + idx) return date.getDate() } + useClickAway(ref, (event) => { + if (!changeYear) return + + setChangeYear(false) + }) + return (
@@ -42,7 +63,44 @@ export const Month = ({ > - {monthLabel} +
+ + {monthLabel.split(' ')[0]} + + {changeYear ? ( +
+ + {monthLabel.split(' ')[1]} + +
+ setYear(year + 1)} + className={calendarClasses.changeYearButton} + color="primary" + /> + setYear(year - 1)} + className={calendarClasses.changeYearButton} + color="primary" + /> +
+
+ ) : ( + setChangeYear(true)} + variant={size === 'large' ? 'label1' : 'label2'} + className={calendarClasses.year} + > + {monthLabel.split(' ')[1]} + + )} +
+ {days.length == 28 && + new Array(7) + .fill(null) + .map((_, idx) => ( + + ))} {days.map((ele, idx) => typeof ele !== 'number' ? ( ) : ( - + disabled={true} + /> ), )} - {days.length % 7 !== 0 && - new Array(7 - (days.length % 7)).fill(null).map((ele, idx) => ( - + {new Array( + days.length % 7 !== 0 && days.length <= 35 + ? 7 - (days.length % 7) + 7 + : 7 - (days.length % 7), + ) + .fill(null) + .map((ele, idx) => ( + ))}
diff --git a/packages/lsd-react/src/components/DateField/ControlledDateField.stories.tsx b/packages/lsd-react/src/components/DateField/ControlledDateField.stories.tsx deleted file mode 100644 index 943160a..0000000 --- a/packages/lsd-react/src/components/DateField/ControlledDateField.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Meta, Story } from '@storybook/react' -import { DateField, DateFieldProps } from './DateField' - -export default { - title: 'ControlledDateField', - component: DateField, - argTypes: { - size: { - type: { - name: 'enum', - value: ['medium', 'large'], - }, - defaultValue: 'large', - }, - }, -} as Meta - -export const Root: Story = ({ ...args }) => { - return -} - -Root.args = { - size: 'large', - supportingText: 'Supporting text', - disabled: false, - value: '2023-01-01', - onChange: undefined, - error: false, - errorIcon: false, - clearButton: true, -} diff --git a/packages/lsd-react/src/components/DateField/DateField.stories.tsx b/packages/lsd-react/src/components/DateField/DateField.stories.tsx index 15b82d1..0f6af24 100644 --- a/packages/lsd-react/src/components/DateField/DateField.stories.tsx +++ b/packages/lsd-react/src/components/DateField/DateField.stories.tsx @@ -15,11 +15,15 @@ export default { }, } as Meta -export const Root: Story = ({ ...args }) => { +export const Uncontrolled: Story = ({ ...args }) => { return } -Root.args = { +export const Controlled: Story = ({ ...args }) => { + return +} + +Uncontrolled.args = { size: 'large', supportingText: 'Supporting text', disabled: false, @@ -29,3 +33,14 @@ Root.args = { errorIcon: false, clearButton: true, } + +Controlled.args = { + size: 'large', + supportingText: 'Supporting text', + disabled: false, + value: '2023-01-01', + onChange: undefined, + error: false, + errorIcon: false, + clearButton: true, +} diff --git a/packages/lsd-react/src/components/DatePicker/ControlledDatePicker.stories.tsx b/packages/lsd-react/src/components/DatePicker/ControlledDatePicker.stories.tsx deleted file mode 100644 index 91a7b80..0000000 --- a/packages/lsd-react/src/components/DatePicker/ControlledDatePicker.stories.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Meta, Story } from '@storybook/react' -import { DatePicker, DatePickerProps } from './DatePicker' - -export default { - title: 'ControlledDatePicker', - component: DatePicker, - argTypes: { - size: { - type: { - name: 'enum', - value: ['medium', 'large'], - }, - defaultValue: 'large', - }, - }, -} as Meta - -export const Root: Story = ({ ...args }) => { - return DatePicker -} - -Root.args = { - size: 'large', - supportingText: 'Supporting text', - disabled: false, - error: false, - value: '2023-01-01', - onChange: undefined, - errorIcon: false, - clearButton: true, - withCalendar: true, -} diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx index 976c3a3..9815917 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx @@ -15,12 +15,15 @@ export default { }, } as Meta -export const Root: Story = ({ ...args }) => { +export const Uncontrolled: Story = ({ ...args }) => { return DatePicker } -Root.args = { - size: 'large', +export const Contolled: Story = ({ ...args }) => { + return DatePicker +} + +Uncontrolled.args = { supportingText: 'Supporting text', disabled: false, error: false, @@ -29,4 +32,17 @@ Root.args = { errorIcon: false, clearButton: true, withCalendar: true, + size: 'large', +} + +Contolled.args = { + supportingText: 'Supporting text', + disabled: false, + error: false, + value: '2023-01-01', + onChange: undefined, + errorIcon: false, + clearButton: true, + withCalendar: true, + size: 'large', } diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx index 447f609..bfe3bd9 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx @@ -11,7 +11,6 @@ export type DatePickerProps = Omit< 'onChange' | 'value' > & Pick, 'onChange'> & { - size?: 'large' | 'medium' error?: boolean errorIcon?: boolean clearButton?: boolean @@ -22,6 +21,7 @@ export type DatePickerProps = Omit< onChange?: (value: string) => void defaultValue?: string placeholder?: string + size?: 'large' | 'medium' inputProps?: React.InputHTMLAttributes } @@ -36,9 +36,12 @@ export const DatePicker: React.FC & { const offset = new Date(date).getTimezoneOffset() const formattedDate = new Date(date.getTime() - offset * 60 * 1000) const value = formattedDate.toISOString().split('T')[0] - setDate(value) - setOpenCalendar(false) - onChange && onChange(value) + + if (typeof onChange === 'function') { + onChange(value) + } else { + setDate(value) + } } return ( @@ -63,8 +66,10 @@ export const DatePicker: React.FC & { setOpenCalendar(false)} handleRef={ref} value={date} + disabled={props.disabled} /> )} From 0c153e39736dde838e29624855763b5cc04566c7 Mon Sep 17 00:00:00 2001 From: jinhojang6 Date: Tue, 21 Mar 2023 20:58:32 +0900 Subject: [PATCH 04/14] refactor: refactor controlled components --- .../src/components/Calendar/Calendar.tsx | 28 +++++++++++++++---- .../src/components/DatePicker/DatePicker.tsx | 12 ++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/lsd-react/src/components/Calendar/Calendar.tsx b/packages/lsd-react/src/components/Calendar/Calendar.tsx index 15d0acb..0a3f9b6 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.tsx @@ -11,13 +11,13 @@ import { Month } from './Month' import { useClickAway } from 'react-use' export type CalendarProps = Omit< - React.HTMLAttributes, + React.HTMLAttributes, 'label' > & { open?: boolean disabled?: boolean value?: string - handleDateFieldChange: (data: Date) => void + onChange: (data: Date) => void handleRef: React.RefObject size?: 'large' | 'medium' onClose?: () => void @@ -28,16 +28,19 @@ export const Calendar: React.FC & { } = ({ open, handleRef, - value = null, + value: _value, size = 'large', disabled = false, - handleDateFieldChange, + onChange, onClose, children, ...props }) => { const ref = useRef(null) const [style, setStyle] = useState({}) + const [value, setValue] = useState( + _value ? new Date(_value) : null, + ) useClickAway(ref, (event) => { if (!open || event.composedPath().includes(handleRef.current!)) return @@ -46,10 +49,23 @@ export const Calendar: React.FC & { }) const handleDateChange = (data: OnDatesChangeProps) => { - handleDateFieldChange(data.startDate ?? new Date()) - onDateFocus(data.startDate ?? new Date()) + if (typeof _value !== 'undefined') { + if (typeof onChange !== 'undefined') { + onChange(data.startDate ?? new Date()) + } + } else { + setValue(data.startDate) + } } + useEffect(() => { + onDateFocus(_value ? new Date(_value) : new Date()) + }, [_value]) + + useEffect(() => { + onDateFocus(value ? new Date(value) : new Date()) + }, [value]) + const { activeMonths, isDateSelected, diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx index bfe3bd9..cd55434 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx @@ -27,18 +27,20 @@ export type DatePickerProps = Omit< export const DatePicker: React.FC & { classes: typeof datePickerClasses -} = ({ value, onChange, withCalendar = true, ...props }) => { +} = ({ value: _value, onChange, withCalendar = true, ...props }) => { const ref = useRef(null) const [openCalendar, setOpenCalendar] = useState(false) - const [date, setDate] = useState(value || '') + const [date, setDate] = useState(_value || '') const handleDateFieldChange = (date: any) => { const offset = new Date(date).getTimezoneOffset() const formattedDate = new Date(date.getTime() - offset * 60 * 1000) const value = formattedDate.toISOString().split('T')[0] - if (typeof onChange === 'function') { - onChange(value) + if (typeof _value !== 'undefined') { + if (typeof onChange !== 'undefined') { + onChange(value) + } } else { setDate(value) } @@ -64,7 +66,7 @@ export const DatePicker: React.FC & { {withCalendar && ( setOpenCalendar(false)} handleRef={ref} From 9b3a548044cfc31c0b1a75f81a9708f17fb0c522 Mon Sep 17 00:00:00 2001 From: Hossein Mehrabi Date: Wed, 22 Mar 2023 14:38:29 +0330 Subject: [PATCH 05/14] fix: fix controlled versions of Calendar and DatePicker --- .../components/Calendar/Calendar.stories.tsx | 14 +------ .../src/components/Calendar/Calendar.tsx | 39 +++++++++--------- .../src/components/DatePicker/DatePicker.tsx | 40 ++++++++++--------- packages/lsd-react/src/utils/date.utils.ts | 15 +++++++ packages/lsd-react/src/utils/useInput.ts | 8 +++- 5 files changed, 64 insertions(+), 52 deletions(-) create mode 100644 packages/lsd-react/src/utils/date.utils.ts diff --git a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx index 7e45d38..48b8f08 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx @@ -21,12 +21,7 @@ export const Uncontrolled: Story = (arg) => { return (
- console.log(date?.toDateString())} - open={true} - handleRef={ref} - > + Calendar
@@ -38,12 +33,7 @@ export const Controlled: Story = (arg) => { return (
- console.log(date?.toDateString())} - open={true} - handleRef={ref} - > + Calendar
diff --git a/packages/lsd-react/src/components/Calendar/Calendar.tsx b/packages/lsd-react/src/components/Calendar/Calendar.tsx index 0a3f9b6..df53bc2 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.tsx @@ -5,14 +5,15 @@ import { } from '@datepicker-react/hooks' import clsx from 'clsx' import React, { useEffect, useRef, useState } from 'react' +import { useClickAway } from 'react-use' +import { safeConvertDateToString } from '../../utils/date.utils' import { calendarClasses } from './Calendar.classes' import { CalendarContext } from './Calendar.context' import { Month } from './Month' -import { useClickAway } from 'react-use' export type CalendarProps = Omit< React.HTMLAttributes, - 'label' + 'label' | 'onChange' > & { open?: boolean disabled?: boolean @@ -28,7 +29,7 @@ export const Calendar: React.FC & { } = ({ open, handleRef, - value: _value, + value: valueProp, size = 'large', disabled = false, onChange, @@ -39,7 +40,7 @@ export const Calendar: React.FC & { const ref = useRef(null) const [style, setStyle] = useState({}) const [value, setValue] = useState( - _value ? new Date(_value) : null, + valueProp ? safeConvertDateToString(valueProp).date : null, ) useClickAway(ref, (event) => { @@ -49,22 +50,11 @@ export const Calendar: React.FC & { }) const handleDateChange = (data: OnDatesChangeProps) => { - if (typeof _value !== 'undefined') { - if (typeof onChange !== 'undefined') { - onChange(data.startDate ?? new Date()) - } - } else { - setValue(data.startDate) - } - } - - useEffect(() => { - onDateFocus(_value ? new Date(_value) : new Date()) - }, [_value]) + if (typeof valueProp !== 'undefined') + return onChange?.(data.startDate ?? new Date()) - useEffect(() => { - onDateFocus(value ? new Date(value) : new Date()) - }, [value]) + setValue(data.startDate) + } const { activeMonths, @@ -87,6 +77,17 @@ export const Calendar: React.FC & { numberOfMonths: 1, }) + useEffect(() => { + onDateFocus(value ? new Date(value) : new Date()) + }, [value]) + + useEffect(() => { + if (typeof valueProp === 'undefined') return + + const { date } = safeConvertDateToString(valueProp) + setValue(date) + }, [valueProp]) + const updateStyle = () => { const { width, height, top, left } = handleRef.current!.getBoundingClientRect() diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx index cd55434..37c5372 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx @@ -1,5 +1,10 @@ import clsx from 'clsx' import React, { useRef, useState } from 'react' +import { + dateToISODateString, + removeDateTimezoneOffset, +} from '../../utils/date.utils' +import { useInput } from '../../utils/useInput' import { Calendar } from '../Calendar' import { DateField } from '../DateField' import { CalendarIcon } from '../Icons' @@ -18,7 +23,6 @@ export type DatePickerProps = Omit< withCalendar?: boolean supportingText?: string value?: string - onChange?: (value: string) => void defaultValue?: string placeholder?: string size?: 'large' | 'medium' @@ -27,24 +31,22 @@ export type DatePickerProps = Omit< export const DatePicker: React.FC & { classes: typeof datePickerClasses -} = ({ value: _value, onChange, withCalendar = true, ...props }) => { +} = ({ value: valueProp, onChange, withCalendar = true, ...props }) => { const ref = useRef(null) const [openCalendar, setOpenCalendar] = useState(false) - const [date, setDate] = useState(_value || '') - const handleDateFieldChange = (date: any) => { - const offset = new Date(date).getTimezoneOffset() - const formattedDate = new Date(date.getTime() - offset * 60 * 1000) - const value = formattedDate.toISOString().split('T')[0] + const input = useInput({ + value: valueProp, + defaultValue: '', + onChange, + getInput: () => + ref.current?.querySelector( + `input.${DateField.classes.input}`, + ) as HTMLInputElement, + }) - if (typeof _value !== 'undefined') { - if (typeof onChange !== 'undefined') { - onChange(value) - } - } else { - setDate(value) - } - } + const handleDateChange = (date: Date) => + input.setValue(dateToISODateString(removeDateTimezoneOffset(date))) return (
& { } onIconClick={() => setOpenCalendar((prev) => !prev)} - value={date} - onChange={(data) => setDate(data.target.value)} + value={input.value} + onChange={input.onChange} style={{ width: '310px' }} {...props} > {withCalendar && ( handleDateChange(date)} open={openCalendar} onClose={() => setOpenCalendar(false)} handleRef={ref} - value={date} + value={input.value} disabled={props.disabled} /> )} diff --git a/packages/lsd-react/src/utils/date.utils.ts b/packages/lsd-react/src/utils/date.utils.ts new file mode 100644 index 0000000..a5dba50 --- /dev/null +++ b/packages/lsd-react/src/utils/date.utils.ts @@ -0,0 +1,15 @@ +export const safeConvertDateToString = (value: string) => { + const date = new Date(value ?? undefined) + const isValid = !Number.isNaN(+date) + + return { + isValid, + date: isValid ? date : new Date(), + } +} + +export const removeDateTimezoneOffset = (date: Date) => + new Date(+date - date.getTimezoneOffset() * 60 * 1000) + +export const dateToISODateString = (date: Date) => + date.toISOString().split('T')[0] diff --git a/packages/lsd-react/src/utils/useInput.ts b/packages/lsd-react/src/utils/useInput.ts index fa60220..2e6db79 100644 --- a/packages/lsd-react/src/utils/useInput.ts +++ b/packages/lsd-react/src/utils/useInput.ts @@ -12,6 +12,7 @@ export type InputProps = { defaultValue: T onChange?: InputOnChangeType ref?: React.RefObject + getInput?: () => HTMLInputElement } export const useInput = ( @@ -42,9 +43,12 @@ export const useInput = ( } const setter = (value: InputValueType) => { - if (!props.ref?.current) return + const element = + props?.ref?.current ?? + (typeof props.getInput === 'function' && props.getInput()) + + if (!element) return - const element = props.ref.current const event = new Event('input', { bubbles: true }) Object.getOwnPropertyDescriptor( From 352d4b5ed1b99cdc6315d4dd536ecd2d684456a7 Mon Sep 17 00:00:00 2001 From: Hossein Mehrabi Date: Wed, 22 Mar 2023 14:46:23 +0330 Subject: [PATCH 06/14] fix: fix disabled calendar days Prevent clicking on disabled calendar days from changing the date. --- packages/lsd-react/src/components/Calendar/Day.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/lsd-react/src/components/Calendar/Day.tsx b/packages/lsd-react/src/components/Calendar/Day.tsx index 895ecbb..3fa20ea 100644 --- a/packages/lsd-react/src/components/Calendar/Day.tsx +++ b/packages/lsd-react/src/components/Calendar/Day.tsx @@ -45,9 +45,9 @@ export const Day = ({ day, date, disabled = false }: DayProps) => { return (
-
+ ) } diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts b/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts index 8af429d..ce145e8 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts @@ -1,4 +1,5 @@ export const datePickerClasses = { root: `lsd-date-picker`, - withCalendar: `lsd-date-picker--with-calendar`, + + calendar: `lsd-date-picker-calendar`, } diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts b/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts index 1a37e7d..c20b358 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts @@ -1,5 +1,4 @@ import { css } from '@emotion/react' -import { calendarClasses } from '../Calendar/Calendar.classes' import { datePickerClasses } from './DatePicker.classes' export const DatePickerStyles = css` @@ -7,7 +6,7 @@ export const DatePickerStyles = css` width: fit-content; } - #lsd-presentation .${calendarClasses.root} { - border-top: none; + .${datePickerClasses.calendar} { + border-top: none !important; } ` diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx index 37c5372..c5c7c3a 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx @@ -51,11 +51,8 @@ export const DatePicker: React.FC & { return (
} @@ -74,6 +71,7 @@ export const DatePicker: React.FC & { handleRef={ref} value={input.value} disabled={props.disabled} + className={datePickerClasses.calendar} /> )} From bab61482dffb1911663fd3f75722cbdbcbde1c40 Mon Sep 17 00:00:00 2001 From: jinhojang6 Date: Mon, 10 Apr 2023 22:00:51 +0900 Subject: [PATCH 10/14] feat: add label element to DateField and adjust styles --- .../components/Calendar/Calendar.context.ts | 2 +- .../components/Calendar/Calendar.stories.tsx | 2 +- .../src/components/Calendar/Calendar.tsx | 2 +- .../components/DateField/DateField.classes.ts | 13 ++- .../DateField/DateField.stories.tsx | 15 +++- .../components/DateField/DateField.styles.ts | 83 +++++++++++++++---- .../src/components/DateField/DateField.tsx | 55 ++++++++---- .../DatePicker/DatePicker.classes.ts | 6 +- .../DatePicker/DatePicker.stories.tsx | 15 +++- .../DatePicker/DatePicker.styles.ts | 19 +++++ .../src/components/DatePicker/DatePicker.tsx | 27 +++++- 11 files changed, 190 insertions(+), 49 deletions(-) diff --git a/packages/lsd-react/src/components/Calendar/Calendar.context.ts b/packages/lsd-react/src/components/Calendar/Calendar.context.ts index 97a2163..46cad87 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.context.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.context.ts @@ -2,7 +2,7 @@ import React from 'react' export type CalendarContextType = { focusedDate: Date | null - size?: 'large' | 'medium' + size?: 'large' | 'medium' | 'small' isDateFocused: (date: Date) => boolean isDateSelected: (date: Date) => boolean isDateHovered: (date: Date) => boolean diff --git a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx index 48b8f08..0b49344 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx @@ -9,7 +9,7 @@ export default { size: { type: { name: 'enum', - value: ['medium', 'large'], + value: ['small', 'medium', 'large'], }, defaultValue: 'large', }, diff --git a/packages/lsd-react/src/components/Calendar/Calendar.tsx b/packages/lsd-react/src/components/Calendar/Calendar.tsx index ebf3a01..3cc0779 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.tsx @@ -20,7 +20,7 @@ export type CalendarProps = Omit< value?: string onChange: (data: Date) => void handleRef: React.RefObject - size?: 'large' | 'medium' + size?: 'large' | 'medium' | 'small' onClose?: () => void } diff --git a/packages/lsd-react/src/components/DateField/DateField.classes.ts b/packages/lsd-react/src/components/DateField/DateField.classes.ts index c39607e..bb8ebca 100644 --- a/packages/lsd-react/src/components/DateField/DateField.classes.ts +++ b/packages/lsd-react/src/components/DateField/DateField.classes.ts @@ -1,10 +1,11 @@ export const dateFieldClasses = { root: `lsd-date-field`, + label: 'lsd-date-field__label', - inputContainer: `lsd-date-field-input-container`, - input: `lsd-date-field-input-container__input`, - icon: `lsd-date-field-input-container__icon`, - iconButton: `lsd-date-field-input-container__icon-button`, + inputContainer: `lsd-date-field__input-container`, + input: `lsd-date-field__input-container__input`, + icon: `lsd-date-field__input-container__icon`, + iconButton: `lsd-date-field__input-container__icon-button`, supportingText: 'lsd-date-field__supporting-text', @@ -13,4 +14,8 @@ export const dateFieldClasses = { large: `lsd-date-field--large`, medium: `lsd-date-field--medium`, + small: `lsd-date-field--small`, + + outlined: `lsd-date-field--outlined`, + outlinedBottom: `lsd-date-field--outlined-bottom`, } diff --git a/packages/lsd-react/src/components/DateField/DateField.stories.tsx b/packages/lsd-react/src/components/DateField/DateField.stories.tsx index 0f6af24..be8b6f8 100644 --- a/packages/lsd-react/src/components/DateField/DateField.stories.tsx +++ b/packages/lsd-react/src/components/DateField/DateField.stories.tsx @@ -8,7 +8,14 @@ export default { size: { type: { name: 'enum', - value: ['medium', 'large'], + value: ['small', 'medium', 'large'], + }, + defaultValue: 'large', + }, + variant: { + type: { + name: 'enum', + value: ['outlined', 'outlined-bottom'], }, defaultValue: 'large', }, @@ -24,6 +31,7 @@ export const Controlled: Story = ({ ...args }) => { } Uncontrolled.args = { + id: 'label', size: 'large', supportingText: 'Supporting text', disabled: false, @@ -32,9 +40,12 @@ Uncontrolled.args = { error: false, errorIcon: false, clearButton: true, + variant: 'outlined-bottom', + label: 'Label', } Controlled.args = { + id: 'label', size: 'large', supportingText: 'Supporting text', disabled: false, @@ -43,4 +54,6 @@ Controlled.args = { error: false, errorIcon: false, clearButton: true, + variant: 'outlined-bottom', + label: 'Label', } diff --git a/packages/lsd-react/src/components/DateField/DateField.styles.ts b/packages/lsd-react/src/components/DateField/DateField.styles.ts index 0fd6002..608b209 100644 --- a/packages/lsd-react/src/components/DateField/DateField.styles.ts +++ b/packages/lsd-react/src/components/DateField/DateField.styles.ts @@ -4,10 +4,27 @@ import { dateFieldClasses } from './DateField.classes' export const DateFieldStyles = css` .${dateFieldClasses.root} { width: auto; - border-bottom: 1px solid rgb(var(--lsd-border-primary)); box-sizing: border-box; } + .${dateFieldClasses.label} { + display: block; + } + + .${dateFieldClasses.icon} { + cursor: pointer; + display: flex; + align-items: center; + } + + .${dateFieldClasses.outlined} { + border: 1px solid rgb(var(--lsd-border-primary)); + } + + .${dateFieldClasses.outlinedBottom} { + border-bottom: 1px solid rgb(var(--lsd-border-primary)); + } + .${dateFieldClasses.inputContainer} { display: flex; align-items: center; @@ -48,32 +65,64 @@ export const DateFieldStyles = css` } .${dateFieldClasses.supportingText} { - width: fit-content; - margin-top: 20px; + position: absolute; } .${dateFieldClasses.large} { width: 208px; - height: 40px; - padding: 10px 14px; + + .${dateFieldClasses.label} { + margin: 0 0 6px 18px; + } + .${dateFieldClasses.inputContainer} { + height: 40px; + } + .${dateFieldClasses.input} { + padding: 9px 13px 9px 17px; + } + .${dateFieldClasses.icon} { + padding: 12px 13px; + } + .${dateFieldClasses.supportingText} { + margin: 6px 18px 0 18px; + } } .${dateFieldClasses.medium} { width: 188px; - height: 32px; - padding: 6px 12px; + .${dateFieldClasses.label} { + margin: 0 0 6px 14px; + } + .${dateFieldClasses.inputContainer} { + height: 32px; + } + .${dateFieldClasses.input} { + padding: 5px 11px 5px 13px; + } + .${dateFieldClasses.icon} { + padding: 8px 11px; + } + .${dateFieldClasses.supportingText} { + margin: 6px 14px 0 14px; + } } - .${dateFieldClasses.iconButton} { - padding: 0; - width: auto; - height: auto; - margin: 0; - border: 0; - - svg { - width: 100%; - height: auto; + .${dateFieldClasses.small} { + width: 164px; + .${dateFieldClasses.label} { + margin: 0 0 6px 12px; + } + .${dateFieldClasses.inputContainer} { + height: 28px; + } + .${dateFieldClasses.input} { + padding: 5px 9px 5px 11px; + } + .${dateFieldClasses.icon} { + padding: 6px 9px; + } + .${dateFieldClasses.supportingText} { + margin: 6px 12px 0 12px; } } ` diff --git a/packages/lsd-react/src/components/DateField/DateField.tsx b/packages/lsd-react/src/components/DateField/DateField.tsx index 7dde40f..d163cff 100644 --- a/packages/lsd-react/src/components/DateField/DateField.tsx +++ b/packages/lsd-react/src/components/DateField/DateField.tsx @@ -1,7 +1,6 @@ import clsx from 'clsx' import React, { useRef } from 'react' import { useInput } from '../../utils/useInput' -import { IconButton } from '../IconButton' import { CloseIcon, ErrorIcon } from '../Icons' import { Typography } from '../Typography' import { dateFieldClasses } from './DateField.classes' @@ -11,7 +10,8 @@ export type DateFieldProps = Omit< 'onChange' | 'value' > & Pick, 'onChange'> & { - size?: 'large' | 'medium' + label?: React.ReactNode + size?: 'large' | 'medium' | 'small' error?: boolean errorIcon?: boolean clearButton?: boolean @@ -23,11 +23,13 @@ export type DateFieldProps = Omit< icon?: React.ReactNode onIconClick?: () => void inputProps?: React.InputHTMLAttributes + variant?: 'outlined' | 'outlined-bottom' } export const DateField: React.FC & { classes: typeof dateFieldClasses } = ({ + label, size = 'large', error = false, errorIcon = false, @@ -42,6 +44,7 @@ export const DateField: React.FC & { icon, onIconClick, inputProps = {}, + variant = 'outlined-bottom', ...props }) => { const ref = useRef(null) @@ -49,6 +52,8 @@ export const DateField: React.FC & { const onCancel = () => input.setValue('') + const inputId = inputProps?.id ?? (props.id || 'date-field') + '-input' + return (
& { error && dateFieldClasses.error, )} > -
+ {label && ( + + {label} + + )} +
& { className={clsx(inputProps.className, dateFieldClasses.input)} /> {icon ? ( - !disabled && onIconClick && onIconClick()} > {icon} - + ) : error && errorIcon ? ( - + + + ) : clearButton && input.filled ? ( - !disabled && onCancel()} - aria-label="clear" - className={dateFieldClasses.iconButton} + className={dateFieldClasses.icon} > - - + + ) : null}
{supportingText && (
- + {supportingText}
diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts b/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts index ce145e8..1e0e2d1 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts @@ -1,5 +1,9 @@ export const datePickerClasses = { root: `lsd-date-picker`, - calendar: `lsd-date-picker-calendar`, + calendar: `lsd-date-picker__calendar`, + + large: `lsd-date-picker--large`, + medium: `lsd-date-picker--medium`, + small: `lsd-date-picker--small`, } diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx index f966d55..cdc9319 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx @@ -8,7 +8,14 @@ export default { size: { type: { name: 'enum', - value: ['medium', 'large'], + value: ['small', 'medium', 'large'], + }, + defaultValue: 'large', + }, + variant: { + type: { + name: 'enum', + value: ['outlined', 'outlined-bottom'], }, defaultValue: 'large', }, @@ -24,6 +31,7 @@ export const Controlled: Story = ({ ...args }) => { } Uncontrolled.args = { + id: 'label', supportingText: 'Supporting text', disabled: false, error: false, @@ -33,9 +41,12 @@ Uncontrolled.args = { clearButton: true, withCalendar: true, size: 'large', + variant: 'outlined-bottom', + label: 'Label', } Controlled.args = { + id: 'label', supportingText: 'Supporting text', disabled: false, error: false, @@ -45,4 +56,6 @@ Controlled.args = { clearButton: true, withCalendar: true, size: 'large', + variant: 'outlined-bottom', + label: 'Label', } diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts b/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts index c20b358..21b3965 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts @@ -1,5 +1,6 @@ import { css } from '@emotion/react' import { datePickerClasses } from './DatePicker.classes' +import { dateFieldClasses } from '../DateField/DateField.classes' export const DatePickerStyles = css` .${datePickerClasses.root} { @@ -9,4 +10,22 @@ export const DatePickerStyles = css` .${datePickerClasses.calendar} { border-top: none !important; } + + .${datePickerClasses.large} { + .${dateFieldClasses.large} { + width: 318px; + } + } + + .${datePickerClasses.medium} { + .${dateFieldClasses.medium} { + width: 290px; + } + } + + .${datePickerClasses.small} { + .${dateFieldClasses.small} { + width: 262px; + } + } ` diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx index c5c7c3a..12e9007 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx @@ -16,6 +16,7 @@ export type DatePickerProps = Omit< 'onChange' | 'value' > & Pick, 'onChange'> & { + label?: React.ReactNode error?: boolean errorIcon?: boolean clearButton?: boolean @@ -25,13 +26,22 @@ export type DatePickerProps = Omit< value?: string defaultValue?: string placeholder?: string - size?: 'large' | 'medium' + size?: 'large' | 'medium' | 'small' + variant?: 'outlined' | 'outlined-bottom' inputProps?: React.InputHTMLAttributes } export const DatePicker: React.FC & { classes: typeof datePickerClasses -} = ({ value: valueProp, onChange, withCalendar = true, ...props }) => { +} = ({ + label, + size = 'large', + value: valueProp, + onChange, + withCalendar = true, + variant = 'outlined-bottom', + ...props +}) => { const ref = useRef(null) const [openCalendar, setOpenCalendar] = useState(false) @@ -48,18 +58,27 @@ export const DatePicker: React.FC & { const handleDateChange = (date: Date) => input.setValue(dateToISODateString(removeDateTimezoneOffset(date))) + const inputId = (props.id || 'date-picker') + '-input' + return (
} onIconClick={() => setOpenCalendar((prev) => !prev)} value={input.value} onChange={input.onChange} - style={{ width: '310px' }} {...props} > From 28983c1b467bb9aff19661061672df07b882abf6 Mon Sep 17 00:00:00 2001 From: jongomez Date: Thu, 28 Sep 2023 10:42:39 +0100 Subject: [PATCH 11/14] feat: added new class dayIsToday and rebased --- .../lsd-react/src/components/CSSBaseline/CSSBaseline.tsx | 3 +++ .../lsd-react/src/components/Calendar/Calendar.classes.ts | 1 + packages/lsd-react/src/components/Calendar/Day.tsx | 7 +++++-- packages/lsd-react/src/index.ts | 3 +++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx index 1b4c35d..f0e0388 100644 --- a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx +++ b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx @@ -77,6 +77,9 @@ const componentStyles: Array | SerializedStyles> = ModalStyles, ModalFooterStyles, ModalBodyStyles, + DatePickerStyles, + DateFieldStyles, + CalendarStyles, ] export const CSSBaseline: React.FC<{ theme?: Theme }> = ({ diff --git a/packages/lsd-react/src/components/Calendar/Calendar.classes.ts b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts index 72eea89..0e1e79a 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.classes.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts @@ -18,6 +18,7 @@ export const calendarClasses = { day: 'lsd-calendar-day', daySelected: 'lsd-calendar-day--selected', dayDisabled: 'lsd-calendar-day--disabled', + dayIsToday: 'lsd-calendar-day--today', todayIndicator: 'lsd-calendar-day__today_indicator', dayRange: 'lsd-calendar-day--range', } diff --git a/packages/lsd-react/src/components/Calendar/Day.tsx b/packages/lsd-react/src/components/Calendar/Day.tsx index 31f985d..4a9a369 100644 --- a/packages/lsd-react/src/components/Calendar/Day.tsx +++ b/packages/lsd-react/src/components/Calendar/Day.tsx @@ -43,6 +43,9 @@ export const Day = ({ day, date, disabled = false }: DayProps) => { return null } + const isToday = + new Date(date).setHours(0, 0, 0, 0) === new Date().setHours(0, 0, 0, 0) + return ( diff --git a/packages/lsd-react/src/components/DateField/DateField.classes.ts b/packages/lsd-react/src/components/DateField/DateField.classes.ts index bb8ebca..f86a3ef 100644 --- a/packages/lsd-react/src/components/DateField/DateField.classes.ts +++ b/packages/lsd-react/src/components/DateField/DateField.classes.ts @@ -4,6 +4,7 @@ export const dateFieldClasses = { inputContainer: `lsd-date-field__input-container`, input: `lsd-date-field__input-container__input`, + inputFilled: `lsd-date-field__input-container__input--filled`, icon: `lsd-date-field__input-container__icon`, iconButton: `lsd-date-field__input-container__icon-button`, diff --git a/packages/lsd-react/src/components/DateField/DateField.styles.ts b/packages/lsd-react/src/components/DateField/DateField.styles.ts index f505260..00af25d 100644 --- a/packages/lsd-react/src/components/DateField/DateField.styles.ts +++ b/packages/lsd-react/src/components/DateField/DateField.styles.ts @@ -42,6 +42,8 @@ export const DateFieldStyles = css` color: rgb(var(--lsd-text-primary)); background: none; width: 100%; + opacity: 0.4; + transition: opacity 0.2s ease-in-out; } .${dateFieldClasses.input}::-webkit-inner-spin-button, @@ -116,11 +118,9 @@ export const DateFieldStyles = css` } } - .${dateFieldClasses.input}::-webkit-datetime-edit-month-field:focus, - .${dateFieldClasses.input}::-webkit-datetime-edit-day-field:focus, - .${dateFieldClasses.input}::-webkit-datetime-edit-year-field:focus { - color: rgb(var(--lsd-text-primary)); - opacity: 0.4; + .${dateFieldClasses.input}:invalid, .${dateFieldClasses.inputFilled} { + color: rgb(var(--lsd-border-primary)); + opacity: 1; } .${dateFieldClasses.error} diff --git a/packages/lsd-react/src/components/DateField/DateField.tsx b/packages/lsd-react/src/components/DateField/DateField.tsx index 53667aa..e47e8fe 100644 --- a/packages/lsd-react/src/components/DateField/DateField.tsx +++ b/packages/lsd-react/src/components/DateField/DateField.tsx @@ -24,6 +24,7 @@ export type DateFieldProps = Omit< onIconClick?: () => void inputProps?: React.InputHTMLAttributes variant?: 'outlined' | 'outlined-bottom' + calendarIconRef?: React.RefObject } export const DateField: React.FC & { @@ -48,7 +49,12 @@ export const DateField: React.FC & { ...props }) => { const ref = useRef(null) - const input = useInput({ defaultValue, value, onChange, ref }) + const input = useInput({ + defaultValue, + value, + onChange, + ref, + }) const onCancel = () => input.setValue('') @@ -90,15 +96,20 @@ export const DateField: React.FC & { placeholder={placeholder} {...inputProps} ref={ref} - value={input.value} + value={input.value || ''} onChange={input.onChange} - className={clsx(inputProps.className, dateFieldClasses.input)} + className={clsx( + inputProps.className, + dateFieldClasses.input, + input.filled && dateFieldClasses.inputFilled, + )} max={inputProps.max || '9999-12-31'} /> {icon ? ( !disabled && onIconClick && onIconClick()} + ref={props.calendarIconRef} > {icon} diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx index 12e9007..d8c3062 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx @@ -43,7 +43,9 @@ export const DatePicker: React.FC & { ...props }) => { const ref = useRef(null) + const calendarIconRef = useRef(null) const [openCalendar, setOpenCalendar] = useState(false) + const isControlled = typeof valueProp !== 'undefined' const input = useInput({ value: valueProp, @@ -77,8 +79,10 @@ export const DatePicker: React.FC & { variant={variant} icon={withCalendar && } onIconClick={() => setOpenCalendar((prev) => !prev)} - value={input.value} + // The DateField component is only controlled when the value prop is provided OR the calendar is open. + value={isControlled || openCalendar ? input.value : undefined} onChange={input.onChange} + calendarIconRef={calendarIconRef} {...props} > @@ -86,7 +90,18 @@ export const DatePicker: React.FC & { handleDateChange(date)} open={openCalendar} - onClose={() => setOpenCalendar(false)} + onCalendarClickaway={(event) => { + // If the calendar icon was clicked, return and don't close the calendar here. + // Let the onIconClick above handle the closing. + if ( + calendarIconRef.current && + event?.composedPath().includes(calendarIconRef.current) + ) { + return + } + + setOpenCalendar(false) + }} handleRef={ref} value={input.value} disabled={props.disabled} diff --git a/packages/lsd-react/src/utils/date.utils.ts b/packages/lsd-react/src/utils/date.utils.ts index a5dba50..6b8f977 100644 --- a/packages/lsd-react/src/utils/date.utils.ts +++ b/packages/lsd-react/src/utils/date.utils.ts @@ -1,13 +1,16 @@ -export const safeConvertDateToString = (value: string) => { +export const safeConvertDateToString = ( + value: string, + minDate: Date, + maxDate: Date, +) => { const date = new Date(value ?? undefined) - const isValid = !Number.isNaN(+date) + const isValid = !Number.isNaN(+date) && date >= minDate && date <= maxDate return { isValid, date: isValid ? date : new Date(), } } - export const removeDateTimezoneOffset = (date: Date) => new Date(+date - date.getTimezoneOffset() * 60 * 1000)