Skip to content

Commit

Permalink
fix: respect time when only allowing upcoming dates in LemonCalendar (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
daibhin authored Apr 24, 2024
1 parent ab3a64d commit 212431e
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 25 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ ShowTime.args = {
showTime: true,
}

export const FromToday: Story = BasicTemplate.bind({})
FromToday.args = {
fromToday: true,
export const OnlyAllowUpcoming: Story = BasicTemplate.bind({})
OnlyAllowUpcoming.args = {
onlyAllowUpcoming: true,
}
4 changes: 2 additions & 2 deletions frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface LemonCalendarProps {
/** Show a time picker */
showTime?: boolean
/** Only allow upcoming dates */
fromToday?: boolean
onlyAllowUpcoming?: boolean
}

export interface GetLemonButtonPropsOpts {
Expand Down Expand Up @@ -137,7 +137,7 @@ export const LemonCalendar = forwardRef(function LemonCalendar(
LemonCalendar__today: date.isSame(today, 'd'),
}),
disabledReason:
props.fromToday && pastDate
props.onlyAllowUpcoming && pastDate
? 'Cannot select dates in the past'
: undefined,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,66 @@ describe('LemonCalendarSelect', () => {
// only changes the hour
expect(onChange).toHaveBeenCalledWith(dayjs('2023-01-15T08:42:00.000Z'))
})

test('onlyAllowUpcoming', async () => {
const onClose = jest.fn()
const onChange = jest.fn()
window.HTMLElement.prototype.scrollIntoView = jest.fn()

jest.useFakeTimers().setSystemTime(new Date('2023-01-10 17:22:08'))

function TestSelect(): JSX.Element {
const [value, setValue] = useState<dayjs.Dayjs | null>(null)
return (
<LemonCalendarSelect
months={1}
value={value}
onClose={onClose}
onChange={(value) => {
setValue(value)
onChange(value)
}}
showTime
onlyAllowUpcoming
/>
)
}
const { container } = render(<TestSelect />)

async function clickOnDate(day: string): Promise<void> {
const element = container.querySelector('.LemonCalendar__month') as HTMLElement
if (element) {
userEvent.click(await within(element).findByText(day))
userEvent.click(getByDataAttr(container, 'lemon-calendar-select-apply'))
}
}

async function clickOnTime(props: GetLemonButtonTimePropsOpts): Promise<void> {
const element = getTimeElement(container.querySelector('.LemonCalendar__time'), props)
if (element) {
userEvent.click(element)
userEvent.click(getByDataAttr(container, 'lemon-calendar-select-apply'))
}
}

// click on minute
await clickOnTime({ unit: 'm', value: 42 })
// time is disabled until a date is clicked
expect(onChange).not.toHaveBeenCalled()

// click on current date
await clickOnDate('9')
// cannot select a date in the past
expect(onChange).not.toHaveBeenCalled()

// click on current date
await clickOnDate('10')
// chooses the current date and sets the time to the current hour and minute
expect(onChange).toHaveBeenCalledWith(dayjs('2023-01-10T17:22:00.000Z'))

// click on an earlier hour
await clickOnTime({ unit: 'a', value: 'am' })
// does not update the date because it is in the past
expect(onChange).lastCalledWith(dayjs('2023-01-10T17:22:00.000Z'))
})
})
64 changes: 45 additions & 19 deletions frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IconX } from '@posthog/icons'
import { dayjs } from 'lib/dayjs'
import { LemonButton, LemonButtonWithSideActionProps, SideAction } from 'lib/lemon-ui/LemonButton'
import { LemonButton, LemonButtonProps, LemonButtonWithSideActionProps, SideAction } from 'lib/lemon-ui/LemonButton'
import { GetLemonButtonTimePropsOpts, LemonCalendar } from 'lib/lemon-ui/LemonCalendar/LemonCalendar'
import { useEffect, useMemo, useRef, useState } from 'react'

Expand Down Expand Up @@ -28,13 +28,27 @@ function scrollToTimeElement(
})
}

function proposedDate(target: dayjs.Dayjs | null, { value, unit }: GetLemonButtonTimePropsOpts): dayjs.Dayjs {
let date = target || dayjs().startOf('day')
if (value != date.format(unit)) {
if (unit === 'h') {
date = date.hour(date.format('a') === 'am' || value === 12 ? Number(value) : Number(value) + 12)
} else if (unit === 'm') {
date = date.minute(Number(value))
} else if (unit === 'a') {
date = value === 'am' ? date.subtract(12, 'hour') : date.add(12, 'hour')
}
}
return date
}

export interface LemonCalendarSelectProps {
value?: dayjs.Dayjs | null
onChange: (date: dayjs.Dayjs) => void
months?: number
onClose?: () => void
showTime?: boolean
fromToday?: boolean
onlyAllowUpcoming?: boolean
}

export function LemonCalendarSelect({
Expand All @@ -43,13 +57,14 @@ export function LemonCalendarSelect({
months,
onClose,
showTime,
fromToday,
onlyAllowUpcoming,
}: LemonCalendarSelectProps): JSX.Element {
const calendarRef = useRef<HTMLDivElement | null>(null)
const [selectValue, setSelectValue] = useState<dayjs.Dayjs | null>(
value ? (showTime ? value : value.startOf('day')) : null
)

const now = dayjs()
const isAM = useMemo(() => selectValue?.format('a') === 'am', [selectValue])

const scrollToTime = (date: dayjs.Dayjs, skipAnimation: boolean): void => {
Expand All @@ -62,8 +77,6 @@ export function LemonCalendarSelect({
}

const onDateClick = (date: dayjs.Dayjs | null): void => {
const now = dayjs()

if (date) {
date = showTime ? date.hour(selectValue === null ? now.hour() : selectValue.hour()) : date.startOf('hour')
date = showTime
Expand All @@ -82,17 +95,7 @@ export function LemonCalendarSelect({
}, [])

const onTimeClick = (props: GetLemonButtonTimePropsOpts): void => {
const { value, unit } = props

let date = selectValue || dayjs().startOf('day')
if (unit === 'h') {
date = date.hour(date.format('a') === 'am' ? Number(value) : Number(value) + 12)
} else if (unit === 'm') {
date = date.minute(Number(value))
} else if (unit === 'a') {
date = value === 'am' ? date.subtract(12, 'hour') : date.add(12, 'hour')
}

const date = proposedDate(selectValue, props)
scrollToTime(date, false)
setSelectValue(date)
}
Expand All @@ -111,17 +114,40 @@ export function LemonCalendarSelect({
leftmostMonth={selectValue?.startOf('month')}
months={months}
getLemonButtonProps={({ date, props }) => {
const modifiedProps: LemonButtonProps = { ...props }
const isDisabled =
onlyAllowUpcoming &&
selectValue &&
date.isSame(now.tz('utc'), 'date') &&
(selectValue.hour() < now.hour() ||
(selectValue.hour() === now.hour() && selectValue.minute() <= now.minute()))

if (isDisabled) {
modifiedProps.disabledReason = 'Pick a time in the future first'
}

if (date.isSame(selectValue, 'd')) {
return { ...props, status: 'default', type: 'primary' }
return { ...modifiedProps, status: 'default', type: 'primary' }
}
return props
return modifiedProps
}}
getLemonButtonTimeProps={(props) => {
const selected = selectValue ? selectValue.format(props.unit) : null
const newDate = proposedDate(selectValue, props)

const disabledReason = onlyAllowUpcoming
? selectValue
? newDate.isBefore(now)
? 'Cannot choose a time in the past'
: undefined
: 'Choose a date first'
: undefined

return {
active: selected === String(props.value),
className: 'rounded-none',
'data-attr': timeDataAttr(props),
disabledReason: disabledReason,
onClick: () => {
if (selected != props.value) {
onTimeClick(props)
Expand All @@ -130,7 +156,7 @@ export function LemonCalendarSelect({
}
}}
showTime={showTime}
fromToday={fromToday}
onlyAllowUpcoming={onlyAllowUpcoming}
/>
<div className="flex space-x-2 justify-end items-center border-t p-2 pt-4">
<LemonButton type="secondary" onClick={onClose} data-attr="lemon-calendar-select-cancel">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/feature-flags/FeatureFlagSchedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ export default function FeatureFlagSchedule(): JSX.Element {
value={scheduleDateMarker}
onChange={(value) => setScheduleDateMarker(value)}
placeholder="Select date"
onlyAllowUpcoming
showTime
fromToday
/>
</div>
</div>
Expand Down

0 comments on commit 212431e

Please sign in to comment.