@kouji-ui/core 0.9.1 → 0.10.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/fesm2022/kouji-ui-core.mjs +135 -12
- package/fesm2022/kouji-ui-core.mjs.map +1 -1
- package/package.json +1 -1
- package/types/kouji-ui-core.d.ts +34 -2
|
@@ -1974,6 +1974,21 @@ function longestRunMs(durations, delays) {
|
|
|
1974
1974
|
}
|
|
1975
1975
|
/** The strategy slots of a bundle, in attach order. */
|
|
1976
1976
|
const STRATEGY_SLOTS = ['mount', 'position', 'backdrop', 'scrollLock', 'focusTrap', 'liveAnnouncer', 'trigger'];
|
|
1977
|
+
/**
|
|
1978
|
+
* The controller each attached strategy instance serves. A strategy is
|
|
1979
|
+
* stateful (it holds one overlay's context and its inert / scroll-lock
|
|
1980
|
+
* release handles), so one instance must never run two overlays at once.
|
|
1981
|
+
*/
|
|
1982
|
+
const strategyOwners = new WeakMap();
|
|
1983
|
+
/**
|
|
1984
|
+
* @internal The controller `strategy` is attached to, if any. `KjOverlayPanel`
|
|
1985
|
+
* uses it to drop strategies it inherited from an enclosing overlay (a
|
|
1986
|
+
* select inside a modal dialog must not run the dialog's scrim, inert and
|
|
1987
|
+
* focus trap as its own).
|
|
1988
|
+
*/
|
|
1989
|
+
function overlayStrategyOwner(strategy) {
|
|
1990
|
+
return strategyOwners.get(strategy);
|
|
1991
|
+
}
|
|
1977
1992
|
/**
|
|
1978
1993
|
* Owns the overlay's open/closed state machine plus the rAF/transition
|
|
1979
1994
|
* orchestration. Strategies (mount/position/backdrop/focus-trap/scroll-lock/
|
|
@@ -2125,13 +2140,21 @@ class KjOverlayController {
|
|
|
2125
2140
|
}
|
|
2126
2141
|
for (const slot of STRATEGY_SLOTS) {
|
|
2127
2142
|
const old = prev[slot];
|
|
2128
|
-
if (old && old !== next[slot])
|
|
2143
|
+
if (old && old !== next[slot]) {
|
|
2129
2144
|
old.detach();
|
|
2145
|
+
if (strategyOwners.get(old) === this)
|
|
2146
|
+
strategyOwners.delete(old);
|
|
2147
|
+
}
|
|
2130
2148
|
}
|
|
2131
2149
|
}
|
|
2132
2150
|
this.strategies = next;
|
|
2133
|
-
for (const slot of STRATEGY_SLOTS)
|
|
2134
|
-
next[slot]
|
|
2151
|
+
for (const slot of STRATEGY_SLOTS) {
|
|
2152
|
+
const strategy = next[slot];
|
|
2153
|
+
if (!strategy)
|
|
2154
|
+
continue;
|
|
2155
|
+
strategyOwners.set(strategy, this);
|
|
2156
|
+
strategy.attach(this.context);
|
|
2157
|
+
}
|
|
2135
2158
|
}
|
|
2136
2159
|
open() {
|
|
2137
2160
|
const cur = this._state();
|
|
@@ -2198,7 +2221,12 @@ class KjOverlayController {
|
|
|
2198
2221
|
return;
|
|
2199
2222
|
const s = this.strategies;
|
|
2200
2223
|
for (const slot of [...STRATEGY_SLOTS].reverse()) {
|
|
2201
|
-
s[slot]
|
|
2224
|
+
const strategy = s[slot];
|
|
2225
|
+
if (!strategy)
|
|
2226
|
+
continue;
|
|
2227
|
+
strategy.detach();
|
|
2228
|
+
if (strategyOwners.get(strategy) === this)
|
|
2229
|
+
strategyOwners.delete(strategy);
|
|
2202
2230
|
}
|
|
2203
2231
|
this.strategies = null;
|
|
2204
2232
|
}
|
|
@@ -2911,6 +2939,36 @@ function resolveTokens(self) {
|
|
|
2911
2939
|
role: inject(KJ_OVERLAY_PANEL_ROLE, opts),
|
|
2912
2940
|
};
|
|
2913
2941
|
}
|
|
2942
|
+
/**
|
|
2943
|
+
* `tokens` minus every strategy already attached to a controller other than
|
|
2944
|
+
* `controller`. Strategies are resolved through the injector chain, so an
|
|
2945
|
+
* overlay rendered inside another one (a select, combobox or date picker in
|
|
2946
|
+
* a service-launched dialog) would otherwise find the enclosing overlay's
|
|
2947
|
+
* own instances for every slot it does not provide itself — and run the
|
|
2948
|
+
* dialog's scrim, `inert` and focus trap as if the dropdown were a second
|
|
2949
|
+
* modal, freezing the dialog it belongs to. Those instances belong to the
|
|
2950
|
+
* enclosing overlay; the nested one falls back to "none" for that slot.
|
|
2951
|
+
*/
|
|
2952
|
+
function withoutForeign(tokens, controller) {
|
|
2953
|
+
if (!controller)
|
|
2954
|
+
return tokens;
|
|
2955
|
+
const mine = (s) => {
|
|
2956
|
+
if (!s)
|
|
2957
|
+
return s;
|
|
2958
|
+
const owner = overlayStrategyOwner(s);
|
|
2959
|
+
return owner && owner !== controller ? null : s;
|
|
2960
|
+
};
|
|
2961
|
+
return {
|
|
2962
|
+
...tokens,
|
|
2963
|
+
mount: mine(tokens.mount),
|
|
2964
|
+
position: mine(tokens.position),
|
|
2965
|
+
backdrop: mine(tokens.backdrop),
|
|
2966
|
+
focusTrap: mine(tokens.focusTrap),
|
|
2967
|
+
scrollLock: mine(tokens.scrollLock),
|
|
2968
|
+
liveAnnouncer: mine(tokens.liveAnnouncer),
|
|
2969
|
+
trigger: mine(tokens.trigger),
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2914
2972
|
/**
|
|
2915
2973
|
* Marks an element as the overlay's panel — wires it to the controller,
|
|
2916
2974
|
* resolves the panel role/aria-modal, and binds DOM state attributes
|
|
@@ -2925,7 +2983,10 @@ function resolveTokens(self) {
|
|
|
2925
2983
|
* must not inherit the dialog's scroll lock, scrim or `alertdialog` role.
|
|
2926
2984
|
* A panel whose controller is on its host chain (a builder-launched body,
|
|
2927
2985
|
* or the root pattern where one directive provides controller and
|
|
2928
|
-
* strategies for the whole subtree) reads them from that chain
|
|
2986
|
+
* strategies for the whole subtree) reads them from that chain, minus any
|
|
2987
|
+
* strategy instance an enclosing overlay is already running: a select inside
|
|
2988
|
+
* a service-launched dialog finds the dialog's scrim, focus trap and scroll
|
|
2989
|
+
* lock up the chain, and must not adopt them.
|
|
2929
2990
|
*
|
|
2930
2991
|
* A portalled panel whose scope provides a backdrop strategy also gets a
|
|
2931
2992
|
* real `<kj-backdrop>` scrim (created next to the panel, moved into the
|
|
@@ -2980,8 +3041,11 @@ class KjOverlayPanel {
|
|
|
2980
3041
|
return !!t && t.controller !== this.hostController;
|
|
2981
3042
|
}, /* @ts-ignore */
|
|
2982
3043
|
...(ngDevMode ? [{ debugName: "scoped" }] : /* istanbul ignore next */ []));
|
|
2983
|
-
/**
|
|
2984
|
-
|
|
3044
|
+
/**
|
|
3045
|
+
* The tokens that apply to this panel. Inherited ones exclude strategies an
|
|
3046
|
+
* enclosing overlay already runs (see {@link withoutForeign}).
|
|
3047
|
+
*/
|
|
3048
|
+
tokens = computed(() => this.scoped() ? this.own : withoutForeign(this.inherited, this.hostController), /* @ts-ignore */
|
|
2985
3049
|
...(ngDevMode ? [{ debugName: "tokens" }] : /* istanbul ignore next */ []));
|
|
2986
3050
|
role = computed(() => this.tokens().role ?? 'dialog', /* @ts-ignore */
|
|
2987
3051
|
...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
|
|
@@ -22092,6 +22156,54 @@ class KjTable {
|
|
|
22092
22156
|
setRowCount(n) {
|
|
22093
22157
|
this._rowCount.set(n);
|
|
22094
22158
|
}
|
|
22159
|
+
/**
|
|
22160
|
+
* Server-side infinite mode. When on, `kjTableData` is index-aligned with
|
|
22161
|
+
* the server's result set and may hold holes (`undefined` / `null`) for
|
|
22162
|
+
* rows that are not loaded yet: the holes are dropped before the rows reach
|
|
22163
|
+
* TanStack, and {@link sourceIndex} maps each loaded row back to its
|
|
22164
|
+
* position in the full result set. Sorting, filtering and pagination are
|
|
22165
|
+
* left to the server (TanStack's `manual*` flags), so a header click only
|
|
22166
|
+
* changes `state.sorting()` — feed it into your loader's request.
|
|
22167
|
+
*/
|
|
22168
|
+
_infinite = signal(false, /* @ts-ignore */
|
|
22169
|
+
...(ngDevMode ? [{ debugName: "_infinite" }] : /* istanbul ignore next */ []));
|
|
22170
|
+
/** Whether server-side infinite mode is on — see {@link setInfinite}. */
|
|
22171
|
+
infinite = this._infinite.asReadonly();
|
|
22172
|
+
/**
|
|
22173
|
+
* Turn server-side infinite mode on or off.
|
|
22174
|
+
* @param on `true` while the data is a sparse window of a server result set.
|
|
22175
|
+
*/
|
|
22176
|
+
setInfinite(on) {
|
|
22177
|
+
this._infinite.set(on);
|
|
22178
|
+
}
|
|
22179
|
+
/** Rows handed to TanStack plus, in infinite mode, their positions in the full result set. */
|
|
22180
|
+
compacted = computed(() => {
|
|
22181
|
+
const data = this.kjTableData();
|
|
22182
|
+
if (!this._infinite())
|
|
22183
|
+
return { rows: data, sourceIndex: null };
|
|
22184
|
+
const rows = [];
|
|
22185
|
+
const sourceIndex = [];
|
|
22186
|
+
// `forEach` skips true holes, so a `new Array(total)` window costs only
|
|
22187
|
+
// its loaded entries.
|
|
22188
|
+
data.forEach((row, i) => {
|
|
22189
|
+
if (row == null)
|
|
22190
|
+
return;
|
|
22191
|
+
rows.push(row);
|
|
22192
|
+
sourceIndex.push(i);
|
|
22193
|
+
});
|
|
22194
|
+
return { rows, sourceIndex };
|
|
22195
|
+
}, /* @ts-ignore */
|
|
22196
|
+
...(ngDevMode ? [{ debugName: "compacted" }] : /* istanbul ignore next */ []));
|
|
22197
|
+
/**
|
|
22198
|
+
* In infinite mode, the result-set position of each loaded top-level row,
|
|
22199
|
+
* indexed by its TanStack `row.index`; `null` otherwise.
|
|
22200
|
+
*/
|
|
22201
|
+
sourceIndex = computed(() => this.compacted().sourceIndex, /* @ts-ignore */
|
|
22202
|
+
...(ngDevMode ? [{ debugName: "sourceIndex" }] : /* istanbul ignore next */ []));
|
|
22203
|
+
/** Position of a top-level row in the full result set (its `row.index` outside infinite mode). */
|
|
22204
|
+
sourceIndexOf(index) {
|
|
22205
|
+
return this.compacted().sourceIndex?.[index] ?? index;
|
|
22206
|
+
}
|
|
22095
22207
|
/**
|
|
22096
22208
|
* Apply a TanStack updater (value or function) to a single state slice.
|
|
22097
22209
|
*
|
|
@@ -22134,11 +22246,15 @@ class KjTable {
|
|
|
22134
22246
|
...(ngDevMode ? [{ debugName: "boundColumns" }] : /* istanbul ignore next */ []));
|
|
22135
22247
|
/** The TanStack table instance. Access rows, headers, and state here. */
|
|
22136
22248
|
table = createAngularTable(() => ({
|
|
22137
|
-
data: this.
|
|
22249
|
+
data: this.compacted().rows,
|
|
22138
22250
|
columns: this.boundColumns(),
|
|
22251
|
+
// In infinite mode a top-level row's index is its result-set position,
|
|
22252
|
+
// so default ids stay stable while pages load around them.
|
|
22139
22253
|
getRowId: this.kjGetRowId()
|
|
22140
|
-
? (row, index) => this.kjGetRowId()(row, index)
|
|
22141
|
-
:
|
|
22254
|
+
? (row, index, parent) => this.kjGetRowId()(row, parent ? index : this.sourceIndexOf(index))
|
|
22255
|
+
: this._infinite()
|
|
22256
|
+
? (_row, index, parent) => (parent ? `${parent.id}.${index}` : String(this.sourceIndexOf(index)))
|
|
22257
|
+
: undefined,
|
|
22142
22258
|
// `columnResizeMode: 'onEnd'` defers the actual column width write until
|
|
22143
22259
|
// the user releases the resize handle. While dragging, the wrapper reads
|
|
22144
22260
|
// `state.columnSizingInfo().deltaOffset` to render a vertical preview
|
|
@@ -22148,7 +22264,9 @@ class KjTable {
|
|
|
22148
22264
|
// TanStack to manual pagination so the loader-page becomes the
|
|
22149
22265
|
// visible page (no client-side slicing) and pagination controls
|
|
22150
22266
|
// derive their totals from the remote count.
|
|
22151
|
-
manualPagination: this._rowCount() !== null,
|
|
22267
|
+
manualPagination: this._rowCount() !== null || this._infinite(),
|
|
22268
|
+
manualSorting: this._infinite(),
|
|
22269
|
+
manualFiltering: this._infinite(),
|
|
22152
22270
|
rowCount: this._rowCount() ?? undefined,
|
|
22153
22271
|
state: { ...this._state(), columnSizingInfo: this._columnSizingInfo() },
|
|
22154
22272
|
onSortingChange: u => this.patch('sorting', u),
|
|
@@ -22450,7 +22568,12 @@ class KjTableRow {
|
|
|
22450
22568
|
...(ngDevMode ? [{ debugName: "kjRow" }] : /* istanbul ignore next */ []));
|
|
22451
22569
|
table = injectParent(KJ_TABLE, { child: 'KjTableRow', parent: '[kjTable]' });
|
|
22452
22570
|
/** 1-based ARIA index — accounts for header row(s). */
|
|
22453
|
-
ariaRowIndex = computed(() =>
|
|
22571
|
+
ariaRowIndex = computed(() => {
|
|
22572
|
+
const row = this.kjRow();
|
|
22573
|
+
// Infinite mode: the row's place in the whole result set, not in the loaded window.
|
|
22574
|
+
const source = row.depth === 0 ? this.table.sourceIndex()?.[row.index] : undefined;
|
|
22575
|
+
return (source ?? row.index) + 2;
|
|
22576
|
+
}, /* @ts-ignore */
|
|
22454
22577
|
...(ngDevMode ? [{ debugName: "ariaRowIndex" }] : /* istanbul ignore next */ []));
|
|
22455
22578
|
isSelectable = computed(() => Object.keys(this.table.state.rowSelection()).length >= 0, /* @ts-ignore */
|
|
22456
22579
|
...(ngDevMode ? [{ debugName: "isSelectable" }] : /* istanbul ignore next */ [])); // always true if selection enabled
|