import type { JSX } from 'solid-js'; import { createComponent, createEffect, mergeProps } from 'solid-js'; import { omitProps } from 'solid-use/props'; import type { DisclosureStateRenderProps } from '../../states/create-disclosure-state'; import { DisclosureStateChild, useDisclosureState } from '../../states/create-disclosure-state'; import createDynamic from '../../utils/create-dynamic'; import type { UnmountableProps } from '../../utils/create-unmountable'; import { createUnmountable } from '../../utils/create-unmountable'; import type { DynamicProps, HeadlessPropsWithRef, ValidConstructor, } from '../../utils/dynamic-prop'; import { createForwardRef } from '../../utils/dynamic-prop'; import { focusFirst, lockFocus } from '../../utils/focus-navigation'; import getFocusableElements from '../../utils/focus-query'; import { createDisabledState, createExpandedState } from '../../utils/state-props'; import type { Prettify } from '../../utils/types'; import useEventListener from '../../utils/use-event-listener'; import { afterTransition } from '../../utils/wait-for-transition'; import { usePopoverContext } from './PopoverContext'; import { POPOVER_PANEL_TAG } from './tags'; export type PopoverPanelBaseProps = Prettify; export type PopoverPanelProps = HeadlessPropsWithRef< T, PopoverPanelBaseProps >; /** * The floating panel of a `Popover`. Position it yourself — the library sets * no coordinates. * * Renders a `
` by default. * * @see {@link https://github.com/lxsmnsyc/terracotta/blob/main/docs/components/popover.md} */ export function PopoverPanel( props: PopoverPanelProps, ): JSX.Element { const context = usePopoverContext('PopoverPanel'); const state = useDisclosureState(); const [internalRef, setInternalRef] = createForwardRef(props); createEffect(() => { const current = internalRef(); if (current instanceof HTMLElement && state.isOpen()) { afterTransition(current, () => { focusFirst(getFocusableElements(current), false); }); useEventListener(current, 'keydown', (e) => { if (!state.disabled()) { // Keys this panel acts on are not passed on: a `Dialog` or another panel // around this one traps `Tab` and closes on `Escape` too, and would // otherwise move focus a second time or close both layers at once. switch (e.key) { case 'Tab': { e.preventDefault(); e.stopPropagation(); lockFocus(current, e.shiftKey, false); break; } case 'Escape': { e.stopPropagation(); state.close(); break; } } } }); useEventListener(current, 'focusout', (e) => { if (context.hovering) { return; } if ( (e.relatedTarget && !current.contains(e.relatedTarget as Node)) || (e.target && !current.contains(e.target as Node)) ) { state.close(); } }); } }); return createUnmountable( props, () => state.isOpen(), () => createDynamic( () => props.as ?? ('div' as T), mergeProps( POPOVER_PANEL_TAG, { id: context.panelID, ref: setInternalRef, get children() { return createComponent(DisclosureStateChild, { get children() { return props.children; }, }); }, }, createDisabledState(() => state.disabled()), createExpandedState(() => state.isOpen()), omitProps(props, ['as', 'unmount', 'children', 'ref']), ) as DynamicProps, ), ); }