@zeyos/zx 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/zx.esm.js CHANGED
@@ -4130,7 +4130,7 @@ function updateFallback(anchor, floating, { placement, offset, flip, matchWidth
4130
4130
  if (matchWidth) floating.style.minWidth = `${anchorRect.width}px`;
4131
4131
  const floatingRect = floating.getBoundingClientRect();
4132
4132
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
4133
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
4133
+ const viewportHeight2 = document.documentElement.clientHeight || window.innerHeight;
4134
4134
  let actualPlacement = placement;
4135
4135
  const side = placementSide(placement);
4136
4136
  const bottomY = anchorRect.bottom + offset;
@@ -4138,9 +4138,9 @@ function updateFallback(anchor, floating, { placement, offset, flip, matchWidth
4138
4138
  const rightX = anchorRect.right + offset;
4139
4139
  const leftX = anchorRect.left - offset - floatingRect.width;
4140
4140
  if (flip) {
4141
- if (side === "bottom" && bottomY + floatingRect.height > viewportHeight && topY >= 0) {
4141
+ if (side === "bottom" && bottomY + floatingRect.height > viewportHeight2 && topY >= 0) {
4142
4142
  actualPlacement = flipPlacement(placement, "bottom", "top");
4143
- } else if (side === "top" && topY < 0 && bottomY + floatingRect.height <= viewportHeight) {
4143
+ } else if (side === "top" && topY < 0 && bottomY + floatingRect.height <= viewportHeight2) {
4144
4144
  actualPlacement = flipPlacement(placement, "top", "bottom");
4145
4145
  } else if (side === "right" && rightX + floatingRect.width > viewportWidth && leftX >= 0) {
4146
4146
  actualPlacement = flipPlacement(placement, "right", "left");
@@ -4166,7 +4166,7 @@ function updateFallback(anchor, floating, { placement, offset, flip, matchWidth
4166
4166
  else top = anchorRect.top + (anchorRect.height - floatingRect.height) / 2;
4167
4167
  }
4168
4168
  floating.style.left = `${Math.round(clamp(left, 0, Math.max(0, viewportWidth - floatingRect.width)))}px`;
4169
- floating.style.top = `${Math.round(clamp(top, 0, Math.max(0, viewportHeight - floatingRect.height)))}px`;
4169
+ floating.style.top = `${Math.round(clamp(top, 0, Math.max(0, viewportHeight2 - floatingRect.height)))}px`;
4170
4170
  }
4171
4171
  function placementSide(placement) {
4172
4172
  return (
@@ -35167,7 +35167,11 @@ var CardView = class extends RecordView {
35167
35167
  variant: "outlined",
35168
35168
  loadingCount: 6,
35169
35169
  headingLevel: 3,
35170
- label: null
35170
+ label: null,
35171
+ keyboard: "tabs",
35172
+ collapsibleGroups: false,
35173
+ collapsedGroups: [],
35174
+ renderGroupHeading: null
35171
35175
  };
35172
35176
  /**
35173
35177
  * Creates or enhances a card view.
@@ -35196,6 +35200,19 @@ var CardView = class extends RecordView {
35196
35200
  this._cardVariant = normalizeCardVariant(options.variant);
35197
35201
  this._cardLoadingCount = normalizeLoadingCount3(options.loadingCount);
35198
35202
  this._cardHeadingLevel = normalizeHeadingLevel5(options.headingLevel);
35203
+ this._cardRoving = options.keyboard === "roving";
35204
+ this._cardIdPrefix = uid("zx-card-view");
35205
+ this._cardCollapsed = new Set(normalizeGroupOrder(options.collapsedGroups));
35206
+ this._cardCurrent = null;
35207
+ this._cardCurrentIndex = 0;
35208
+ this._cardTabStop = null;
35209
+ this._cardStickyX = null;
35210
+ this._cardMoving = false;
35211
+ this._cardHeadersById = /* @__PURE__ */ new Map();
35212
+ this._cardGroupOfId = /* @__PURE__ */ new Map();
35213
+ this._cardSectionIds = /* @__PURE__ */ new Map();
35214
+ this._cardIdOfNode = /* @__PURE__ */ new WeakMap();
35215
+ if (this._cardRoving) root.dataset.keyboard = "roving";
35199
35216
  try {
35200
35217
  this._initRecordView(root);
35201
35218
  root.setAttribute("aria-label", String(options.label || this.msg("cardView.label")));
@@ -35220,10 +35237,22 @@ var CardView = class extends RecordView {
35220
35237
  /** @type {MouseEvent} */
35221
35238
  event
35222
35239
  ));
35223
- this.listen(content, "keydown", (event) => this._cardKeydown(
35224
- /** @type {KeyboardEvent} */
35225
- event
35226
- ));
35240
+ this.listen(content, "keydown", (event) => {
35241
+ if (this._cardRoving) this._cardRovingKeydown(
35242
+ /** @type {KeyboardEvent} */
35243
+ event
35244
+ );
35245
+ else this._cardKeydown(
35246
+ /** @type {KeyboardEvent} */
35247
+ event
35248
+ );
35249
+ });
35250
+ if (this._cardRoving) {
35251
+ this.listen(content, "focusin", (event) => this._cardFocusIn(
35252
+ /** @type {FocusEvent} */
35253
+ event
35254
+ ));
35255
+ }
35227
35256
  this.listen(content, "error", (event) => this._cardPreviewError(event), true);
35228
35257
  this._applyCardLayout();
35229
35258
  this._renderCardSort();
@@ -35242,7 +35271,8 @@ var CardView = class extends RecordView {
35242
35271
  groupOrder: [...this._cardGroupOrder],
35243
35272
  minCardWidth: this._cardMinWidth,
35244
35273
  maxColumns: this._cardMaxColumns,
35245
- variant: this._cardVariant
35274
+ variant: this._cardVariant,
35275
+ collapsedGroups: [...this._cardCollapsed]
35246
35276
  };
35247
35277
  }
35248
35278
  /**
@@ -35265,9 +35295,36 @@ var CardView = class extends RecordView {
35265
35295
  if (Object.prototype.hasOwnProperty.call(source, "variant")) {
35266
35296
  this._cardVariant = normalizeCardVariant(source.variant);
35267
35297
  }
35298
+ if (Object.prototype.hasOwnProperty.call(source, "collapsedGroups")) {
35299
+ this._cardCollapsed = new Set(normalizeGroupOrder(source.collapsedGroups));
35300
+ }
35268
35301
  this._applyCardLayout();
35269
35302
  return super.setViewState(source, options);
35270
35303
  }
35304
+ /**
35305
+ * Collapses or expands one group. A collapsed group keeps its heading and hides its cards.
35306
+ * @param {unknown} id Group id, as `groupBy` produces it.
35307
+ * @param {boolean} [collapsed=true] Next state.
35308
+ * @param {{silent?:boolean}} [options={}] Set `silent` to suppress `groupcollapse` and `statechange`.
35309
+ * @returns {this}
35310
+ * @fires CardView#groupcollapse
35311
+ */
35312
+ setGroupCollapsed(id, collapsed = true, options = {}) {
35313
+ const groupId = normalizeGroupId(id);
35314
+ if (this._cardCollapsed.has(groupId) === Boolean(collapsed)) return this;
35315
+ if (collapsed) this._cardCollapsed.add(groupId);
35316
+ else this._cardCollapsed.delete(groupId);
35317
+ this._renderCards();
35318
+ if (!options.silent) {
35319
+ this.emit("groupcollapse", { id: groupId, collapsed: Boolean(collapsed), collapsedGroups: [...this._cardCollapsed] });
35320
+ this.emit("statechange", { state: this.getViewState(), reason: "groupcollapse" });
35321
+ }
35322
+ return this;
35323
+ }
35324
+ /** Returns the collapsed group ids. @returns {string[]} */
35325
+ getCollapsedGroups() {
35326
+ return [...this._cardCollapsed];
35327
+ }
35271
35328
  /** Restores an enhanced target exactly, or removes an owned root. @returns {void} */
35272
35329
  destroy() {
35273
35330
  if (this._cardCleaned) return;
@@ -35337,22 +35394,30 @@ var CardView = class extends RecordView {
35337
35394
  /** @type {HTMLElement} */
35338
35395
  this.refs.content
35339
35396
  );
35397
+ const documentRef = content.ownerDocument;
35398
+ const activeElement = documentRef?.activeElement ?? null;
35399
+ const hadFocus = Boolean(activeElement && activeElement !== documentRef.body && content.contains(activeElement));
35340
35400
  const active = (
35341
35401
  /** @type {Element|null} */
35342
35402
  content.querySelector(".zx-record-card:focus")
35343
35403
  );
35344
- const activeIndex = active ? Number(
35345
- /** @type {HTMLElement} */
35346
- active.dataset.recordIndex
35347
- ) : -1;
35348
- const activeRecord = activeIndex >= 0 ? this._viewData[activeIndex] : null;
35349
- const activeId = activeRecord ? this._viewRecordId(activeRecord) : null;
35404
+ const activeId = active && this._cardIdOfNode.has(active) ? this._cardIdOfNode.get(active) : null;
35350
35405
  this._cardNodesById = /* @__PURE__ */ new Map();
35406
+ this._cardHeadersById = /* @__PURE__ */ new Map();
35407
+ this._cardGroupOfId = /* @__PURE__ */ new Map();
35408
+ this._cardSectionIds = /* @__PURE__ */ new Map();
35409
+ this._cardTabStop = null;
35410
+ if (this._cardRoving) {
35411
+ content.removeAttribute("role");
35412
+ content.removeAttribute("aria-label");
35413
+ content.removeAttribute("aria-multiselectable");
35414
+ }
35351
35415
  if (
35352
35416
  /** @type {HTMLElement} */
35353
35417
  this.el.dataset.loading === "true"
35354
35418
  ) {
35355
35419
  content.replaceChildren(this._createCardSkeletons());
35420
+ this._syncCardEmptyFocus(hadFocus);
35356
35421
  return;
35357
35422
  }
35358
35423
  const groups = groupCardRecords(
@@ -35364,12 +35429,25 @@ var CardView = class extends RecordView {
35364
35429
  );
35365
35430
  if (this._viewData.length === 0 && groups.length === 0) {
35366
35431
  content.replaceChildren(this._createCardEmpty());
35432
+ this._syncCardEmptyFocus(hadFocus);
35367
35433
  return;
35368
35434
  }
35369
35435
  const fields = this.getFields();
35370
35436
  const visibleFields = this.getVisibleFields();
35371
35437
  const sections = groups.map((group, groupIndex) => this._createCardGroup(group, fields, visibleFields, groupIndex));
35372
35438
  content.replaceChildren(...sections);
35439
+ if (this._cardRoving) {
35440
+ content.setAttribute("role", "grid");
35441
+ content.setAttribute(
35442
+ "aria-label",
35443
+ /** @type {HTMLElement} */
35444
+ this.el.getAttribute("aria-label") ?? ""
35445
+ );
35446
+ if (this.options.selectable === "multi") content.setAttribute("aria-multiselectable", "true");
35447
+ this._syncCardEmptyFocus(false);
35448
+ this._syncCardRoving(hadFocus);
35449
+ return;
35450
+ }
35373
35451
  if (activeId !== null) {
35374
35452
  const next = this._cardNodesById.get(activeId);
35375
35453
  if (next) queueMicrotask(() => next.isConnected && next.focus());
@@ -35378,14 +35456,25 @@ var CardView = class extends RecordView {
35378
35456
  /** @private @param {CardRecordGroup} group @param {ViewField[]} fields @param {ViewField[]} visibleFields @param {number} groupIndex @returns {HTMLElement} */
35379
35457
  _createCardGroup(group, fields, visibleFields, groupIndex) {
35380
35458
  const options = this._cardOptions();
35381
- const headingId = group.id === null ? null : `zx-card-view-group-${safeDomPart(group.id)}-${groupIndex}`;
35459
+ const roving = this._cardRoving;
35460
+ const headingId = group.id === null ? null : `${this._cardIdPrefix}-group-${safeDomPart(group.id)}-${groupIndex}`;
35461
+ const collapsible = group.id !== null && Boolean(options.collapsibleGroups);
35462
+ const collapsed = collapsible && this._cardCollapsed.has(
35463
+ /** @type {string} */
35464
+ group.id
35465
+ );
35382
35466
  const list = h("ul", {
35383
35467
  class: "zx-card-view__list",
35384
- ariaLabelledby: headingId ?? void 0
35468
+ id: headingId ? `${headingId}-list` : void 0,
35469
+ role: roving ? group.id === null ? "rowgroup" : "none" : void 0,
35470
+ ariaLabelledby: headingId ?? void 0,
35471
+ hidden: collapsed
35385
35472
  });
35386
35473
  for (const record of group.records) {
35387
- const index = this._viewData.indexOf(record);
35388
35474
  const id = this._viewRecordId(record);
35475
+ if (group.id !== null) this._cardGroupOfId.set(id, group.id);
35476
+ if (collapsed) continue;
35477
+ const index = this._viewData.indexOf(record);
35389
35478
  const card = createRecordCard(record, index, {
35390
35479
  fields,
35391
35480
  visibleFields,
@@ -35402,19 +35491,287 @@ var CardView = class extends RecordView {
35402
35491
  headingLevel: this._cardHeadingLevel,
35403
35492
  message: (key, ...args) => this.msg(key, ...args)
35404
35493
  });
35494
+ if (roving) this._makeCardRow(card, id);
35405
35495
  this._cardNodesById.set(id, card);
35496
+ this._cardIdOfNode.set(card, id);
35406
35497
  list.append(card);
35407
35498
  }
35408
- if (group.records.length === 0) {
35409
- list.append(h("li", { class: "zx-card-view__group-empty" }, this.msg("cardView.groupEmpty")));
35499
+ if (group.records.length === 0 && !collapsed) {
35500
+ const text4 = this.msg("cardView.groupEmpty");
35501
+ list.append(roving ? h("li", { class: "zx-card-view__group-empty", role: "row" }, h("span", { role: "gridcell" }, text4)) : h("li", { class: "zx-card-view__group-empty" }, text4));
35410
35502
  }
35411
35503
  if (group.id === null) return list;
35412
- return h(
35413
- "section",
35414
- { class: "zx-card-view__group", ariaLabelledby: headingId },
35415
- h("h2", { class: "zx-card-view__group-heading", id: headingId }, group.label),
35416
- list
35504
+ const header = this._createGroupHeader(
35505
+ group,
35506
+ /** @type {string} */
35507
+ headingId,
35508
+ collapsible,
35509
+ collapsed
35510
+ );
35511
+ const section = h("section", {
35512
+ class: "zx-card-view__group",
35513
+ role: roving ? "rowgroup" : void 0,
35514
+ ariaLabelledby: headingId,
35515
+ dataset: { collapsed: collapsible ? String(collapsed) : null }
35516
+ }, header, list);
35517
+ this._cardSectionIds.set(section, group.id);
35518
+ return section;
35519
+ }
35520
+ /**
35521
+ * Builds a group heading: the label (or `renderGroupHeading` content), inside an APG
35522
+ * disclosure button when the group can collapse.
35523
+ * @private
35524
+ * @param {CardRecordGroup} group Group.
35525
+ * @param {string} headingId Heading id.
35526
+ * @param {boolean} collapsible Whether the group can collapse.
35527
+ * @param {boolean} collapsed Whether it is collapsed.
35528
+ * @returns {HTMLElement}
35529
+ */
35530
+ _createGroupHeader(group, headingId, collapsible, collapsed) {
35531
+ const id = (
35532
+ /** @type {string} */
35533
+ group.id
35534
+ );
35535
+ const render = this._cardOptions().renderGroupHeading;
35536
+ const custom = typeof render === "function" ? render({
35537
+ id,
35538
+ label: group.label,
35539
+ count: group.records.length,
35540
+ records: [...group.records],
35541
+ collapsed,
35542
+ toggle: () => this.setGroupCollapsed(id, !this._cardCollapsed.has(id))
35543
+ }) : null;
35544
+ const label = custom == null ? group.label : isNode5(custom) ? custom : String(custom);
35545
+ const labelNode = h("span", { class: "zx-card-view__group-label" }, label);
35546
+ const roving = this._cardRoving;
35547
+ const content = collapsible ? h("button", {
35548
+ class: "zx-card-view__group-toggle",
35549
+ type: "button",
35550
+ tabindex: roving ? -1 : void 0,
35551
+ ariaExpanded: String(!collapsed),
35552
+ ariaControls: `${headingId}-list`,
35553
+ dataset: { groupToggle: "" }
35554
+ }, icon("chevron-down", { size: 14 }), labelNode) : labelNode;
35555
+ const heading = h("h2", { class: "zx-card-view__group-heading", id: headingId }, content);
35556
+ const header = h("div", {
35557
+ class: "zx-card-view__group-header",
35558
+ role: roving ? "row" : void 0,
35559
+ ariaExpanded: roving && collapsible ? String(!collapsed) : void 0,
35560
+ ariaLabelledby: roving ? headingId : void 0,
35561
+ tabindex: roving && collapsible ? -1 : void 0,
35562
+ dataset: { cardItem: roving && collapsible ? "group" : null }
35563
+ }, h("div", { class: "zx-card-view__group-cell", role: roving ? "rowheader" : void 0 }, heading));
35564
+ if (roving && collapsible) this._cardHeadersById.set(id, header);
35565
+ return header;
35566
+ }
35567
+ /**
35568
+ * Turns a rendered card into a grid row for the roving model: row focus, `aria-selected` when
35569
+ * selectable, and the controls inside it out of the Tab order.
35570
+ * @private
35571
+ * @param {HTMLElement} card Card element.
35572
+ * @param {unknown} id Record id.
35573
+ * @returns {void}
35574
+ */
35575
+ _makeCardRow(card, id) {
35576
+ card.setAttribute("role", "row");
35577
+ card.tabIndex = -1;
35578
+ card.dataset.cardItem = "card";
35579
+ card.removeAttribute("aria-description");
35580
+ card.removeAttribute("aria-keyshortcuts");
35581
+ if (this.options.selectable) card.setAttribute("aria-selected", String(this._isViewSelected(id)));
35582
+ for (const child of card.children) child.setAttribute("role", "gridcell");
35583
+ for (const control of card.querySelectorAll(FOCUSABLE3)) {
35584
+ if (control !== card) control.tabIndex = -1;
35585
+ }
35586
+ }
35587
+ /**
35588
+ * Moves the single Tab stop to the current item after a render — the same record or group by
35589
+ * id; else the heading of the collapsed group that now hides the card; else the item now at its
35590
+ * old position — and focuses it again, without scrolling, when the collection had focus.
35591
+ * @private
35592
+ * @param {boolean} hadFocus Whether focus was inside the collection before the render.
35593
+ * @returns {void}
35594
+ */
35595
+ _syncCardRoving(hadFocus) {
35596
+ const items = this._cardItems();
35597
+ if (items.length === 0) {
35598
+ this._syncCardEmptyFocus(hadFocus);
35599
+ return;
35600
+ }
35601
+ let item = this._cardItemFor(this._cardCurrent);
35602
+ if (!item && this._cardCurrent?.kind === "card") {
35603
+ const groupId = this._cardGroupOfId.get(this._cardCurrent.id);
35604
+ item = groupId === void 0 ? null : this._cardHeadersById.get(groupId) ?? null;
35605
+ }
35606
+ if (!item) item = items[Math.min(Math.max(this._cardCurrentIndex, 0), items.length - 1)];
35607
+ this._setCardTabStop(item, items);
35608
+ if (hadFocus) item.focus({ preventScroll: true });
35609
+ }
35610
+ /**
35611
+ * @private
35612
+ * @param {HTMLElement} item New Tab stop.
35613
+ * @param {HTMLElement[]} [items] Every item, when already collected.
35614
+ * @returns {void}
35615
+ */
35616
+ _setCardTabStop(item, items = this._cardItems()) {
35617
+ for (const candidate of items) if (candidate !== item) candidate.tabIndex = -1;
35618
+ item.tabIndex = 0;
35619
+ this._cardTabStop = item;
35620
+ const key = this._cardItemKey(item);
35621
+ if (key) this._cardCurrent = key;
35622
+ this._cardCurrentIndex = Math.max(0, items.indexOf(item));
35623
+ }
35624
+ /**
35625
+ * While loading or empty there is no card to hold focus: the content takes it rather than
35626
+ * letting it fall to the document, and gives the Tab order its old shape back afterwards.
35627
+ * @private
35628
+ * @param {boolean} hold Whether focus was inside and there is nothing to put it on.
35629
+ * @returns {void}
35630
+ */
35631
+ _syncCardEmptyFocus(hold) {
35632
+ if (!this._cardRoving) return;
35633
+ const content = (
35634
+ /** @type {HTMLElement} */
35635
+ this.refs.content
35636
+ );
35637
+ if (hold) {
35638
+ if (!content.hasAttribute("tabindex")) {
35639
+ content.tabIndex = -1;
35640
+ content.setAttribute("data-empty-focus", "");
35641
+ }
35642
+ content.focus({ preventScroll: true });
35643
+ } else if (content.hasAttribute("data-empty-focus")) {
35644
+ content.removeAttribute("tabindex");
35645
+ content.removeAttribute("data-empty-focus");
35646
+ }
35647
+ }
35648
+ /** @private @returns {HTMLElement[]} Roving items in display order. */
35649
+ _cardItems() {
35650
+ return (
35651
+ /** @type {HTMLElement[]} */
35652
+ [...this.refs.content.querySelectorAll("[data-card-item]")]
35653
+ );
35654
+ }
35655
+ /** @private @param {HTMLElement} item @returns {{kind:'card'|'group', id:unknown}|null} */
35656
+ _cardItemKey(item) {
35657
+ if (item.dataset.cardItem === "group") {
35658
+ for (const [id, header] of this._cardHeadersById) if (header === item) return { kind: "group", id };
35659
+ return null;
35660
+ }
35661
+ return this._cardIdOfNode.has(item) ? { kind: "card", id: this._cardIdOfNode.get(item) } : null;
35662
+ }
35663
+ /** @private @param {{kind:'card'|'group', id:unknown}|null} key @returns {HTMLElement|null} */
35664
+ _cardItemFor(key) {
35665
+ if (!key) return null;
35666
+ if (key.kind === "group") return this._cardHeadersById.get(
35667
+ /** @type {string} */
35668
+ key.id
35669
+ ) ?? null;
35670
+ return (
35671
+ /** @type {HTMLElement|undefined} */
35672
+ this._cardNodesById.get(key.id) ?? null
35673
+ );
35674
+ }
35675
+ /** @private @param {FocusEvent} event @returns {void} */
35676
+ _cardFocusIn(event) {
35677
+ const target = (
35678
+ /** @type {Element|null} */
35679
+ event.target
35680
+ );
35681
+ const item = (
35682
+ /** @type {HTMLElement|null} */
35683
+ target?.closest?.("[data-card-item]") ?? null
35417
35684
  );
35685
+ if (!item || !this.refs.content.contains(item)) return;
35686
+ if (this._cardTabStop !== item) this._setCardTabStop(item);
35687
+ if (!this._cardMoving) this._cardStickyX = item.getBoundingClientRect().left;
35688
+ }
35689
+ /**
35690
+ * The roving keyboard model; see the class description.
35691
+ * @private
35692
+ * @param {KeyboardEvent} event Key event.
35693
+ * @returns {void}
35694
+ */
35695
+ _cardRovingKeydown(event) {
35696
+ const target = (
35697
+ /** @type {Element|null} */
35698
+ event.target
35699
+ );
35700
+ const item = (
35701
+ /** @type {HTMLElement|null} */
35702
+ target?.closest?.("[data-card-item]") ?? null
35703
+ );
35704
+ if (!item || !this.refs.content.contains(item)) return;
35705
+ const onItem = target === item;
35706
+ if (!onItem && target?.matches?.(TEXT_ENTRY)) return;
35707
+ const items = this._cardItems();
35708
+ const index = items.indexOf(item);
35709
+ const kind = item.dataset.cardItem;
35710
+ const key = event.key;
35711
+ const multi = this.options.selectable === "multi";
35712
+ if ((event.ctrlKey || event.metaKey) && (key === "a" || key === "A")) {
35713
+ if (!multi) return;
35714
+ event.preventDefault();
35715
+ this.setSelection(this._cardIdsOf(items));
35716
+ return;
35717
+ }
35718
+ if (onItem && (key === "Enter" || key === " ")) {
35719
+ event.preventDefault();
35720
+ if (kind === "group") {
35721
+ const groupId = (
35722
+ /** @type {string} */
35723
+ this._cardItemKey(item)?.id
35724
+ );
35725
+ this.setGroupCollapsed(groupId, !this._cardCollapsed.has(groupId));
35726
+ return;
35727
+ }
35728
+ const record = this._viewData[Number(item.dataset.recordIndex)];
35729
+ if (!record) return;
35730
+ const id = this._viewRecordId(record);
35731
+ if (key === " " && this.options.selectable) {
35732
+ this.toggleSelection(id);
35733
+ return;
35734
+ }
35735
+ this._activateCard(record, id, Number(item.dataset.recordIndex), event);
35736
+ if (this.options.selectable === "single") this.toggleSelection(id, { selected: true });
35737
+ return;
35738
+ }
35739
+ if (kind === "group" && onItem && (key === "ArrowLeft" || key === "ArrowRight")) {
35740
+ event.preventDefault();
35741
+ const groupId = (
35742
+ /** @type {string} */
35743
+ this._cardItemKey(item)?.id
35744
+ );
35745
+ this.setGroupCollapsed(groupId, key === "ArrowLeft");
35746
+ return;
35747
+ }
35748
+ if (!NAVIGATION_KEYS.has(key)) return;
35749
+ const rects = items.map((candidate) => candidate.getBoundingClientRect());
35750
+ const vertical = key === "ArrowUp" || key === "ArrowDown" || key === "PageUp" || key === "PageDown";
35751
+ const x = vertical ? this._cardStickyX ?? rects[index].left : rects[index].left;
35752
+ const page = cardPageSize(viewportHeight(this.refs.content), rects[index].height);
35753
+ const next = cardGridTarget(rects, index, key, { page, x });
35754
+ if (next === null) return;
35755
+ event.preventDefault();
35756
+ if (next === index) return;
35757
+ const destination = items[next];
35758
+ if (!vertical) this._cardStickyX = rects[next].left;
35759
+ this._cardMoving = true;
35760
+ try {
35761
+ this._setCardTabStop(destination, items);
35762
+ destination.focus({ preventScroll: true });
35763
+ destination.scrollIntoView?.({ block: "nearest", inline: "nearest" });
35764
+ } finally {
35765
+ this._cardMoving = false;
35766
+ }
35767
+ if (event.shiftKey && multi) {
35768
+ const span = items.slice(Math.min(index, next), Math.max(index, next) + 1);
35769
+ this.setSelection([...this.getSelectionIds(), ...this._cardIdsOf(span)]);
35770
+ }
35771
+ }
35772
+ /** @private @param {HTMLElement[]} items @returns {unknown[]} Record ids of the card items. */
35773
+ _cardIdsOf(items) {
35774
+ return items.filter((item) => item.dataset.cardItem === "card").map((item) => this._viewData[Number(item.dataset.recordIndex)]).filter(Boolean).map((record) => this._viewRecordId(record));
35418
35775
  }
35419
35776
  /** @private @returns {HTMLElement} */
35420
35777
  _createCardSkeletons() {
@@ -35445,10 +35802,14 @@ var CardView = class extends RecordView {
35445
35802
  for (const [id, card] of this._cardNodesById) {
35446
35803
  const selected = this._isViewSelected(id);
35447
35804
  card.dataset.selected = String(selected);
35448
- if (this.options.selectable) card.setAttribute(
35449
- "aria-description",
35450
- this.msg(selected ? "recordCard.selected" : "recordCard.notSelected")
35451
- );
35805
+ if (this._cardRoving) {
35806
+ if (this.options.selectable) card.setAttribute("aria-selected", String(selected));
35807
+ } else if (this.options.selectable) {
35808
+ card.setAttribute(
35809
+ "aria-description",
35810
+ this.msg(selected ? "recordCard.selected" : "recordCard.notSelected")
35811
+ );
35812
+ }
35452
35813
  const checkbox = (
35453
35814
  /** @type {HTMLInputElement|null} */
35454
35815
  card.querySelector("[data-record-selection]")
@@ -35462,6 +35823,16 @@ var CardView = class extends RecordView {
35462
35823
  /** @type {Element|null} */
35463
35824
  event.target
35464
35825
  );
35826
+ const toggle = (
35827
+ /** @type {HTMLElement|null} */
35828
+ target?.closest?.("[data-group-toggle]")
35829
+ );
35830
+ if (toggle && this.refs.content.contains(toggle)) {
35831
+ const section = toggle.closest(".zx-card-view__group");
35832
+ const id2 = section ? this._cardSectionIds.get(section) : void 0;
35833
+ if (id2 !== void 0) this.setGroupCollapsed(id2, !this._cardCollapsed.has(id2));
35834
+ return;
35835
+ }
35465
35836
  const card = (
35466
35837
  /** @type {HTMLElement|null} */
35467
35838
  target?.closest?.(".zx-record-card")
@@ -35569,6 +35940,58 @@ var CardView = class extends RecordView {
35569
35940
  );
35570
35941
  }
35571
35942
  };
35943
+ var FOCUSABLE3 = 'a[href],button,input,select,textarea,summary,[tabindex]:not([tabindex="-1"]),[contenteditable="true"]';
35944
+ var TEXT_ENTRY = 'input:not([type="checkbox"]):not([type="radio"]),select,textarea,[contenteditable="true"]';
35945
+ var NAVIGATION_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
35946
+ function cardGridRows(rects, tolerance = 4) {
35947
+ const order = rects.map((rect, index) => ({ index, top: Number(rect.top) || 0, left: Number(rect.left) || 0 })).sort((a, b) => a.top - b.top || a.left - b.left);
35948
+ const rows = [];
35949
+ for (const entry of order) {
35950
+ const row2 = rows.at(-1);
35951
+ if (row2 && Math.abs(entry.top - row2.top) <= tolerance) row2.items.push(entry);
35952
+ else rows.push({ top: entry.top, items: [entry] });
35953
+ }
35954
+ return rows.map((row2) => row2.items.sort((a, b) => a.left - b.left).map((entry) => entry.index));
35955
+ }
35956
+ function cardGridTarget(rects, index, key, options = {}) {
35957
+ if (rects.length === 0) return null;
35958
+ const current = Math.min(Math.max(Math.trunc(Number(index) || 0), 0), rects.length - 1);
35959
+ if (key === "Home") return 0;
35960
+ if (key === "End") return rects.length - 1;
35961
+ const rows = cardGridRows(rects);
35962
+ const rowIndex = rows.findIndex((row3) => row3.includes(current));
35963
+ const row2 = rows[rowIndex];
35964
+ if (key === "ArrowLeft" || key === "ArrowRight") {
35965
+ const position2 = row2.indexOf(current) + (key === "ArrowLeft" ? -1 : 1);
35966
+ return position2 < 0 || position2 >= row2.length ? current : row2[position2];
35967
+ }
35968
+ const page = Math.max(1, Math.trunc(Number(options.page) || 10));
35969
+ const step = { ArrowUp: -1, ArrowDown: 1, PageUp: -page, PageDown: page }[key];
35970
+ if (step === void 0) return null;
35971
+ const targetRow = rows[Math.min(Math.max(rowIndex + step, 0), rows.length - 1)];
35972
+ if (targetRow === row2) return current;
35973
+ const x = Number.isFinite(Number(options.x)) ? Number(options.x) : Number(rects[current].left) || 0;
35974
+ let best = targetRow[0];
35975
+ for (const candidate of targetRow) {
35976
+ if (Math.abs((Number(rects[candidate].left) || 0) - x) < Math.abs((Number(rects[best].left) || 0) - x)) best = candidate;
35977
+ }
35978
+ return best;
35979
+ }
35980
+ function cardPageSize(viewport, rowHeight, fallback = 10) {
35981
+ const visible = Number(viewport);
35982
+ const height = Number(rowHeight);
35983
+ if (!(visible > 0) || !(height > 0)) return Math.max(1, Math.trunc(fallback) || 10);
35984
+ return Math.max(1, Math.floor(visible / height) - 1);
35985
+ }
35986
+ function viewportHeight(element) {
35987
+ let node = element.parentElement;
35988
+ while (node && node !== element.ownerDocument.body) {
35989
+ const overflow = getComputedStyle(node).overflowY;
35990
+ if (/(auto|scroll|overlay)/.test(overflow) && node.scrollHeight > node.clientHeight) return node.clientHeight;
35991
+ node = node.parentElement;
35992
+ }
35993
+ return element.ownerDocument.defaultView?.innerHeight ?? 0;
35994
+ }
35572
35995
  function groupCardRecords(records, groupBy2, groupOrder = [], fields = [], ungroupedLabel = "Ungrouped") {
35573
35996
  if (!Array.isArray(records)) throw new TypeError("Card view records must be an array");
35574
35997
  if (groupBy2 == null) return records.length ? [{ id: null, label: "", records: [...records] }] : [];