mirror of https://github.com/acid-info/lsd.git
feat: implement date picker component
This commit is contained in:
parent
41b2c62e63
commit
51d995a264
|
@ -14,6 +14,7 @@
|
||||||
"prepublish": "yarn build"
|
"prepublish": "yarn build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@datepicker-react/hooks": "^2.8.4",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { BadgeStyles } from '../Badge/Badge.styles'
|
||||||
import { BreadcrumbStyles } from '../Breadcrumb/Breadcrumb.styles'
|
import { BreadcrumbStyles } from '../Breadcrumb/Breadcrumb.styles'
|
||||||
import { BreadcrumbItemStyles } from '../BreadcrumbItem/BreadcrumbItem.styles'
|
import { BreadcrumbItemStyles } from '../BreadcrumbItem/BreadcrumbItem.styles'
|
||||||
import { ButtonStyles } from '../Button/Button.styles'
|
import { ButtonStyles } from '../Button/Button.styles'
|
||||||
|
import { CalendarStyles } from '../Calendar/Calendar.styles'
|
||||||
import { CardStyles } from '../Card/Card.styles'
|
import { CardStyles } from '../Card/Card.styles'
|
||||||
import { CardBodyStyles } from '../CardBody/CardBody.styles'
|
import { CardBodyStyles } from '../CardBody/CardBody.styles'
|
||||||
import { CardHeaderStyles } from '../CardHeader/CardHeader.styles'
|
import { CardHeaderStyles } from '../CardHeader/CardHeader.styles'
|
||||||
|
@ -12,6 +13,8 @@ import { CheckboxStyles } from '../Checkbox/Checkbox.styles'
|
||||||
import { CheckboxGroupStyles } from '../CheckboxGroup/CheckboxGroup.styles'
|
import { CheckboxGroupStyles } from '../CheckboxGroup/CheckboxGroup.styles'
|
||||||
import { CollapseStyles } from '../Collapse/Collapse.styles'
|
import { CollapseStyles } from '../Collapse/Collapse.styles'
|
||||||
import { CollapseHeaderStyles } from '../CollapseHeader/CollapseHeader.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 { DropdownStyles } from '../Dropdown/Dropdown.styles'
|
||||||
import { DropdownItemStyles } from '../DropdownItem/DropdownItem.styles'
|
import { DropdownItemStyles } from '../DropdownItem/DropdownItem.styles'
|
||||||
import { IconButtonStyles } from '../IconButton/IconButton.styles'
|
import { IconButtonStyles } from '../IconButton/IconButton.styles'
|
||||||
|
|
|
@ -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',
|
||||||
|
}
|
|
@ -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)
|
|
@ -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: '',
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
`
|
|
@ -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<HTMLUListElement>,
|
||||||
|
'label'
|
||||||
|
> & {
|
||||||
|
open?: boolean
|
||||||
|
value?: string
|
||||||
|
handleDateFieldChange: (data: Date) => void
|
||||||
|
handleRef: React.RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Calendar: React.FC<CalendarProps> & {
|
||||||
|
classes: typeof calendarClasses
|
||||||
|
} = ({
|
||||||
|
open,
|
||||||
|
handleRef,
|
||||||
|
value = null,
|
||||||
|
handleDateFieldChange,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<div className={calendarClasses.header}>
|
||||||
|
<button
|
||||||
|
className={clsx(calendarClasses.button)}
|
||||||
|
type="button"
|
||||||
|
onClick={goToPreviousMonths}
|
||||||
|
>
|
||||||
|
<NavigateBeforeIcon color="primary" />
|
||||||
|
</button>
|
||||||
|
<Typography variant="label1">{monthLabel}</Typography>
|
||||||
|
<button
|
||||||
|
className={clsx(calendarClasses.button)}
|
||||||
|
type="button"
|
||||||
|
onClick={goToNextMonths}
|
||||||
|
>
|
||||||
|
<NavigateNextIcon color="primary" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={clsx(calendarClasses.grid)}>
|
||||||
|
{weekdayLabels.map((dayLabel, idx) => (
|
||||||
|
<Typography
|
||||||
|
key={idx}
|
||||||
|
variant="label2"
|
||||||
|
style={{ textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
{dayLabel[0]}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={clsx(calendarClasses.grid)}>
|
||||||
|
{days.map((ele, idx) =>
|
||||||
|
typeof ele !== 'number' ? (
|
||||||
|
<Day date={ele.date} day={ele.dayLabel} key={ele.dayLabel} />
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
key={`prev-${idx}`}
|
||||||
|
className={clsx(calendarClasses.day, calendarClasses.dayDisabled)}
|
||||||
|
>
|
||||||
|
<Typography variant="label2">
|
||||||
|
{renderOtherDays(
|
||||||
|
idx - days.filter((day) => day === 0).length,
|
||||||
|
days[days.lastIndexOf(0) + 1].date,
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{days.length % 7 !== 0 &&
|
||||||
|
new Array(7 - (days.length % 7)).fill(null).map((ele, idx) => (
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
key={`after-${ele}`}
|
||||||
|
className={clsx(calendarClasses.day, calendarClasses.dayDisabled)}
|
||||||
|
>
|
||||||
|
<Typography variant="label2">
|
||||||
|
{renderOtherDays(idx, days[days.lastIndexOf(0) + 1].date)}
|
||||||
|
</Typography>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './Calendar'
|
|
@ -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`,
|
||||||
|
}
|
|
@ -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<DateFieldProps> = ({ ...args }) => {
|
||||||
|
return <DateField {...args} />
|
||||||
|
}
|
||||||
|
|
||||||
|
Root.args = {
|
||||||
|
size: 'large',
|
||||||
|
supportingText: 'Supporting text',
|
||||||
|
disabled: false,
|
||||||
|
error: false,
|
||||||
|
errorIcon: false,
|
||||||
|
clearButton: true,
|
||||||
|
onChange: undefined,
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -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<HTMLDivElement>,
|
||||||
|
'onChange' | 'value'
|
||||||
|
> &
|
||||||
|
Pick<React.InputHTMLAttributes<HTMLInputElement>, '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<HTMLInputElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateField: React.FC<DateFieldProps> & {
|
||||||
|
classes: typeof dateFieldClasses
|
||||||
|
} = ({
|
||||||
|
size = 'large',
|
||||||
|
error = false,
|
||||||
|
errorIcon = false,
|
||||||
|
clearButton,
|
||||||
|
supportingText,
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
defaultValue,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
icon,
|
||||||
|
onIconClick,
|
||||||
|
inputProps = {},
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLInputElement>(null)
|
||||||
|
const input = useInput({ defaultValue, value, onChange, ref })
|
||||||
|
|
||||||
|
const onCancel = () => input.setValue('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-disabled={disabled ? 'true' : 'false'}
|
||||||
|
{...props}
|
||||||
|
className={clsx(
|
||||||
|
props.className,
|
||||||
|
dateFieldClasses.root,
|
||||||
|
dateFieldClasses[size],
|
||||||
|
disabled && dateFieldClasses.disabled,
|
||||||
|
error && dateFieldClasses.error,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={dateFieldClasses.inputContainer}>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...inputProps}
|
||||||
|
ref={ref}
|
||||||
|
value={input.value}
|
||||||
|
onChange={input.onChange}
|
||||||
|
className={clsx(inputProps.className, dateFieldClasses.input)}
|
||||||
|
/>
|
||||||
|
{icon ? (
|
||||||
|
<IconButton
|
||||||
|
disabled={disabled}
|
||||||
|
className={dateFieldClasses.iconButton}
|
||||||
|
onClick={() => !disabled && onIconClick && onIconClick()}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</IconButton>
|
||||||
|
) : error && errorIcon ? (
|
||||||
|
<ErrorIcon color="primary" className={dateFieldClasses.icon} />
|
||||||
|
) : clearButton && input.filled ? (
|
||||||
|
<IconButton
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => !disabled && onCancel()}
|
||||||
|
aria-label="clear"
|
||||||
|
className={dateFieldClasses.iconButton}
|
||||||
|
>
|
||||||
|
<CloseIcon color="primary" className={dateFieldClasses.icon} />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{supportingText && (
|
||||||
|
<div className={clsx(dateFieldClasses.supportingText)}>
|
||||||
|
<Typography
|
||||||
|
variant={size === 'large' ? 'label1' : 'label2'}
|
||||||
|
component="p"
|
||||||
|
>
|
||||||
|
{supportingText}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DateField.classes = dateFieldClasses
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './DateField'
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const datePickerClasses = {
|
||||||
|
root: `lsd-date-picker`,
|
||||||
|
withCalendar: `lsd-date-picker--with-calendar`,
|
||||||
|
}
|
|
@ -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<DatePickerProps> = ({ ...args }) => {
|
||||||
|
return <DatePicker {...args}>DatePicker</DatePicker>
|
||||||
|
}
|
||||||
|
|
||||||
|
Root.args = {
|
||||||
|
size: 'large',
|
||||||
|
supportingText: 'Supporting text',
|
||||||
|
disabled: false,
|
||||||
|
error: false,
|
||||||
|
value: '',
|
||||||
|
onChange: undefined,
|
||||||
|
errorIcon: false,
|
||||||
|
clearButton: true,
|
||||||
|
withCalendar: true,
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
`
|
|
@ -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<HTMLDivElement>,
|
||||||
|
'onChange' | 'value'
|
||||||
|
> &
|
||||||
|
Pick<React.InputHTMLAttributes<HTMLInputElement>, '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<HTMLInputElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatePicker: React.FC<DatePickerProps> & {
|
||||||
|
classes: typeof datePickerClasses
|
||||||
|
} = ({ value, onChange, withCalendar = true, ...props }) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const [openCalendar, setOpenCalendar] = useState(false)
|
||||||
|
const [date, setDate] = useState<string>(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 (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={clsx(
|
||||||
|
props.className,
|
||||||
|
datePickerClasses.root,
|
||||||
|
withCalendar && datePickerClasses.withCalendar,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DateField
|
||||||
|
icon={withCalendar && <CalendarIcon color="primary" />}
|
||||||
|
onIconClick={() => setOpenCalendar((prev) => !prev)}
|
||||||
|
value={date}
|
||||||
|
onChange={(data) => setDate(data.target.value)}
|
||||||
|
style={{ width: '310px' }}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Portal id="calendar">
|
||||||
|
{withCalendar && (
|
||||||
|
<Calendar
|
||||||
|
handleDateFieldChange={handleDateFieldChange}
|
||||||
|
open={openCalendar}
|
||||||
|
handleRef={ref}
|
||||||
|
value={date}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Portal>
|
||||||
|
</DateField>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DatePicker.classes = datePickerClasses
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './DatePicker'
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { LsdIcon } from '../LsdIcon'
|
||||||
|
|
||||||
|
export const CalendarIcon = LsdIcon(
|
||||||
|
(props) => (
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M11.0833 2.33332H10.5V1.16666H9.33333V2.33332H4.66667V1.16666H3.5V2.33332H2.91667C2.26917 2.33332 1.75 2.85832 1.75 3.49999V11.6667C1.75 12.3083 2.26917 12.8333 2.91667 12.8333H11.0833C11.725 12.8333 12.25 12.3083 12.25 11.6667V3.49999C12.25 2.85832 11.725 2.33332 11.0833 2.33332ZM11.0833 11.6667H2.91667V5.24999H11.0833V11.6667ZM3.79167 7.58332C3.79167 6.77832 4.445 6.12499 5.25 6.12499C6.055 6.12499 6.70833 6.77832 6.70833 7.58332C6.70833 8.38832 6.055 9.04166 5.25 9.04166C4.445 9.04166 3.79167 8.38832 3.79167 7.58332Z"
|
||||||
|
fill="black"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
filled: true,
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './CalendarIcon'
|
|
@ -19,4 +19,5 @@ export * from './SearchIcon'
|
||||||
export * from './PickIcon'
|
export * from './PickIcon'
|
||||||
export * from './RadioButtonIcon'
|
export * from './RadioButtonIcon'
|
||||||
export * from './RadioButtonFilledIcon'
|
export * from './RadioButtonFilledIcon'
|
||||||
|
export * from './CalendarIcon'
|
||||||
export * from './RemoveIcon'
|
export * from './RemoveIcon'
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -1344,6 +1344,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/trace-mapping" "0.3.9"
|
"@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":
|
"@design-systems/utils@2.12.0":
|
||||||
version "2.12.0"
|
version "2.12.0"
|
||||||
resolved "https://registry.npmjs.org/@design-systems/utils/-/utils-2.12.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz"
|
||||||
integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==
|
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:
|
dateformat@^3.0.0:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz"
|
resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz"
|
||||||
|
|
Loading…
Reference in New Issue