mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 228db8541c | |||
| a729a6935f |
@@ -1,4 +1,4 @@
|
||||
// FE-COMP-JOURNEYDATEPICKER-001 to FE-COMP-JOURNEYDATEPICKER-007
|
||||
// FE-COMP-JOURNEYDATEPICKER-001 to FE-COMP-JOURNEYDATEPICKER-008
|
||||
import React from 'react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '../../../tests/helpers/render'
|
||||
@@ -119,3 +119,28 @@ describe('DatePicker week start (#2029)', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('DatePicker inside a clipping dialog (#2858)', () => {
|
||||
it('FE-COMP-JOURNEYDATEPICKER-008: the calendar floats over the page, stays inside the viewport and closes on a click away', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<DatePicker value="2026-03-15" onChange={vi.fn()} />
|
||||
</div>,
|
||||
)
|
||||
const trigger = screen.getByText(/Mar 15, 2026/).closest('button') as HTMLButtonElement
|
||||
// A trigger at the right edge, where the end date of a range sits.
|
||||
trigger.getBoundingClientRect = () => ({ top: 100, bottom: 136, left: window.innerWidth - 120, right: window.innerWidth - 8, width: 112, height: 36, x: 0, y: 0, toJSON: () => ({}) })
|
||||
await user.click(trigger)
|
||||
|
||||
const day31 = screen.getByRole('button', { name: '31' })
|
||||
// Rendered outside the dialog that would clip it.
|
||||
expect(container.contains(day31)).toBe(false)
|
||||
const panel = day31.closest('.fixed') as HTMLElement
|
||||
expect(panel.style.top).toBe('140px')
|
||||
expect(parseFloat(panel.style.left)).toBe(window.innerWidth - 280 - 8)
|
||||
|
||||
await user.click(document.querySelector('[role="presentation"]') as HTMLElement)
|
||||
expect(screen.queryByRole('button', { name: '31' })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { localIsoDate } from '../../utils/localDate'
|
||||
import { ArrowLeft, ChevronRight, Calendar } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useWeekStartDay } from '../../hooks/useWeekStartDay'
|
||||
import { leadingBlanks, weekdayLabels } from '../../utils/calendarWeek'
|
||||
import { useAnchoredPosition } from '../../hooks/useAnchoredPosition'
|
||||
|
||||
/**
|
||||
* The header steps up a level on each click, days → months → years, and a
|
||||
@@ -14,6 +16,9 @@ import { leadingBlanks, weekdayLabels } from '../../utils/calendarWeek'
|
||||
*/
|
||||
type CalendarView = 'days' | 'months' | 'years'
|
||||
const YEAR_PAGE_SIZE = 12
|
||||
/** The calendar's width; `w-[280px]` below. */
|
||||
const PANEL_WIDTH = 280
|
||||
const VIEWPORT_PAD = 8
|
||||
|
||||
export function DatePicker({ value, onChange, tripDates }: {
|
||||
value: string
|
||||
@@ -23,6 +28,13 @@ export function DatePicker({ value, onChange, tripDates }: {
|
||||
const { t, locale } = useTranslation()
|
||||
const weekStart = useWeekStartDay()
|
||||
const [open, setOpen] = useState(false)
|
||||
const triggerRef = useRef<HTMLButtonElement>(null)
|
||||
// The calendar used to hang absolutely under its trigger, inside dialogs that clip
|
||||
// their content: the photo picker cut off the last week and, for the end date in
|
||||
// the right-hand column, the last weekday (#2858). It now floats over the page,
|
||||
// pinned to the trigger, flipped above it when below is too short and kept inside
|
||||
// the viewport sideways.
|
||||
const anchored = useAnchoredPosition(triggerRef, open, { estimatedHeight: 340, matchWidth: false })
|
||||
const [view, setView] = useState<CalendarView>('days')
|
||||
const [yearPageStart, setYearPageStart] = useState(0)
|
||||
const [viewMonth, setViewMonth] = useState(() => {
|
||||
@@ -107,6 +119,7 @@ export function DatePicker({ value, onChange, tripDates }: {
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={toggleOpen}
|
||||
className="w-full px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white text-start flex items-center justify-between"
|
||||
@@ -122,12 +135,20 @@ export function DatePicker({ value, onChange, tripDates }: {
|
||||
<Calendar size={13} className="text-zinc-400" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
{open && anchored && createPortal(
|
||||
<>
|
||||
{/* Click-away catcher — no semantics of its own; the trigger button
|
||||
above closes the popover again from the keyboard. */}
|
||||
<div role="presentation" className="fixed inset-0 z-[10]" onClick={() => setOpen(false)} />
|
||||
<div className="absolute top-full start-0 mt-1 z-[20] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-lg p-3 w-[280px]">
|
||||
<div role="presentation" className="fixed inset-0 z-[var(--z-toast)]" onClick={() => setOpen(false)} />
|
||||
<div
|
||||
className="fixed z-[var(--z-toast)] overflow-y-auto bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-lg p-3 w-[280px]"
|
||||
style={{
|
||||
top: anchored.top,
|
||||
bottom: anchored.bottom,
|
||||
left: Math.max(VIEWPORT_PAD, Math.min(anchored.left, window.innerWidth - PANEL_WIDTH - VIEWPORT_PAD)),
|
||||
maxHeight: anchored.maxHeight,
|
||||
}}
|
||||
>
|
||||
{/* Header: arrows step the current view, the label climbs to the next one up */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<button type="button" onClick={handlePrev} aria-label={prevLabel} className={navButton}>
|
||||
@@ -200,7 +221,7 @@ export function DatePicker({ value, onChange, tripDates }: {
|
||||
>
|
||||
{day}
|
||||
{isTrip && !isSelected && (
|
||||
<span className="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-indigo-500" />
|
||||
<span className="absolute bottom-1 inset-x-0 mx-auto w-1 h-1 rounded-full bg-indigo-500" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
@@ -209,7 +230,8 @@ export function DatePicker({ value, onChange, tripDates }: {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -375,11 +375,12 @@ describe('ProviderPicker', () => {
|
||||
await user.click(screen.getByRole('button', { name: 'Search' }))
|
||||
expect(ranges).toHaveLength(0)
|
||||
|
||||
// The calendar opens in a portal, so its days are looked up on the page, not under the trigger.
|
||||
const [fromPicker, toPicker] = Array.from(baseElement.querySelectorAll('.flex-1 > .relative')) as HTMLElement[]
|
||||
await user.click(within(fromPicker).getByRole('button'))
|
||||
await user.click(within(fromPicker).getByRole('button', { name: '3' }))
|
||||
await user.click(screen.getByRole('button', { name: '3' }))
|
||||
await user.click(within(toPicker).getByRole('button'))
|
||||
await user.click(within(toPicker).getByRole('button', { name: '9' }))
|
||||
await user.click(screen.getByRole('button', { name: '9' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Search' }))
|
||||
|
||||
await waitFor(() => expect(ranges).toHaveLength(1))
|
||||
|
||||
@@ -59,7 +59,7 @@ export const SYSTEM_NOTICES: SystemNotice[] = [
|
||||
titleKey: 'system_notice.release_notes.headline',
|
||||
bodyKey: 'system_notice.release_notes.intro',
|
||||
release: {
|
||||
version: '5.0.0',
|
||||
version: '5.0.1',
|
||||
eyebrowKey: 'system_notice.release_notes.eyebrow',
|
||||
headlineKey: 'system_notice.release_notes.headline',
|
||||
introKey: 'system_notice.release_notes.intro',
|
||||
@@ -95,7 +95,7 @@ export const SYSTEM_NOTICES: SystemNotice[] = [
|
||||
footnoteKey: 'system_notice.release_notes.footnote',
|
||||
notes: {
|
||||
labelKey: 'system_notice.release_notes.notes_label',
|
||||
href: 'https://github.com/liketrek/TREK/releases/tag/v5.0.0',
|
||||
href: 'https://github.com/liketrek/TREK/releases/tag/v5.0.1',
|
||||
},
|
||||
note: {
|
||||
eyebrowKey: 'system_notice.release_notes.note_eyebrow',
|
||||
|
||||
Reference in New Issue
Block a user