From 317e39a0bb9c768ce135f3fbe3157f9e3fefeebe Mon Sep 17 00:00:00 2001 From: HeavyGee <133152184+heavygee@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:15:50 +0100 Subject: [PATCH] fix(web): 48px session search clear with calendar left of it (#2034) * fix(web): 48px session search clear with calendar left of it (#2019) Quest/touch: expanded search uses a min 48x48 clear end-cap as the furthest-right control, with the calendar embedded immediately to its left inside the field. Collapsed search stays shrink-0 with a larger clear chip so it does not steal the toolbar row. Co-authored-by: Cursor * fix(web): anchor date picker to search group; let collapsed chip shrink (#2019) HAPI Bot: mount the calendar popup on the search control (not the 48px button) so left/right align stays on-screen at narrow widths. Drop shrink-0 on the collapsed group so the query chip can yield space while calendar/clear stay fixed hit targets. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .../SessionList.directory-action.test.tsx | 55 +++- web/src/components/SessionList.tsx | 238 ++++++++++-------- 2 files changed, 183 insertions(+), 110 deletions(-) diff --git a/web/src/components/SessionList.directory-action.test.tsx b/web/src/components/SessionList.directory-action.test.tsx index 7e399cbb..a1397457 100644 --- a/web/src/components/SessionList.directory-action.test.tsx +++ b/web/src/components/SessionList.directory-action.test.tsx @@ -129,7 +129,8 @@ describe('SessionList directory action', () => { fireEvent.click(screen.getByRole('button', { name: SEARCH_LABEL })) const searchInput = screen.getByPlaceholderText(SEARCH_PLACEHOLDER) - const headerRow = searchInput.parentElement?.parentElement + // input → field row → session-list-search → header actions row + const headerRow = searchInput.parentElement?.parentElement?.parentElement expect(headerRow).toHaveClass('px-2') expect(headerRow).toHaveClass('py-1') }) @@ -194,9 +195,10 @@ describe('SessionList time filter', () => { const searchButton = screen.getByRole('button', { name: SEARCH_LABEL }) const filterButton = screen.getByRole('button', { name: 'Filter sessions by last activity' }) const searchControl = searchButton.parentElement - expect(searchControl?.nextElementSibling).toBe(filterButton) - expect(searchControl?.parentElement).toBe(filterButton.parentElement) - expect(searchControl?.parentElement).toHaveClass('relative', 'gap-1') + expect(searchControl?.nextElementSibling).toBe(filterButton.parentElement) + expect(searchControl?.parentElement).toBe(filterButton.parentElement?.parentElement) + expect(searchControl?.parentElement).toHaveClass('relative', 'gap-1', 'min-w-0') + expect(searchControl?.parentElement?.className.split(/\s+/)).not.toContain('shrink-0') expect(screen.queryByPlaceholderText(SEARCH_PLACEHOLDER)).toBeNull() fireEvent.click(filterButton) @@ -211,8 +213,8 @@ describe('SessionList time filter', () => { expect(screen.getByRole('button', { name: /Recent session/ })).toBeInTheDocument() expect(screen.queryByRole('button', { name: /Old session/ })).toBeNull() expect(screen.queryByPlaceholderText(SEARCH_PLACEHOLDER)).toBeNull() - expect(filterButton).toHaveAttribute('title', '2026-07-17 – 2026-07-18') - expect(filterButton).toHaveAccessibleName('Filter sessions by last activity: 2026-07-17 – 2026-07-18') + expect(filterButton).toHaveAttribute('title', '2026-07-17 - 2026-07-18') + expect(filterButton).toHaveAccessibleName('Filter sessions by last activity: 2026-07-17 - 2026-07-18') expect(filterButton).toHaveFocus() }) @@ -277,7 +279,7 @@ describe('SessionList time filter', () => { fireEvent.click(screen.getByRole('button', { name: `${new Date(2026, 6, 18).toLocaleDateString()}, has session activity` })) expect(filterButton).toHaveAttribute('aria-expanded', 'false') - expect(filterButton).toHaveAttribute('title', '2026-07-01 – 2026-07-18') + expect(filterButton).toHaveAttribute('title', '2026-07-01 - 2026-07-18') }) it('returns focus to the search input after clearing the date range', () => { @@ -1091,6 +1093,45 @@ describe('SessionList search toggle', () => { expect(screen.getByRole('button', { name: /Other task/ })).toBeInTheDocument() }) + it('gives expanded search a 48px clear end-cap with calendar immediately left (tiann/hapi#2019)', () => { + renderWithProviders( + + ) + + fireEvent.click(screen.getByRole('button', { name: SEARCH_LABEL })) + const input = screen.getByPlaceholderText(SEARCH_PLACEHOLDER) + fireEvent.change(input, { target: { value: 'DJ' } }) + + const clearButton = screen.getByRole('button', { name: 'Clear search' }) + expect(clearButton.className).toMatch(/session-list-search-clear-endcap/) + expect(clearButton.className).toMatch(/min-h-\[48px\]/) + expect(clearButton.className).toMatch(/min-w-\[48px\]/) + + const dateButton = screen.getByTestId('session-list-search-date-embedded') + expect(dateButton.className).toMatch(/min-h-\[48px\]/) + expect(dateButton.className).toMatch(/min-w-\[48px\]/) + expect(clearButton.parentElement?.contains(dateButton)).toBe(true) + + const fieldChildren = Array.from(clearButton.parentElement?.children ?? []) + const dateIndex = fieldChildren.findIndex((node) => node === dateButton.parentElement || node.contains(dateButton)) + const clearIndex = fieldChildren.indexOf(clearButton) + expect(dateIndex).toBeGreaterThanOrEqual(0) + expect(clearIndex).toBeGreaterThan(dateIndex) + }) + it('stays expanded with focus on the input after clearing the query', () => { renderWithProviders( { - const returnFocus = () => { - (variant === 'embedded' ? inputRef.current : dateButtonRef.current)?.focus() + const returnDateFocus = () => { + if (props.expanded) { + inputRef.current?.focus() + return } - - return ( - <> - - {datePickerOpen ? ( - <> - + + ) + + const datePicker = datePickerOpen ? ( + <> + ) : null} - {renderDateFilter('standalone')} + {renderDateButton('standalone')} + {datePicker} ) } return (
{ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { @@ -892,40 +925,39 @@ export function SessionListSearch(props: { } }} > -
- -
- props.onChange(event.target.value)} - placeholder={t('sessions.search.placeholder')} - aria-label={searchLabel} - title={searchLabel} - className={cn( - 'w-full appearance-none rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] py-1.5 pl-8 text-sm text-[var(--app-fg)] outline-none transition-colors placeholder:text-[var(--app-hint)] [text-overflow:ellipsis] focus:border-[var(--app-link)] [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden', - props.value ? 'pr-16' : 'pr-7' - )} - /> - {props.value ? ( - - ) : null} -
- {renderDateFilter('embedded')} +
+
+ +
+ props.onChange(event.target.value)} + placeholder={t('sessions.search.placeholder')} + aria-label={searchLabel} + title={searchLabel} + className="min-w-0 flex-1 appearance-none bg-transparent px-2 py-1.5 text-sm text-[var(--app-fg)] outline-none placeholder:text-[var(--app-hint)] [text-overflow:ellipsis] [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden" + /> + {renderDateButton('embedded')} + {props.value ? ( + + ) : null}
+ {datePicker}
) }