-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #94 from perimetre/date-input
date input
- Loading branch information
Showing
16 changed files
with
3,801 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
src/components/DatePickerInput/DateCalendar/CalendarCell.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
/* eslint-disable jsx-a11y/no-static-element-interactions */ | ||
/* eslint-disable jsx-a11y/click-events-have-key-events */ | ||
import { CalendarDate, getDayOfWeek, isSameDay } from '@internationalized/date'; | ||
import { CalendarState, RangeCalendarState } from '@react-stately/calendar'; | ||
import classnames from 'classnames'; | ||
import React, { useRef } from 'react'; | ||
import { mergeProps, useCalendarCell, useFocusRing } from 'react-aria'; | ||
import { Instance as TippyInstance } from 'tippy.js'; | ||
|
||
type CalendarCellProps = { | ||
/** | ||
* The calendar date value | ||
*/ | ||
date: CalendarDate; | ||
/** | ||
* The calendar state from `useCalendarState` | ||
*/ | ||
state: CalendarState | RangeCalendarState; | ||
/** | ||
* The current app locale | ||
*/ | ||
locale: string; | ||
/** | ||
* Instance for the tooltip component | ||
*/ | ||
tippyInstance?: TippyInstance; | ||
}; | ||
|
||
/** | ||
* Renders a single calendar cell | ||
* | ||
* @param props Props for the calendar cell | ||
* @param props.state The calendar state from `useCalendarState` | ||
* @param props.date The calendar date value | ||
* @param props.locale The current app locale | ||
* @param props.tippyInstance Instance for the tooltip component | ||
*/ | ||
export const CalendarCell: React.FC<CalendarCellProps> = ({ state, date, locale, tippyInstance }) => { | ||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
const { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, formattedDate, isInvalid } = | ||
useCalendarCell({ date }, state, ref); | ||
|
||
// IF this is a range selection calendar | ||
// The start and end date of the selected range will have | ||
// an emphasized appearance. | ||
const isSelectionStart = (state as RangeCalendarState).highlightedRange | ||
? isSameDay(date, (state as RangeCalendarState).highlightedRange.start) | ||
: isSelected; | ||
const isSelectionEnd = (state as RangeCalendarState).highlightedRange | ||
? isSameDay(date, (state as RangeCalendarState).highlightedRange.end) | ||
: isSelected; | ||
|
||
// We add rounded corners on the left for the first day of the month, | ||
// the first day of each week, and the start date of the selection. | ||
// We add rounded corners on the right for the last day of the month, | ||
// the last day of each week, and the end date of the selection. | ||
const dayOfWeek = getDayOfWeek(date, locale); | ||
const isRoundedLeft = isSelected && (isSelectionStart || dayOfWeek === 0 || date.day === 1); | ||
const isRoundedRight = | ||
isSelected && (isSelectionEnd || dayOfWeek === 6 || date.day === date.calendar.getDaysInMonth(date)); | ||
|
||
const { focusProps, isFocusVisible } = useFocusRing(); | ||
const finalButtonProps = mergeProps(buttonProps, focusProps); | ||
|
||
return ( | ||
<td {...cellProps} className={`relative py-0.5 ${isFocusVisible ? 'z-10' : 'z-0'}`}> | ||
<div | ||
{...finalButtonProps} | ||
onPointerUp={(e) => { | ||
finalButtonProps.onPointerUp?.(e); | ||
|
||
if ('highlightedRange' in state ? !state.isDragging : true) { | ||
// Makes sure to run hide on the next tick | ||
setTimeout(() => tippyInstance?.hide(), 10); | ||
} | ||
}} | ||
onKeyUp={(e) => { | ||
finalButtonProps.onKeyUp?.(e); | ||
|
||
// If it's finished selecting a range, or if it's a single date picker | ||
if (('highlightedRange' in state ? !state.isDragging : true) && e.key === 'Enter') { | ||
// Makes sure to run hide on the next tick | ||
setTimeout(() => tippyInstance?.hide(), 10); | ||
} | ||
}} | ||
ref={ref} | ||
hidden={isOutsideVisibleRange} | ||
className={classnames( | ||
'group h-10 w-10 outline-none', | ||
{ | ||
'rounded-l-full': isRoundedLeft, | ||
'rounded-r-full': isRoundedRight, | ||
disabled: isDisabled, | ||
// Hover state for non-selected cells. | ||
'hover:bg-gray-100': !isSelected && !isDisabled | ||
}, | ||
isSelected ? (isInvalid ? 'bg-red-300' : 'bg-gray-300') : undefined | ||
)} | ||
> | ||
<div | ||
className={classnames( | ||
'flex h-full w-full items-center justify-center rounded-full', | ||
{ | ||
'text-gray-400': isDisabled && !isInvalid, | ||
// Focus ring, visible while the cell has keyboard focus. | ||
'group-focus:z-2 ring-2 ring-pui-placeholder-color ring-offset-2': isFocusVisible | ||
}, | ||
// Darker selection background for the start and end. | ||
isSelectionStart || isSelectionEnd | ||
? isInvalid | ||
? 'bg-red-600 text-white hover:bg-red-700' | ||
: 'bg-pui-placeholder-color text-white hover:bg-pui-placeholder-color' | ||
: undefined, | ||
// Hover state for cells in the middle of the range. | ||
isSelected && !isDisabled && !(isSelectionStart || isSelectionEnd) | ||
? isInvalid | ||
? 'hover:bg-red-400' | ||
: 'hover:bg-pui-placeholder-color' | ||
: undefined, | ||
'cursor-default' | ||
)} | ||
> | ||
{formattedDate} | ||
</div> | ||
</div> | ||
</td> | ||
); | ||
}; |
65 changes: 65 additions & 0 deletions
65
src/components/DatePickerInput/DateCalendar/CalendarGrid.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { getWeeksInMonth } from '@internationalized/date'; | ||
import { CalendarState, RangeCalendarState } from '@react-stately/calendar'; | ||
import React from 'react'; | ||
import { AriaCalendarGridProps, useCalendarGrid } from 'react-aria'; | ||
import { CalendarCell } from './CalendarCell'; | ||
import { Instance as TippyInstance } from 'tippy.js'; | ||
|
||
type CalendarGridProps = AriaCalendarGridProps & { | ||
/** | ||
* The calendar state from `useCalendarState` | ||
*/ | ||
state: CalendarState | RangeCalendarState; | ||
/** | ||
* The current app locale | ||
*/ | ||
locale: string; | ||
/** | ||
* Instance for the tooltip component | ||
*/ | ||
tippyInstance?: TippyInstance; | ||
}; | ||
|
||
/** | ||
* Renders the table with days in a grid for the date input | ||
* | ||
* @param props Props for the calendar grid | ||
* @param props.state The calendar state from `useCalendarState` | ||
* @param props.locale The current app locale | ||
* @param props.tippyInstance Instance for the tooltip component | ||
*/ | ||
export const CalendarGrid: React.FC<CalendarGridProps> = ({ state, locale, tippyInstance, ...props }) => { | ||
const { gridProps, headerProps, weekDays } = useCalendarGrid(props, state); | ||
|
||
// Get the number of weeks in the month so we can render the proper number of rows. | ||
const weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale); | ||
|
||
return ( | ||
<table {...gridProps} cellPadding="0" className="flex-1"> | ||
<thead {...headerProps} className="text-gray-600"> | ||
<tr> | ||
{weekDays.map((day, index) => ( | ||
<th key={index} className="text-center"> | ||
{day} | ||
</th> | ||
))} | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{[...new Array(weeksInMonth).keys()].map((weekIndex) => ( | ||
<tr key={weekIndex}> | ||
{state | ||
.getDatesInWeek(weekIndex) | ||
.map((date, i) => | ||
date ? ( | ||
<CalendarCell tippyInstance={tippyInstance} locale={locale} key={i} date={date} state={state} /> | ||
) : ( | ||
<td key={i} /> | ||
) | ||
)} | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'; | ||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||
import { createCalendar } from '@internationalized/date'; | ||
import classnames from 'classnames'; | ||
import React, { PropsWithChildren, useRef } from 'react'; | ||
import { AriaButtonProps, AriaCalendarProps, mergeProps, useButton, useCalendar, useFocusRing } from 'react-aria'; | ||
import { useCalendarState } from 'react-stately'; | ||
import { DateValue } from '..'; | ||
import { Button } from '../../Button'; | ||
import { CalendarGrid } from './CalendarGrid'; | ||
import { Instance as TippyInstance } from 'tippy.js'; | ||
|
||
type CalendarButtonProps = AriaButtonProps<'button'> & { | ||
/** | ||
* Whether the button is currently pressed | ||
*/ | ||
isPressed?: boolean; | ||
}; | ||
|
||
/** | ||
* Renders a single button for the calendar | ||
* | ||
* @param props Button props | ||
* @param props.children Content for the button | ||
*/ | ||
export const CalendarButton: React.FC<PropsWithChildren<CalendarButtonProps>> = ({ children, ...props }) => { | ||
const ref = useRef<HTMLButtonElement>(null); | ||
|
||
const { buttonProps } = useButton(props, ref); | ||
const { focusProps, isFocusVisible } = useFocusRing(); | ||
|
||
return ( | ||
<Button | ||
variant="icon" | ||
{...mergeProps(buttonProps, focusProps)} | ||
ref={ref} | ||
className={classnames( | ||
'rounded-full p-2 outline-none', | ||
props.isDisabled ? 'text-gray-400' : 'hover:bg-gray-100 active:bg-gray-200', | ||
{ | ||
'ring-2 ring-pui-placeholder-color ring-offset-2': isFocusVisible | ||
} | ||
)} | ||
> | ||
{children} | ||
</Button> | ||
); | ||
}; | ||
|
||
type DateCalendarProps<T extends DateValue = DateValue> = AriaCalendarProps<T> & { | ||
/** | ||
* The current app locale | ||
*/ | ||
locale: string; | ||
/** | ||
* Instance for the tooltip component | ||
*/ | ||
tippyInstance?: TippyInstance; | ||
}; | ||
|
||
/** | ||
* Calendar that renders the days in a grid for the date input | ||
* | ||
* @param props Calendar props for `useCalendar` | ||
* @param props.locale The current app locale | ||
* @param props.tippyInstance Instance for the tooltip component | ||
*/ | ||
export const DateCalendar: React.FC<DateCalendarProps> = ({ locale, tippyInstance, ...props }) => { | ||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
const state = useCalendarState({ | ||
...props, | ||
locale, | ||
createCalendar | ||
}); | ||
|
||
const { calendarProps, prevButtonProps, nextButtonProps, title } = useCalendar(props, state); | ||
|
||
return ( | ||
<div {...calendarProps} ref={ref} className="inline-block text-gray-800"> | ||
<div className="flex items-center pb-4"> | ||
<h2 className="ml-2 flex-1 text-xl font-bold">{title}</h2> | ||
<CalendarButton {...prevButtonProps}> | ||
<FontAwesomeIcon className="h-6 w-6" icon={faChevronLeft} /> | ||
</CalendarButton> | ||
<CalendarButton {...nextButtonProps}> | ||
<FontAwesomeIcon className="h-6 w-6" icon={faChevronRight} /> | ||
</CalendarButton> | ||
</div> | ||
<CalendarGrid locale={locale} state={state} tippyInstance={tippyInstance} /> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.