-
Notifications
You must be signed in to change notification settings - Fork 1
[FE-Feat] Date Picker 컴포넌트 구현 #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
30b368a
4623a7d
c22f406
9e29326
596c17b
176867a
474fdb3
f94014e
73e5180
fc6d388
d7b7caf
d74ac12
e164314
e46fd5a
a326e91
0a21728
f108e50
4c97ae3
dd3eae7
ca1b852
aa95f5b
0a3bfd7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { style } from '@vanilla-extract/css'; | ||
| import { recipe } from '@vanilla-extract/recipes'; | ||
|
|
||
| import { vars } from '@/theme/index.css'; | ||
|
|
||
| export const avatarContainerStyle = style({ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| }); | ||
|
|
||
| export const avatarItemStyle = recipe({ | ||
| base: { | ||
| backgroundColor: vars.color.Ref.Netural['White'], | ||
| borderRadius: vars.radius['Max'], | ||
| border: `2px solid ${vars.color.Ref.Netural[500]}`, | ||
| }, | ||
| variants: { | ||
| size: { | ||
| sm: { | ||
| width: '28px', | ||
| height: '28px', | ||
| selectors: { | ||
| '&:not(:first-child)': { | ||
| marginLeft: '-12px', | ||
| }, | ||
| }, | ||
| }, | ||
| lg: { | ||
| width: '42px', | ||
| height: '42px', | ||
| selectors: { | ||
| '&:not(:first-child)': { | ||
| marginLeft: '-14px', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| export const avatarCountStyle = recipe({ | ||
| base: { | ||
| borderRadius: vars.radius['Max'], | ||
| backgroundColor: vars.color.Ref.Netural['White'], | ||
| color: vars.color.Ref.Netural[500], | ||
| border: `2px solid ${vars.color.Ref.Netural[100]}`, | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
|
|
||
| import DatePicker from '.'; | ||
|
|
||
| const meta: Meta = { | ||
| title: 'Calendar/DatePicker', | ||
| component: DatePicker, | ||
| argTypes: { | ||
| calendarType: { | ||
| control: { type: 'radio' }, | ||
| options: ['select', 'range'], | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof DatePicker>; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof DatePicker>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| calendarType: 'select', | ||
| }, | ||
| }; | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { createContext } from 'react'; | ||
|
|
||
| import type { UseMonthCalendarReturn } from '@/hooks/useDatePicker'; | ||
|
|
||
| import type { CellStyleProps, DatePickerProps } from '.'; | ||
|
|
||
| interface DatePickerContextProps extends UseMonthCalendarReturn, DatePickerProps { | ||
| todayCellStyle: CellStyleProps; | ||
| selectedCellStyle: CellStyleProps; | ||
| } | ||
|
|
||
| export const DatePickerContext = createContext<DatePickerContextProps | null>(null); | ||
dioo1461 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { style } from '@vanilla-extract/css'; | ||
|
|
||
| export const headerStyle = style({ | ||
| display: 'flex', | ||
| paddingLeft: '8px', | ||
| justifyContent: 'space-between', | ||
| alignItems: 'center', | ||
| alignSelf: 'stretch', | ||
| }); | ||
|
|
||
| export const chevronWrapper = style({ | ||
| display: 'flex', | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| width: 32, | ||
| height: 32, | ||
| }); | ||
dioo1461 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { Flex } from '@/components/Flex'; | ||
| import { ChevronLeft, ChevronRight } from '@/components/Icon'; | ||
| import { Text } from '@/components/Text'; | ||
| import { useSafeContext } from '@/hooks/useSafeContext'; | ||
| import { vars } from '@/theme/index.css'; | ||
| import { getDateParts } from '@/utils/date/calendar'; | ||
|
|
||
| import { DatePickerContext } from '../DatePickerContext'; | ||
| import { chevronWrapper, headerStyle } from './index.css'; | ||
|
|
||
| const Header = () => { | ||
| const { | ||
| baseDate, | ||
| goToPrevMonth, | ||
| goToNextMonth, | ||
| } = useSafeContext(DatePickerContext); | ||
| const { year: currentYear, month: currentMonth } = getDateParts(baseDate); | ||
| return ( | ||
| <div className={headerStyle}> | ||
| <Text typo='b1M'>{`${currentYear}년 ${currentMonth + 1}월`}</Text> | ||
| <Flex direction='row'> | ||
| <span className={chevronWrapper}> | ||
| <ChevronLeft | ||
| aria-label='이전 달로 이동' | ||
| clickable={true} | ||
| fill={vars.color.Ref.Netural[500]} | ||
| onClick={goToPrevMonth} | ||
| onKeyDown={(e) => e.key === 'Enter' && goToPrevMonth()} | ||
| role='button' | ||
| tabIndex={0} | ||
| /> | ||
| </span> | ||
| <span className={chevronWrapper}> | ||
| <ChevronRight | ||
| aria-label='다음 달로 이동' | ||
| clickable={true} | ||
| fill={vars.color.Ref.Netural[500]} | ||
| onClick={goToNextMonth} | ||
| onKeyDown={(e) => e.key === 'Enter' && goToNextMonth()} | ||
| role='button' | ||
| tabIndex={0} | ||
| /> | ||
| </span> | ||
| </Flex> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default Header; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
|
|
||
| import type { PropsWithChildren } from 'react'; | ||
|
|
||
| import clsx from '@/utils/clsx'; | ||
|
|
||
| import { Text } from '../../../Text'; | ||
| import { cellWrapperStyle } from './index.css'; | ||
|
|
||
| interface CellWrapperProps extends PropsWithChildren { | ||
| cursorType: 'default' | 'pointer' | 'not-allowed'; | ||
| className?: string; | ||
| style?: object; | ||
| onClick?: () => void; | ||
| } | ||
| export const CellWrapper = ({ | ||
| className, | ||
| cursorType, | ||
| style, | ||
| onClick, | ||
| children, | ||
| }: CellWrapperProps) => ( | ||
| <div | ||
| className={clsx(cellWrapperStyle({ cursorType }), className )} | ||
| onClick={onClick} | ||
| style={style} | ||
| > | ||
| <Text typo='caption'>{children}</Text> | ||
| </div> | ||
| ); | ||
|
|
||
| export default CellWrapper; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { assignInlineVars } from '@vanilla-extract/dynamic'; | ||
|
|
||
| import { useDateSelect } from '@/hooks/useDatePicker/useDateSelect'; | ||
| import { useSafeContext } from '@/hooks/useSafeContext'; | ||
| import { isSameDate, isSaturday, isSunday } from '@/utils/date'; | ||
|
|
||
| import type { CalendarType } from '../..'; | ||
| import { DatePickerContext } from '../../DatePickerContext'; | ||
| import type { HighlightState } from '../Highlight'; | ||
| import { cellThemeVars } from '../index.css'; | ||
| import CellWrapper from './CellWrapper'; | ||
| import { | ||
| holidayCellStyle, | ||
| otherMonthCellStyle, | ||
| saturdayCellStyle, | ||
| selectedCellStyle, | ||
| todayCellStyle, | ||
| weekdayCellStyle, | ||
| } from './index.css'; | ||
|
|
||
| export interface DateCellProps { | ||
| date: Date; | ||
| baseDate: Date; | ||
| selected: boolean; | ||
| highlightState: HighlightState; | ||
| } | ||
|
|
||
| export const DateCell = ({ date, selected, baseDate, highlightState }: DateCellProps) => { | ||
| const { | ||
| calendarType, | ||
| todayCellStyle, | ||
| selectedCellStyle, | ||
| } = useSafeContext(DatePickerContext); | ||
| const inlineCellStyles = assignInlineVars(cellThemeVars, { | ||
| todayCellBackgroundColor: todayCellStyle.backgroundColor ?? 'transparent', | ||
| todayCellColor: todayCellStyle.color ?? 'transparent', | ||
| selectedCellBackgroundColor: selectedCellStyle.backgroundColor ?? 'transparent', | ||
| selectedCellColor: selectedCellStyle.color ?? 'transparent', | ||
| }); | ||
|
|
||
|
||
| const dateCellType = getDateCellType(date, calendarType, selected, baseDate, highlightState); | ||
| const selectDate = useDateSelect(date); | ||
| const handleDateCellClick = () => { | ||
| if (calendarType === 'range' && dateCellType === 'otherMonth') return; | ||
| selectDate(); | ||
| }; | ||
| // TODO: cell에 대한 cursor style과 컨트롤을 묶어서 처리할 방법 모색 | ||
| return ( | ||
| <CellWrapper | ||
| className={getDateCellStyle(dateCellType)} | ||
| cursorType={calendarType === 'range' && dateCellType === 'otherMonth' | ||
| ? 'not-allowed' | ||
| : 'pointer'} | ||
| onClick={handleDateCellClick} | ||
| style={inlineCellStyles} | ||
| > | ||
| {date.getDate()} | ||
| </CellWrapper> | ||
| ); | ||
| }; | ||
|
|
||
| type DateCellType = 'weekday' | 'saturday' | 'holiday' | 'otherMonth' | 'today' | 'selected'; | ||
|
|
||
| const getDateCellType = ( | ||
| date: Date, | ||
| calendarType: CalendarType, | ||
| selected: boolean, | ||
| baseDate: Date, | ||
| highlightState: HighlightState, | ||
| ): DateCellType => { | ||
| if (selected) return 'selected'; | ||
| if (calendarType === 'range' && ( | ||
| highlightState === 'startOfRange' || highlightState === 'endOfRange' | ||
| )) { | ||
| return 'selected'; | ||
| } | ||
|
|
||
| if (calendarType === 'select' && isSameDate(date, new Date())) return 'today'; | ||
| if (date.getMonth() !== baseDate.getMonth()) return 'otherMonth'; | ||
| // TODO: 공휴일도 함께 체크 | ||
| if (isSunday(date)) return 'holiday'; | ||
| if (isSaturday(date)) return 'saturday'; | ||
| return 'weekday'; | ||
| }; | ||
|
|
||
| const getDateCellStyle = ( | ||
| dateCellType: DateCellType, | ||
| ) => { | ||
| switch (dateCellType) { | ||
| case 'selected': | ||
| return selectedCellStyle; | ||
| case 'today': | ||
| return todayCellStyle; | ||
| case 'otherMonth': | ||
| return otherMonthCellStyle; | ||
| case 'holiday': | ||
| return holidayCellStyle; | ||
| case 'saturday': | ||
| return saturdayCellStyle; | ||
| case 'weekday': | ||
| default: | ||
| return weekdayCellStyle; | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import type { PropsWithChildren } from 'react'; | ||
|
|
||
| import CellWrapper from './CellWrapper'; | ||
|
|
||
| interface DowCellProps extends PropsWithChildren { | ||
| className: string; | ||
| } | ||
|
|
||
| export const DowCell = ({ className, children }: DowCellProps) => ( | ||
| <CellWrapper className={className} cursorType={'default'}> | ||
| {children} | ||
| </CellWrapper> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { style } from '@vanilla-extract/css'; | ||
| import { recipe } from '@vanilla-extract/recipes'; | ||
|
|
||
| import { vars } from '@/theme/index.css'; | ||
|
|
||
| import { cellThemeVars } from '../index.css'; | ||
|
|
||
| export const cellWrapperStyle = recipe({ | ||
| base: { | ||
| display: 'flex', | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| width: '24px', | ||
| height: '24px', | ||
| }, | ||
| variants: { | ||
| cursorType: { | ||
| pointer: { | ||
| cursor: 'pointer', | ||
| }, | ||
| default: { | ||
| cursor: 'default', | ||
| }, | ||
| 'not-allowed': { | ||
| cursor: 'not-allowed', | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| export const weekdayCellStyle = style({ | ||
| color: vars.color.Ref.Netural[700], | ||
hamo-o marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| export const todayCellStyle = style({ | ||
| borderRadius: vars.radius[200], | ||
| backgroundColor: cellThemeVars.todayCellBackgroundColor, | ||
| color: cellThemeVars.todayCellColor, | ||
| }); | ||
|
|
||
| export const selectedCellStyle = style({ | ||
| borderRadius: vars.radius[200], | ||
| backgroundColor: cellThemeVars.selectedCellBackgroundColor, | ||
| color: cellThemeVars.selectedCellColor, | ||
| }); | ||
|
|
||
| export const otherMonthCellStyle = style({ | ||
| color: vars.color.Ref.Netural[400], | ||
| }); | ||
|
|
||
| export const saturdayCellStyle = style({ | ||
| color: vars.color.Ref.Primary[500], | ||
| }); | ||
|
|
||
| export const holidayCellStyle = style({ | ||
| color: vars.color.Ref.Red[500], | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './DateCell.tsx'; | ||
| export * from './DowCell.tsx'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
|
|
||
| import type { HighlightProps } from '.'; | ||
| import { highlightBoxStyle } from './index.css'; | ||
|
|
||
| const HighlightBox = ({ highlightState, children }: HighlightProps) => ( | ||
| <div className={highlightBoxStyle({ highlightState })}> | ||
| {children} | ||
| </div> | ||
| ); | ||
|
|
||
| export default HighlightBox; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix typo in color variable name.
There's a typo in the color variable name: "Netural" should be "Neutral".
Apply this diff to fix the typo:
Also applies to: 44-46