@kouji-ui/core 0.9.0 → 0.9.1

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, input, booleanAttribute, Directive, PLATFORM_ID, signal, DestroyRef, Injectable, ElementRef, afterNextRender, forwardRef, InjectionToken, Injector, computed, effect, untracked, viewChild, ViewContainerRef, ViewEncapsulation, ChangeDetectionStrategy, Component, ApplicationRef, EnvironmentInjector, createComponent, model, isSignal, DOCUMENT as DOCUMENT$1, output, numberAttribute, afterRenderEffect, contentChildren, makeEnvironmentProviders, LOCALE_ID, provideEnvironmentInitializer, contentChild, linkedSignal, PendingTasks, TemplateRef, runInInjectionContext, resource } from '@angular/core';
2
+ import { inject, input, booleanAttribute, Directive, PLATFORM_ID, signal, DestroyRef, Injectable, ElementRef, afterNextRender, untracked, forwardRef, InjectionToken, Injector, computed, effect, viewChild, ViewContainerRef, ViewEncapsulation, ChangeDetectionStrategy, Component, ApplicationRef, EnvironmentInjector, createComponent, model, isSignal, DOCUMENT as DOCUMENT$1, output, numberAttribute, afterRenderEffect, contentChildren, makeEnvironmentProviders, LOCALE_ID, provideEnvironmentInitializer, contentChild, linkedSignal, PendingTasks, TemplateRef, runInInjectionContext, resource } from '@angular/core';
3
3
  import { DOCUMENT, isPlatformBrowser } from '@angular/common';
4
4
  import { NG_VALUE_ACCESSOR, NG_VALIDATORS, NgForm, FormGroupDirective, FormGroup, FormArray } from '@angular/forms';
5
5
  import { Subject } from 'rxjs';
@@ -282,10 +282,13 @@ class KjFocusRing {
282
282
  afterNextRender(() => {
283
283
  if (!isPlatformBrowser(this.platformId))
284
284
  return;
285
- const onFocus = () => {
286
- this._focusVisible.set(this.inputModality.modality() === 'keyboard');
287
- };
288
- const onBlur = () => { this._focusVisible.set(false); };
285
+ // `focus` / `blur` can fire synchronously while Angular renders — a
286
+ // focused element that a binding disables or removes blurs at once in
287
+ // Chromium — and a raw listener inherits the template's reactive
288
+ // context, where a signal write throws NG0600. `untracked` runs the
289
+ // handlers outside it (Angular's own listeners do the same).
290
+ const onFocus = () => untracked(() => this._focusVisible.set(this.inputModality.modality() === 'keyboard'));
291
+ const onBlur = () => untracked(() => this._focusVisible.set(false));
289
292
  const releaseModality = this.inputModality.retain();
290
293
  this.el.nativeElement.addEventListener('focus', onFocus);
291
294
  this.el.nativeElement.addEventListener('blur', onBlur);
@@ -3105,7 +3108,9 @@ function releaseInert(el) {
3105
3108
  * Makes everything behind a modal `panel` inert: every child of `<body>`
3106
3109
  * outside the overlay container (except script/style nodes and live
3107
3110
  * regions), plus every overlay wrapper opened before the panel's own — a
3108
- * dialog stacked on a dialog freezes the one below. A body child that
3111
+ * dialog stacked on a dialog freezes the one below. The container itself is
3112
+ * never inerted, even for a panel mounted outside it, so overlays the modal
3113
+ * opens afterwards (dropdowns, pickers) stay usable. A body child that
3109
3114
  * contains the panel (an inline-mounted modal) is left alone, because
3110
3115
  * inerting it would inert the panel itself.
3111
3116
  *
@@ -3117,10 +3122,19 @@ function inertSiblingsOf(panel) {
3117
3122
  const body = panel.ownerDocument?.body;
3118
3123
  if (!body)
3119
3124
  return () => { };
3120
- const container = panel.closest('.kj-overlay-container');
3125
+ // Overlay roots are never inerted, whether or not the panel lives in one.
3126
+ // An inline-mounted (`inPlace`) modal sits outside the root, yet the
3127
+ // overlays it opens — a select, combobox or date picker dropdown — mount
3128
+ // into it after the modal opened: inerting the root would freeze them. The
3129
+ // wrappers open at this point (below the modal) are frozen one by one
3130
+ // instead; later ones stay live.
3131
+ const containers = Array.from(body.children).filter(isOverlayContainer);
3132
+ const own = panel.closest('.kj-overlay-container');
3133
+ if (own && !containers.includes(own))
3134
+ containers.push(own);
3121
3135
  const targets = [];
3122
3136
  for (const child of Array.from(body.children)) {
3123
- if (child === container)
3137
+ if (isOverlayContainer(child))
3124
3138
  continue;
3125
3139
  if (child.contains(panel))
3126
3140
  continue;
@@ -3130,12 +3144,13 @@ function inertSiblingsOf(panel) {
3130
3144
  continue;
3131
3145
  targets.push(child);
3132
3146
  }
3133
- if (container) {
3134
- const own = Array.from(container.children).find((w) => w.contains(panel));
3147
+ for (const container of containers) {
3148
+ const ownWrapper = Array.from(container.children).find((w) => w.contains(panel));
3135
3149
  for (const wrapper of Array.from(container.children)) {
3136
- if (wrapper === own)
3150
+ if (wrapper === ownWrapper)
3137
3151
  continue;
3138
- const below = !own || !!(wrapper.compareDocumentPosition(own) & Node.DOCUMENT_POSITION_FOLLOWING);
3152
+ const below = !ownWrapper ||
3153
+ !!(wrapper.compareDocumentPosition(ownWrapper) & Node.DOCUMENT_POSITION_FOLLOWING);
3139
3154
  if (below)
3140
3155
  targets.push(wrapper);
3141
3156
  }
@@ -3151,6 +3166,10 @@ function inertSiblingsOf(panel) {
3151
3166
  releaseInert(el);
3152
3167
  };
3153
3168
  }
3169
+ /** Whether `el` is an overlay root (`getOverlayContainer()`'s element, or a look-alike). */
3170
+ function isOverlayContainer(el) {
3171
+ return (el.hasAttribute('data-kj-overlay-container') || el.classList.contains('kj-overlay-container'));
3172
+ }
3154
3173
 
3155
3174
  /**
3156
3175
  * Mount strategy for declarative overlays whose panel is rendered inline
@@ -4515,6 +4534,9 @@ const read$2 = (v, fallback) => {
4515
4534
  * do not bubble, and elements with `display: contents` (e.g. `<kj-button>`)
4516
4535
  * never receive these events because they have no rendered box. Walk to the
4517
4536
  * first descendant with a layout box.
4537
+ *
4538
+ * Measures, so it forces a layout: call it only on a hover, never on attach —
4539
+ * a table with a hover trigger per row would otherwise lay out once per row.
4518
4540
  */
4519
4541
  const effectiveHoverTarget = (el) => {
4520
4542
  if (!el.ownerDocument?.defaultView)
@@ -4542,7 +4564,9 @@ function onHover(initialOpts = {}) {
4542
4564
  let openTimer = 0, closeTimer = 0;
4543
4565
  let onEnter = null;
4544
4566
  let onLeave = null;
4545
- let listenTarget = null;
4567
+ let onFirstOver = null;
4568
+ let triggerTarget = null;
4569
+ let boxTarget = null;
4546
4570
  let panelTarget = null;
4547
4571
  const cancelClose = () => {
4548
4572
  if (closeTimer) {
@@ -4584,11 +4608,12 @@ function onHover(initialOpts = {}) {
4584
4608
  const trigger = ctx.triggerEl();
4585
4609
  if (!trigger || onEnter)
4586
4610
  return;
4587
- listenTarget = effectiveHoverTarget(trigger);
4588
4611
  onEnter = () => {
4589
4612
  cancelClose();
4590
4613
  wirePanel();
4591
- if (ctx?.isOpen())
4614
+ // The first hover can reach here twice (the bubbled `pointerover` that
4615
+ // resolves the box target, then that target's own `pointerenter`).
4616
+ if (ctx?.isOpen() || openTimer)
4592
4617
  return;
4593
4618
  openTimer = setTimeout(() => {
4594
4619
  openTimer = 0;
@@ -4602,8 +4627,27 @@ function onHover(initialOpts = {}) {
4602
4627
  wirePanel();
4603
4628
  scheduleClose();
4604
4629
  };
4605
- listenTarget.addEventListener('pointerenter', onEnter);
4606
- listenTarget.addEventListener('pointerleave', onLeave);
4630
+ // Nothing is measured here. The trigger listens directly (enough when it
4631
+ // has a box); a `display: contents` trigger only sees the bubbled
4632
+ // `pointerover` of a descendant, so the first one resolves — and measures,
4633
+ // once — the descendant that actually receives enter / leave.
4634
+ onFirstOver = () => {
4635
+ if (!onEnter || !onLeave || !triggerTarget)
4636
+ return;
4637
+ triggerTarget.removeEventListener('pointerover', onFirstOver);
4638
+ onFirstOver = null;
4639
+ const target = effectiveHoverTarget(triggerTarget);
4640
+ if (target !== triggerTarget) {
4641
+ boxTarget = target;
4642
+ target.addEventListener('pointerenter', onEnter);
4643
+ target.addEventListener('pointerleave', onLeave);
4644
+ }
4645
+ onEnter();
4646
+ };
4647
+ triggerTarget = trigger;
4648
+ trigger.addEventListener('pointerenter', onEnter);
4649
+ trigger.addEventListener('pointerleave', onLeave);
4650
+ trigger.addEventListener('pointerover', onFirstOver);
4607
4651
  };
4608
4652
  return {
4609
4653
  ariaHasPopup: null,
@@ -4618,10 +4662,16 @@ function onHover(initialOpts = {}) {
4618
4662
  onOpen() { },
4619
4663
  onClose() { },
4620
4664
  detach() {
4621
- if (listenTarget && onEnter)
4622
- listenTarget.removeEventListener('pointerenter', onEnter);
4623
- if (listenTarget && onLeave)
4624
- listenTarget.removeEventListener('pointerleave', onLeave);
4665
+ for (const el of [triggerTarget, boxTarget]) {
4666
+ if (!el)
4667
+ continue;
4668
+ if (onEnter)
4669
+ el.removeEventListener('pointerenter', onEnter);
4670
+ if (onLeave)
4671
+ el.removeEventListener('pointerleave', onLeave);
4672
+ }
4673
+ if (triggerTarget && onFirstOver)
4674
+ triggerTarget.removeEventListener('pointerover', onFirstOver);
4625
4675
  if (panelTarget) {
4626
4676
  panelTarget.removeEventListener('pointerenter', cancelClose);
4627
4677
  panelTarget.removeEventListener('pointerleave', scheduleClose);
@@ -4630,8 +4680,8 @@ function onHover(initialOpts = {}) {
4630
4680
  clearTimeout(openTimer);
4631
4681
  if (closeTimer)
4632
4682
  clearTimeout(closeTimer);
4633
- onEnter = onLeave = null;
4634
- listenTarget = null;
4683
+ onEnter = onLeave = onFirstOver = null;
4684
+ triggerTarget = boxTarget = null;
4635
4685
  panelTarget = null;
4636
4686
  openTimer = closeTimer = 0;
4637
4687
  toggle = null;
@@ -22495,7 +22545,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
22495
22545
  }]
22496
22546
  }], propDecorators: { kjCell: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjCell", required: true }] }] } });
22497
22547
 
22498
- /** `Node.DOCUMENT_POSITION_PRECEDING` — inlined so the DOM-order sort also runs on the server. */
22548
+ /** `Node.DOCUMENT_POSITION_PRECEDING`, inlined so the module never touches the `Node` global. */
22499
22549
  const DOCUMENT_POSITION_PRECEDING = 2;
22500
22550
  /** Text-entry controls whose own arrow / Home / End handling must win over grid navigation. */
22501
22551
  const TEXT_ENTRY_SELECTOR = 'input:not([type="button"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="reset"]), textarea, select, [contenteditable]:not([contenteditable="false"])';
@@ -22533,23 +22583,32 @@ class KjTableKeyboardNav {
22533
22583
  /** Id of the cell the user focused last; survives the cell leaving the DOM. */
22534
22584
  activeId = signal(null, /* @ts-ignore */
22535
22585
  ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
22586
+ /** Tab stop resolved after the last render (see {@link tabStopId}). */
22587
+ resolvedStopId = signal(null, /* @ts-ignore */
22588
+ ...(ngDevMode ? [{ debugName: "resolvedStopId" }] : /* istanbul ignore next */ []));
22536
22589
  /**
22537
22590
  * Id of the cell that owns `tabindex="0"`: the last focused cell while it
22538
22591
  * is rendered, else the first rendered cell in DOM order.
22592
+ *
22593
+ * Every cell reads this, so it must not touch the `cells` query: each
22594
+ * stamped cell view dirties the query, and a per-cell read would re-collect
22595
+ * it and re-sort the DOM once per cell — O(n²) for an n-cell body. It is
22596
+ * resolved once per render instead, after the cells are in the DOM. Until
22597
+ * then (and on the server) no cell is a Tab stop.
22539
22598
  */
22540
- tabStopId = computed(() => {
22541
- const cells = this.cells();
22542
- if (cells.length === 0)
22543
- return null;
22544
- const active = this.activeId();
22545
- if (active !== null && cells.some((c) => c.cellId() === active))
22546
- return active;
22547
- return firstInDomOrder(cells).cellId();
22548
- }, /* @ts-ignore */
22549
- ...(ngDevMode ? [{ debugName: "tabStopId" }] : /* istanbul ignore next */ []));
22599
+ tabStopId = this.resolvedStopId.asReadonly();
22600
+ constructor() {
22601
+ afterRenderEffect(() => {
22602
+ const cells = this.cells();
22603
+ const active = this.activeId();
22604
+ this.resolvedStopId.set(resolveTabStop(cells, active));
22605
+ });
22606
+ }
22550
22607
  /** Record `cellId` as the last focused cell. @param cellId TanStack `cell.id`. */
22551
22608
  setActive(cellId) {
22552
22609
  this.activeId.set(cellId);
22610
+ // A cell that just took focus is rendered: it is the Tab stop right away.
22611
+ this.resolvedStopId.set(cellId);
22553
22612
  }
22554
22613
  onKeyDown(event) {
22555
22614
  if (event.defaultPrevented)
@@ -22651,7 +22710,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
22651
22710
  providers: [{ provide: KJ_TABLE_KEYBOARD_NAV, useExisting: KjTableKeyboardNav }],
22652
22711
  host: { '(keydown)': 'onKeyDown($event)' },
22653
22712
  }]
22654
- }], propDecorators: { cells: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => KjTableCell), { ...{ descendants: true }, isSignal: true }] }] } });
22713
+ }], ctorParameters: () => [], propDecorators: { cells: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => KjTableCell), { ...{ descendants: true }, isSignal: true }] }] } });
22714
+ /** The active cell while it is rendered, else the first rendered cell in DOM order. */
22715
+ function resolveTabStop(cells, active) {
22716
+ if (cells.length === 0)
22717
+ return null;
22718
+ if (active !== null && cells.some((c) => c.cellId() === active))
22719
+ return active;
22720
+ return firstInDomOrder(cells).cellId();
22721
+ }
22655
22722
  /**
22656
22723
  * The first cell in DOM order. Query results follow declaration order, which
22657
22724
  * a row template stamped into several bodies (pinned top, centre, pinned