Skip to content

Commit

Permalink
fix: resolving dateTime picker bugs with content releases (#8455)
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanl17 authored and bjoerge committed Jan 30, 2025
1 parent 90d82fb commit 93d76ce
Show file tree
Hide file tree
Showing 16 changed files with 374 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const DatePicker = forwardRef(function DatePicker(
monthPickerVariant?: CalendarProps['monthPickerVariant']
padding?: number
showTimezone?: boolean
isPastDisabled?: boolean
},
ref: ForwardedRef<HTMLDivElement>,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import {CalendarIcon} from '@sanity/icons'
import {Box, Flex, LayerProvider, useClickOutsideEvent} from '@sanity/ui'
import {Box, Card, Flex, LayerProvider, Text, useClickOutsideEvent} from '@sanity/ui'
import {isPast} from 'date-fns'
import {
type FocusEvent,
type ForwardedRef,
forwardRef,
type KeyboardEvent,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import FocusLock from 'react-focus-lock'

import {Button} from '../../../../ui-components/button/Button'
import {Popover} from '../../../../ui-components/popover/Popover'
import {useTranslation} from '../../../i18n'
import {type CalendarProps} from './calendar/Calendar'
import {type CalendarLabels} from './calendar/types'
import {DatePicker} from './DatePicker'
Expand All @@ -35,6 +39,7 @@ export interface DateTimeInputProps {
monthPickerVariant?: CalendarProps['monthPickerVariant']
padding?: number
disableInput?: boolean
isPastDisabled?: boolean
}

export const DateTimeInput = forwardRef(function DateTimeInput(
Expand All @@ -53,17 +58,28 @@ export const DateTimeInput = forwardRef(function DateTimeInput(
constrainSize = true,
monthPickerVariant,
padding,
disableInput,
isPastDisabled,
...rest
} = props
const {t} = useTranslation()
const popoverRef = useRef<HTMLDivElement | null>(null)
const ref = useRef<HTMLInputElement | null>(null)
const buttonRef = useRef(null)

const [referenceElement, setReferenceElement] = useState<HTMLInputElement | null>(null)

useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
forwardedRef,
() => ref.current,
)

/**
* Setting referenceElement in effect makes sure it's up to date after the initial render
* cycle - avoiding referenceElement used byPopover from being out of sync with render state
*/
useEffect(() => setReferenceElement(ref.current), [])

const [isPickerOpen, setPickerOpen] = useState(false)

useClickOutsideEvent(
Expand All @@ -84,6 +100,11 @@ export const DateTimeInput = forwardRef(function DateTimeInput(

const handleClick = useCallback(() => setPickerOpen(true), [])

const isDateInPastWarningShown = useMemo(
() => inputValue && isPastDisabled && isPast(new Date(inputValue)),
[inputValue, isPastDisabled],
)

const suffix = readOnly ? null : (
<Flex style={{padding: '5px'}}>
<Button
Expand All @@ -104,7 +125,7 @@ export const DateTimeInput = forwardRef(function DateTimeInput(
<LazyTextInput
ref={ref}
{...rest}
readOnly={readOnly}
readOnly={disableInput || readOnly}
value={inputValue}
onChange={onInputChange}
suffix={
Expand All @@ -116,17 +137,24 @@ export const DateTimeInput = forwardRef(function DateTimeInput(
<Popover
constrainSize={constrainSize}
data-testid="date-input-dialog"
referenceElement={referenceElement}
portal
content={
<Box overflow="auto">
<FocusLock onDeactivation={handleDeactivation}>
{isDateInPastWarningShown && (
<Card margin={1} padding={2} radius={2} shadow={1} tone="critical">
<Text size={1}>{t('inputs.dateTime.past-date-warning')}</Text>
</Card>
)}
<DatePicker
monthPickerVariant={monthPickerVariant}
calendarLabels={calendarLabels}
selectTime={selectTime}
timeStep={timeStep}
onKeyUp={handleKeyUp}
value={value}
isPastDisabled={isPastDisabled}
onChange={onChange}
padding={padding}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type CalendarProps = Omit<ComponentProps<'div'>, 'onSelect'> & {
monthPickerVariant?: (typeof MONTH_PICKER_VARIANT)[keyof typeof MONTH_PICKER_VARIANT]
padding?: number
showTimezone?: boolean
isPastDisabled?: boolean
}

// This is used to maintain focus on a child element of the calendar-grid between re-renders
Expand Down Expand Up @@ -76,6 +77,7 @@ export const Calendar = forwardRef(function Calendar(
timeStep = 1,
onSelect,
labels,
isPastDisabled,
monthPickerVariant = 'select',
padding = 2,
showTimezone = false,
Expand Down Expand Up @@ -232,12 +234,14 @@ export const Calendar = forwardRef(function Calendar(
icon={ChevronLeftIcon}
mode="bleed"
onClick={() => moveFocusedDate(-1)}
data-testid="calendar-prev-month"
tooltipProps={{content: 'Previous month'}}
/>
<Button
icon={ChevronRightIcon}
mode="bleed"
onClick={() => moveFocusedDate(1)}
data-testid="calendar-next-month"
tooltipProps={{content: 'Next month'}}
/>
</TooltipDelayGroupProvider>
Expand Down Expand Up @@ -312,6 +316,7 @@ export const Calendar = forwardRef(function Calendar(
focused={focusedDate}
onSelect={handleDateChange}
selected={selectedDate}
isPastDisabled={isPastDisabled}
/>
{PRESERVE_FOCUS_ELEMENT}
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Card, Text} from '@sanity/ui'
import {isPast} from 'date-fns'
import {useCallback} from 'react'

interface CalendarDayProps {
Expand All @@ -8,10 +9,11 @@ interface CalendarDayProps {
isCurrentMonth?: boolean
isToday: boolean
selected?: boolean
isPastDisabled?: boolean
}

export function CalendarDay(props: CalendarDayProps) {
const {date, focused, isCurrentMonth, isToday, onSelect, selected} = props
const {date, focused, isCurrentMonth, isToday, onSelect, selected, isPastDisabled} = props

const handleClick = useCallback(() => {
onSelect(date)
Expand All @@ -28,6 +30,7 @@ export function CalendarDay(props: CalendarDayProps) {
data-focused={focused ? 'true' : ''}
role="button"
tabIndex={-1}
disabled={isPastDisabled && !isToday && isPast(date)}
onClick={handleClick}
padding={2}
radius={2}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface CalendarMonthProps {
selected?: Date
onSelect: (date: Date) => void
hidden?: boolean
isPastDisabled?: boolean
weekDayNames: [
mon: string,
tue: string,
Expand Down Expand Up @@ -62,6 +63,7 @@ export function CalendarMonth(props: CalendarMonthProps) {
key={`${weekIdx}-${dayIdx}`}
onSelect={props.onSelect}
selected={selected}
isPastDisabled={props.isPastDisabled}
/>
)
}),
Expand Down
10 changes: 9 additions & 1 deletion packages/sanity/src/core/i18n/bundles/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
'inputs.array.resolving-initial-value': 'Resolving initial value…',
/** Tooltip content when boolean input is disabled */
'inputs.boolean.disabled': 'Disabled',
/** Warning label when selected datetime is in the past */
'inputs.dateTime.past-date-warning': 'Select a date in the future.',
/** Placeholder value for datetime input */
'inputs.datetime.placeholder': 'e.g. {{example}}',
/** Acessibility label for button to open file options menu */
Expand Down Expand Up @@ -1160,6 +1162,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
/* Relative time, just now */
'relative-time.just-now': 'just now',

/** Action message to add document to new release */
'release.action.add-to-new-release': 'Add to release',
/** Action message to add document to release */
'release.action.add-to-release': 'Add to {{title}}',
/** Action message for when document is already in release */
Expand Down Expand Up @@ -1210,6 +1214,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
'release.chip.tooltip.unknown-date': 'Unknown date',
/** Label for tooltip on deleted release */
'release.deleted-tooltip': 'This release has been deleted',
/** Title for copying version to a new release dialog */
'release.dialog.copy-to-release.title': 'Copy version to new release',
/** Title for creating releases dialog */
'release.dialog.create.title': 'Create release',
/** Label for description in tooltip to explain release types */
Expand All @@ -1232,8 +1238,10 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
'release.navbar.tooltip': 'Releases',
/** The placeholder text when the release doesn't have a title */
'release.placeholder-untitled-release': 'Untitled release',
/**The toast title that will be shown when the user has a release perspective which is now archived */
/** The toast title that will be shown when the user has a release perspective which is now archived */
'release.toast.archived-release.title': "The '{{title}}' release was archived",
/** The toast tiele that will be shown the creating a release fails */
'release.toast.create-release-error.title': 'Failed to create release',
/**The toast title that will be shown when the user has a release perspective which is now deleted */
'release.toast.not-found-release.title': "The '{{title}}' release could not be found",
/** Label for when a version of a document has already been added to the release */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {EarthGlobeIcon} from '@sanity/icons'
import {Flex} from '@sanity/ui'
import {format, isValid, parse} from 'date-fns'
import {useCallback, useMemo} from 'react'

import {Button} from '../../../ui-components/button'
import {MONTH_PICKER_VARIANT} from '../../components/inputs/DateInputs/calendar/Calendar'
import {type CalendarLabels} from '../../components/inputs/DateInputs/calendar/types'
import {DateTimeInput} from '../../components/inputs/DateInputs/DateTimeInput'
import {getCalendarLabels} from '../../form/inputs/DateInputs'
import {useTranslation} from '../../i18n/hooks/useTranslation'
import useDialogTimeZone from '../../scheduledPublishing/hooks/useDialogTimeZone'
import useTimeZone from '../../scheduledPublishing/hooks/useTimeZone'

interface ScheduleDatePickerProps {
initialValue: Date
onChange: (date: Date) => void
}

const inputDateFormat = 'PP HH:mm'

export const ScheduleDatePicker = ({
initialValue: inputValue,
onChange,
}: ScheduleDatePickerProps) => {
const {t} = useTranslation()
const {timeZone} = useTimeZone()
const {dialogTimeZoneShow} = useDialogTimeZone()

const handlePublishAtCalendarChange = (date: Date | null) => {
if (!date) return

onChange(date)
}

const handlePublishAtInputChange = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
const date = event.currentTarget.value
const parsedDate = parse(date, inputDateFormat, new Date())

if (isValid(parsedDate)) onChange(parsedDate)
},
[onChange],
)

const calendarLabels: CalendarLabels = useMemo(() => getCalendarLabels(t), [t])

return (
<Flex flex={1} justify="space-between">
<DateTimeInput
selectTime
monthPickerVariant={MONTH_PICKER_VARIANT.carousel}
onChange={handlePublishAtCalendarChange}
onInputChange={handlePublishAtInputChange}
calendarLabels={calendarLabels}
value={inputValue}
inputValue={format(inputValue, inputDateFormat)}
constrainSize={false}
padding={0}
isPastDisabled
/>

<Button
icon={EarthGlobeIcon}
mode="bleed"
size="default"
text={`${timeZone.abbreviation}`}
onClick={dialogTimeZoneShow}
/>
</Flex>
)
}
Loading

0 comments on commit 93d76ce

Please sign in to comment.