@kouji-ui/core 0.12.0 → 0.13.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.
@@ -1254,9 +1254,13 @@ class KjOverlayStack {
1254
1254
  if (e.key !== 'Escape')
1255
1255
  return;
1256
1256
  const top = this.topmost();
1257
- if (!top || !top.opts.closeOnEsc)
1257
+ if (!top)
1258
1258
  return;
1259
- top.opts.onClose('escape');
1259
+ // Handled (closed, or swallowed by an overlay that must be answered):
1260
+ // app-level Escape listeners must not also close what sits behind it.
1261
+ e.preventDefault();
1262
+ if (top.opts.closeOnEsc)
1263
+ top.opts.onClose('escape');
1260
1264
  }
1261
1265
  /** Is `target` a press outside `entry`'s content — a candidate dismissal? */
1262
1266
  isOutside(entry, target) {
@@ -26445,7 +26449,19 @@ class KjSelect {
26445
26449
  * inside a palette, a menu inside a select) answer to that composite,
26446
26450
  * not to this one.
26447
26451
  */
26448
- items = ownListItems(this, this.allItems);
26452
+ _viewItems = signal(null, /* @ts-ignore */
26453
+ ...(ngDevMode ? [{ debugName: "_viewItems" }] : /* istanbul ignore next */ []));
26454
+ viewItems = computed(() => this._viewItems()?.() ?? null, /* @ts-ignore */
26455
+ ...(ngDevMode ? [{ debugName: "viewItems" }] : /* istanbul ignore next */ []));
26456
+ /**
26457
+ * @internal Register rows a wrapper stamps in its OWN view
26458
+ * (`<kj-select [kjOptions]>`). A content query never crosses into a
26459
+ * component's view, so those rows join `items()` only through here.
26460
+ */
26461
+ _setViewItems(items) {
26462
+ this._viewItems.set(items);
26463
+ }
26464
+ items = ownListItems(this, this.allItems, this.viewItems);
26449
26465
  /**
26450
26466
  * Implements `KjListNavigatorConfig.visibleItems`: the items a wrapper has
26451
26467
  * not hidden through `KjListItem.setVisible` (a search filter, a folded
@@ -26987,6 +27003,15 @@ class KjCombobox {
26987
27003
  /** Filter-aware visible items, exposed for KjListNavigatorConfig. */
26988
27004
  visibleItems = computed(() => this.filter.visibleItems(), /* @ts-ignore */
26989
27005
  ...(ngDevMode ? [{ debugName: "visibleItems" }] : /* istanbul ignore next */ []));
27006
+ /**
27007
+ * Text the user typed since the list last closed or a value was picked —
27008
+ * the only text that filters the list. `kjQuery` also carries the picked
27009
+ * row's name (so the field reads as a picker), and filtering by it would
27010
+ * list nothing but the picked row on every later open.
27011
+ */
27012
+ _typed = signal('', /* @ts-ignore */
27013
+ ...(ngDevMode ? [{ debugName: "_typed" }] : /* istanbul ignore next */ []));
27014
+ filterQuery = this._typed.asReadonly();
26990
27015
  /** Public surface read by children. */
26991
27016
  query = this.kjQuery.asReadonly();
26992
27017
  open = this.controller.isOpen;
@@ -27041,7 +27066,7 @@ class KjCombobox {
27041
27066
  constructor() {
27042
27067
  this.filter.bind({
27043
27068
  items: this.items,
27044
- query: this.kjQuery,
27069
+ query: this._typed,
27045
27070
  filterFn: this.adaptedFilter,
27046
27071
  // A windowed wrapper has already filtered the DATA; every rendered
27047
27072
  // row is a match by construction.
@@ -27055,55 +27080,69 @@ class KjCombobox {
27055
27080
  compareBy: this.compareBy,
27056
27081
  });
27057
27082
  effect(() => this.kjQueryChange.emit(this.kjQuery()));
27058
- // Auto-activate first visible item on query change so Enter can
27059
- // commit it without an explicit ArrowDown press. APG combobox 1.2
27060
- // discoverability pattern.
27083
+ // Typed text lasts until the list closes (a pick closes it too).
27061
27084
  effect(() => {
27062
- this.kjQuery();
27063
- const autoFirst = this.kjAutoActivateFirst();
27064
- const nav = this._nav();
27065
- if (!autoFirst || !nav || this._virtual() !== null)
27066
- return;
27067
- untracked(() => {
27068
- const visible = this.filter.visibleItems();
27069
- nav.setActive(visible.length ? visible[0].id : null);
27070
- });
27085
+ if (!this.open())
27086
+ untracked(() => this._typed.set(''));
27071
27087
  });
27072
- // Seed the active item once the navigator attaches (input mounts).
27088
+ // Place the cursor on open, on typing and once the navigator attaches.
27089
+ // Typed text: the first match, so Enter commits it without an ArrowDown
27090
+ // (APG combobox 1.2). No typed text: the picked row when it is listed,
27091
+ // else the first — never the first row while another one is picked, or
27092
+ // two rows read as chosen.
27073
27093
  effect(() => {
27094
+ const typed = this._typed();
27095
+ this.open();
27074
27096
  const nav = this._nav();
27075
- if (!nav || !this.kjAutoActivateFirst() || this._virtual() !== null)
27076
- return;
27077
- if (nav.activeId() !== null)
27097
+ if (!this.kjAutoActivateFirst() || !nav || this._virtual() !== null)
27078
27098
  return;
27079
27099
  untracked(() => {
27080
27100
  const visible = this.filter.visibleItems();
27081
- if (visible.length > 0)
27082
- nav.setActive(visible[0].id);
27101
+ const picked = typed === '' ? visible.find(i => this.isPicked(i)) : undefined;
27102
+ nav.setActive((picked ?? visible[0])?.id ?? null);
27083
27103
  });
27084
27104
  });
27085
- // Seed `kjQuery` from the selected item's label whenever the input
27086
- // would otherwise render blank. Covers the case where `kjValue` is
27087
- // bound up-front (preset) but no query text has been typed yet.
27088
- // A windowed wrapper owns this too: `items()` is a window, so a preset
27089
- // value whose row is scrolled out would never be found here.
27105
+ // Write the picked row's name into the field when the VALUE changes, or
27106
+ // when that row's text first renders (`label()` falls back to the row's
27107
+ // text after its first render). Never because the user emptied the
27108
+ // field: an empty field lists every option. A windowed wrapper owns this
27109
+ // too: `items()` is a window, so a value whose row is scrolled out would
27110
+ // never be found here.
27111
+ let shown = null;
27112
+ let seeded = false;
27090
27113
  effect(() => {
27091
- const items = this.items();
27092
27114
  const v = this.kjValue();
27093
- if (v === null || v === undefined || this._virtual() !== null)
27115
+ if (v === null || v === undefined) {
27116
+ shown = null;
27094
27117
  return;
27095
- if (this.kjQuery() !== '')
27118
+ }
27119
+ if (this._virtual() !== null)
27120
+ return;
27121
+ const label = this.items().find(i => i.value() === v)?.label();
27122
+ if (!label)
27096
27123
  return;
27124
+ if (shown && Object.is(shown.value, v) && shown.label === label)
27125
+ return;
27126
+ shown = { value: v, label };
27127
+ const first = !seeded;
27128
+ seeded = true;
27097
27129
  untracked(() => {
27098
- const match = items.find(i => i.value() === v);
27099
- if (match)
27100
- this.kjQuery.set(match.label());
27130
+ // A consumer-bound `kjQuery` beats the preset value's name on first render.
27131
+ if (first && this.kjQuery() !== '')
27132
+ return;
27133
+ this.kjQuery.set(label);
27101
27134
  });
27102
27135
  });
27103
27136
  }
27137
+ isPicked(item) {
27138
+ const v = item.value();
27139
+ return v !== undefined && this._selection.isSelected(v);
27140
+ }
27141
+ /** Text typed into the input: sets the query, filters the list by it and opens it. */
27104
27142
  setQuery(value) {
27105
27143
  if (this.kjQuery() !== value)
27106
27144
  this.kjQuery.set(value);
27145
+ this._typed.set(value);
27107
27146
  this.controller.open();
27108
27147
  }
27109
27148
  /**
@@ -27337,6 +27376,17 @@ class KjComboboxListbox {
27337
27376
  offset: this.kjOffset,
27338
27377
  matchTriggerWidth: 'min',
27339
27378
  });
27379
+ // Keep the cursor's row in view: `aria-activedescendant` never moves DOM
27380
+ // focus, so nothing else scrolls the list to it (a picked row far down the
27381
+ // list on open, ArrowDown past the last visible row).
27382
+ const doc = inject(DOCUMENT);
27383
+ const injector = inject(Injector);
27384
+ effect(() => {
27385
+ const id = this.ctx.activeId();
27386
+ if (!id || !this.ctx.open())
27387
+ return;
27388
+ afterNextRender(() => doc.getElementById(id)?.scrollIntoView?.({ block: 'nearest' }), { injector });
27389
+ });
27340
27390
  }
27341
27391
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjComboboxListbox, deps: [], target: i0.ɵɵFactoryTarget.Directive });
27342
27392
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjComboboxListbox, isStandalone: true, selector: "[kjComboboxListbox]", inputs: { kjSide: { classPropertyName: "kjSide", publicName: "kjSide", isSignal: true, isRequired: false, transformFunction: null }, kjAlign: { classPropertyName: "kjAlign", publicName: "kjAlign", isSignal: true, isRequired: false, transformFunction: null }, kjOffset: { classPropertyName: "kjOffset", publicName: "kjOffset", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mousedown": "$event.preventDefault()" } }, providers: [