@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
1 lines • 5.91 kB
Source Map (JSON)
{"version":3,"file":"destroyOnInvalidActiveTriggerElement.mjs","names":["current: Element | null"],"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;AAC/D,KAAI,CAAC,GAAI,QAAO;AAEhB,KAAI,CAAC,GAAG,YAAa,QAAO;AAK5B,KAAI;EAEF,IAAIA,UAA0B;AAC9B,SAAO,SAAS;AACd,OAAI,iBAAiB,QAAQ,CAAC,YAAY,OACxC,QAAO;AAET,aAAU,QAAQ;;AAEpB,SAAO;SACD;AACN,SAAO;;;;;;;;;;AAWX,MAAa,2CACX,OACA,SACA,YAMG;CACH,MAAM,UAAU,SAAS,WAAW;CAIpC,MAAM,eACH,MAAM,WAAW,OAAO,IAA4B,QAAQ,MAAM,MAAM,KAAK;CAChF,MAAM,cAAc,WAAW;AAI/B,uBAAsB;AACpB,MAAI,CAAC,YAAa;EAElB,IAAI,MAAM;EAEV,MAAM,aAAa;AACjB,OAAI,wBAAwB,MAAM,MAAM,wBAAwB,KAAK,EAAE;AACrE,aAAS;AACT;;AAEF,SAAM,OAAO,sBAAsB,KAAK;;AAG1C,QAAM;AACN,eAAa,OAAO,qBAAqB,IAAI;IAC5C;EAAC;EAAS;EAAa;EAAM,CAAC;;;;;;AAOnC,MAAa,sCACX,OACA,YAWG;CACH,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,YAAY,SAAS,aAAa;CAExC,MAAM,eACH,MAAM,WAAW,OAAO,IAA4B,QAAQ,MAAM,MAAM,KAAK;CAChF,MAAM,4BACH,MAAM,WAAW,oBAAoB,IACtC,MAAM,MAAM,qBACZ;AAEF,uBAAsB;EACpB,MAAM,eAAe,MAAM,MAAM,qBAAqB;AAEtD,MAAI,CAAC,WAAW,CAAC,gBAAgB,CAAC,cAAc;AAC9C,OAAI,aAAc,QAAO,aAAa,QAAQ;AAC9C;;EAGF,IAAI,MAAM;EACV,MAAM,aAAa;GACjB,MAAM,UAAU,MAAM,MAAM,qBAAqB;AACjD,OAAI,CAAC,QAAS;GAEd,MAAM,OAAO,QAAQ,uBAAuB;AAE5C,OADiB,KAAK,IAAI,KAAK,KAAK,IAAI,aAAa,KAAK,IAAI,KAAK,IAAI,IAAI,UAC7D,SAAQ,QAAQ,aAAa;OACtC,QAAO,QAAQ,QAAQ;AAE5B,SAAM,OAAO,sBAAsB,KAAK;;AAG1C,QAAM;AACN,eAAa;AACX,UAAO,qBAAqB,IAAI;GAChC,MAAM,UAAU,MAAM,MAAM,qBAAqB;AACjD,OAAI,QAAS,QAAO,QAAQ,QAAQ;;IAErC;EAAC;EAAS;EAAc;EAA2B;EAAO;EAAU,CAAC"}