diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 5977d7fce..5ac0da3cf 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -294,6 +294,14 @@ and `frontend-modern/src/components/shared/searchInputModel.ts` owns the shared search-input contract plus shortcut-hint and trailing-control policy. Future search-input work should extend those owners instead of pushing type-to-search or enhancement wiring back into the shared shell. +The shared search tips popover now follows that same owner split. +`frontend-modern/src/components/shared/SearchTipsPopover.tsx` stays the render +shell, `frontend-modern/src/components/shared/useSearchTipsPopoverState.ts` +owns open-state, pointer/focus continuity, and outside-click/Escape listener +runtime, and `frontend-modern/src/components/shared/searchTipsPopoverModel.ts` +owns trigger variant, label/id defaults, hover policy, and trigger/popover +class selection. Future search-tips work should extend those owners instead of +pushing listener lifecycle or trigger policy back into the shared shell. The shared tooltip now follows that same owner split. `frontend-modern/src/components/shared/Tooltip.tsx` stays the render shell and singleton API boundary, `frontend-modern/src/components/shared/useTooltipState.ts` diff --git a/frontend-modern/src/components/shared/SearchTipsPopover.tsx b/frontend-modern/src/components/shared/SearchTipsPopover.tsx index 28dd934d8..06e1fcd0b 100644 --- a/frontend-modern/src/components/shared/SearchTipsPopover.tsx +++ b/frontend-modern/src/components/shared/SearchTipsPopover.tsx @@ -1,111 +1,49 @@ -import { Component, Show, For, createEffect, createSignal, onCleanup } from 'solid-js'; +import { Component, Show, For } from 'solid-js'; +import { + getSearchTipsPopoverButtonLabel, + getSearchTipsPopoverId, + getSearchTipsPopoverPositionClass, + getSearchTipsPopoverTitle, + getSearchTipsPopoverTriggerClass, + getSearchTipsPopoverTriggerVariant, + shouldSearchTipsPopoverOpenOnHover, + type SearchTipsPopoverProps, +} from './searchTipsPopoverModel'; +import { useSearchTipsPopoverState } from './useSearchTipsPopoverState'; -interface SearchTip { - code: string; - description: string; -} - -interface SearchTipsPopoverProps { - buttonLabel?: string; - title?: string; - intro?: string; - tips: SearchTip[]; - footerText?: string; - footerHighlight?: string; - popoverId?: string; - align?: 'left' | 'right'; - class?: string; - triggerVariant?: 'button' | 'link' | 'icon'; - openOnHover?: boolean; -} +export type { SearchTip, SearchTipsPopoverProps } from './searchTipsPopoverModel'; export const SearchTipsPopover: Component = (props) => { - const [open, setOpen] = createSignal(false); - let popoverRef: HTMLDivElement | undefined; - let triggerRef: HTMLButtonElement | undefined; - let pointerInside = false; - - const close = () => setOpen(false); - - createEffect(() => { - if (!open()) { - return; - } - - const handlePointerDown = (event: PointerEvent) => { - const target = event.target as Node; - const insidePopover = popoverRef?.contains(target) ?? false; - const onTrigger = triggerRef?.contains(target) ?? false; - - if (!insidePopover && !onTrigger) { - close(); - } - }; - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - close(); - } - }; - - window.addEventListener('pointerdown', handlePointerDown); - window.addEventListener('keydown', handleKeyDown); - - onCleanup(() => { - window.removeEventListener('pointerdown', handlePointerDown); - window.removeEventListener('keydown', handleKeyDown); - }); + const triggerVariant = () => getSearchTipsPopoverTriggerVariant(props.triggerVariant); + const buttonLabel = () => getSearchTipsPopoverButtonLabel(props.buttonLabel); + const title = () => getSearchTipsPopoverTitle(props.title); + const popoverId = () => getSearchTipsPopoverId(props.popoverId); + const positionClass = () => getSearchTipsPopoverPositionClass(props.align); + const triggerClass = () => getSearchTipsPopoverTriggerClass(triggerVariant()); + const openOnHover = () => shouldSearchTipsPopoverOpenOnHover(props.openOnHover); + const state = useSearchTipsPopoverState({ + buttonLabel, + openOnHover, }); - const popoverPositionClass = props.align === 'left' ? 'left-0' : 'right-0'; - const popoverId = props.popoverId ?? 'search-tips-popover'; - - const triggerVariant = props.triggerVariant ?? 'button'; - const openOnHover = props.openOnHover ?? false; - const buttonLabel = props.buttonLabel ?? 'Search tips'; - - const triggerBaseClasses = - 'text-xs font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 focus:ring-offset-white dark:focus:ring-blue-400'; - - const triggerClasses = - triggerVariant === 'button' - ? `rounded-md border border-border px-2.5 py-1 text-muted transition-colors hover:bg-surface-hover ${triggerBaseClasses}` - : triggerVariant === 'link' - ? `rounded px-1 py-0.5 underline decoration-dotted underline-offset-4 transition-colors hover:text-base-content ${triggerBaseClasses}` - : `flex h-5 w-5 items-center justify-center rounded-full transition-colors hover:text-muted ${triggerBaseClasses}`; - - const handleMouseEnter = () => { - pointerInside = true; - setOpen(true); - }; - - const handleMouseLeave = () => { - pointerInside = false; - setOpen(false); - }; - return (
- +
(popoverRef = el)} - id={popoverId} + ref={state.setPopoverRef} + id={popoverId()} role="dialog" - aria-label={props.title ?? 'Search tips'} - class={`absolute ${popoverPositionClass} z-50 mt-2 w-72 overflow-hidden rounded-md border bg-surface text-left shadow-sm`} + aria-label={title()} + class={`absolute ${positionClass()} z-50 mt-2 w-72 overflow-hidden rounded-md border bg-surface text-left shadow-sm`} >
- - {props.title ?? 'Search tips'} - + {title()}