Find the cost of adding an npm package to your app's bundle size teardown.kelinci.dev
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: popover

Mary 429ddd10 30bba122

+353
+8
src/primitives/popover.tsx
··· 1 + export { default as Root } from './popover/popover-root'; 2 + export { default as Surface } from './popover/popover-surface'; 3 + export { default as Trigger } from './popover/popover-trigger'; 4 + 5 + export type { PopoverRootProps } from './popover/popover-root'; 6 + export type { PopoverSurfaceProps } from './popover/popover-surface'; 7 + export type { PopoverTriggerChildProps, PopoverTriggerProps } from './popover/popover-trigger'; 8 + export type { PopoverContextValue } from './popover/context';
+45
src/primitives/popover/context.tsx
··· 1 + import type { Placement } from '@floating-ui/dom'; 2 + import { createContext, useContext, type Accessor } from 'solid-js'; 3 + 4 + // #region types 5 + 6 + export interface PopoverContextValue { 7 + /** whether the popover is open */ 8 + open: Accessor<boolean>; 9 + /** set the popover open state */ 10 + setOpen: (open: boolean, eventType?: string) => void; 11 + /** the trigger element ref */ 12 + triggerRef: Accessor<HTMLElement | null>; 13 + /** set the trigger element ref */ 14 + setTriggerRef: (el: HTMLElement | null) => void; 15 + /** the surface element ref */ 16 + surfaceRef: Accessor<HTMLElement | null>; 17 + /** set the surface element ref */ 18 + setSurfaceRef: (el: HTMLElement | null) => void; 19 + /** unique ID for the trigger */ 20 + triggerId: string; 21 + /** unique ID for the surface */ 22 + surfaceId: string; 23 + /** placement for the surface */ 24 + placement: Accessor<Placement>; 25 + /** whether to open on hover */ 26 + openOnHover: Accessor<boolean>; 27 + } 28 + 29 + // #endregion 30 + 31 + // #region context 32 + 33 + const PopoverContext = createContext<PopoverContextValue>(); 34 + 35 + export const PopoverProvider = PopoverContext.Provider; 36 + 37 + export function usePopoverContext(): PopoverContextValue { 38 + const ctx = useContext(PopoverContext); 39 + if (!ctx) { 40 + throw new Error('Popover components must be used within a Popover.Root'); 41 + } 42 + return ctx; 43 + } 44 + 45 + // #endregion
+107
src/primitives/popover/popover-root.tsx
··· 1 + import type { Placement } from '@floating-ui/dom'; 2 + import { createMemo, createSignal, createUniqueId, onCleanup, type JSX } from 'solid-js'; 3 + 4 + import { PopoverProvider, type PopoverContextValue } from './context'; 5 + 6 + // #region types 7 + 8 + export interface PopoverRootProps { 9 + /** popover content (PopoverTrigger and PopoverSurface) */ 10 + children: JSX.Element; 11 + /** controlled open state */ 12 + open?: boolean; 13 + /** default open state for uncontrolled usage */ 14 + defaultOpen?: boolean; 15 + /** callback when open state changes */ 16 + onOpenChange?: (open: boolean) => void; 17 + /** whether to open on hover instead of click */ 18 + openOnHover?: boolean; 19 + /** delay in ms before opening on hover */ 20 + mouseEnterDelay?: number; 21 + /** delay in ms before closing after mouse leaves */ 22 + mouseLeaveDelay?: number; 23 + /** placement of the surface relative to trigger */ 24 + placement?: Placement; 25 + } 26 + 27 + // #endregion 28 + 29 + // #region component 30 + 31 + const PopoverRoot = (props: PopoverRootProps) => { 32 + const triggerId = createUniqueId(); 33 + const surfaceId = createUniqueId(); 34 + 35 + const [triggerRef, setTriggerRef] = createSignal<HTMLElement | null>(null); 36 + const [surfaceRef, setSurfaceRef] = createSignal<HTMLElement | null>(null); 37 + 38 + // support both controlled and uncontrolled modes 39 + const [internalOpen, setInternalOpen] = createSignal(props.defaultOpen ?? false); 40 + 41 + const open = () => props.open ?? internalOpen(); 42 + 43 + const mouseEnterDelay = () => props.mouseEnterDelay ?? 0; 44 + const mouseLeaveDelay = () => props.mouseLeaveDelay ?? 500; 45 + 46 + let openTimeout: ReturnType<typeof setTimeout> | undefined; 47 + let closeTimeout: ReturnType<typeof setTimeout> | undefined; 48 + 49 + const clearTimeouts = () => { 50 + if (openTimeout) { 51 + clearTimeout(openTimeout); 52 + openTimeout = undefined; 53 + } 54 + if (closeTimeout) { 55 + clearTimeout(closeTimeout); 56 + closeTimeout = undefined; 57 + } 58 + }; 59 + 60 + onCleanup(clearTimeouts); 61 + 62 + const setOpen = (value: boolean, eventType?: string) => { 63 + clearTimeouts(); 64 + 65 + const updateState = (newValue: boolean) => { 66 + if (props.open === undefined) { 67 + setInternalOpen(newValue); 68 + } 69 + props.onOpenChange?.(newValue); 70 + }; 71 + 72 + if (eventType === 'pointerenter') { 73 + // opening on hover - apply enter delay 74 + const delay = mouseEnterDelay(); 75 + if (delay > 0) { 76 + openTimeout = setTimeout(() => updateState(true), delay); 77 + } else { 78 + updateState(true); 79 + } 80 + } else if (eventType === 'pointerleave') { 81 + // closing on hover - apply leave delay 82 + closeTimeout = setTimeout(() => updateState(false), mouseLeaveDelay()); 83 + } else { 84 + // click or other events - immediate 85 + updateState(value); 86 + } 87 + }; 88 + 89 + const context: PopoverContextValue = { 90 + open, 91 + setOpen, 92 + triggerRef, 93 + setTriggerRef, 94 + surfaceRef, 95 + setSurfaceRef, 96 + triggerId, 97 + surfaceId, 98 + placement: createMemo(() => props.placement ?? 'bottom-start'), 99 + openOnHover: createMemo(() => props.openOnHover ?? false), 100 + }; 101 + 102 + return <PopoverProvider value={context}>{props.children}</PopoverProvider>; 103 + }; 104 + 105 + export default PopoverRoot; 106 + 107 + // #endregion
+125
src/primitives/popover/popover-surface.tsx
··· 1 + import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'; 2 + import { createEffect, onCleanup, onMount, type JSX } from 'solid-js'; 3 + import { Portal } from 'solid-js/web'; 4 + 5 + import { usePopoverContext } from './context'; 6 + 7 + // #region types 8 + 9 + export interface PopoverSurfaceProps { 10 + /** popover content */ 11 + children: JSX.Element; 12 + /** additional class names */ 13 + class?: string; 14 + } 15 + 16 + // #endregion 17 + 18 + // #region component 19 + 20 + const PopoverSurface = (props: PopoverSurfaceProps) => { 21 + const ctx = usePopoverContext(); 22 + 23 + const handlePointerEnter = () => { 24 + if (ctx.openOnHover()) { 25 + ctx.setOpen(true, 'pointerenter'); 26 + } 27 + }; 28 + 29 + const handlePointerLeave = () => { 30 + if (ctx.openOnHover()) { 31 + ctx.setOpen(false, 'pointerleave'); 32 + } 33 + }; 34 + 35 + return ( 36 + <> 37 + {ctx.open() && ( 38 + <Portal> 39 + <div 40 + ref={(el) => { 41 + ctx.setSurfaceRef(el); 42 + 43 + onMount(() => { 44 + createEffect(() => { 45 + const trigger = ctx.triggerRef(); 46 + if (!trigger) { 47 + return; 48 + } 49 + 50 + const placement = ctx.placement(); 51 + 52 + const updatePosition = async () => { 53 + const { x, y } = await computePosition(trigger, el, { 54 + placement: placement, 55 + strategy: 'absolute', 56 + middleware: [offset(4), flip(), shift({ padding: 8 })], 57 + }); 58 + 59 + Object.assign(el.style, { 60 + position: 'absolute', 61 + left: `${x}px`, 62 + top: `${y}px`, 63 + }); 64 + }; 65 + 66 + onCleanup(autoUpdate(trigger, el, updatePosition)); 67 + }); 68 + 69 + { 70 + // handle click outside to close (only in click mode) 71 + const handleClickOutside = (ev: MouseEvent) => { 72 + if (ctx.openOnHover()) { 73 + return; 74 + } 75 + const currentTrigger = ctx.triggerRef(); 76 + if ( 77 + !el.contains(ev.target as Node) && 78 + currentTrigger && 79 + !currentTrigger.contains(ev.target as Node) 80 + ) { 81 + ctx.setOpen(false, 'clickoutside'); 82 + } 83 + }; 84 + 85 + // handle escape key to close 86 + const handleKeyDown = (ev: KeyboardEvent) => { 87 + if (ev.key === 'Escape') { 88 + ev.preventDefault(); 89 + ctx.setOpen(false, 'escape'); 90 + ctx.triggerRef()?.focus(); 91 + } 92 + }; 93 + 94 + document.addEventListener('mousedown', handleClickOutside); 95 + document.addEventListener('keydown', handleKeyDown); 96 + 97 + onCleanup(() => { 98 + document.removeEventListener('mousedown', handleClickOutside); 99 + document.removeEventListener('keydown', handleKeyDown); 100 + }); 101 + } 102 + }); 103 + 104 + onCleanup(() => { 105 + ctx.setSurfaceRef(null); 106 + }); 107 + }} 108 + id={ctx.surfaceId} 109 + role="dialog" 110 + aria-labelledby={ctx.triggerId} 111 + onPointerEnter={handlePointerEnter} 112 + onPointerLeave={handlePointerLeave} 113 + class={`fixed z-50 box-border rounded-md border border-transparent-stroke bg-neutral-background-1 p-3 text-base-300 text-neutral-foreground-1 shadow-16 ${props.class ?? ''}`} 114 + > 115 + {props.children} 116 + </div> 117 + </Portal> 118 + )} 119 + </> 120 + ); 121 + }; 122 + 123 + export default PopoverSurface; 124 + 125 + // #endregion
+68
src/primitives/popover/popover-trigger.tsx
··· 1 + import type { JSX } from 'solid-js'; 2 + 3 + import { usePopoverContext } from './context'; 4 + 5 + // #region types 6 + 7 + export interface PopoverTriggerChildProps { 8 + ref: (el: HTMLElement) => void; 9 + id: string; 10 + 'aria-haspopup': 'dialog'; 11 + 'aria-expanded': boolean; 12 + 'aria-controls': string | undefined; 13 + onClick: () => void; 14 + onPointerEnter: () => void; 15 + onPointerLeave: () => void; 16 + } 17 + 18 + export interface PopoverTriggerProps { 19 + /** render prop that receives trigger props to spread onto your element */ 20 + children: (props: PopoverTriggerChildProps) => JSX.Element; 21 + } 22 + 23 + // #endregion 24 + 25 + // #region component 26 + 27 + const PopoverTrigger = (props: PopoverTriggerProps) => { 28 + const ctx = usePopoverContext(); 29 + 30 + const handleClick = () => { 31 + if (!ctx.openOnHover()) { 32 + ctx.setOpen(!ctx.open(), 'click'); 33 + } 34 + }; 35 + 36 + const handlePointerEnter = () => { 37 + if (ctx.openOnHover()) { 38 + ctx.setOpen(true, 'pointerenter'); 39 + } 40 + }; 41 + 42 + const handlePointerLeave = () => { 43 + if (ctx.openOnHover()) { 44 + ctx.setOpen(false, 'pointerleave'); 45 + } 46 + }; 47 + 48 + const childProps: PopoverTriggerChildProps = { 49 + ref: (el: HTMLElement) => ctx.setTriggerRef(el), 50 + id: ctx.triggerId, 51 + 'aria-haspopup': 'dialog', 52 + get 'aria-expanded'() { 53 + return ctx.open(); 54 + }, 55 + get 'aria-controls'() { 56 + return ctx.open() ? ctx.surfaceId : undefined; 57 + }, 58 + onClick: handleClick, 59 + onPointerEnter: handlePointerEnter, 60 + onPointerLeave: handlePointerLeave, 61 + }; 62 + 63 + return <>{props.children(childProps)}</>; 64 + }; 65 + 66 + export default PopoverTrigger; 67 + 68 + // #endregion