Skeleton, Breadcrumbs, Link, Button, Image, Meter, NumberField Floating components: Tooltip (hover/focus), Popover (click, with focus trap and dismiss), HoverCard (hover with safe area). Simple components: Alert (role=alert), Badge (role=status), Skeleton (loading placeholder with data attributes). Navigation: Breadcrumbs (nav>ol>li with separators), Link (accessible anchor with disabled), Button (with disabled click suppression). Data/Form: Image (Img+Fallback with loading status), Meter (like Progress for known ranges), NumberField (spinbutton with inc/dec). 302 tests across 46 files, typecheck clean, build produces 176 files.
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
// packages/core/src/components/popover/popover-content.tsx
|
|
import type { JSX } from "solid-js";
|
|
import { Show, createEffect, onCleanup, splitProps } from "solid-js";
|
|
import { createDismiss } from "../../utilities/dismiss/create-dismiss";
|
|
import { createFocusTrap } from "../../utilities/focus-trap/create-focus-trap";
|
|
import { createScrollLock } from "../../utilities/scroll-lock/create-scroll-lock";
|
|
import { useInternalPopoverContext } from "./popover-context";
|
|
|
|
export interface PopoverContentProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
/** Keep mounted even when closed (for animation control). */
|
|
forceMount?: boolean | undefined;
|
|
children?: JSX.Element;
|
|
}
|
|
|
|
/**
|
|
* Popover content panel with `role="dialog"`. Uses floating positioning.
|
|
* When modal: focus trap + scroll lock. When non-modal: Tab closes popover.
|
|
* Wrap with Popover.Portal to render outside the DOM tree.
|
|
*/
|
|
export function PopoverContent(props: PopoverContentProps): JSX.Element {
|
|
const [local, rest] = splitProps(props, ["children", "forceMount", "style"]);
|
|
const ctx = useInternalPopoverContext();
|
|
|
|
const focusTrap = createFocusTrap(() => ctx.contentRef());
|
|
const scrollLock = createScrollLock();
|
|
const dismiss = createDismiss({
|
|
getContainer: () => ctx.contentRef(),
|
|
onDismiss: () => ctx.setOpen(false),
|
|
});
|
|
|
|
/** Non-modal Tab handler: Tab closes popover instead of trapping focus. */
|
|
const handleKeyDown: JSX.EventHandler<HTMLDivElement, KeyboardEvent> = (e) => {
|
|
if (!ctx.modal() && e.key === "Tab") {
|
|
ctx.setOpen(false);
|
|
}
|
|
};
|
|
|
|
createEffect(() => {
|
|
if (ctx.isOpen()) {
|
|
dismiss.attach();
|
|
if (ctx.modal()) {
|
|
focusTrap.activate();
|
|
scrollLock.lock();
|
|
}
|
|
} else {
|
|
focusTrap.deactivate();
|
|
scrollLock.unlock();
|
|
dismiss.detach();
|
|
}
|
|
onCleanup(() => {
|
|
focusTrap.deactivate();
|
|
scrollLock.unlock();
|
|
dismiss.detach();
|
|
});
|
|
});
|
|
|
|
return (
|
|
<Show when={local.forceMount || ctx.isOpen()}>
|
|
<div
|
|
ref={(el) => ctx.setContentRef(el)}
|
|
id={ctx.contentId()}
|
|
role="dialog"
|
|
aria-modal={ctx.modal() || undefined}
|
|
data-state={ctx.isOpen() ? "open" : "closed"}
|
|
style={
|
|
typeof local.style === "string"
|
|
? `${styleToString(ctx.floatingStyle())};${local.style}`
|
|
: { ...ctx.floatingStyle(), ...(local.style as JSX.CSSProperties) }
|
|
}
|
|
onKeyDown={handleKeyDown}
|
|
{...rest}
|
|
>
|
|
{local.children}
|
|
</div>
|
|
</Show>
|
|
);
|
|
}
|
|
|
|
/** Convert a JSX.CSSProperties object to an inline CSS string. */
|
|
function styleToString(style: JSX.CSSProperties): string {
|
|
return Object.entries(style)
|
|
.map(([k, v]) => `${k}:${v}`)
|
|
.join(";");
|
|
}
|