UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

1 lines 5.94 kB
{"version":3,"file":"destroyOnInvalidActiveTriggerElement.mjs","names":[],"sources":["../../src/utils/destroyOnInvalidActiveTriggerElement.ts"],"sourcesContent":["import { useLayoutEffect } from 'react';\n\ntype PopupStoreLike = {\n // Base UI store's `useState` has a strongly-typed key union; we keep it loose here on purpose.\n state: {\n activeTriggerElement?: Element | null;\n open?: boolean;\n positionerElement?: HTMLElement | null;\n };\n useState?: (...args: any[]) => unknown;\n};\n\nconst isInvalidTriggerElement = (el: Element | null): boolean => {\n if (!el) return true;\n\n if (!el.isConnected) return true;\n\n // \"display: none\" on self or an ancestor effectively hides the trigger.\n // `getComputedStyle` can throw in some edge cases (e.g. non-Element in old envs),\n // so we guard it defensively.\n try {\n // Check self and all ancestors for display: none\n let current: Element | null = el;\n while (current) {\n if (getComputedStyle(current).display === 'none') {\n return true;\n }\n current = current.parentElement;\n }\n return false;\n } catch {\n return false;\n }\n};\n\n/**\n * Destroys (hard reset) a group popup (Tooltip/Popover) when its active trigger element becomes\n * disconnected from the DOM or is effectively hidden via `display: none`.\n *\n * We intentionally poll while open to also catch CSS-driven visibility changes that won't\n * necessarily trigger DOM mutation observers.\n */\nexport const useDestroyOnInvalidActiveTriggerElement = (\n store: PopupStoreLike,\n destroy: () => void,\n options?: {\n /**\n * @default true\n */\n enabled?: boolean;\n },\n) => {\n const enabled = options?.enabled ?? true;\n\n // Subscribe with `useState` (reactive), but read from `state` (immediate) inside the loop.\n // Base UI note: `state` updates immediately, while `useState` reflects updates before the next render.\n const openReactive =\n (store.useState?.('open') as boolean | undefined) ?? Boolean(store.state.open);\n const shouldWatch = enabled && openReactive;\n\n // Use layout effect so the first check runs right after React commits DOM updates.\n // Then keep watching via rAF while open to also capture CSS-driven visibility changes.\n useLayoutEffect(() => {\n if (!shouldWatch) return;\n\n let raf = 0;\n\n const loop = () => {\n if (isInvalidTriggerElement(store.state.activeTriggerElement ?? null)) {\n destroy();\n return;\n }\n raf = window.requestAnimationFrame(loop);\n };\n\n loop();\n return () => window.cancelAnimationFrame(raf);\n }, [destroy, shouldWatch, store]);\n};\n\n/**\n * UI-only fallback: If the positioner ends up at viewport (0,0), hide it to avoid a visible flash\n * in the corner. This doesn't replace \"destroy on invalid trigger\"; it's purely a visual guard.\n */\nexport const useHidePopupWhenPositionerAtOrigin = (\n store: PopupStoreLike,\n options?: {\n /**\n * @default true\n */\n enabled?: boolean;\n /**\n * Pixel threshold to consider the element \"at origin\".\n * @default 0.5\n */\n threshold?: number;\n },\n) => {\n const enabled = options?.enabled ?? true;\n const threshold = options?.threshold ?? 0.5;\n\n const openReactive =\n (store.useState?.('open') as boolean | undefined) ?? Boolean(store.state.open);\n const positionerElementReactive =\n (store.useState?.('positionerElement') as HTMLElement | null | undefined) ??\n store.state.positionerElement ??\n null;\n\n useLayoutEffect(() => {\n const positionerEl = store.state.positionerElement ?? positionerElementReactive;\n\n if (!enabled || !openReactive || !positionerEl) {\n if (positionerEl) delete positionerEl.dataset.zeroOrigin;\n return;\n }\n\n let raf = 0;\n const loop = () => {\n const current = store.state.positionerElement ?? positionerEl;\n if (!current) return;\n\n const rect = current.getBoundingClientRect();\n const atOrigin = Math.abs(rect.left) <= threshold && Math.abs(rect.top) <= threshold;\n if (atOrigin) current.dataset.zeroOrigin = 'true';\n else delete current.dataset.zeroOrigin;\n\n raf = window.requestAnimationFrame(loop);\n };\n\n loop();\n return () => {\n window.cancelAnimationFrame(raf);\n const current = store.state.positionerElement ?? positionerEl;\n if (current) delete current.dataset.zeroOrigin;\n };\n }, [enabled, openReactive, positionerElementReactive, store, threshold]);\n};\n"],"mappings":";;AAYA,MAAM,2BAA2B,OAAgC;CAC/D,IAAI,CAAC,IAAI,OAAO;CAEhB,IAAI,CAAC,GAAG,aAAa,OAAO;CAK5B,IAAI;EAEF,IAAI,UAA0B;EAC9B,OAAO,SAAS;GACd,IAAI,iBAAiB,OAAO,CAAC,CAAC,YAAY,QACxC,OAAO;GAET,UAAU,QAAQ;EACpB;EACA,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;AASA,MAAa,2CACX,OACA,SACA,YAMG;CACH,MAAM,UAAU,SAAS,WAAW;CAIpC,MAAM,eACH,MAAM,WAAW,MAAM,KAA6B,QAAQ,MAAM,MAAM,IAAI;CAC/E,MAAM,cAAc,WAAW;CAI/B,sBAAsB;EACpB,IAAI,CAAC,aAAa;EAElB,IAAI,MAAM;EAEV,MAAM,aAAa;GACjB,IAAI,wBAAwB,MAAM,MAAM,wBAAwB,IAAI,GAAG;IACrE,QAAQ;IACR;GACF;GACA,MAAM,OAAO,sBAAsB,IAAI;EACzC;EAEA,KAAK;EACL,aAAa,OAAO,qBAAqB,GAAG;CAC9C,GAAG;EAAC;EAAS;EAAa;CAAK,CAAC;AAClC;;;;;AAMA,MAAa,sCACX,OACA,YAWG;CACH,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,YAAY,SAAS,aAAa;CAExC,MAAM,eACH,MAAM,WAAW,MAAM,KAA6B,QAAQ,MAAM,MAAM,IAAI;CAC/E,MAAM,4BACH,MAAM,WAAW,mBAAmB,KACrC,MAAM,MAAM,qBACZ;CAEF,sBAAsB;EACpB,MAAM,eAAe,MAAM,MAAM,qBAAqB;EAEtD,IAAI,CAAC,WAAW,CAAC,gBAAgB,CAAC,cAAc;GAC9C,IAAI,cAAc,OAAO,aAAa,QAAQ;GAC9C;EACF;EAEA,IAAI,MAAM;EACV,MAAM,aAAa;GACjB,MAAM,UAAU,MAAM,MAAM,qBAAqB;GACjD,IAAI,CAAC,SAAS;GAEd,MAAM,OAAO,QAAQ,sBAAsB;GAE3C,IADiB,KAAK,IAAI,KAAK,IAAI,KAAK,aAAa,KAAK,IAAI,KAAK,GAAG,KAAK,WAC7D,QAAQ,QAAQ,aAAa;QACtC,OAAO,QAAQ,QAAQ;GAE5B,MAAM,OAAO,sBAAsB,IAAI;EACzC;EAEA,KAAK;EACL,aAAa;GACX,OAAO,qBAAqB,GAAG;GAC/B,MAAM,UAAU,MAAM,MAAM,qBAAqB;GACjD,IAAI,SAAS,OAAO,QAAQ,QAAQ;EACtC;CACF,GAAG;EAAC;EAAS;EAAc;EAA2B;EAAO;CAAS,CAAC;AACzE"}