@rozie-ui/data-table-solid 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Idiomatic **solid** `DataTable` — a headless, fully-accessible (WAI-ARIA) data
8
8
  npm i @rozie-ui/data-table-solid
9
9
  ```
10
10
 
11
- Peer dependencies: `solid-js + @tanstack/table-core + @tanstack/virtual-core + @floating-ui/dom`. Install them alongside this package.
11
+ Peer dependencies: `solid-js + @tanstack/table-core + @tanstack/virtual-core + @rozie-ui/popover-solid`. Install them alongside this package.
12
12
 
13
13
  ## Usage
14
14
 
package/dist/index.cjs CHANGED
@@ -2,286 +2,35 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
+ //#region \0rolldown/runtime.js
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
14
+ key = keys[i];
15
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
16
+ get: ((k) => from[k]).bind(null, key),
17
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18
+ });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+ //#endregion
5
27
  let Solid_js = require("solid-js");
6
28
  let _solid_primitives_keyed = require("@solid-primitives/keyed");
7
29
  let _rozie_runtime_solid = require("@rozie/runtime-solid");
8
- let _floating_ui_dom = require("@floating-ui/dom");
30
+ let _rozie_ui_popover_solid = require("@rozie-ui/popover-solid");
31
+ _rozie_ui_popover_solid = __toESM(_rozie_ui_popover_solid);
9
32
  let _tanstack_table_core = require("@tanstack/table-core");
10
33
  let _tanstack_virtual_core = require("@tanstack/virtual-core");
11
- //#region src/internal/middleware.ts
12
- /**
13
- * Build the ordered middleware array for `computePosition`. Pure — no engine
14
- * import, no DOM read beyond the passed-in arrow element.
15
- */
16
- function buildMiddleware(factories, config) {
17
- const mw = [factories.offset(config.offset)];
18
- if (!config.disableFlip) mw.push(factories.flip());
19
- if (!config.disableShift) mw.push(factories.shift());
20
- if (config.arrow && config.arrowEl) mw.push(factories.arrow({ element: config.arrowEl }));
21
- return mw;
22
- }
23
- //#endregion
24
- //#region src/Popover.tsx
25
- (0, _rozie_runtime_solid.__rozieInjectStyle)("Popover-c6cf02ea", `.rozie-popover[data-rozie-s-c6cf02ea] {
26
- display: contents;
27
- }
28
- .rozie-popover-anchor[data-rozie-s-c6cf02ea] {
29
- display: inline-block;
30
- }
31
- .rozie-popover-floating[data-rozie-s-c6cf02ea] {
32
- position: absolute;
33
- left: 0;
34
- top: 0;
35
- z-index: var(--rozie-popover-z, 1000);
36
- width: max-content;
37
- max-width: var(--rozie-popover-max-width, calc(100vw - 16px));
38
- background: var(--rozie-popover-bg, #fff);
39
- color: var(--rozie-popover-color, inherit);
40
- border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
41
- border-radius: var(--rozie-popover-radius, 8px);
42
- box-shadow: var(--rozie-popover-shadow, 0 8px 24px rgba(0, 0, 0, 0.12));
43
- padding: var(--rozie-popover-padding, 8px 12px);
44
- }
45
- .rozie-popover-arrow[data-rozie-s-c6cf02ea] {
46
- position: absolute;
47
- width: var(--rozie-popover-arrow-size, 8px);
48
- height: var(--rozie-popover-arrow-size, 8px);
49
- background: var(--rozie-popover-bg, #fff);
50
- border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
51
- transform: rotate(45deg);
52
- }`);
53
- function Popover(_props) {
54
- const [local, attrs] = (0, Solid_js.splitProps)((0, Solid_js.mergeProps)({
55
- placement: "bottom",
56
- trigger: "click",
57
- offset: 8,
58
- disableFlip: false,
59
- disableShift: false,
60
- arrow: false,
61
- disabled: false,
62
- modal: false,
63
- strategy: "absolute"
64
- }, _props), [
65
- "open",
66
- "placement",
67
- "trigger",
68
- "offset",
69
- "disableFlip",
70
- "disableShift",
71
- "arrow",
72
- "disabled",
73
- "modal",
74
- "strategy",
75
- "children",
76
- "ref"
77
- ]);
78
- const resolved = (0, Solid_js.children)(() => local.children);
79
- (0, Solid_js.onMount)(() => {
80
- local.ref?.({
81
- show,
82
- hide,
83
- toggle,
84
- reposition
85
- });
86
- });
87
- const [open, setOpen] = (0, _rozie_runtime_solid.createControllableSignal)(_props, "open", false);
88
- (0, Solid_js.onMount)(() => {
89
- const _cleanup = (() => {
90
- anchorNode = anchorElRef;
91
- if (open() && !local.disabled) {
92
- floatingNode = floatingElRef;
93
- arrowNode = arrowElRef;
94
- startTracking();
95
- }
96
- })();
97
- if (_cleanup) (0, Solid_js.onCleanup)(_cleanup);
98
- (0, Solid_js.onCleanup)(() => {
99
- stopTracking();
100
- });
101
- });
102
- (0, Solid_js.createEffect)((0, Solid_js.on)(() => open(), (v) => (0, Solid_js.untrack)(() => ((isOpen) => {
103
- if (isOpen && !local.disabled) queueMicrotask(() => {
104
- if (!open() || local.disabled) return;
105
- floatingNode = floatingElRef;
106
- arrowNode = arrowElRef;
107
- startTracking();
108
- });
109
- else stopTracking();
110
- })(v)), { defer: true }));
111
- (0, Solid_js.createEffect)((0, Solid_js.on)(() => local.placement, (v) => (0, Solid_js.untrack)(() => (() => {
112
- if (open()) position();
113
- })()), { defer: true }));
114
- (0, Solid_js.createEffect)((0, Solid_js.on)(() => local.offset, (v) => (0, Solid_js.untrack)(() => (() => {
115
- if (open()) position();
116
- })()), { defer: true }));
117
- (0, Solid_js.createEffect)((0, Solid_js.on)(() => local.disableFlip, (v) => (0, Solid_js.untrack)(() => (() => {
118
- if (open()) position();
119
- })()), { defer: true }));
120
- (0, Solid_js.createEffect)((0, Solid_js.on)(() => local.disableShift, (v) => (0, Solid_js.untrack)(() => (() => {
121
- if (open()) position();
122
- })()), { defer: true }));
123
- (0, Solid_js.createEffect)((0, Solid_js.on)(() => local.strategy, (v) => (0, Solid_js.untrack)(() => (() => {
124
- if (open()) position();
125
- })()), { defer: true }));
126
- let anchorElRef = null;
127
- let floatingElRef = null;
128
- let arrowElRef = null;
129
- let anchorNode = null;
130
- let floatingNode = null;
131
- let arrowNode = null;
132
- let stopAutoUpdate = null;
133
- let lastFocusedEl = null;
134
- function deepActiveElement() {
135
- let el = document.activeElement;
136
- while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
137
- return el;
138
- }
139
- function requestOpen(next) {
140
- if (open() === next) return;
141
- if (next && local.trigger === "click") lastFocusedEl = deepActiveElement();
142
- setOpen(next);
143
- _props.onChange?.(next);
144
- if (!next && local.trigger === "click" && lastFocusedEl && lastFocusedEl.isConnected && typeof lastFocusedEl.focus === "function") lastFocusedEl.focus();
145
- if (!next) lastFocusedEl = null;
146
- }
147
- function applyPosition(x, y, middlewareData) {
148
- if (!floatingNode) return;
149
- floatingNode.style.left = x + "px";
150
- floatingNode.style.top = y + "px";
151
- if (arrowNode && middlewareData && middlewareData.arrow) {
152
- const ax = middlewareData.arrow.x;
153
- const ay = middlewareData.arrow.y;
154
- arrowNode.style.left = ax == null ? "" : ax + "px";
155
- arrowNode.style.top = ay == null ? "" : ay + "px";
156
- }
157
- }
158
- function position() {
159
- if (!anchorNode || !floatingNode) return;
160
- const middleware = buildMiddleware({
161
- offset: _floating_ui_dom.offset,
162
- flip: _floating_ui_dom.flip,
163
- shift: _floating_ui_dom.shift,
164
- arrow: _floating_ui_dom.arrow
165
- }, {
166
- offset: local.offset,
167
- disableFlip: local.disableFlip,
168
- disableShift: local.disableShift,
169
- arrow: local.arrow,
170
- arrowEl: arrowNode
171
- });
172
- if (local.strategy === "fixed") floatingNode.style.position = "fixed";
173
- else floatingNode.style.position = "";
174
- let opts = null;
175
- opts = {
176
- placement: local.placement,
177
- strategy: local.strategy,
178
- middleware
179
- };
180
- (0, _floating_ui_dom.computePosition)(anchorNode, floatingNode, opts).then((result) => {
181
- applyPosition(result.x, result.y, result.middlewareData);
182
- });
183
- }
184
- function startTracking() {
185
- if (!anchorNode || !floatingNode) return;
186
- if (stopAutoUpdate) {
187
- stopAutoUpdate();
188
- stopAutoUpdate = null;
189
- }
190
- stopAutoUpdate = (0, _floating_ui_dom.autoUpdate)(anchorNode, floatingNode, position);
191
- }
192
- function stopTracking() {
193
- if (stopAutoUpdate) {
194
- stopAutoUpdate();
195
- stopAutoUpdate = null;
196
- }
197
- }
198
- function onAnchorClick() {
199
- if (local.disabled) return;
200
- requestOpen(!open());
201
- }
202
- function onAnchorPointerEnter() {
203
- if (local.disabled) return;
204
- requestOpen(true);
205
- }
206
- function onAnchorPointerLeave() {
207
- if (local.disabled) return;
208
- requestOpen(false);
209
- }
210
- function onAnchorFocus() {
211
- if (local.disabled) return;
212
- requestOpen(true);
213
- }
214
- function onAnchorBlur() {
215
- if (local.disabled) return;
216
- requestOpen(false);
217
- }
218
- function dismiss() {
219
- requestOpen(false);
220
- }
221
- function isTooltip() {
222
- return local.trigger === "hover" || local.trigger === "focus";
223
- }
224
- function floatingRole() {
225
- return isTooltip() ? "tooltip" : local.modal ? "dialog" : void 0;
226
- }
227
- function show() {
228
- if (!local.disabled) requestOpen(true);
229
- }
230
- function hide() {
231
- requestOpen(false);
232
- }
233
- function toggle() {
234
- if (!local.disabled) requestOpen(!open());
235
- }
236
- function reposition() {
237
- position();
238
- }
239
- (0, Solid_js.createEffect)(() => {
240
- if (!open()) return;
241
- const _rozieHandler = ($event) => {
242
- if ($event.key !== "Escape") return;
243
- dismiss();
244
- };
245
- document.addEventListener("keydown", _rozieHandler);
246
- (0, Solid_js.onCleanup)(() => document.removeEventListener("keydown", _rozieHandler));
247
- });
248
- (0, _rozie_runtime_solid.createOutsideClick)([() => anchorElRef, () => floatingElRef], dismiss, () => open());
249
- return <>
250
- <div {...attrs} class={"rozie-popover" + (attrs.class ? " " + attrs.class : "")} data-rozie-s-c6cf02ea="">
251
-
252
-
253
- <div aria-haspopup="dialog" aria-expanded={!!open()} aria-describedby={(0, _rozie_runtime_solid.rozieAttr)(isTooltip() && open() ? "rozie-popover-floating" : null)} class={"rozie-popover-anchor"} ref={(el) => {
254
- anchorElRef = el;
255
- }} onClick={($event) => {
256
- local.trigger === "click" && onAnchorClick();
257
- }} onPointerEnter={($event) => {
258
- local.trigger === "hover" && onAnchorPointerEnter();
259
- }} onPointerLeave={($event) => {
260
- local.trigger === "hover" && onAnchorPointerLeave();
261
- }} onFocusIn={($event) => {
262
- local.trigger === "focus" && onAnchorFocus();
263
- }} onFocusOut={($event) => {
264
- local.trigger === "focus" && onAnchorBlur();
265
- }} data-rozie-s-c6cf02ea="">
266
- {(_props.anchorSlot ?? _props.slots?.["anchor"])?.({
267
- open: open(),
268
- toggle,
269
- show,
270
- hide
271
- })}
272
- </div>
273
-
274
-
275
- {<Solid_js.Show when={open() && !local.disabled}><div class={"rozie-popover-floating"} ref={(el) => {
276
- floatingElRef = el;
277
- }} id="rozie-popover-floating" role={(0, _rozie_runtime_solid.rozieAttr)(floatingRole())} aria-modal={!!(floatingRole() === "dialog")} data-rozie-s-c6cf02ea="">
278
- {<Solid_js.Show when={local.arrow}><div class={"rozie-popover-arrow"} ref={(el) => {
279
- arrowElRef = el;
280
- }} data-rozie-s-c6cf02ea="" /></Solid_js.Show>}{resolved()}
281
- </div></Solid_js.Show>}</div>
282
- </>;
283
- }
284
- //#endregion
285
34
  //#region src/DataTable.tsx
286
35
  (0, _rozie_runtime_solid.__rozieInjectStyle)("DataTable-d5dcab4c", `.rozie-data-table[data-rozie-s-d5dcab4c] {
287
36
  border-collapse: collapse;
@@ -1723,8 +1472,7 @@ function DataTable(_props) {
1723
1472
  const base = pinStyle(colId);
1724
1473
  if (isExpanderColumn(colId) && row && row.depth) {
1725
1474
  const pad = "padding-left:" + (.5 + row.depth * 1.25) + "rem";
1726
- const sep = base.endsWith(";") ? "" : ";";
1727
- return base ? base + sep + pad : pad;
1475
+ return base ? base + pad : pad;
1728
1476
  }
1729
1477
  return base;
1730
1478
  }
@@ -3958,7 +3706,7 @@ function DataTable(_props) {
3958
3706
  }) ?? (0, _rozie_runtime_solid.rozieDisplay)(headerLabel(header().column.id))}
3959
3707
  </span>
3960
3708
  <span class={"rdt-sort-ind"} aria-hidden="true" data-rozie-s-d5dcab4c="">{(0, _rozie_runtime_solid.rozieDisplay)(sortIndicator(header().column.id))}</span>
3961
- </button></Solid_js.Show>}<Popover trigger="click" placement="bottom-end" strategy="fixed" offset={4} data-rozie-s-d5dcab4c="" anchorSlot={() => <>
3709
+ </button></Solid_js.Show>}<_rozie_ui_popover_solid.default trigger="click" placement="bottom-end" strategy="fixed" offset={4} data-rozie-s-d5dcab4c="" anchorSlot={() => <>
3962
3710
  <button type="button" class={"rdt-col-menu-trigger"} aria-label={(0, _rozie_runtime_solid.rozieAttr)("Column options for " + headerLabel(header().column.id))} data-rozie-s-d5dcab4c="">⋯</button>
3963
3711
  </>}><div class={"rdt-col-menu"} role="menu" data-rozie-s-d5dcab4c="">
3964
3712
  <button type="button" role="menuitem" aria-pressed={columnPinSide(header().column.id) === "left"} class={"rdt-col-menu-item"} onClick={($event) => {
@@ -3974,7 +3722,7 @@ function DataTable(_props) {
3974
3722
  <button type="button" role="menuitem" class={"rdt-col-menu-item"} onClick={($event) => {
3975
3723
  onHideColumn(header().column.id, $event);
3976
3724
  }} data-rozie-s-d5dcab4c="">Hide column</button>
3977
- </div></Popover>
3725
+ </div></_rozie_ui_popover_solid.default>
3978
3726
 
3979
3727
  <button type="button" aria-label={(0, _rozie_runtime_solid.rozieAttr)("Resize " + headerLabel(header().column.id))} class={"rdt-resize-handle"} onPointerDown={($event) => {
3980
3728
  onResizeStart(header().column.id, $event);
@@ -4142,7 +3890,7 @@ function DataTable(_props) {
4142
3890
  }) ?? (0, _rozie_runtime_solid.rozieDisplay)(headerLabel(header().column.id))}
4143
3891
  </span>
4144
3892
  <span class={"rdt-sort-ind"} aria-hidden="true" data-rozie-s-d5dcab4c="">{(0, _rozie_runtime_solid.rozieDisplay)(sortIndicator(header().column.id))}</span>
4145
- </button></Solid_js.Show>}<Popover trigger="click" placement="bottom-end" strategy="fixed" offset={4} data-rozie-s-d5dcab4c="" anchorSlot={() => <>
3893
+ </button></Solid_js.Show>}<_rozie_ui_popover_solid.default trigger="click" placement="bottom-end" strategy="fixed" offset={4} data-rozie-s-d5dcab4c="" anchorSlot={() => <>
4146
3894
  <button type="button" class={"rdt-col-menu-trigger"} aria-label={(0, _rozie_runtime_solid.rozieAttr)("Column options for " + headerLabel(header().column.id))} data-rozie-s-d5dcab4c="">⋯</button>
4147
3895
  </>}><div class={"rdt-col-menu"} role="menu" data-rozie-s-d5dcab4c="">
4148
3896
  <button type="button" role="menuitem" aria-pressed={columnPinSide(header().column.id) === "left"} class={"rdt-col-menu-item"} onClick={($event) => {
@@ -4158,7 +3906,7 @@ function DataTable(_props) {
4158
3906
  <button type="button" role="menuitem" class={"rdt-col-menu-item"} onClick={($event) => {
4159
3907
  onHideColumn(header().column.id, $event);
4160
3908
  }} data-rozie-s-d5dcab4c="">Hide column</button>
4161
- </div></Popover>
3909
+ </div></_rozie_ui_popover_solid.default>
4162
3910
  <button type="button" aria-label={(0, _rozie_runtime_solid.rozieAttr)("Resize " + headerLabel(header().column.id))} class={"rdt-resize-handle"} onPointerDown={($event) => {
4163
3911
  onResizeStart(header().column.id, $event);
4164
3912
  }} onTouchStart={($event) => {
package/dist/index.mjs CHANGED
@@ -1,283 +1,9 @@
1
- import { Show, children, createEffect, createSignal, mergeProps, on, onCleanup, onMount, splitProps, untrack, useContext } from "solid-js";
1
+ import { Show, createEffect, createSignal, mergeProps, on, onCleanup, onMount, splitProps, untrack, useContext } from "solid-js";
2
2
  import { Key } from "@solid-primitives/keyed";
3
- import { __rozieInjectStyle, createControllableSignal, createOutsideClick, parseInlineStyle, rozieAttr, rozieClass, rozieContext, rozieDisplay } from "@rozie/runtime-solid";
4
- import { arrow, autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
3
+ import { __rozieInjectStyle, createControllableSignal, parseInlineStyle, rozieAttr, rozieClass, rozieContext, rozieDisplay } from "@rozie/runtime-solid";
4
+ import Popover from "@rozie-ui/popover-solid";
5
5
  import { createTable, getCoreRowModel, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getGroupedRowModel, getPaginationRowModel, getSortedRowModel } from "@tanstack/table-core";
6
6
  import { Virtualizer, elementScroll, measureElement, observeElementOffset, observeElementRect } from "@tanstack/virtual-core";
7
- //#region src/internal/middleware.ts
8
- /**
9
- * Build the ordered middleware array for `computePosition`. Pure — no engine
10
- * import, no DOM read beyond the passed-in arrow element.
11
- */
12
- function buildMiddleware(factories, config) {
13
- const mw = [factories.offset(config.offset)];
14
- if (!config.disableFlip) mw.push(factories.flip());
15
- if (!config.disableShift) mw.push(factories.shift());
16
- if (config.arrow && config.arrowEl) mw.push(factories.arrow({ element: config.arrowEl }));
17
- return mw;
18
- }
19
- //#endregion
20
- //#region src/Popover.tsx
21
- __rozieInjectStyle("Popover-c6cf02ea", `.rozie-popover[data-rozie-s-c6cf02ea] {
22
- display: contents;
23
- }
24
- .rozie-popover-anchor[data-rozie-s-c6cf02ea] {
25
- display: inline-block;
26
- }
27
- .rozie-popover-floating[data-rozie-s-c6cf02ea] {
28
- position: absolute;
29
- left: 0;
30
- top: 0;
31
- z-index: var(--rozie-popover-z, 1000);
32
- width: max-content;
33
- max-width: var(--rozie-popover-max-width, calc(100vw - 16px));
34
- background: var(--rozie-popover-bg, #fff);
35
- color: var(--rozie-popover-color, inherit);
36
- border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
37
- border-radius: var(--rozie-popover-radius, 8px);
38
- box-shadow: var(--rozie-popover-shadow, 0 8px 24px rgba(0, 0, 0, 0.12));
39
- padding: var(--rozie-popover-padding, 8px 12px);
40
- }
41
- .rozie-popover-arrow[data-rozie-s-c6cf02ea] {
42
- position: absolute;
43
- width: var(--rozie-popover-arrow-size, 8px);
44
- height: var(--rozie-popover-arrow-size, 8px);
45
- background: var(--rozie-popover-bg, #fff);
46
- border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
47
- transform: rotate(45deg);
48
- }`);
49
- function Popover(_props) {
50
- const [local, attrs] = splitProps(mergeProps({
51
- placement: "bottom",
52
- trigger: "click",
53
- offset: 8,
54
- disableFlip: false,
55
- disableShift: false,
56
- arrow: false,
57
- disabled: false,
58
- modal: false,
59
- strategy: "absolute"
60
- }, _props), [
61
- "open",
62
- "placement",
63
- "trigger",
64
- "offset",
65
- "disableFlip",
66
- "disableShift",
67
- "arrow",
68
- "disabled",
69
- "modal",
70
- "strategy",
71
- "children",
72
- "ref"
73
- ]);
74
- const resolved = children(() => local.children);
75
- onMount(() => {
76
- local.ref?.({
77
- show,
78
- hide,
79
- toggle,
80
- reposition
81
- });
82
- });
83
- const [open, setOpen] = createControllableSignal(_props, "open", false);
84
- onMount(() => {
85
- const _cleanup = (() => {
86
- anchorNode = anchorElRef;
87
- if (open() && !local.disabled) {
88
- floatingNode = floatingElRef;
89
- arrowNode = arrowElRef;
90
- startTracking();
91
- }
92
- })();
93
- if (_cleanup) onCleanup(_cleanup);
94
- onCleanup(() => {
95
- stopTracking();
96
- });
97
- });
98
- createEffect(on(() => open(), (v) => untrack(() => ((isOpen) => {
99
- if (isOpen && !local.disabled) queueMicrotask(() => {
100
- if (!open() || local.disabled) return;
101
- floatingNode = floatingElRef;
102
- arrowNode = arrowElRef;
103
- startTracking();
104
- });
105
- else stopTracking();
106
- })(v)), { defer: true }));
107
- createEffect(on(() => local.placement, (v) => untrack(() => (() => {
108
- if (open()) position();
109
- })()), { defer: true }));
110
- createEffect(on(() => local.offset, (v) => untrack(() => (() => {
111
- if (open()) position();
112
- })()), { defer: true }));
113
- createEffect(on(() => local.disableFlip, (v) => untrack(() => (() => {
114
- if (open()) position();
115
- })()), { defer: true }));
116
- createEffect(on(() => local.disableShift, (v) => untrack(() => (() => {
117
- if (open()) position();
118
- })()), { defer: true }));
119
- createEffect(on(() => local.strategy, (v) => untrack(() => (() => {
120
- if (open()) position();
121
- })()), { defer: true }));
122
- let anchorElRef = null;
123
- let floatingElRef = null;
124
- let arrowElRef = null;
125
- let anchorNode = null;
126
- let floatingNode = null;
127
- let arrowNode = null;
128
- let stopAutoUpdate = null;
129
- let lastFocusedEl = null;
130
- function deepActiveElement() {
131
- let el = document.activeElement;
132
- while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
133
- return el;
134
- }
135
- function requestOpen(next) {
136
- if (open() === next) return;
137
- if (next && local.trigger === "click") lastFocusedEl = deepActiveElement();
138
- setOpen(next);
139
- _props.onChange?.(next);
140
- if (!next && local.trigger === "click" && lastFocusedEl && lastFocusedEl.isConnected && typeof lastFocusedEl.focus === "function") lastFocusedEl.focus();
141
- if (!next) lastFocusedEl = null;
142
- }
143
- function applyPosition(x, y, middlewareData) {
144
- if (!floatingNode) return;
145
- floatingNode.style.left = x + "px";
146
- floatingNode.style.top = y + "px";
147
- if (arrowNode && middlewareData && middlewareData.arrow) {
148
- const ax = middlewareData.arrow.x;
149
- const ay = middlewareData.arrow.y;
150
- arrowNode.style.left = ax == null ? "" : ax + "px";
151
- arrowNode.style.top = ay == null ? "" : ay + "px";
152
- }
153
- }
154
- function position() {
155
- if (!anchorNode || !floatingNode) return;
156
- const middleware = buildMiddleware({
157
- offset,
158
- flip,
159
- shift,
160
- arrow
161
- }, {
162
- offset: local.offset,
163
- disableFlip: local.disableFlip,
164
- disableShift: local.disableShift,
165
- arrow: local.arrow,
166
- arrowEl: arrowNode
167
- });
168
- if (local.strategy === "fixed") floatingNode.style.position = "fixed";
169
- else floatingNode.style.position = "";
170
- let opts = null;
171
- opts = {
172
- placement: local.placement,
173
- strategy: local.strategy,
174
- middleware
175
- };
176
- computePosition(anchorNode, floatingNode, opts).then((result) => {
177
- applyPosition(result.x, result.y, result.middlewareData);
178
- });
179
- }
180
- function startTracking() {
181
- if (!anchorNode || !floatingNode) return;
182
- if (stopAutoUpdate) {
183
- stopAutoUpdate();
184
- stopAutoUpdate = null;
185
- }
186
- stopAutoUpdate = autoUpdate(anchorNode, floatingNode, position);
187
- }
188
- function stopTracking() {
189
- if (stopAutoUpdate) {
190
- stopAutoUpdate();
191
- stopAutoUpdate = null;
192
- }
193
- }
194
- function onAnchorClick() {
195
- if (local.disabled) return;
196
- requestOpen(!open());
197
- }
198
- function onAnchorPointerEnter() {
199
- if (local.disabled) return;
200
- requestOpen(true);
201
- }
202
- function onAnchorPointerLeave() {
203
- if (local.disabled) return;
204
- requestOpen(false);
205
- }
206
- function onAnchorFocus() {
207
- if (local.disabled) return;
208
- requestOpen(true);
209
- }
210
- function onAnchorBlur() {
211
- if (local.disabled) return;
212
- requestOpen(false);
213
- }
214
- function dismiss() {
215
- requestOpen(false);
216
- }
217
- function isTooltip() {
218
- return local.trigger === "hover" || local.trigger === "focus";
219
- }
220
- function floatingRole() {
221
- return isTooltip() ? "tooltip" : local.modal ? "dialog" : void 0;
222
- }
223
- function show() {
224
- if (!local.disabled) requestOpen(true);
225
- }
226
- function hide() {
227
- requestOpen(false);
228
- }
229
- function toggle() {
230
- if (!local.disabled) requestOpen(!open());
231
- }
232
- function reposition() {
233
- position();
234
- }
235
- createEffect(() => {
236
- if (!open()) return;
237
- const _rozieHandler = ($event) => {
238
- if ($event.key !== "Escape") return;
239
- dismiss();
240
- };
241
- document.addEventListener("keydown", _rozieHandler);
242
- onCleanup(() => document.removeEventListener("keydown", _rozieHandler));
243
- });
244
- createOutsideClick([() => anchorElRef, () => floatingElRef], dismiss, () => open());
245
- return <>
246
- <div {...attrs} class={"rozie-popover" + (attrs.class ? " " + attrs.class : "")} data-rozie-s-c6cf02ea="">
247
-
248
-
249
- <div aria-haspopup="dialog" aria-expanded={!!open()} aria-describedby={rozieAttr(isTooltip() && open() ? "rozie-popover-floating" : null)} class={"rozie-popover-anchor"} ref={(el) => {
250
- anchorElRef = el;
251
- }} onClick={($event) => {
252
- local.trigger === "click" && onAnchorClick();
253
- }} onPointerEnter={($event) => {
254
- local.trigger === "hover" && onAnchorPointerEnter();
255
- }} onPointerLeave={($event) => {
256
- local.trigger === "hover" && onAnchorPointerLeave();
257
- }} onFocusIn={($event) => {
258
- local.trigger === "focus" && onAnchorFocus();
259
- }} onFocusOut={($event) => {
260
- local.trigger === "focus" && onAnchorBlur();
261
- }} data-rozie-s-c6cf02ea="">
262
- {(_props.anchorSlot ?? _props.slots?.["anchor"])?.({
263
- open: open(),
264
- toggle,
265
- show,
266
- hide
267
- })}
268
- </div>
269
-
270
-
271
- {<Show when={open() && !local.disabled}><div class={"rozie-popover-floating"} ref={(el) => {
272
- floatingElRef = el;
273
- }} id="rozie-popover-floating" role={rozieAttr(floatingRole())} aria-modal={!!(floatingRole() === "dialog")} data-rozie-s-c6cf02ea="">
274
- {<Show when={local.arrow}><div class={"rozie-popover-arrow"} ref={(el) => {
275
- arrowElRef = el;
276
- }} data-rozie-s-c6cf02ea="" /></Show>}{resolved()}
277
- </div></Show>}</div>
278
- </>;
279
- }
280
- //#endregion
281
7
  //#region src/DataTable.tsx
282
8
  __rozieInjectStyle("DataTable-d5dcab4c", `.rozie-data-table[data-rozie-s-d5dcab4c] {
283
9
  border-collapse: collapse;
@@ -1719,8 +1445,7 @@ function DataTable(_props) {
1719
1445
  const base = pinStyle(colId);
1720
1446
  if (isExpanderColumn(colId) && row && row.depth) {
1721
1447
  const pad = "padding-left:" + (.5 + row.depth * 1.25) + "rem";
1722
- const sep = base.endsWith(";") ? "" : ";";
1723
- return base ? base + sep + pad : pad;
1448
+ return base ? base + pad : pad;
1724
1449
  }
1725
1450
  return base;
1726
1451
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rozie-ui/data-table-solid",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@
52
52
  "solid-js": "^1.8",
53
53
  "@tanstack/table-core": "^8.21",
54
54
  "@tanstack/virtual-core": "^3",
55
- "@floating-ui/dom": "^1.7.2"
55
+ "@rozie-ui/popover-solid": "^0.1.0"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "solid-js": {
@@ -64,13 +64,11 @@
64
64
  "@tanstack/virtual-core": {
65
65
  "optional": false
66
66
  },
67
- "@floating-ui/dom": {
67
+ "@rozie-ui/popover-solid": {
68
68
  "optional": false
69
69
  }
70
70
  },
71
- "devDependencies": {
72
- "@floating-ui/dom": "^1.7.2"
73
- },
71
+ "devDependencies": {},
74
72
  "publishConfig": {
75
73
  "access": "public"
76
74
  },
package/src/Column.tsx CHANGED
@@ -64,7 +64,7 @@ interface ColumnProps {
64
64
  }
65
65
 
66
66
  export default function Column(_props: ColumnProps): JSX.Element {
67
- const _merged = mergeProps({ id: '', field: '', header: '', sortable: false, filterable: false, pinned: '', width: '', expandable: false, groupable: true, aggregationFn: null, editable: false, editor: 'text', editorOptions: (() => [])(), validate: null }, _props);
67
+ const _merged = mergeProps({ id: '', field: '', header: '', sortable: false, filterable: false, pinned: '', width: '', expandable: false, groupable: true, aggregationFn: null, editable: false, editor: 'text', editorOptions: (() => [])() as any[], validate: null }, _props);
68
68
  const [local, attrs] = splitProps(_merged, ['id', 'field', 'header', 'sortable', 'filterable', 'pinned', 'width', 'expandable', 'groupable', 'aggregationFn', 'editable', 'editor', 'editorOptions', 'validate']);
69
69
 
70
70
  const registry = useContext(rozieContext("data-table:columns"));
package/src/DataTable.tsx CHANGED
@@ -2,7 +2,7 @@ import type { JSX } from 'solid-js';
2
2
  import { Show, createEffect, createSignal, mergeProps, on, onCleanup, onMount, splitProps, untrack } from 'solid-js';
3
3
  import { Key } from '@solid-primitives/keyed';
4
4
  import { __rozieInjectStyle, createControllableSignal, parseInlineStyle, rozieAttr, rozieClass, rozieContext, rozieDisplay } from '@rozie/runtime-solid';
5
- import Popover from './Popover';
5
+ import Popover from '@rozie-ui/popover-solid';
6
6
  import { createTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, getGroupedRowModel,
7
7
  // Faceted filtering (phase 50 reqs 8-9, D-03). All three are supplied UNCONDITIONALLY
8
8
  // (mirrors the expand/group models) — inert until a consumer READS a column facet via the
@@ -565,7 +565,7 @@ export interface DataTableHandle {
565
565
  }
566
566
 
567
567
  export default function DataTable(_props: DataTableProps): JSX.Element {
568
- const _merged = mergeProps({ columns: (() => [])(), selectionMode: 'none', manual: false, rowCount: null, pageCount: null, expandable: false, getSubRows: null, groupable: false, stickyHeader: false, interactionMode: 'table', singleClickEdit: false, undoable: false, undoLimit: 100, virtual: false, estimateRowHeight: 40, maxHeight: '' }, _props);
568
+ const _merged = mergeProps({ columns: (() => [])() as any[], selectionMode: 'none', manual: false, rowCount: null, pageCount: null, expandable: false, getSubRows: null, groupable: false, stickyHeader: false, interactionMode: 'table', singleClickEdit: false, undoable: false, undoLimit: 100, virtual: false, estimateRowHeight: 40, maxHeight: '' }, _props);
569
569
  const [local, attrs] = splitProps(_merged, ['data', 'columns', 'selectionMode', 'sorting', 'globalFilter', 'columnFilters', 'pagination', 'manual', 'rowCount', 'pageCount', 'expandable', 'expanded', 'getSubRows', 'groupable', 'grouping', 'rowSelection', 'columnVisibility', 'columnSizing', 'columnOrder', 'columnPinning', 'stickyHeader', 'interactionMode', 'singleClickEdit', 'undoable', 'undoLimit', 'virtual', 'estimateRowHeight', 'maxHeight', 'children', 'ref']);
570
570
  const resolved = () => local.children;
571
571
  onMount(() => { local.ref?.({ sortColumn, clearSorting, toggleRowExpanded, expandAll, collapseAll, getExpandedRows, applyGrouping, clearGrouping, getFacetedUniqueValues, getFacetedMinMaxValues, getColumnDefs, toggleAllRows, clearSelection, getSelectedRows, setPage, setRowsPerPage, toggleColumnVisibility, applyColumnOrder, resetColumnSizing, pinColumn, focusCell, getActiveCell, clearActiveCell, getRowIndexRelativeToPage, editCell, commitEditing, editRow, getSelectedRange, cut, undo, redo, canUndo, canRedo, clearHistory }); });
@@ -579,8 +579,8 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
579
579
  pageIndex: 0,
580
580
  pageSize: 10
581
581
  }))());
582
- const [expanded, setExpanded] = createControllableSignal<Record<string, any> | boolean>(_props as unknown as Record<string, unknown>, 'expanded', null);
583
- const [grouping, setGrouping] = createControllableSignal<any[]>(_props as unknown as Record<string, unknown>, 'grouping', null);
582
+ const [expanded, setExpanded] = createControllableSignal<Record<string, any> | boolean | null>(_props as unknown as Record<string, unknown>, 'expanded', null);
583
+ const [grouping, setGrouping] = createControllableSignal<any[] | null>(_props as unknown as Record<string, unknown>, 'grouping', null);
584
584
  const [rowSelection, setRowSelection] = createControllableSignal<Record<string, any>>(_props as unknown as Record<string, unknown>, 'rowSelection', (() => ({}))());
585
585
  const [columnVisibility, setColumnVisibility] = createControllableSignal<Record<string, any>>(_props as unknown as Record<string, unknown>, 'columnVisibility', (() => ({}))());
586
586
  const [columnSizing, setColumnSizing] = createControllableSignal<Record<string, any>>(_props as unknown as Record<string, unknown>, 'columnSizing', (() => ({}))());
@@ -2606,15 +2606,9 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2606
2606
  function bodyCellStyle(row: any, colId: any) {
2607
2607
  const base = pinStyle(colId);
2608
2608
  if (isExpanderColumn(colId) && row && row.depth) {
2609
+ // Only the expander column indents (the tree affordance lives here).
2609
2610
  const pad = 'padding-left:' + (0.5 + row.depth * 1.25) + 'rem';
2610
- // pinStyle() already ends with ';', so append `pad` directly — inserting a
2611
- // second ';' produces a `;;` empty declaration. Browsers tolerate that in a
2612
- // raw style attribute (React/Vue/Svelte/Lit) and Solid's cssText path does
2613
- // too, but Angular's [style] binding parses the string via ɵɵstyleMap and
2614
- // DROPS the declaration following the empty one — so the depth padding-left
2615
- // silently vanished on Angular only. Emit a single, well-formed separator.
2616
- const sep = base.endsWith(';') ? '' : ';';
2617
- return base ? base + sep + pad : pad;
2611
+ return base ? base + pad : pad;
2618
2612
  }
2619
2613
  return base;
2620
2614
  }
@@ -35,7 +35,7 @@ interface EditorSelectProps {
35
35
  }
36
36
 
37
37
  export default function EditorSelect(_props: EditorSelectProps): JSX.Element {
38
- const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, options: (() => [])() }, _props);
38
+ const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, options: (() => [])() as any[] }, _props);
39
39
  const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'options']);
40
40
 
41
41
  const [draft, setDraft] = createSignal('');
@@ -27,7 +27,7 @@ interface FilterSelectProps {
27
27
  }
28
28
 
29
29
  export default function FilterSelect(_props: FilterSelectProps): JSX.Element {
30
- const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null, uniqueValues: (() => [])() }, _props);
30
+ const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null, uniqueValues: (() => [])() as any[] }, _props);
31
31
  const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'value', 'setFilter', 'uniqueValues']);
32
32
 
33
33
  // The <select> value binding coerced to a string. $props.value is typed `unknown`
package/src/GroupBar.tsx CHANGED
@@ -81,7 +81,7 @@ interface GroupBarProps {
81
81
  }
82
82
 
83
83
  export default function GroupBar(_props: GroupBarProps): JSX.Element {
84
- const _merged = mergeProps({ grouping: (() => [])(), groupableColumns: (() => [])(), applyGrouping: null, clearGrouping: null }, _props);
84
+ const _merged = mergeProps({ grouping: (() => [])() as any[], groupableColumns: (() => [])() as any[], applyGrouping: null, clearGrouping: null }, _props);
85
85
  const [local, attrs] = splitProps(_merged, ['grouping', 'groupableColumns', 'applyGrouping', 'clearGrouping']);
86
86
 
87
87
  const [draggingId, setDraggingId] = createSignal('');
package/src/Popover.tsx DELETED
@@ -1,416 +0,0 @@
1
- import type { JSX } from 'solid-js';
2
- import { Show, children, createEffect, mergeProps, on, onCleanup, onMount, splitProps, untrack } from 'solid-js';
3
- import { __rozieInjectStyle, createControllableSignal, createOutsideClick, rozieAttr } from '@rozie/runtime-solid';
4
- // The `offset` AND `arrow` middleware factories are ALIASED on import: both are
5
- // ALSO author PROP names (`offset`, `arrow`). A bare `offset`/`arrow` shorthand in
6
- // the buildMiddleware factories object resolves to the PROP — on Vue/Svelte the
7
- // destructured prop local shadows the import, and on Angular the emitter rewrites
8
- // the bare shorthand to the prop signal (`offset: this.offset()`, a number) instead
9
- // of the middleware function (TS2322). Aliasing both severs the import↔prop clash.
10
- // (The Cropper import-name==component-name class, applied to imports vs PROP names —
11
- // two collisions, not one.) computePosition/autoUpdate/flip/shift carry no clash.
12
- import { computePosition, autoUpdate, offset as offsetMiddleware, flip, shift, arrow as arrowMiddleware } from '@floating-ui/dom';
13
- import { buildMiddleware } from './internal/middleware';
14
-
15
- // null-lets so the bundled-leaf typeNeutralize pass annotates them `any`:
16
- // anchorNode/floatingNode/arrowNode hold the resolved ref ELEMENTS (read ONLY in
17
- // $onMount/handlers, ROZ123). They are deliberately named DIFFERENTLY from the
18
- // `ref="anchorEl"` / `ref="floatingEl"` / `ref="arrowEl"` template ref names: the
19
- // React/Svelte emitters declare a `const anchorEl = useRef(...)` for the ref, and a
20
- // top-level `let anchorEl` hoisted to its own `useRef` would REDECLARE it (TS2451 —
21
- // the local-name==ref-name self-shadow class, here in its `let X = null; X = $refs.X`
22
- // variant, which deconflictRefShadows does NOT auto-rewrite since it only fires on the
23
- // `const X = $refs.X` init shape).
24
- // stopAutoUpdate is the autoUpdate teardown handle — a TOP-LEVEL `let` so the Solid
25
- // onMount→onCleanup split (teardown is a separate closure) can still see it.
26
- // lastFocusedEl (phase 72-06b) holds whatever had DOM focus at the moment a
27
- // `trigger="click"` popover opened (natively the clicked trigger element itself,
28
- // since a mousedown focuses a native `<button>` before its `click` fires) —
29
- // restored on dismissal so Escape/click-outside don't drop focus to `<body>`.
30
- // Same null-let convention as the others: read/written only in handlers, `any`
31
- // via typeNeutralize.
32
-
33
- __rozieInjectStyle('Popover-c6cf02ea', `.rozie-popover[data-rozie-s-c6cf02ea] {
34
- display: contents;
35
- }
36
- .rozie-popover-anchor[data-rozie-s-c6cf02ea] {
37
- display: inline-block;
38
- }
39
- .rozie-popover-floating[data-rozie-s-c6cf02ea] {
40
- position: absolute;
41
- left: 0;
42
- top: 0;
43
- z-index: var(--rozie-popover-z, 1000);
44
- width: max-content;
45
- max-width: var(--rozie-popover-max-width, calc(100vw - 16px));
46
- background: var(--rozie-popover-bg, #fff);
47
- color: var(--rozie-popover-color, inherit);
48
- border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
49
- border-radius: var(--rozie-popover-radius, 8px);
50
- box-shadow: var(--rozie-popover-shadow, 0 8px 24px rgba(0, 0, 0, 0.12));
51
- padding: var(--rozie-popover-padding, 8px 12px);
52
- }
53
- .rozie-popover-arrow[data-rozie-s-c6cf02ea] {
54
- position: absolute;
55
- width: var(--rozie-popover-arrow-size, 8px);
56
- height: var(--rozie-popover-arrow-size, 8px);
57
- background: var(--rozie-popover-bg, #fff);
58
- border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
59
- transform: rotate(45deg);
60
- }`);
61
-
62
- interface AnchorSlotCtx { open: any; toggle: any; show: any; hide: any; }
63
-
64
- interface PopoverProps {
65
- /**
66
- * Whether the floating content is open. The sole `model: true` prop — two-way bind it (`r-model:open` / `v-model:open` / `bind:open` / `[(open)]`) and Popover writes the new state back whenever the trigger or a dismissal toggles it. Left unbound it falls back to an uncontrolled default.
67
- */
68
- open?: boolean;
69
- defaultOpen?: boolean;
70
- onOpenChange?: (open: boolean) => void;
71
- /**
72
- * Floating UI placement of the content relative to the anchor — one of `top`/`right`/`bottom`/`left`, each optionally suffixed `-start`/`-end` (e.g. `bottom-start`). With `disableFlip` off, the content may flip to the opposite side when it would overflow the viewport. Reconciled at runtime.
73
- */
74
- placement?: string;
75
- /**
76
- * How the anchor opens the content: `'click'` toggles on click, `'hover'` opens on pointer-enter and closes on pointer-leave (tooltip-style), `'focus'` opens on focus and closes on blur. Drives both the gesture handlers and the ARIA role (`'hover'`/`'focus'` → tooltip, `'click'` → popover dialog).
77
- */
78
- trigger?: string;
79
- /**
80
- * Distance in pixels between the anchor and the floating content (the Floating UI `offset` middleware). Reconciled at runtime.
81
- */
82
- offset?: number;
83
- /**
84
- * Disable the Floating UI `flip` middleware. By default the content flips to the opposite side of the anchor when it would overflow the viewport; set this to keep it pinned to `placement` regardless.
85
- */
86
- disableFlip?: boolean;
87
- /**
88
- * Disable the Floating UI `shift` middleware. By default the content shifts along its axis to stay within the viewport; set this to keep it strictly aligned to the anchor.
89
- */
90
- disableShift?: boolean;
91
- /**
92
- * Opt in to a positioned arrow element. When set, Popover renders an arrow `<div>` and runs the Floating UI `arrow` middleware against it so it points at the anchor. Style it via the `--rozie-popover-*` arrow CSS custom properties.
93
- */
94
- arrow?: boolean;
95
- /**
96
- * Disable the control entirely: the trigger no longer opens the content and any open content is suppressed.
97
- */
98
- disabled?: boolean;
99
- /**
100
- * Opt in to modal dialog semantics for a `click` popover. **Off by default:** a click popover is a non-modal, click-outside-dismissable layer, so its panel is rendered role-neutral (the slot content owns its own ARIA role — e.g. a `role="menu"`) and carries NO `aria-modal`. Set `modal` for a genuinely modal dialog popover: the panel then gets `role="dialog"` + `aria-modal="true"`. **Note:** Popover ships no focus trap (it stays a minimal headless primitive); if you set `modal`, provide your own focus containment so the `aria-modal` claim holds. Ignored for `hover`/`focus` triggers (always tooltip-flavored).
101
- */
102
- modal?: boolean;
103
- /**
104
- * Floating UI positioning strategy — 'absolute' (default) or 'fixed'. Use 'fixed' to escape a scrollable/overflow-clipping ancestor (e.g. a sticky table header). Reconciled at runtime.
105
- */
106
- strategy?: string;
107
- onChange?: (...args: unknown[]) => void;
108
- anchorSlot?: (ctx: AnchorSlotCtx) => JSX.Element;
109
- // D-131: default slot resolved via children() at body top
110
- children?: JSX.Element;
111
- slots?: Record<string, (ctx: any) => JSX.Element>;
112
- ref?: (h: PopoverHandle) => void;
113
- }
114
-
115
- export interface PopoverHandle {
116
- show: (...args: any[]) => any;
117
- hide: (...args: any[]) => any;
118
- toggle: (...args: any[]) => any;
119
- reposition: (...args: any[]) => any;
120
- }
121
-
122
- export default function Popover(_props: PopoverProps): JSX.Element {
123
- const _merged = mergeProps({ placement: 'bottom', trigger: 'click', offset: 8, disableFlip: false, disableShift: false, arrow: false, disabled: false, modal: false, strategy: 'absolute' }, _props);
124
- const [local, attrs] = splitProps(_merged, ['open', 'placement', 'trigger', 'offset', 'disableFlip', 'disableShift', 'arrow', 'disabled', 'modal', 'strategy', 'children', 'ref']);
125
- const resolved = children(() => local.children);
126
- onMount(() => { local.ref?.({ show, hide, toggle, reposition }); });
127
-
128
- const [open, setOpen] = createControllableSignal<boolean>(_props as unknown as Record<string, unknown>, 'open', false);
129
- onMount(() => {
130
- const _cleanup = (() => {
131
- // $refs read ONLY here (ROZ123). The floating + arrow elements live behind r-if
132
- // and may be null until open; startTracking re-reads via the watch path.
133
- anchorNode = anchorElRef;
134
- if (open() && !local.disabled) {
135
- // floatingNode is populated by its r-if having rendered; read it lazily inside
136
- // the watch/handlers too. Position on next tick when it exists.
137
- floatingNode = floatingElRef;
138
- arrowNode = arrowElRef;
139
- startTracking();
140
- }
141
- })() as unknown;
142
- if (_cleanup) onCleanup(_cleanup as () => void);
143
- onCleanup(() => {
144
- stopTracking();
145
- });
146
- });
147
- createEffect(on(() => (() => open())(), (v) => untrack(() => ((isOpen: any) => {
148
- if (isOpen && !local.disabled) {
149
- queueMicrotask(() => {
150
- if (!open() || local.disabled) return;
151
- floatingNode = floatingElRef;
152
- arrowNode = arrowElRef;
153
- startTracking();
154
- });
155
- } else {
156
- stopTracking();
157
- }
158
- })(v)), { defer: true }));
159
- createEffect(on(() => (() => local.placement)(), (v) => untrack(() => (() => {
160
- if (open()) position();
161
- })()), { defer: true }));
162
- createEffect(on(() => (() => local.offset)(), (v) => untrack(() => (() => {
163
- if (open()) position();
164
- })()), { defer: true }));
165
- createEffect(on(() => (() => local.disableFlip)(), (v) => untrack(() => (() => {
166
- if (open()) position();
167
- })()), { defer: true }));
168
- createEffect(on(() => (() => local.disableShift)(), (v) => untrack(() => (() => {
169
- if (open()) position();
170
- })()), { defer: true }));
171
- createEffect(on(() => (() => local.strategy)(), (v) => untrack(() => (() => {
172
- if (open()) position();
173
- })()), { defer: true }));
174
- let anchorElRef: HTMLElement | null = null;
175
- let floatingElRef: HTMLElement | null = null;
176
- let arrowElRef: HTMLElement | null = null;
177
-
178
- // null-lets so the bundled-leaf typeNeutralize pass annotates them `any`:
179
- // anchorNode/floatingNode/arrowNode hold the resolved ref ELEMENTS (read ONLY in
180
- // $onMount/handlers, ROZ123). They are deliberately named DIFFERENTLY from the
181
- // `ref="anchorEl"` / `ref="floatingEl"` / `ref="arrowEl"` template ref names: the
182
- // React/Svelte emitters declare a `const anchorEl = useRef(...)` for the ref, and a
183
- // top-level `let anchorEl` hoisted to its own `useRef` would REDECLARE it (TS2451 —
184
- // the local-name==ref-name self-shadow class, here in its `let X = null; X = $refs.X`
185
- // variant, which deconflictRefShadows does NOT auto-rewrite since it only fires on the
186
- // `const X = $refs.X` init shape).
187
- // stopAutoUpdate is the autoUpdate teardown handle — a TOP-LEVEL `let` so the Solid
188
- // onMount→onCleanup split (teardown is a separate closure) can still see it.
189
- // lastFocusedEl (phase 72-06b) holds whatever had DOM focus at the moment a
190
- // `trigger="click"` popover opened (natively the clicked trigger element itself,
191
- // since a mousedown focuses a native `<button>` before its `click` fires) —
192
- // restored on dismissal so Escape/click-outside don't drop focus to `<body>`.
193
- // Same null-let convention as the others: read/written only in handlers, `any`
194
- // via typeNeutralize.
195
- let anchorNode: any = null;
196
- let floatingNode: any = null;
197
- let arrowNode: any = null;
198
- let stopAutoUpdate: any = null;
199
- let lastFocusedEl: any = null;
200
-
201
- // `document.activeElement` stops at the OUTERMOST shadow-DOM host when focus
202
- // lives inside a NESTED shadow tree — e.g. a Lit consumer that composes
203
- // `<rozie-popover>` inside its own shadow root (data-table's vendored copy):
204
- // clicking the trigger focuses a real element several shadow boundaries deep,
205
- // but `document.activeElement` only resolves as far as the outermost custom
206
- // element (`<rozie-data-table>`), not the actual focused node. Walking
207
- // `.shadowRoot.activeElement` recursively drills to the true focused element.
208
- // On the other 5 targets (no shadow DOM) `el.shadowRoot` is always
209
- // null/undefined, so the loop is a no-op and this degrades to a plain
210
- // `document.activeElement` read — one implementation, safe on every target.
211
- function deepActiveElement() {
212
- let el = document.activeElement;
213
- while (el && el.shadowRoot && el.shadowRoot.activeElement) {
214
- el = el.shadowRoot.activeElement;
215
- }
216
- return el;
217
- }
218
-
219
- // Drive the two-way model + emit in one place. Named `requestOpen` (NOT `setOpen`)
220
- // to dodge the React generated `setOpen` setter for the `open` model (ROZ524).
221
- //
222
- // Focus-return (phase 72-06b, D-08 a11y finding): scoped to `trigger === 'click'`
223
- // only — click-triggered popovers are genuinely interactive (a real dialog the
224
- // user tabs/clicks into), so restoring focus to the trigger on dismissal matches
225
- // standard disclosure-widget a11y practice. Deliberately NOT applied to
226
- // `hover`/`focus` triggers (tooltip-flavored, see `isTooltip()`): those close on
227
- // pointerleave/blur constantly during normal mouse/keyboard traversal, and
228
- // forcing a focus() call on every such close would fight the user's own focus
229
- // movement rather than restore anything lost.
230
- function requestOpen(next: any) {
231
- if (open() === next) return;
232
- if (next && local.trigger === 'click') {
233
- lastFocusedEl = deepActiveElement();
234
- }
235
- setOpen(next);
236
- _props.onChange?.(next);
237
- if (!next && local.trigger === 'click' && lastFocusedEl && lastFocusedEl.isConnected && typeof lastFocusedEl.focus === 'function') {
238
- lastFocusedEl.focus();
239
- }
240
- if (!next) {
241
- lastFocusedEl = null;
242
- }
243
- }
244
-
245
- // Apply the resolved x/y (and arrow offset, when present) onto the floating element.
246
- function applyPosition(x: any, y: any, middlewareData: any) {
247
- if (!floatingNode) return;
248
- floatingNode.style.left = x + 'px';
249
- floatingNode.style.top = y + 'px';
250
- if (arrowNode && middlewareData && middlewareData.arrow) {
251
- const ax = middlewareData.arrow.x;
252
- const ay = middlewareData.arrow.y;
253
- arrowNode.style.left = ax == null ? '' : ax + 'px';
254
- arrowNode.style.top = ay == null ? '' : ay + 'px';
255
- }
256
- }
257
-
258
- // Recompute the position once. Pure engine call; safe to invoke whenever both
259
- // elements exist and the content is open. `opts` is a null-let (→ `any`) so the
260
- // loosely-typed `<props>` placement (string) + the `unknown[]` middleware array don't
261
- // fail the strict leaf tsc against Floating UI's `Placement` / `Middleware[]` types
262
- // (the cropper `let cfg = null` constructor-args idiom).
263
- function position() {
264
- if (!anchorNode || !floatingNode) return;
265
- const middleware = buildMiddleware({
266
- offset: offsetMiddleware,
267
- flip,
268
- shift,
269
- arrow: arrowMiddleware
270
- }, {
271
- offset: local.offset,
272
- disableFlip: local.disableFlip,
273
- disableShift: local.disableShift,
274
- arrow: local.arrow,
275
- arrowEl: arrowNode
276
- });
277
- // 'fixed' inline position MUST be written before computePosition measures the
278
- // floating element's offset parent (fixed vs absolute changes the containing
279
- // block). Default 'absolute' explicitly CLEARS any inline position instead of
280
- // writing `position: absolute` — so a never-fixed popover still writes no
281
- // visible inline position (byte-identical-off preserved: `style.position = ''`
282
- // is a no-op when the property was never set), while a live `strategy`
283
- // reconcile (fixed → absolute, see the $watch below) correctly resets the
284
- // stale inline `fixed` so the stylesheet's `position: absolute` rule re-takes
285
- // over instead of positioning `fixed` with absolute-computed coordinates
286
- // (72-REVIEW.md WR-01).
287
- if (local.strategy === 'fixed') {
288
- floatingNode.style.position = 'fixed';
289
- } else {
290
- floatingNode.style.position = '';
291
- }
292
- let opts: any = null;
293
- opts = {
294
- placement: local.placement,
295
- strategy: local.strategy,
296
- middleware
297
- };
298
- computePosition(anchorNode, floatingNode, opts).then((result: any) => {
299
- applyPosition(result.x, result.y, result.middlewareData);
300
- });
301
- }
302
-
303
- // Start autoUpdate (idempotent — stop any prior subscription first) and do an
304
- // initial position. Floating UI's autoUpdate keeps the position fresh on scroll/
305
- // resize/ancestor-layout changes and returns its own teardown.
306
- function startTracking() {
307
- if (!anchorNode || !floatingNode) return;
308
- if (stopAutoUpdate) {
309
- stopAutoUpdate();
310
- stopAutoUpdate = null;
311
- }
312
- stopAutoUpdate = autoUpdate(anchorNode, floatingNode, position);
313
- }
314
- function stopTracking() {
315
- if (stopAutoUpdate) {
316
- stopAutoUpdate();
317
- stopAutoUpdate = null;
318
- }
319
- }
320
- // ─── trigger gesture handlers (wired conditionally on the anchor by `trigger`) ──
321
- function onAnchorClick() {
322
- if (local.disabled) return;
323
- requestOpen(!open());
324
- }
325
- function onAnchorPointerEnter() {
326
- if (local.disabled) return;
327
- requestOpen(true);
328
- }
329
- function onAnchorPointerLeave() {
330
- if (local.disabled) return;
331
- requestOpen(false);
332
- }
333
- function onAnchorFocus() {
334
- if (local.disabled) return;
335
- requestOpen(true);
336
- }
337
- function onAnchorBlur() {
338
- if (local.disabled) return;
339
- requestOpen(false);
340
- }
341
-
342
- // Dismissal handler — method reference for the <listeners> block (an inline
343
- // handler referencing $event leaks into React's useEffect deps → TS2552; every
344
- // corpus <listener> uses a method-ref + modifiers).
345
- function dismiss() {
346
- requestOpen(false);
347
- }
348
-
349
- // ─── role helpers (plain functions; tooltip vs popover-dialog by trigger) ───────
350
- // hover/focus triggers are tooltip-flavored; click is an interactive popover.
351
- function isTooltip() {
352
- return local.trigger === 'hover' || local.trigger === 'focus';
353
- }
354
- // Role: hover/focus → 'tooltip'; a click popover is 'dialog' ONLY when the consumer
355
- // opts into `modal` (which is what also emits aria-modal). A default (non-modal)
356
- // click popover returns `undefined` — a role-NEUTRAL positioned container, so the slot
357
- // content owns its own semantics (e.g. the data-table ⋯ menu declares role="menu").
358
- // Emitting role="dialog" + aria-modal="true" on a click-outside-dismissable panel
359
- // with no focus trap wrongly tells assistive tech that sibling content is inert (IN-03).
360
- // `undefined` (not `null`) for the neutral case: the Vue `:role` binding target is
361
- // `string | undefined`, and under strict vue-tsc `null` is not assignable to it —
362
- // `undefined` drops the attribute identically (Vue/Solid nullish-attr drop treats both
363
- // alike) while keeping the emitted leaf's inferred type a clean `'tooltip' | 'dialog' | undefined`.
364
- function floatingRole() {
365
- return isTooltip() ? 'tooltip' : local.modal ? 'dialog' : undefined;
366
- }
367
-
368
- // ─── imperative handle ($expose) ────────────────────────────────────────────────
369
- // Verbs: show/hide/toggle/reposition. NOT `update` (reserved Lit lifecycle) → the
370
- // reposition verb is `reposition`. None collide with the `change` emit, the `open`
371
- // model, or its React `setOpen` setter, nor with inherited HTMLElement members.
372
- function show() {
373
- if (!local.disabled) requestOpen(true);
374
- }
375
- function hide() {
376
- requestOpen(false);
377
- }
378
- function toggle() {
379
- if (!local.disabled) requestOpen(!open());
380
- }
381
- function reposition() {
382
- position();
383
- }
384
-
385
- createEffect(() => {
386
- if (!(open())) return;
387
- const _rozieHandler = ($event: KeyboardEvent) => {
388
- if ($event.key !== 'Escape') return;
389
- dismiss();
390
- };
391
- document.addEventListener('keydown', _rozieHandler);
392
- onCleanup(() => document.removeEventListener('keydown', _rozieHandler));
393
- });
394
-
395
- createOutsideClick(
396
- [() => anchorElRef, () => floatingElRef],
397
- dismiss,
398
- () => open(),
399
- );
400
-
401
- return (
402
- <>
403
- <div {...attrs} class={"rozie-popover" + (((attrs as unknown as Record<string, unknown>).class as string | undefined) ? " " + ((attrs as unknown as Record<string, unknown>).class as string | undefined) : "")} data-rozie-s-c6cf02ea="">
404
-
405
-
406
- <div aria-haspopup="dialog" aria-expanded={!!open()} aria-describedby={rozieAttr(isTooltip() && open() ? 'rozie-popover-floating' : null)} class={"rozie-popover-anchor"} ref={(el) => { anchorElRef = el as HTMLElement; }} onClick={($event: MouseEvent & { currentTarget: HTMLDivElement; target: Element }) => { local.trigger === 'click' && onAnchorClick(); }} onPointerEnter={($event: PointerEvent & { currentTarget: HTMLDivElement; target: Element }) => { local.trigger === 'hover' && onAnchorPointerEnter(); }} onPointerLeave={($event: PointerEvent & { currentTarget: HTMLDivElement; target: Element }) => { local.trigger === 'hover' && onAnchorPointerLeave(); }} onFocusIn={($event: FocusEvent & { currentTarget: HTMLDivElement; target: Element }) => { local.trigger === 'focus' && onAnchorFocus(); }} onFocusOut={($event: FocusEvent & { currentTarget: HTMLDivElement; target: Element }) => { local.trigger === 'focus' && onAnchorBlur(); }} data-rozie-s-c6cf02ea="">
407
- {(_props.anchorSlot ?? _props.slots?.['anchor'])?.({ open: open(), toggle, show, hide })}
408
- </div>
409
-
410
-
411
- {<Show when={open() && !local.disabled}><div class={"rozie-popover-floating"} ref={(el) => { floatingElRef = el as HTMLElement; }} id="rozie-popover-floating" role={rozieAttr(floatingRole())} aria-modal={!!(floatingRole() === 'dialog')} data-rozie-s-c6cf02ea="">
412
- {<Show when={local.arrow}><div class={"rozie-popover-arrow"} ref={(el) => { arrowElRef = el as HTMLElement; }} data-rozie-s-c6cf02ea="" /></Show>}{resolved()}
413
- </div></Show>}</div>
414
- </>
415
- );
416
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * middleware.ts — pure construction of the Floating UI middleware stack from the
3
- * Popover's first-class props.
4
- *
5
- * This is the one piece of BRANCHY engine glue in Popover.rozie (offset always on,
6
- * flip/shift opt-out via `disableFlip`/`disableShift`, arrow opt-in via `arrow` +
7
- * an element). Extracted to `src/internal/` so it can be unit-tested in isolation
8
- * (codegen vendors `src/internal/` into every leaf via copyInternal, excluding
9
- * `*.test.ts`).
10
- *
11
- * It takes the Floating UI middleware FACTORIES as arguments (rather than importing
12
- * `@floating-ui/dom` itself) so the unit test can pass lightweight stand-ins and
13
- * assert ORDER + presence without pulling the real engine — and so the vendored
14
- * copy in each leaf has zero engine import of its own (the leaf's Popover.* owns
15
- * the single engine import).
16
- *
17
- * Floating UI ordering contract: offset → flip → shift → arrow. `offset` first so
18
- * the gap is measured before collision detection; `arrow` last so it reads the
19
- * final resolved placement.
20
- */
21
-
22
- export interface MiddlewareFactories {
23
- offset: (value: number) => unknown;
24
- flip: () => unknown;
25
- shift: () => unknown;
26
- arrow: (opts: { element: Element }) => unknown;
27
- }
28
-
29
- export interface MiddlewareConfig {
30
- offset: number;
31
- disableFlip: boolean;
32
- disableShift: boolean;
33
- arrow: boolean;
34
- arrowEl: Element | null;
35
- }
36
-
37
- /**
38
- * Build the ordered middleware array for `computePosition`. Pure — no engine
39
- * import, no DOM read beyond the passed-in arrow element.
40
- */
41
- export function buildMiddleware(
42
- factories: MiddlewareFactories,
43
- config: MiddlewareConfig,
44
- ): unknown[] {
45
- const mw: unknown[] = [factories.offset(config.offset)];
46
- if (!config.disableFlip) mw.push(factories.flip());
47
- if (!config.disableShift) mw.push(factories.shift());
48
- // The arrow middleware needs a real element to position; opt-in only when both
49
- // the `arrow` prop is set AND the arrow element has mounted.
50
- if (config.arrow && config.arrowEl) {
51
- mw.push(factories.arrow({ element: config.arrowEl }));
52
- }
53
- return mw;
54
- }