mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
fix(ui): keep responsive filter panels reachable
This commit is contained in:
@@ -1,49 +1,81 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "ed0f3613aeda63f41248cd1ef981b5ca2639bd8b",
|
||||
"verified_at": "2026-08-06T21:47:15Z",
|
||||
"base_sha": "b7651974260b3bccb8085754b002bd3727040832",
|
||||
"verified_at": "2026-08-06T20:28:35Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/alerts/alertOverridesModel.ts",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsAvailabilityMutations.ts",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts",
|
||||
"frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts",
|
||||
"frontend-modern/src/features/docker/DockerContainersTable.tsx",
|
||||
"frontend-modern/src/utils/alertThresholdsPresentation.ts"
|
||||
"frontend-modern/src/components/shared/FilterToolbar.tsx",
|
||||
"frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx",
|
||||
"frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx",
|
||||
"frontend-modern/src/components/shared/SearchTipsPopover.tsx",
|
||||
"frontend-modern/src/components/shared/searchTipsPopoverModel.ts",
|
||||
"frontend-modern/src/components/shared/useSearchTipsPopoverState.ts",
|
||||
"frontend-modern/src/index.css"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/alerts/alertOverridesModel.ts": "9729ff9ad413c3b9ece2a1ab3449939cba563bf80acd16e678b52d668751ed63",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsAvailabilityMutations.ts": "b8b89dd3d756b13dc730f6cc7e0c3fe2f4a79525753eeaa6fa203766c7e592ba",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts": "92b0fd9836d86930cae26e5ccecb3dc56babdba2170a454e820a12dd27549f2b",
|
||||
"frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts": "e71f230e5fb8ad5f970cf9385afa31b18221aedd81453febbce64a0d0f60783c",
|
||||
"frontend-modern/src/features/docker/DockerContainersTable.tsx": "45b125474f0d5058e9861c44fab0a33cf93b4a82952001d7f9010f18419dac4e",
|
||||
"frontend-modern/src/utils/alertThresholdsPresentation.ts": "6605976a9b561c26578f1eb78785ccd5d6739c2a0a3b91246e23828c8433a49c"
|
||||
"frontend-modern/src/components/shared/FilterToolbar.tsx": "e866a9276b5ab6a901ffea4adab59f9f1438376295017e4290a1e58f47e775f0",
|
||||
"frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx": "0266c7abf8ab90941d53b977f0fd1af7cd1f795d0c5849fdabe425a01d6cb02b",
|
||||
"frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx": "877570b2fbdbd7378a0cd94bb3c65a9edb0a22348dd8ab9a106b0f15b92f0aa8",
|
||||
"frontend-modern/src/components/shared/SearchTipsPopover.tsx": "d570941c7f089dad7805c514b51c6f7a21647c91d6409f3b85f71f91e8b73d6a",
|
||||
"frontend-modern/src/components/shared/searchTipsPopoverModel.ts": "b558ce94eea59a645fd2878bc6bd2fd1f0184cc86cfd649ac6991f303f888131",
|
||||
"frontend-modern/src/components/shared/useSearchTipsPopoverState.ts": "be3c9840f580d1e975096a98f94da6ca6a0e554b9d93b1d0e2aac77403eb5b60",
|
||||
"frontend-modern/src/index.css": "ac6b159890d180e044153555e6e9299d02d7775991f9aab3f15a6a31f113c509"
|
||||
},
|
||||
"routes": [
|
||||
"/alerts/thresholds/docker",
|
||||
"/docker"
|
||||
],
|
||||
"routes": ["/proxmox/overview", "/standalone/machines"],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 375,
|
||||
"height": 812
|
||||
"width": 568,
|
||||
"height": 320
|
||||
},
|
||||
{
|
||||
"width": 844,
|
||||
"height": 390
|
||||
},
|
||||
{
|
||||
"width": 1180,
|
||||
"height": 650
|
||||
},
|
||||
{
|
||||
"width": 1279,
|
||||
"height": 650
|
||||
},
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 500
|
||||
},
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 650
|
||||
},
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 768
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Docker thresholds tab with mock container rows grouped by host, ignored-container-patterns card showing the new wildcard copy",
|
||||
"loki container rows rendering the persisted docker:orion-2-mock/loki name-keyed override as disabled after a full page reload",
|
||||
"alerts.json converged to name-keyed overrides after live monitor-sync migration re-homed a seeded legacy container-ID key and a unified-hash key and pruned an orphaned 12-hex key",
|
||||
"Docker page containers table with CPU/memory meters resolving thresholds through the new name-first candidate chain",
|
||||
"mobile 375px docker thresholds view rendering the wildcard copy and container sections"
|
||||
"Short-height landscape platform tables with fixed bottom navigation",
|
||||
"Expanded Proxmox node details and mobile filter actions",
|
||||
"View preferences with the inline Proxmox column picker expanded",
|
||||
"View preferences with all eleven Machine column choices expanded",
|
||||
"Saved Views empty state and save-name form",
|
||||
"Search Tips with content taller than the available landscape space",
|
||||
"Exact bottom-navigation to desktop-navigation transition at 1279px and 1280px",
|
||||
"Short desktop windows below the safe anchored-popover height"
|
||||
],
|
||||
"interactions": [
|
||||
"searched containers for 'loki' in the thresholds table",
|
||||
"toggled the per-container alert disable switch on 'loki' and clicked Save Changes; verified alerts.json stored docker:orion-2-mock/loki (host+name key)",
|
||||
"reloaded /alerts/thresholds/docker and confirmed the saved override re-bound to the container rows via the name candidate",
|
||||
"seeded legacy docker:{host}/{containerID}, docker:{host}/{unified-hash} and orphan hex overrides via PUT /api/alerts/config and watched the monitor sync migrate all of them to the name key on disk"
|
||||
"Verified the horizontally scrollable mobile platform rail keeps Machines reachable and centres it after activation",
|
||||
"Expanded a Proxmox node at 568x320 and confirmed the table shell and detail row create no horizontal document overflow",
|
||||
"Opened View preferences at 568x320, 844x390, 1180x650, 1279x650, 1280x500, and 1280x768",
|
||||
"Expanded the Machine column picker, scrolled internally to Kernel, toggled it, and reset the saved preference",
|
||||
"Removed and restored the Proxmox Disk column through the compact View panel",
|
||||
"Opened Saved Views and its save-name form at compact mobile and short desktop sizes",
|
||||
"Confirmed View and Saved Views remain fully above the bottom navigation and entirely inside the viewport",
|
||||
"Confirmed short desktop windows use a viewport-fixed panel while 1280x768 returns to the toolbar anchor",
|
||||
"Opened Search Tips at 568x320, 844x390, 1280x500, and 1280x768 and verified collision-aware height, internal scrolling, and desktop anchoring",
|
||||
"Measured zero document overflow throughout the interaction pass"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -310,6 +310,7 @@ describe('FilterBar', () => {
|
||||
expect(label).toHaveClass('sr-only');
|
||||
expect(select).toHaveClass('min-h-11');
|
||||
expect(select).toHaveClass('sm:h-7');
|
||||
expect(select).toHaveClass('sm:min-h-0');
|
||||
expect(select).toHaveClass('w-[7.5rem]');
|
||||
expect(select).not.toHaveClass('min-w-[7.5rem]');
|
||||
expect(select.parentElement).not.toHaveClass('p-0.5');
|
||||
|
||||
@@ -51,16 +51,13 @@ describe('SavedViewsMenu', () => {
|
||||
expect(trigger).toHaveFocus();
|
||||
});
|
||||
|
||||
it('anchors from the leading edge on narrow rails and the trailing edge on desktop', () => {
|
||||
it('stays above mobile navigation and returns to the trailing anchor on desktop', () => {
|
||||
renderMenu('responsive-anchor-contract');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Saved views' }));
|
||||
|
||||
expect(screen.getByRole('dialog', { name: 'Saved views' })).toHaveClass(
|
||||
'left-0',
|
||||
'right-auto',
|
||||
'md:left-auto',
|
||||
'md:right-0',
|
||||
'filter-bottom-nav-aware-panel',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ import BookmarkIcon from 'lucide-solid/icons/bookmark';
|
||||
import PlusIcon from 'lucide-solid/icons/plus';
|
||||
import StarIcon from 'lucide-solid/icons/star';
|
||||
import XIcon from 'lucide-solid/icons/x';
|
||||
import { FilterPopoverTrigger, FilterToolbarPanel } from '@/components/shared/FilterToolbar';
|
||||
import {
|
||||
FilterPopoverTrigger,
|
||||
FilterToolbarPanel,
|
||||
filterBottomNavAwarePanelClass,
|
||||
} from '@/components/shared/FilterToolbar';
|
||||
import { useSavedViews, type SavedView } from './useSavedViews';
|
||||
|
||||
interface SavedViewsMenuProps {
|
||||
@@ -124,7 +128,7 @@ export const SavedViewsMenu: Component<SavedViewsMenuProps> = (props) => {
|
||||
role="dialog"
|
||||
aria-label="Saved views"
|
||||
widthClass="w-64 max-w-[calc(100vw-2rem)]"
|
||||
class="left-0 right-auto top-[calc(100%+0.25rem)] z-50 p-0 md:left-auto md:right-0"
|
||||
class={`${filterBottomNavAwarePanelClass} z-50 p-0`}
|
||||
>
|
||||
<Show
|
||||
when={savePromptOpen()}
|
||||
|
||||
@@ -19,8 +19,7 @@ describe('ViewOptionsMenu', () => {
|
||||
|
||||
expect(screen.getByRole('dialog', { name: 'View preferences' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog', { name: 'View preferences' })).toHaveClass(
|
||||
'left-auto',
|
||||
'right-0',
|
||||
'filter-bottom-nav-aware-panel',
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'View' }).parentElement).toHaveClass(
|
||||
'static',
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
} from 'solid-js';
|
||||
import SlidersHorizontalIcon from 'lucide-solid/icons/sliders-horizontal';
|
||||
|
||||
import { FilterPopoverTrigger, FilterToolbarPanel } from '@/components/shared/FilterToolbar';
|
||||
import {
|
||||
FilterPopoverTrigger,
|
||||
FilterToolbarPanel,
|
||||
filterBottomNavAwarePanelClass,
|
||||
} from '@/components/shared/FilterToolbar';
|
||||
|
||||
interface ViewOptionsMenuProps {
|
||||
children: JSX.Element;
|
||||
@@ -69,7 +73,7 @@ export const ViewOptionsMenu: Component<ViewOptionsMenuProps> = (props) => {
|
||||
role="dialog"
|
||||
aria-label={props.label ?? 'View preferences'}
|
||||
widthClass="w-80 max-w-[calc(100vw-2rem)]"
|
||||
class="left-auto right-0 top-[calc(100%+0.25rem)] z-50 max-h-[min(38rem,calc(100vh-8rem))] overflow-y-auto p-3"
|
||||
class={`${filterBottomNavAwarePanelClass} z-50 p-3`}
|
||||
>
|
||||
<div class="mb-3 border-b border-border-subtle pb-2">
|
||||
<div class="text-xs font-medium text-base-content">
|
||||
|
||||
@@ -27,6 +27,7 @@ export const filterDividerClass = 'hidden h-5 w-px bg-surface-hover sm:block';
|
||||
export const filterPanelClass =
|
||||
'absolute right-0 top-[calc(100%+0.5rem)] z-[80] rounded-md border border-border bg-surface p-3 shadow-lg';
|
||||
export const filterPanelDefaultWidthClass = 'w-[min(40rem,calc(100vw-2rem))]';
|
||||
export const filterBottomNavAwarePanelClass = 'filter-bottom-nav-aware-panel';
|
||||
export const filterPanelTitleClass = 'text-sm font-medium text-base-content';
|
||||
export const filterPanelDescriptionClass = 'text-xs text-muted';
|
||||
export const mobileFiltersButtonClass =
|
||||
|
||||
@@ -67,7 +67,7 @@ export const SearchTipsPopover: Component<SearchTipsPopoverProps> = (props) => {
|
||||
role="dialog"
|
||||
aria-label={title()}
|
||||
style={state.popoverStyle()}
|
||||
class={`fixed ${positionClass()} z-50 w-auto overflow-hidden rounded-md border bg-surface text-left shadow-sm sm:absolute sm:mt-2 sm:w-72`}
|
||||
class={`!fixed ${positionClass()} z-50 w-auto overflow-y-auto rounded-md border bg-surface text-left shadow-sm xl:!absolute xl:mt-2 xl:w-72`}
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-border-subtle px-3 py-2">
|
||||
<span class="text-sm font-semibold text-base-content">{title()}</span>
|
||||
|
||||
@@ -8275,13 +8275,15 @@ describe('shared primitive guardrails', () => {
|
||||
|
||||
expect(savedViewsMenuSource).toContain("from './useSavedViews';");
|
||||
expect(filterToolbarSource).toContain('export const FilterPopoverTrigger');
|
||||
expect(filterToolbarSource).toContain('export const filterBottomNavAwarePanelClass');
|
||||
expect(filterToolbarSource).toContain(
|
||||
"'min-h-11 rounded-md text-xs font-medium sm:h-7 sm:min-h-0'",
|
||||
);
|
||||
expect(viewOptionsMenuSource).toContain('<FilterPopoverTrigger');
|
||||
expect(viewOptionsMenuSource).toContain('class="static ml-auto shrink-0 sm:relative sm:ml-0"');
|
||||
expect(viewOptionsMenuSource).toContain('class="left-auto right-0');
|
||||
expect(viewOptionsMenuSource).toContain('filterBottomNavAwarePanelClass');
|
||||
expect(savedViewsMenuSource).toContain('<FilterPopoverTrigger');
|
||||
expect(savedViewsMenuSource).toContain('filterBottomNavAwarePanelClass');
|
||||
expect(filterCatalogSource).toContain('leadingControls?: JSX.Element;');
|
||||
expect(filterCatalogSource).toContain('viewOptions?: JSX.Element;');
|
||||
expect(filterCatalogSource).toContain('trailingControls?: JSX.Element;');
|
||||
@@ -8310,9 +8312,7 @@ describe('shared primitive guardrails', () => {
|
||||
expect(savedViewsMenuSource).toContain('queueMicrotask(() => nameInputRef?.focus())');
|
||||
expect(savedViewsMenuSource).toContain('queueMicrotask(() => triggerRef?.focus())');
|
||||
expect(savedViewsMenuSource).toContain('for={nameInputId}');
|
||||
expect(savedViewsMenuSource).toContain(
|
||||
'class="left-0 right-auto top-[calc(100%+0.25rem)] z-50 p-0 md:left-auto md:right-0"',
|
||||
);
|
||||
expect(savedViewsMenuSource).toContain('class={`${filterBottomNavAwarePanelClass} z-50 p-0`}');
|
||||
expect(savedViewsMenuSource).toContain('opacity-100 transition-opacity');
|
||||
expect(savedViewsMenuSource).toContain('sm:opacity-0');
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { SearchTipsPopover } from '@/components/shared/SearchTipsPopover';
|
||||
import searchTipsPopoverSource from '@/components/shared/SearchTipsPopover.tsx?raw';
|
||||
import searchTipsPopoverModelSource from '@/components/shared/searchTipsPopoverModel.ts?raw';
|
||||
@@ -8,6 +8,7 @@ import searchTipsPopoverStateSource from '@/components/shared/useSearchTipsPopov
|
||||
describe('SearchTipsPopover', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('keeps search tips popover on shell, runtime, and model owners', () => {
|
||||
@@ -23,14 +24,16 @@ describe('SearchTipsPopover', () => {
|
||||
expect(searchTipsPopoverStateSource).toContain('createEffect');
|
||||
expect(searchTipsPopoverStateSource).toContain('window.addEventListener');
|
||||
expect(searchTipsPopoverStateSource).toContain('pointerInside');
|
||||
expect(searchTipsPopoverStateSource).toContain('window.innerWidth >= 640');
|
||||
expect(searchTipsPopoverStateSource).toContain('window.innerWidth >= 1280');
|
||||
expect(searchTipsPopoverStateSource).toContain('window.innerHeight >= 768');
|
||||
expect(searchTipsPopoverStateSource).toContain('window.innerWidth - viewportMargin - width');
|
||||
expect(searchTipsPopoverStateSource).toContain('nav[aria-label="Mobile navigation"]');
|
||||
expect(searchTipsPopoverStateSource).toContain(
|
||||
"window.addEventListener('scroll', updatePopoverPosition, true)",
|
||||
);
|
||||
expect(searchTipsPopoverSource).toContain('style={state.popoverStyle()}');
|
||||
expect(searchTipsPopoverSource).toContain('fixed ${positionClass()}');
|
||||
expect(searchTipsPopoverSource).toContain('sm:absolute sm:mt-2 sm:w-72');
|
||||
expect(searchTipsPopoverSource).toContain('!fixed ${positionClass()}');
|
||||
expect(searchTipsPopoverSource).toContain('xl:!absolute xl:mt-2 xl:w-72');
|
||||
|
||||
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerClass');
|
||||
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverPositionClass');
|
||||
@@ -80,4 +83,70 @@ describe('SearchTipsPopover', () => {
|
||||
fireEvent.keyDown(window, { key: 'Escape' });
|
||||
expect(screen.queryByRole('dialog', { name: 'Search tips' })).toBeNull();
|
||||
});
|
||||
|
||||
it('bounds landscape tips above the visible mobile navigation', async () => {
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(844);
|
||||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(390);
|
||||
|
||||
render(() => (
|
||||
<>
|
||||
<nav aria-label="Mobile navigation" />
|
||||
<SearchTipsPopover tips={[{ code: 'name:web', description: 'Filter by name' }]} />
|
||||
</>
|
||||
));
|
||||
|
||||
const nav = screen.getByRole('navigation', { name: 'Mobile navigation' });
|
||||
vi.spyOn(nav, 'getBoundingClientRect').mockReturnValue(
|
||||
DOMRect.fromRect({ y: 328.5, height: 61.5, width: 844 }),
|
||||
);
|
||||
const trigger = screen.getByRole('button', { name: 'Search tips' });
|
||||
vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue(
|
||||
DOMRect.fromRect({ x: 752, y: 150, width: 28, height: 20 }),
|
||||
);
|
||||
|
||||
fireEvent.click(trigger);
|
||||
const dialog = await screen.findByRole('dialog', { name: 'Search tips' });
|
||||
vi.spyOn(dialog, 'getBoundingClientRect').mockReturnValue(
|
||||
DOMRect.fromRect({ x: 492, y: 16, width: 288, height: 326 }),
|
||||
);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(dialog.style.position).toBe('fixed');
|
||||
expect(dialog.style.getPropertyPriority('position')).toBe('important');
|
||||
expect(dialog.style.top).toBe('16px');
|
||||
expect(dialog.style.maxHeight).toBe('296.5px');
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores the hidden mobile nav when bounding a short desktop window', async () => {
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1280);
|
||||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(500);
|
||||
|
||||
render(() => (
|
||||
<>
|
||||
<nav aria-label="Mobile navigation" />
|
||||
<SearchTipsPopover tips={[{ code: 'name:web', description: 'Filter by name' }]} />
|
||||
</>
|
||||
));
|
||||
|
||||
const nav = screen.getByRole('navigation', { name: 'Mobile navigation' });
|
||||
vi.spyOn(nav, 'getBoundingClientRect').mockReturnValue(DOMRect.fromRect({ height: 0 }));
|
||||
const trigger = screen.getByRole('button', { name: 'Search tips' });
|
||||
vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue(
|
||||
DOMRect.fromRect({ x: 1173, y: 200, width: 28, height: 20 }),
|
||||
);
|
||||
|
||||
fireEvent.click(trigger);
|
||||
const dialog = await screen.findByRole('dialog', { name: 'Search tips' });
|
||||
vi.spyOn(dialog, 'getBoundingClientRect').mockReturnValue(
|
||||
DOMRect.fromRect({ x: 913, y: 16, width: 288, height: 326 }),
|
||||
);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(dialog.style.position).toBe('fixed');
|
||||
expect(dialog.style.maxHeight).toBe('468px');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
-4
@@ -13,15 +13,15 @@ const TRIGGER_BASE_CLASSES =
|
||||
describe('searchTipsPopoverModel.branchcov0712c', () => {
|
||||
describe('getSearchTipsPopoverPositionClass', () => {
|
||||
it("returns 'left-0' when align === 'left' (truthy ternary arm)", () => {
|
||||
expect(getSearchTipsPopoverPositionClass('left')).toBe('sm:left-0');
|
||||
expect(getSearchTipsPopoverPositionClass('left')).toBe('xl:left-0');
|
||||
});
|
||||
|
||||
it("returns 'right-0' when align === 'right' (falsy ternary arm)", () => {
|
||||
expect(getSearchTipsPopoverPositionClass('right')).toBe('sm:right-0');
|
||||
expect(getSearchTipsPopoverPositionClass('right')).toBe('xl:right-0');
|
||||
});
|
||||
|
||||
it("returns 'right-0' when align is omitted (undefined -> falsy ternary arm)", () => {
|
||||
expect(getSearchTipsPopoverPositionClass()).toBe('sm:right-0');
|
||||
expect(getSearchTipsPopoverPositionClass()).toBe('xl:right-0');
|
||||
});
|
||||
|
||||
it('returns the else-arm value for an unexpected align value (defensive)', () => {
|
||||
@@ -29,7 +29,7 @@ describe('searchTipsPopoverModel.branchcov0712c', () => {
|
||||
// wrong-typed value that slips past the union at runtime) must fall to
|
||||
// the 'right-0' arm.
|
||||
const bogus = 'center' as unknown as Parameters<typeof getSearchTipsPopoverPositionClass>[0];
|
||||
expect(getSearchTipsPopoverPositionClass(bogus)).toBe('sm:right-0');
|
||||
expect(getSearchTipsPopoverPositionClass(bogus)).toBe('xl:right-0');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface SearchTipsPopoverProps {
|
||||
}
|
||||
|
||||
export function getSearchTipsPopoverPositionClass(align?: 'left' | 'right'): string {
|
||||
return align === 'left' ? 'sm:left-0' : 'sm:right-0';
|
||||
return align === 'left' ? 'xl:left-0' : 'xl:right-0';
|
||||
}
|
||||
|
||||
export function getSearchTipsPopoverId(popoverId?: string): string {
|
||||
|
||||
@@ -30,7 +30,7 @@ export function useSearchTipsPopoverState(
|
||||
|
||||
const close = () => setOpen(false);
|
||||
const updatePopoverPosition = () => {
|
||||
if (!triggerRef || !popoverRef || window.innerWidth >= 640) {
|
||||
if (!triggerRef || !popoverRef || (window.innerWidth >= 1280 && window.innerHeight >= 768)) {
|
||||
setPopoverStyle(undefined);
|
||||
return;
|
||||
}
|
||||
@@ -39,18 +39,29 @@ export function useSearchTipsPopoverState(
|
||||
const triggerGap = 8;
|
||||
const width = Math.min(288, window.innerWidth - viewportMargin * 2);
|
||||
const triggerRect = triggerRef.getBoundingClientRect();
|
||||
const popoverHeight = popoverRef.getBoundingClientRect().height;
|
||||
const mobileNavRect = document
|
||||
.querySelector<HTMLElement>('nav[aria-label="Mobile navigation"]')
|
||||
?.getBoundingClientRect();
|
||||
const mobileNavTop = mobileNavRect && mobileNavRect.height > 0 ? mobileNavRect.top : undefined;
|
||||
const availableBottom = Math.min(
|
||||
window.innerHeight - viewportMargin,
|
||||
mobileNavTop == null ? Number.POSITIVE_INFINITY : mobileNavTop - viewportMargin,
|
||||
);
|
||||
const availableHeight = Math.max(96, availableBottom - viewportMargin);
|
||||
const popoverHeight = Math.min(popoverRef.getBoundingClientRect().height, availableHeight);
|
||||
const left = Math.min(
|
||||
Math.max(triggerRect.right - width, viewportMargin),
|
||||
window.innerWidth - viewportMargin - width,
|
||||
);
|
||||
const belowTop = triggerRect.bottom + triggerGap;
|
||||
const top =
|
||||
belowTop + popoverHeight <= window.innerHeight - viewportMargin
|
||||
belowTop + popoverHeight <= availableBottom
|
||||
? belowTop
|
||||
: Math.max(viewportMargin, triggerRect.top - triggerGap - popoverHeight);
|
||||
|
||||
setPopoverStyle(`left:${left}px;top:${top}px;width:${width}px`);
|
||||
setPopoverStyle(
|
||||
`position:fixed !important;left:${left}px;top:${top}px;width:${width}px;max-height:${availableHeight}px`,
|
||||
);
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
@@ -381,6 +381,38 @@
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
/*
|
||||
Filter utility panels must stay reachable in short windows as well as
|
||||
narrow ones. While the bottom navigation is present they sit above it;
|
||||
short desktop windows use the viewport edge, and roomy desktop windows
|
||||
return to the trigger-anchored dropdown.
|
||||
*/
|
||||
.filter-bottom-nav-aware-panel {
|
||||
position: fixed !important;
|
||||
inset-inline: auto 1rem !important;
|
||||
top: auto !important;
|
||||
bottom: 1rem !important;
|
||||
max-height: calc(100dvh - 2rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 79.999rem) {
|
||||
.filter-bottom-nav-aware-panel {
|
||||
bottom: calc(5rem + env(safe-area-inset-bottom)) !important;
|
||||
max-height: calc(100dvh - 6rem - env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 80rem) and (min-height: 48rem) {
|
||||
.filter-bottom-nav-aware-panel {
|
||||
position: absolute !important;
|
||||
inset-inline: auto 0 !important;
|
||||
top: calc(100% + 0.25rem) !important;
|
||||
bottom: auto !important;
|
||||
max-height: min(38rem, calc(100vh - 8rem));
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-shell {
|
||||
width: 100%;
|
||||
max-width: var(--pulse-shell-max-width);
|
||||
|
||||
Reference in New Issue
Block a user