@bsuite/ui 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * useOnClickOutside
3
+ *
4
+ * Dismisses an open dropdown/menu/popover when the user clicks (or taps)
5
+ * outside of its container, or presses Escape.
6
+ *
7
+ * Why not a full-screen backdrop `<div>`? A `fixed inset-0` backdrop only
8
+ * reliably covers the whole viewport when none of its ancestors establish a
9
+ * new containing block for fixed-position descendants. `backdrop-filter`,
10
+ * `filter`, `transform`, and `will-change: transform` on an ancestor all do
11
+ * exactly that — and BSuite's glass-panel header chrome
12
+ * (`backdropFilter: blur(...)`) is one of them. A backdrop nested inside such
13
+ * an ancestor silently shrinks to that ancestor's box instead of the full
14
+ * page, so clicks on the rest of the page never reach it and the menu never
15
+ * closes. This is what broke click-outside dismissal for crm7's header
16
+ * tenant switchers (operator report, 2026-07-17) — see crm7's
17
+ * `TenantSwitcher.tsx` / `UserTenantSwitcher.tsx` / `CRM7Header.tsx`, the
18
+ * hook this one is promoted from.
19
+ *
20
+ * A `document`-level pointerdown listener sidesteps the problem entirely:
21
+ * JS event listeners are not affected by CSS stacking contexts or
22
+ * containing-block quirks, so this works regardless of what ancestors do.
23
+ *
24
+ * This is the canonical, shared version — promoted here so every consumer
25
+ * app (and every `@bsuite/*` package that depends on `@bsuite/ui`) gets one
26
+ * implementation instead of a fresh hand-rolled copy per app. crm7 carries
27
+ * its own pre-existing copy at `src/hooks/useClickOutside.ts`; that one is
28
+ * already correct and untouched by this promotion — a future consolidation
29
+ * onto this shared copy is a reasonable low-risk follow-up, not a bug fix.
30
+ *
31
+ * Usage:
32
+ * const containerRef = useRef<HTMLDivElement>(null);
33
+ * useOnClickOutside(containerRef, () => setIsOpen(false), isOpen);
34
+ *
35
+ * return (
36
+ * <div ref={containerRef} className="relative">
37
+ * <button onClick={() => setIsOpen((o) => !o)}>...</button>
38
+ * {isOpen && <div className="absolute ...">...menu...</div>}
39
+ * </div>
40
+ * );
41
+ */
42
+ import { type RefObject } from 'react';
43
+ export declare function useOnClickOutside<T extends HTMLElement>(ref: RefObject<T | null>, handler: () => void, enabled?: boolean): void;
44
+ //# sourceMappingURL=useOnClickOutside.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOnClickOutside.d.ts","sourceRoot":"","sources":["../../src/hooks/useOnClickOutside.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,WAAW,EACrD,GAAG,EAAE,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,EACxB,OAAO,EAAE,MAAM,IAAI,EACnB,OAAO,GAAE,OAAc,GACtB,IAAI,CAiCN"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * useOnClickOutside
3
+ *
4
+ * Dismisses an open dropdown/menu/popover when the user clicks (or taps)
5
+ * outside of its container, or presses Escape.
6
+ *
7
+ * Why not a full-screen backdrop `<div>`? A `fixed inset-0` backdrop only
8
+ * reliably covers the whole viewport when none of its ancestors establish a
9
+ * new containing block for fixed-position descendants. `backdrop-filter`,
10
+ * `filter`, `transform`, and `will-change: transform` on an ancestor all do
11
+ * exactly that — and BSuite's glass-panel header chrome
12
+ * (`backdropFilter: blur(...)`) is one of them. A backdrop nested inside such
13
+ * an ancestor silently shrinks to that ancestor's box instead of the full
14
+ * page, so clicks on the rest of the page never reach it and the menu never
15
+ * closes. This is what broke click-outside dismissal for crm7's header
16
+ * tenant switchers (operator report, 2026-07-17) — see crm7's
17
+ * `TenantSwitcher.tsx` / `UserTenantSwitcher.tsx` / `CRM7Header.tsx`, the
18
+ * hook this one is promoted from.
19
+ *
20
+ * A `document`-level pointerdown listener sidesteps the problem entirely:
21
+ * JS event listeners are not affected by CSS stacking contexts or
22
+ * containing-block quirks, so this works regardless of what ancestors do.
23
+ *
24
+ * This is the canonical, shared version — promoted here so every consumer
25
+ * app (and every `@bsuite/*` package that depends on `@bsuite/ui`) gets one
26
+ * implementation instead of a fresh hand-rolled copy per app. crm7 carries
27
+ * its own pre-existing copy at `src/hooks/useClickOutside.ts`; that one is
28
+ * already correct and untouched by this promotion — a future consolidation
29
+ * onto this shared copy is a reasonable low-risk follow-up, not a bug fix.
30
+ *
31
+ * Usage:
32
+ * const containerRef = useRef<HTMLDivElement>(null);
33
+ * useOnClickOutside(containerRef, () => setIsOpen(false), isOpen);
34
+ *
35
+ * return (
36
+ * <div ref={containerRef} className="relative">
37
+ * <button onClick={() => setIsOpen((o) => !o)}>...</button>
38
+ * {isOpen && <div className="absolute ...">...menu...</div>}
39
+ * </div>
40
+ * );
41
+ */
42
+ import { useEffect } from 'react';
43
+ export function useOnClickOutside(ref, handler, enabled = true) {
44
+ useEffect(() => {
45
+ // Only attach listeners while the menu is actually open — avoids
46
+ // unnecessary global listeners on every page.
47
+ if (!enabled)
48
+ return;
49
+ const handlePointerDown = (event) => {
50
+ const container = ref.current;
51
+ if (!container)
52
+ return;
53
+ if (event.target instanceof Node && container.contains(event.target)) {
54
+ // Click landed inside the container (trigger button or menu body) —
55
+ // let the component's own click handlers deal with it.
56
+ return;
57
+ }
58
+ handler();
59
+ };
60
+ const handleKeyDown = (event) => {
61
+ if (event.key === 'Escape')
62
+ handler();
63
+ };
64
+ // `pointerdown` fires before `click`, so the menu is already closed by
65
+ // the time a click on an element behind it fires — clicking "through"
66
+ // to another control outside the menu both closes the menu AND performs
67
+ // that control's own action, matching standard menu/popover UX.
68
+ document.addEventListener('pointerdown', handlePointerDown);
69
+ document.addEventListener('keydown', handleKeyDown);
70
+ return () => {
71
+ document.removeEventListener('pointerdown', handlePointerDown);
72
+ document.removeEventListener('keydown', handleKeyDown);
73
+ };
74
+ }, [ref, handler, enabled]);
75
+ }
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { LoadingSpinner, type LoadingSpinnerProps } from './loading-spinner.js';
13
13
  export { StatusBadge, type StatusBadgeProps, type StatusBadgeTone } from './status-badge.js';
14
14
  export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, } from './command.js';
15
15
  export { Popover, PopoverTrigger, PopoverContent } from './popover.js';
16
+ export { useOnClickOutside } from './hooks/useOnClickOutside.js';
16
17
  export { EntitySelector, type EntitySelectorProps, type EntitySelectorQuery, type EntitySelectorLogger, type EntitySelectorSupabaseClient, } from './entity-selector.js';
17
18
  export { BrandingCard, ColorEditorSheet, OklchColorPicker, type BrandingCardProps, type ColorEditorSheetProps, type OklchColorPickerProps, } from './branding/index.js';
18
19
  export { sanitizeCustomCss } from './branding/sanitizeCustomCss.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAA;AAC/B,OAAO,EACL,IAAI,EACJ,cAAc,EACd,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,OAAO,GACb,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAC5E,OAAO,EACL,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,MAAM,EACN,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,SAAS,GACf,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAC5E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAC5F,OAAO,EACL,OAAO,EACP,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,eAAe,GAChB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACtE,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,4BAA4B,GAClC,MAAM,sBAAsB,CAAA;AAG7B,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAA;AAC/B,OAAO,EACL,IAAI,EACJ,cAAc,EACd,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,OAAO,GACb,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAC5E,OAAO,EACL,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,MAAM,EACN,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,SAAS,GACf,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAC5E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAC5F,OAAO,EACL,OAAO,EACP,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,eAAe,GAChB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAChE,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,4BAA4B,GAClC,MAAM,sBAAsB,CAAA;AAG7B,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA"}
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export { LoadingSpinner } from './loading-spinner.js';
13
13
  export { StatusBadge } from './status-badge.js';
14
14
  export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, } from './command.js';
15
15
  export { Popover, PopoverTrigger, PopoverContent } from './popover.js';
16
+ export { useOnClickOutside } from './hooks/useOnClickOutside.js';
16
17
  export { EntitySelector, } from './entity-selector.js';
17
18
  // Branding components (v0.3.0+)
18
19
  export { BrandingCard, ColorEditorSheet, OklchColorPicker, } from './branding/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/ui",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -79,6 +79,10 @@
79
79
  "import": "./dist/entity-selector.js",
80
80
  "types": "./dist/entity-selector.d.ts"
81
81
  },
82
+ "./use-on-click-outside": {
83
+ "import": "./dist/hooks/useOnClickOutside.js",
84
+ "types": "./dist/hooks/useOnClickOutside.d.ts"
85
+ },
82
86
  "./assets/d2c-default-logo.svg": "./src/assets/d2c-default-logo.svg"
83
87
  },
84
88
  "scripts": {