Skip to content

Commit

Permalink
feat(calender/datepicker): allow fixed height and mobile UX improveme…
Browse files Browse the repository at this point in the history
…nts (#412)

* feat(Calendar): add showOutsideDays prop

* feat(calendar): allow hiding today button

* feat: set filler row for fixed calendar height

* feat(Calendar): improve UX of mobile variant

- move the navigation buttons to be on the first calendar if in mobile view
- reduce spacing between calendars in mobile view

* feat(datepicker): refactor prop drilling and pass new calendar props
  • Loading branch information
karrui authored Jun 27, 2023
1 parent fde2e57 commit 88ca90a
Show file tree
Hide file tree
Showing 18 changed files with 140 additions and 57 deletions.
10 changes: 10 additions & 0 deletions react/src/Calendar/Calendar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ CalendarWithValue.args = {
value: new Date('2001-01-01'),
}

export const HideOutsideDays = CalendarOnlyTemplate.bind({})
HideOutsideDays.args = {
showOutsideDays: false,
}

export const HideTodayButton = CalendarOnlyTemplate.bind({})
HideTodayButton.args = {
showTodayButton: false,
}

export const CalendarWeekdayOnly = CalendarOnlyTemplate.bind({})
CalendarWeekdayOnly.args = {
isDateUnavailable: (d) => isWeekend(d),
Expand Down
12 changes: 9 additions & 3 deletions react/src/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import {
CalendarProvider,
CalendarStylesProvider,
CalendarTodayButton,
UseProvideCalendarProps,
} from './CalendarBase'

export interface CalendarProps extends CalendarBaseProps {
export interface CalendarProps
extends CalendarBaseProps,
Pick<UseProvideCalendarProps, 'showOutsideDays'> {
/**
* The current selected date.
* If provided, the input will be a controlled input, and `onChange` must be provided.
Expand All @@ -37,7 +40,10 @@ export interface CalendarProps extends CalendarBaseProps {
}

export const Calendar = forwardRef<CalendarProps, 'input'>(
({ value, onChange, defaultValue, ...props }, initialFocusRef) => {
(
{ value, onChange, defaultValue, showTodayButton = true, ...props },
initialFocusRef,
) => {
const styles = useMultiStyleConfig('Calendar', props)

const [internalValue, setInternalValue] = useControllableState({
Expand All @@ -56,7 +62,7 @@ export const Calendar = forwardRef<CalendarProps, 'input'>(
<CalendarAria />
<Stack spacing={0} divider={<StackDivider />} sx={styles.container}>
<CalendarPanel ref={initialFocusRef} />
<CalendarTodayButton />
{showTodayButton && <CalendarTodayButton />}
</Stack>
</CalendarStylesProvider>
</CalendarProvider>
Expand Down
15 changes: 13 additions & 2 deletions react/src/Calendar/CalendarBase/CalendarContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ type PassthroughProps = {

/** Size of the component */
size?: ThemingProps<'Calendar'>['size']

/**
* Whether to set the calendar to always be a fixed height.
* This is useful for ensuring that the calendar does not jump around when the user switches between months
* if the months have different numbers of weeks and the calendar is positioned from the bottom.
* @default false
*/
isCalendarFixedHeight?: boolean
}

// Removed - and _ from alphabets for simpler classnames
Expand All @@ -86,7 +94,7 @@ const nanoid = customAlphabet(
)

export interface UseProvideCalendarProps
extends Pick<DayzedProps, 'monthsToDisplay'>,
extends Pick<DayzedProps, 'monthsToDisplay' | 'showOutsideDays'>,
PassthroughProps,
WithSsr {
/** The date to focus when calendar first renders. */
Expand Down Expand Up @@ -153,6 +161,8 @@ const useProvideCalendar = ({
size,
ssr,
defaultFocusedDate,
showOutsideDays,
isCalendarFixedHeight,
}: UseProvideCalendarProps) => {
const isMobile = useIsMobile({ ssr })
// Ensure that calculations are always made based on date of initial render,
Expand Down Expand Up @@ -279,7 +289,7 @@ const useProvideCalendar = ({
const renderProps = useDayzed({
date: today,
onDateSelected: ({ date }) => handleDateSelected(date),
showOutsideDays: monthsToDisplay === 1,
showOutsideDays: showOutsideDays ?? monthsToDisplay === 1,
offset: getMonthOffsetFromToday(today, currMonth, currYear),
onOffsetChanged,
selected: !Array.isArray(selectedDates)
Expand Down Expand Up @@ -339,5 +349,6 @@ const useProvideCalendar = ({
colorScheme,
size,
monthsToDisplay,
isCalendarFixedHeight,
}
}
10 changes: 9 additions & 1 deletion react/src/Calendar/CalendarBase/CalendarHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,24 @@ export const CalendarHeader = memo(
const {
renderProps: { calendars, getBackProps, getForwardProps },
size,
isMobile,
} = useCalendar()

const displayNavigateButtons = useMemo(() => {
if (isMobile) {
return monthOffset === 0
}
return calendars.length - 1 === monthOffset
}, [calendars.length, isMobile, monthOffset])

return (
<Flex sx={styles.monthYearSelectorContainer}>
{monthOffset === 0 ? (
<SelectableMonthYear />
) : (
<MonthYear monthOffset={monthOffset} />
)}
{calendars.length - 1 === monthOffset ? (
{displayNavigateButtons ? (
<Flex sx={styles.monthArrowContainer}>
<IconButton
variant="clear"
Expand Down
15 changes: 14 additions & 1 deletion react/src/Calendar/CalendarBase/CalendarPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useCallback } from 'react'
import {
chakra,
forwardRef,
Expand All @@ -22,12 +23,23 @@ export const CalendarPanel = forwardRef<{}, 'button'>(
dateToFocus,
onMouseLeaveCalendar,
renderProps: { calendars, getDateProps },
isCalendarFixedHeight,
} = useCalendar()

const createFillerRows = useCallback(
(weeksDisplayed: number) => {
if (!isCalendarFixedHeight) return null
const weeksToFill = 6 - weeksDisplayed
const singleRow = <chakra.tr aria-hidden sx={styles.fillerRow} />
return Array.from({ length: weeksToFill }, () => singleRow)
},
[isCalendarFixedHeight, styles.fillerRow],
)

return (
<Stack
direction={{ base: 'column', md: 'row' }}
spacing="2rem"
spacing={{ md: '2rem' }}
sx={styles.calendarContainer}
onMouseLeave={onMouseLeaveCalendar}
>
Expand Down Expand Up @@ -94,6 +106,7 @@ export const CalendarPanel = forwardRef<{}, 'button'>(
</chakra.tr>
)
})}
{createFillerRows(calendar.weeks.length)}
</chakra.tbody>
</chakra.table>
<VisuallyHidden aria-live="polite">
Expand Down
9 changes: 8 additions & 1 deletion react/src/Calendar/CalendarBase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export type CalendarBaseProps = Pick<
| 'monthsToDisplay'
| 'size'
| 'defaultFocusedDate'
>
| 'isCalendarFixedHeight'
> & {
/**
* Whether to show or hide the button to focus on Today.
* @default true
*/
showTodayButton?: boolean
}

export type DateRangeValue = [Date, Date] | [Date, null] | [null, null]
5 changes: 5 additions & 0 deletions react/src/Calendar/RangeCalendar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ RangeCalendarWeekdayOnly.args = {
isDateUnavailable: (d) => isWeekend(d),
}

export const HideTodayButton = RangeCalendarOnlyTemplate.bind({})
HideTodayButton.args = {
showTodayButton: false,
}

export const SizeSmall = RangeCalendarOnlyTemplate.bind({})
SizeSmall.args = {
size: 'sm',
Expand Down
3 changes: 2 additions & 1 deletion react/src/Calendar/RangeCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const RangeCalendar = forwardRef<RangeCalendarProps, 'input'>(
onChange,
defaultValue = [null, null],
monthsToDisplay = 2,
showTodayButton = true,
...props
},
initialFocusRef,
Expand Down Expand Up @@ -145,7 +146,7 @@ export const RangeCalendar = forwardRef<RangeCalendarProps, 'input'>(
<CalendarAria />
<Stack spacing={0} divider={<StackDivider />} sx={styles.container}>
<CalendarPanel ref={initialFocusRef} />
<CalendarTodayButton />
{showTodayButton && <CalendarTodayButton />}
</Stack>
</CalendarStylesProvider>
</CalendarProvider>
Expand Down
5 changes: 5 additions & 0 deletions react/src/DatePicker/DatePicker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ DatePickerDisallowManualInput.args = {
defaultValue: new Date('2021-09-13'),
}

export const FixedHeightCalendarPopover = Template.bind({})
FixedHeightCalendarPopover.args = {
isCalendarFixedHeight: true,
}

export const SizeSmall = Template.bind({})
SizeSmall.args = {
size: 'sm',
Expand Down
35 changes: 16 additions & 19 deletions react/src/DatePicker/DatePickerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import {
} from '@chakra-ui/react'
import { format, isValid, parse } from 'date-fns'

import { type CalendarProps } from '~/Calendar'
import { useIsMobile } from '~/hooks'

import { DatePickerProps } from './DatePicker'
import { pickCalendarProps } from './utils'

interface DatePickerContextReturn {
isMobile: boolean
Expand All @@ -44,10 +46,16 @@ interface DatePickerContextReturn {
allowManualInput: boolean
colorScheme?: ThemingProps<'DatePicker'>['colorScheme']
size?: ThemingProps<'DatePicker'>['size']
isDateUnavailable?: (date: Date) => boolean
disclosureProps: UseDisclosureReturn
monthsToDisplay?: number
defaultFocusedDate?: Date
calendarProps: Pick<
CalendarProps,
| 'isCalendarFixedHeight'
| 'monthsToDisplay'
| 'isDateUnavailable'
| 'defaultFocusedDate'
| 'showOutsideDays'
| 'showTodayButton'
>
}

const DatePickerContext = createContext<DatePickerContextReturn | null>(null)
Expand Down Expand Up @@ -86,23 +94,22 @@ const useProvideDatePicker = ({
isRequired: isRequiredProp,
isInvalid: isInvalidProp,
locale,
isDateUnavailable,
allowManualInput = true,
allowInvalidDates = true,
closeCalendarOnChange = true,
onBlur,
onClick,
colorScheme,
monthsToDisplay,
refocusOnClose = true,
ssr,
size,
defaultFocusedDate,
...props
}: DatePickerProps): DatePickerContextReturn => {
const initialFocusRef = useRef<HTMLInputElement>(null)
const inputRef = useRef<HTMLInputElement>(null)

const calendarProps = pickCalendarProps(props)

const isMobile = useIsMobile({ ssr })

const disclosureProps = useDisclosure({
Expand Down Expand Up @@ -145,11 +152,7 @@ const useProvideDatePicker = ({

const handleInputBlur: FocusEventHandler<HTMLInputElement> = useCallback(
(e) => {
const date = parse(
internalInputValue,
dateFormat,
new Date(),
)
const date = parse(internalInputValue, dateFormat, new Date())
// Clear if input is invalid on blur if invalid dates are not allowed.
if (!allowInvalidDates && !isValid(date)) {
setInternalValue(null)
Expand Down Expand Up @@ -204,11 +207,7 @@ const useProvideDatePicker = ({

const handleInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const date = parse(
event.target.value,
dateFormat,
new Date(),
)
const date = parse(event.target.value, dateFormat, new Date())
setInternalInputValue(event.target.value)
if (isValid(date)) {
setInternalValue(date)
Expand Down Expand Up @@ -256,9 +255,7 @@ const useProvideDatePicker = ({
allowManualInput,
colorScheme,
size,
isDateUnavailable,
disclosureProps,
monthsToDisplay,
defaultFocusedDate,
calendarProps,
}
}
9 changes: 3 additions & 6 deletions react/src/DatePicker/components/DatePickerCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,24 @@ export const DatePickerCalendar = (): JSX.Element => {
const {
colorScheme,
internalValue,
isDateUnavailable,
handleDateChange,
initialFocusRef,
monthsToDisplay,
size,
isMobile,
defaultFocusedDate,
calendarProps: { isCalendarFixedHeight, ...restCalendarProps },
} = useDatePicker()

const displayedSize = isMobile ? 'sm' : size

return (
<Calendar
size={displayedSize}
monthsToDisplay={monthsToDisplay}
colorScheme={colorScheme}
value={internalValue ?? undefined}
isDateUnavailable={isDateUnavailable}
onChange={handleDateChange}
ref={initialFocusRef}
defaultFocusedDate={defaultFocusedDate}
isCalendarFixedHeight={isMobile ? true : isCalendarFixedHeight}
{...restCalendarProps}
/>
)
}
6 changes: 0 additions & 6 deletions react/src/DatePicker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,4 @@ export interface DatePickerBaseProps
refocusOnClose?: boolean
/** date-fns's Locale of the date to be applied if provided. */
locale?: Locale
/**
* Time zone of date created.
* Defaults to `'UTC'`.
* Accepts all possible `Intl.Locale.prototype.timeZones` values
*/
timeZone?: string
}
1 change: 1 addition & 0 deletions react/src/DatePicker/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './pickCalendarProps'
15 changes: 15 additions & 0 deletions react/src/DatePicker/utils/pickCalendarProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { pick } from 'lodash'

import { DatePickerProps } from '../DatePicker'

export const pickCalendarProps = (props: DatePickerProps) => {
return pick(
props,
'isCalendarFixedHeight',
'monthsToDisplay',
'isDateUnavailable',
'defaultFocusedDate',
'showOutsideDays',
'showTodayButton',
)
}
5 changes: 5 additions & 0 deletions react/src/DateRangePicker/DateRangePicker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ DateRangePickerDisallowManualInput.args = {
defaultValue: [new Date('2021-09-13'), null],
}

export const FixedHeightCalendarPopover = Template.bind({})
FixedHeightCalendarPopover.args = {
isCalendarFixedHeight: true,
}

export const DatePickerInvalid = Template.bind({})
DatePickerInvalid.args = {
isInvalid: true,
Expand Down
Loading

0 comments on commit 88ca90a

Please sign in to comment.