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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@datepicker-react/hooks": "^2.8.4",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"clsx": "^1.2.1",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 './RadioButtonIcon'
|
||||
export * from './RadioButtonFilledIcon'
|
||||
export * from './CalendarIcon'
|
||||
export * from './RemoveIcon'
|
||||
|
|
12
yarn.lock
12
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"
|
||||
|
|
Loading…
Reference in New Issue