@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.global.js CHANGED
@@ -4434,7 +4434,7 @@ var zx = (() => {
4434
4434
  if (matchWidth) floating.style.minWidth = `${anchorRect.width}px`;
4435
4435
  const floatingRect = floating.getBoundingClientRect();
4436
4436
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
4437
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
4437
+ const viewportHeight2 = document.documentElement.clientHeight || window.innerHeight;
4438
4438
  let actualPlacement = placement;
4439
4439
  const side = placementSide(placement);
4440
4440
  const bottomY = anchorRect.bottom + offset;
@@ -4442,9 +4442,9 @@ var zx = (() => {
4442
4442
  const rightX = anchorRect.right + offset;
4443
4443
  const leftX = anchorRect.left - offset - floatingRect.width;
4444
4444
  if (flip) {
4445
- if (side === "bottom" && bottomY + floatingRect.height > viewportHeight && topY >= 0) {
4445
+ if (side === "bottom" && bottomY + floatingRect.height > viewportHeight2 && topY >= 0) {
4446
4446
  actualPlacement = flipPlacement(placement, "bottom", "top");
4447
- } else if (side === "top" && topY < 0 && bottomY + floatingRect.height <= viewportHeight) {
4447
+ } else if (side === "top" && topY < 0 && bottomY + floatingRect.height <= viewportHeight2) {
4448
4448
  actualPlacement = flipPlacement(placement, "top", "bottom");
4449
4449
  } else if (side === "right" && rightX + floatingRect.width > viewportWidth && leftX >= 0) {
4450
4450
  actualPlacement = flipPlacement(placement, "right", "left");
@@ -4470,7 +4470,7 @@ var zx = (() => {
4470
4470
  else top = anchorRect.top + (anchorRect.height - floatingRect.height) / 2;
4471
4471
  }
4472
4472
  floating.style.left = `${Math.round(clamp(left, 0, Math.max(0, viewportWidth - floatingRect.width)))}px`;
4473
- floating.style.top = `${Math.round(clamp(top, 0, Math.max(0, viewportHeight - floatingRect.height)))}px`;
4473
+ floating.style.top = `${Math.round(clamp(top, 0, Math.max(0, viewportHeight2 - floatingRect.height)))}px`;
4474
4474
  }
4475
4475
  function placementSide(placement) {
4476
4476
  return (
@@ -35471,7 +35471,11 @@ var zx = (() => {
35471
35471
  variant: "outlined",
35472
35472
  loadingCount: 6,
35473
35473
  headingLevel: 3,
35474
- label: null
35474
+ label: null,
35475
+ keyboard: "tabs",
35476
+ collapsibleGroups: false,
35477
+ collapsedGroups: [],
35478
+ renderGroupHeading: null
35475
35479
  };
35476
35480
  /**
35477
35481
  * Creates or enhances a card view.
@@ -35500,6 +35504,19 @@ var zx = (() => {
35500
35504
  this._cardVariant = normalizeCardVariant(options.variant);
35501
35505
  this._cardLoadingCount = normalizeLoadingCount3(options.loadingCount);
35502
35506
  this._cardHeadingLevel = normalizeHeadingLevel5(options.headingLevel);
35507
+ this._cardRoving = options.keyboard === "roving";
35508
+ this._cardIdPrefix = uid("zx-card-view");
35509
+ this._cardCollapsed = new Set(normalizeGroupOrder(options.collapsedGroups));
35510
+ this._cardCurrent = null;
35511
+ this._cardCurrentIndex = 0;
35512
+ this._cardTabStop = null;
35513
+ this._cardStickyX = null;
35514
+ this._cardMoving = false;
35515
+ this._cardHeadersById = /* @__PURE__ */ new Map();
35516
+ this._cardGroupOfId = /* @__PURE__ */ new Map();
35517
+ this._cardSectionIds = /* @__PURE__ */ new Map();
35518
+ this._cardIdOfNode = /* @__PURE__ */ new WeakMap();
35519
+ if (this._cardRoving) root.dataset.keyboard = "roving";
35503
35520
  try {
35504
35521
  this._initRecordView(root);
35505
35522
  root.setAttribute("aria-label", String(options.label || this.msg("cardView.label")));
@@ -35524,10 +35541,22 @@ var zx = (() => {
35524
35541
  /** @type {MouseEvent} */
35525
35542
  event
35526
35543
  ));
35527
- this.listen(content, "keydown", (event) => this._cardKeydown(
35528
- /** @type {KeyboardEvent} */
35529
- event
35530
- ));
35544
+ this.listen(content, "keydown", (event) => {
35545
+ if (this._cardRoving) this._cardRovingKeydown(
35546
+ /** @type {KeyboardEvent} */
35547
+ event
35548
+ );
35549
+ else this._cardKeydown(
35550
+ /** @type {KeyboardEvent} */
35551
+ event
35552
+ );
35553
+ });
35554
+ if (this._cardRoving) {
35555
+ this.listen(content, "focusin", (event) => this._cardFocusIn(
35556
+ /** @type {FocusEvent} */
35557
+ event
35558
+ ));
35559
+ }
35531
35560
  this.listen(content, "error", (event) => this._cardPreviewError(event), true);
35532
35561
  this._applyCardLayout();
35533
35562
  this._renderCardSort();
@@ -35546,7 +35575,8 @@ var zx = (() => {
35546
35575
  groupOrder: [...this._cardGroupOrder],
35547
35576
  minCardWidth: this._cardMinWidth,
35548
35577
  maxColumns: this._cardMaxColumns,
35549
- variant: this._cardVariant
35578
+ variant: this._cardVariant,
35579
+ collapsedGroups: [...this._cardCollapsed]
35550
35580
  };
35551
35581
  }
35552
35582
  /**
@@ -35569,9 +35599,36 @@ var zx = (() => {
35569
35599
  if (Object.prototype.hasOwnProperty.call(source, "variant")) {
35570
35600
  this._cardVariant = normalizeCardVariant(source.variant);
35571
35601
  }
35602
+ if (Object.prototype.hasOwnProperty.call(source, "collapsedGroups")) {
35603
+ this._cardCollapsed = new Set(normalizeGroupOrder(source.collapsedGroups));
35604
+ }
35572
35605
  this._applyCardLayout();
35573
35606
  return super.setViewState(source, options);
35574
35607
  }
35608
+ /**
35609
+ * Collapses or expands one group. A collapsed group keeps its heading and hides its cards.
35610
+ * @param {unknown} id Group id, as `groupBy` produces it.
35611
+ * @param {boolean} [collapsed=true] Next state.
35612
+ * @param {{silent?:boolean}} [options={}] Set `silent` to suppress `groupcollapse` and `statechange`.
35613
+ * @returns {this}
35614
+ * @fires CardView#groupcollapse
35615
+ */
35616
+ setGroupCollapsed(id, collapsed = true, options = {}) {
35617
+ const groupId = normalizeGroupId(id);
35618
+ if (this._cardCollapsed.has(groupId) === Boolean(collapsed)) return this;
35619
+ if (collapsed) this._cardCollapsed.add(groupId);
35620
+ else this._cardCollapsed.delete(groupId);
35621
+ this._renderCards();
35622
+ if (!options.silent) {
35623
+ this.emit("groupcollapse", { id: groupId, collapsed: Boolean(collapsed), collapsedGroups: [...this._cardCollapsed] });
35624
+ this.emit("statechange", { state: this.getViewState(), reason: "groupcollapse" });
35625
+ }
35626
+ return this;
35627
+ }
35628
+ /** Returns the collapsed group ids. @returns {string[]} */
35629
+ getCollapsedGroups() {
35630
+ return [...this._cardCollapsed];
35631
+ }
35575
35632
  /** Restores an enhanced target exactly, or removes an owned root. @returns {void} */
35576
35633
  destroy() {
35577
35634
  if (this._cardCleaned) return;
@@ -35641,22 +35698,30 @@ var zx = (() => {
35641
35698
  /** @type {HTMLElement} */
35642
35699
  this.refs.content
35643
35700
  );
35701
+ const documentRef = content.ownerDocument;
35702
+ const activeElement = documentRef?.activeElement ?? null;
35703
+ const hadFocus = Boolean(activeElement && activeElement !== documentRef.body && content.contains(activeElement));
35644
35704
  const active = (
35645
35705
  /** @type {Element|null} */
35646
35706
  content.querySelector(".zx-record-card:focus")
35647
35707
  );
35648
- const activeIndex = active ? Number(
35649
- /** @type {HTMLElement} */
35650
- active.dataset.recordIndex
35651
- ) : -1;
35652
- const activeRecord = activeIndex >= 0 ? this._viewData[activeIndex] : null;
35653
- const activeId = activeRecord ? this._viewRecordId(activeRecord) : null;
35708
+ const activeId = active && this._cardIdOfNode.has(active) ? this._cardIdOfNode.get(active) : null;
35654
35709
  this._cardNodesById = /* @__PURE__ */ new Map();
35710
+ this._cardHeadersById = /* @__PURE__ */ new Map();
35711
+ this._cardGroupOfId = /* @__PURE__ */ new Map();
35712
+ this._cardSectionIds = /* @__PURE__ */ new Map();
35713
+ this._cardTabStop = null;
35714
+ if (this._cardRoving) {
35715
+ content.removeAttribute("role");
35716
+ content.removeAttribute("aria-label");
35717
+ content.removeAttribute("aria-multiselectable");
35718
+ }
35655
35719
  if (
35656
35720
  /** @type {HTMLElement} */
35657
35721
  this.el.dataset.loading === "true"
35658
35722
  ) {
35659
35723
  content.replaceChildren(this._createCardSkeletons());
35724
+ this._syncCardEmptyFocus(hadFocus);
35660
35725
  return;
35661
35726
  }
35662
35727
  const groups = groupCardRecords(
@@ -35668,12 +35733,25 @@ var zx = (() => {
35668
35733
  );
35669
35734
  if (this._viewData.length === 0 && groups.length === 0) {
35670
35735
  content.replaceChildren(this._createCardEmpty());
35736
+ this._syncCardEmptyFocus(hadFocus);
35671
35737
  return;
35672
35738
  }
35673
35739
  const fields = this.getFields();
35674
35740
  const visibleFields = this.getVisibleFields();
35675
35741
  const sections = groups.map((group, groupIndex) => this._createCardGroup(group, fields, visibleFields, groupIndex));
35676
35742
  content.replaceChildren(...sections);
35743
+ if (this._cardRoving) {
35744
+ content.setAttribute("role", "grid");
35745
+ content.setAttribute(
35746
+ "aria-label",
35747
+ /** @type {HTMLElement} */
35748
+ this.el.getAttribute("aria-label") ?? ""
35749
+ );
35750
+ if (this.options.selectable === "multi") content.setAttribute("aria-multiselectable", "true");
35751
+ this._syncCardEmptyFocus(false);
35752
+ this._syncCardRoving(hadFocus);
35753
+ return;
35754
+ }
35677
35755
  if (activeId !== null) {
35678
35756
  const next = this._cardNodesById.get(activeId);
35679
35757
  if (next) queueMicrotask(() => next.isConnected && next.focus());
@@ -35682,14 +35760,25 @@ var zx = (() => {
35682
35760
  /** @private @param {CardRecordGroup} group @param {ViewField[]} fields @param {ViewField[]} visibleFields @param {number} groupIndex @returns {HTMLElement} */
35683
35761
  _createCardGroup(group, fields, visibleFields, groupIndex) {
35684
35762
  const options = this._cardOptions();
35685
- const headingId = group.id === null ? null : `zx-card-view-group-${safeDomPart(group.id)}-${groupIndex}`;
35763
+ const roving = this._cardRoving;
35764
+ const headingId = group.id === null ? null : `${this._cardIdPrefix}-group-${safeDomPart(group.id)}-${groupIndex}`;
35765
+ const collapsible = group.id !== null && Boolean(options.collapsibleGroups);
35766
+ const collapsed = collapsible && this._cardCollapsed.has(
35767
+ /** @type {string} */
35768
+ group.id
35769
+ );
35686
35770
  const list = h("ul", {
35687
35771
  class: "zx-card-view__list",
35688
- ariaLabelledby: headingId ?? void 0
35772
+ id: headingId ? `${headingId}-list` : void 0,
35773
+ role: roving ? group.id === null ? "rowgroup" : "none" : void 0,
35774
+ ariaLabelledby: headingId ?? void 0,
35775
+ hidden: collapsed
35689
35776
  });
35690
35777
  for (const record of group.records) {
35691
- const index = this._viewData.indexOf(record);
35692
35778
  const id = this._viewRecordId(record);
35779
+ if (group.id !== null) this._cardGroupOfId.set(id, group.id);
35780
+ if (collapsed) continue;
35781
+ const index = this._viewData.indexOf(record);
35693
35782
  const card = createRecordCard(record, index, {
35694
35783
  fields,
35695
35784
  visibleFields,
@@ -35706,19 +35795,287 @@ var zx = (() => {
35706
35795
  headingLevel: this._cardHeadingLevel,
35707
35796
  message: (key, ...args) => this.msg(key, ...args)
35708
35797
  });
35798
+ if (roving) this._makeCardRow(card, id);
35709
35799
  this._cardNodesById.set(id, card);
35800
+ this._cardIdOfNode.set(card, id);
35710
35801
  list.append(card);
35711
35802
  }
35712
- if (group.records.length === 0) {
35713
- list.append(h("li", { class: "zx-card-view__group-empty" }, this.msg("cardView.groupEmpty")));
35803
+ if (group.records.length === 0 && !collapsed) {
35804
+ const text4 = this.msg("cardView.groupEmpty");
35805
+ 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));
35714
35806
  }
35715
35807
  if (group.id === null) return list;
35716
- return h(
35717
- "section",
35718
- { class: "zx-card-view__group", ariaLabelledby: headingId },
35719
- h("h2", { class: "zx-card-view__group-heading", id: headingId }, group.label),
35720
- list
35808
+ const header = this._createGroupHeader(
35809
+ group,
35810
+ /** @type {string} */
35811
+ headingId,
35812
+ collapsible,
35813
+ collapsed
35814
+ );
35815
+ const section = h("section", {
35816
+ class: "zx-card-view__group",
35817
+ role: roving ? "rowgroup" : void 0,
35818
+ ariaLabelledby: headingId,
35819
+ dataset: { collapsed: collapsible ? String(collapsed) : null }
35820
+ }, header, list);
35821
+ this._cardSectionIds.set(section, group.id);
35822
+ return section;
35823
+ }
35824
+ /**
35825
+ * Builds a group heading: the label (or `renderGroupHeading` content), inside an APG
35826
+ * disclosure button when the group can collapse.
35827
+ * @private
35828
+ * @param {CardRecordGroup} group Group.
35829
+ * @param {string} headingId Heading id.
35830
+ * @param {boolean} collapsible Whether the group can collapse.
35831
+ * @param {boolean} collapsed Whether it is collapsed.
35832
+ * @returns {HTMLElement}
35833
+ */
35834
+ _createGroupHeader(group, headingId, collapsible, collapsed) {
35835
+ const id = (
35836
+ /** @type {string} */
35837
+ group.id
35838
+ );
35839
+ const render = this._cardOptions().renderGroupHeading;
35840
+ const custom = typeof render === "function" ? render({
35841
+ id,
35842
+ label: group.label,
35843
+ count: group.records.length,
35844
+ records: [...group.records],
35845
+ collapsed,
35846
+ toggle: () => this.setGroupCollapsed(id, !this._cardCollapsed.has(id))
35847
+ }) : null;
35848
+ const label = custom == null ? group.label : isNode5(custom) ? custom : String(custom);
35849
+ const labelNode = h("span", { class: "zx-card-view__group-label" }, label);
35850
+ const roving = this._cardRoving;
35851
+ const content = collapsible ? h("button", {
35852
+ class: "zx-card-view__group-toggle",
35853
+ type: "button",
35854
+ tabindex: roving ? -1 : void 0,
35855
+ ariaExpanded: String(!collapsed),
35856
+ ariaControls: `${headingId}-list`,
35857
+ dataset: { groupToggle: "" }
35858
+ }, icon("chevron-down", { size: 14 }), labelNode) : labelNode;
35859
+ const heading = h("h2", { class: "zx-card-view__group-heading", id: headingId }, content);
35860
+ const header = h("div", {
35861
+ class: "zx-card-view__group-header",
35862
+ role: roving ? "row" : void 0,
35863
+ ariaExpanded: roving && collapsible ? String(!collapsed) : void 0,
35864
+ ariaLabelledby: roving ? headingId : void 0,
35865
+ tabindex: roving && collapsible ? -1 : void 0,
35866
+ dataset: { cardItem: roving && collapsible ? "group" : null }
35867
+ }, h("div", { class: "zx-card-view__group-cell", role: roving ? "rowheader" : void 0 }, heading));
35868
+ if (roving && collapsible) this._cardHeadersById.set(id, header);
35869
+ return header;
35870
+ }
35871
+ /**
35872
+ * Turns a rendered card into a grid row for the roving model: row focus, `aria-selected` when
35873
+ * selectable, and the controls inside it out of the Tab order.
35874
+ * @private
35875
+ * @param {HTMLElement} card Card element.
35876
+ * @param {unknown} id Record id.
35877
+ * @returns {void}
35878
+ */
35879
+ _makeCardRow(card, id) {
35880
+ card.setAttribute("role", "row");
35881
+ card.tabIndex = -1;
35882
+ card.dataset.cardItem = "card";
35883
+ card.removeAttribute("aria-description");
35884
+ card.removeAttribute("aria-keyshortcuts");
35885
+ if (this.options.selectable) card.setAttribute("aria-selected", String(this._isViewSelected(id)));
35886
+ for (const child of card.children) child.setAttribute("role", "gridcell");
35887
+ for (const control of card.querySelectorAll(FOCUSABLE3)) {
35888
+ if (control !== card) control.tabIndex = -1;
35889
+ }
35890
+ }
35891
+ /**
35892
+ * Moves the single Tab stop to the current item after a render — the same record or group by
35893
+ * id; else the heading of the collapsed group that now hides the card; else the item now at its
35894
+ * old position — and focuses it again, without scrolling, when the collection had focus.
35895
+ * @private
35896
+ * @param {boolean} hadFocus Whether focus was inside the collection before the render.
35897
+ * @returns {void}
35898
+ */
35899
+ _syncCardRoving(hadFocus) {
35900
+ const items = this._cardItems();
35901
+ if (items.length === 0) {
35902
+ this._syncCardEmptyFocus(hadFocus);
35903
+ return;
35904
+ }
35905
+ let item = this._cardItemFor(this._cardCurrent);
35906
+ if (!item && this._cardCurrent?.kind === "card") {
35907
+ const groupId = this._cardGroupOfId.get(this._cardCurrent.id);
35908
+ item = groupId === void 0 ? null : this._cardHeadersById.get(groupId) ?? null;
35909
+ }
35910
+ if (!item) item = items[Math.min(Math.max(this._cardCurrentIndex, 0), items.length - 1)];
35911
+ this._setCardTabStop(item, items);
35912
+ if (hadFocus) item.focus({ preventScroll: true });
35913
+ }
35914
+ /**
35915
+ * @private
35916
+ * @param {HTMLElement} item New Tab stop.
35917
+ * @param {HTMLElement[]} [items] Every item, when already collected.
35918
+ * @returns {void}
35919
+ */
35920
+ _setCardTabStop(item, items = this._cardItems()) {
35921
+ for (const candidate of items) if (candidate !== item) candidate.tabIndex = -1;
35922
+ item.tabIndex = 0;
35923
+ this._cardTabStop = item;
35924
+ const key = this._cardItemKey(item);
35925
+ if (key) this._cardCurrent = key;
35926
+ this._cardCurrentIndex = Math.max(0, items.indexOf(item));
35927
+ }
35928
+ /**
35929
+ * While loading or empty there is no card to hold focus: the content takes it rather than
35930
+ * letting it fall to the document, and gives the Tab order its old shape back afterwards.
35931
+ * @private
35932
+ * @param {boolean} hold Whether focus was inside and there is nothing to put it on.
35933
+ * @returns {void}
35934
+ */
35935
+ _syncCardEmptyFocus(hold) {
35936
+ if (!this._cardRoving) return;
35937
+ const content = (
35938
+ /** @type {HTMLElement} */
35939
+ this.refs.content
35940
+ );
35941
+ if (hold) {
35942
+ if (!content.hasAttribute("tabindex")) {
35943
+ content.tabIndex = -1;
35944
+ content.setAttribute("data-empty-focus", "");
35945
+ }
35946
+ content.focus({ preventScroll: true });
35947
+ } else if (content.hasAttribute("data-empty-focus")) {
35948
+ content.removeAttribute("tabindex");
35949
+ content.removeAttribute("data-empty-focus");
35950
+ }
35951
+ }
35952
+ /** @private @returns {HTMLElement[]} Roving items in display order. */
35953
+ _cardItems() {
35954
+ return (
35955
+ /** @type {HTMLElement[]} */
35956
+ [...this.refs.content.querySelectorAll("[data-card-item]")]
35957
+ );
35958
+ }
35959
+ /** @private @param {HTMLElement} item @returns {{kind:'card'|'group', id:unknown}|null} */
35960
+ _cardItemKey(item) {
35961
+ if (item.dataset.cardItem === "group") {
35962
+ for (const [id, header] of this._cardHeadersById) if (header === item) return { kind: "group", id };
35963
+ return null;
35964
+ }
35965
+ return this._cardIdOfNode.has(item) ? { kind: "card", id: this._cardIdOfNode.get(item) } : null;
35966
+ }
35967
+ /** @private @param {{kind:'card'|'group', id:unknown}|null} key @returns {HTMLElement|null} */
35968
+ _cardItemFor(key) {
35969
+ if (!key) return null;
35970
+ if (key.kind === "group") return this._cardHeadersById.get(
35971
+ /** @type {string} */
35972
+ key.id
35973
+ ) ?? null;
35974
+ return (
35975
+ /** @type {HTMLElement|undefined} */
35976
+ this._cardNodesById.get(key.id) ?? null
35977
+ );
35978
+ }
35979
+ /** @private @param {FocusEvent} event @returns {void} */
35980
+ _cardFocusIn(event) {
35981
+ const target = (
35982
+ /** @type {Element|null} */
35983
+ event.target
35984
+ );
35985
+ const item = (
35986
+ /** @type {HTMLElement|null} */
35987
+ target?.closest?.("[data-card-item]") ?? null
35721
35988
  );
35989
+ if (!item || !this.refs.content.contains(item)) return;
35990
+ if (this._cardTabStop !== item) this._setCardTabStop(item);
35991
+ if (!this._cardMoving) this._cardStickyX = item.getBoundingClientRect().left;
35992
+ }
35993
+ /**
35994
+ * The roving keyboard model; see the class description.
35995
+ * @private
35996
+ * @param {KeyboardEvent} event Key event.
35997
+ * @returns {void}
35998
+ */
35999
+ _cardRovingKeydown(event) {
36000
+ const target = (
36001
+ /** @type {Element|null} */
36002
+ event.target
36003
+ );
36004
+ const item = (
36005
+ /** @type {HTMLElement|null} */
36006
+ target?.closest?.("[data-card-item]") ?? null
36007
+ );
36008
+ if (!item || !this.refs.content.contains(item)) return;
36009
+ const onItem = target === item;
36010
+ if (!onItem && target?.matches?.(TEXT_ENTRY)) return;
36011
+ const items = this._cardItems();
36012
+ const index = items.indexOf(item);
36013
+ const kind = item.dataset.cardItem;
36014
+ const key = event.key;
36015
+ const multi = this.options.selectable === "multi";
36016
+ if ((event.ctrlKey || event.metaKey) && (key === "a" || key === "A")) {
36017
+ if (!multi) return;
36018
+ event.preventDefault();
36019
+ this.setSelection(this._cardIdsOf(items));
36020
+ return;
36021
+ }
36022
+ if (onItem && (key === "Enter" || key === " ")) {
36023
+ event.preventDefault();
36024
+ if (kind === "group") {
36025
+ const groupId = (
36026
+ /** @type {string} */
36027
+ this._cardItemKey(item)?.id
36028
+ );
36029
+ this.setGroupCollapsed(groupId, !this._cardCollapsed.has(groupId));
36030
+ return;
36031
+ }
36032
+ const record = this._viewData[Number(item.dataset.recordIndex)];
36033
+ if (!record) return;
36034
+ const id = this._viewRecordId(record);
36035
+ if (key === " " && this.options.selectable) {
36036
+ this.toggleSelection(id);
36037
+ return;
36038
+ }
36039
+ this._activateCard(record, id, Number(item.dataset.recordIndex), event);
36040
+ if (this.options.selectable === "single") this.toggleSelection(id, { selected: true });
36041
+ return;
36042
+ }
36043
+ if (kind === "group" && onItem && (key === "ArrowLeft" || key === "ArrowRight")) {
36044
+ event.preventDefault();
36045
+ const groupId = (
36046
+ /** @type {string} */
36047
+ this._cardItemKey(item)?.id
36048
+ );
36049
+ this.setGroupCollapsed(groupId, key === "ArrowLeft");
36050
+ return;
36051
+ }
36052
+ if (!NAVIGATION_KEYS.has(key)) return;
36053
+ const rects = items.map((candidate) => candidate.getBoundingClientRect());
36054
+ const vertical = key === "ArrowUp" || key === "ArrowDown" || key === "PageUp" || key === "PageDown";
36055
+ const x = vertical ? this._cardStickyX ?? rects[index].left : rects[index].left;
36056
+ const page = cardPageSize(viewportHeight(this.refs.content), rects[index].height);
36057
+ const next = cardGridTarget(rects, index, key, { page, x });
36058
+ if (next === null) return;
36059
+ event.preventDefault();
36060
+ if (next === index) return;
36061
+ const destination = items[next];
36062
+ if (!vertical) this._cardStickyX = rects[next].left;
36063
+ this._cardMoving = true;
36064
+ try {
36065
+ this._setCardTabStop(destination, items);
36066
+ destination.focus({ preventScroll: true });
36067
+ destination.scrollIntoView?.({ block: "nearest", inline: "nearest" });
36068
+ } finally {
36069
+ this._cardMoving = false;
36070
+ }
36071
+ if (event.shiftKey && multi) {
36072
+ const span = items.slice(Math.min(index, next), Math.max(index, next) + 1);
36073
+ this.setSelection([...this.getSelectionIds(), ...this._cardIdsOf(span)]);
36074
+ }
36075
+ }
36076
+ /** @private @param {HTMLElement[]} items @returns {unknown[]} Record ids of the card items. */
36077
+ _cardIdsOf(items) {
36078
+ return items.filter((item) => item.dataset.cardItem === "card").map((item) => this._viewData[Number(item.dataset.recordIndex)]).filter(Boolean).map((record) => this._viewRecordId(record));
35722
36079
  }
35723
36080
  /** @private @returns {HTMLElement} */
35724
36081
  _createCardSkeletons() {
@@ -35749,10 +36106,14 @@ var zx = (() => {
35749
36106
  for (const [id, card] of this._cardNodesById) {
35750
36107
  const selected = this._isViewSelected(id);
35751
36108
  card.dataset.selected = String(selected);
35752
- if (this.options.selectable) card.setAttribute(
35753
- "aria-description",
35754
- this.msg(selected ? "recordCard.selected" : "recordCard.notSelected")
35755
- );
36109
+ if (this._cardRoving) {
36110
+ if (this.options.selectable) card.setAttribute("aria-selected", String(selected));
36111
+ } else if (this.options.selectable) {
36112
+ card.setAttribute(
36113
+ "aria-description",
36114
+ this.msg(selected ? "recordCard.selected" : "recordCard.notSelected")
36115
+ );
36116
+ }
35756
36117
  const checkbox = (
35757
36118
  /** @type {HTMLInputElement|null} */
35758
36119
  card.querySelector("[data-record-selection]")
@@ -35766,6 +36127,16 @@ var zx = (() => {
35766
36127
  /** @type {Element|null} */
35767
36128
  event.target
35768
36129
  );
36130
+ const toggle = (
36131
+ /** @type {HTMLElement|null} */
36132
+ target?.closest?.("[data-group-toggle]")
36133
+ );
36134
+ if (toggle && this.refs.content.contains(toggle)) {
36135
+ const section = toggle.closest(".zx-card-view__group");
36136
+ const id2 = section ? this._cardSectionIds.get(section) : void 0;
36137
+ if (id2 !== void 0) this.setGroupCollapsed(id2, !this._cardCollapsed.has(id2));
36138
+ return;
36139
+ }
35769
36140
  const card = (
35770
36141
  /** @type {HTMLElement|null} */
35771
36142
  target?.closest?.(".zx-record-card")
@@ -35873,6 +36244,58 @@ var zx = (() => {
35873
36244
  );
35874
36245
  }
35875
36246
  };
36247
+ var FOCUSABLE3 = 'a[href],button,input,select,textarea,summary,[tabindex]:not([tabindex="-1"]),[contenteditable="true"]';
36248
+ var TEXT_ENTRY = 'input:not([type="checkbox"]):not([type="radio"]),select,textarea,[contenteditable="true"]';
36249
+ var NAVIGATION_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
36250
+ function cardGridRows(rects, tolerance = 4) {
36251
+ 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);
36252
+ const rows = [];
36253
+ for (const entry of order) {
36254
+ const row2 = rows.at(-1);
36255
+ if (row2 && Math.abs(entry.top - row2.top) <= tolerance) row2.items.push(entry);
36256
+ else rows.push({ top: entry.top, items: [entry] });
36257
+ }
36258
+ return rows.map((row2) => row2.items.sort((a, b) => a.left - b.left).map((entry) => entry.index));
36259
+ }
36260
+ function cardGridTarget(rects, index, key, options = {}) {
36261
+ if (rects.length === 0) return null;
36262
+ const current = Math.min(Math.max(Math.trunc(Number(index) || 0), 0), rects.length - 1);
36263
+ if (key === "Home") return 0;
36264
+ if (key === "End") return rects.length - 1;
36265
+ const rows = cardGridRows(rects);
36266
+ const rowIndex = rows.findIndex((row3) => row3.includes(current));
36267
+ const row2 = rows[rowIndex];
36268
+ if (key === "ArrowLeft" || key === "ArrowRight") {
36269
+ const position2 = row2.indexOf(current) + (key === "ArrowLeft" ? -1 : 1);
36270
+ return position2 < 0 || position2 >= row2.length ? current : row2[position2];
36271
+ }
36272
+ const page = Math.max(1, Math.trunc(Number(options.page) || 10));
36273
+ const step = { ArrowUp: -1, ArrowDown: 1, PageUp: -page, PageDown: page }[key];
36274
+ if (step === void 0) return null;
36275
+ const targetRow = rows[Math.min(Math.max(rowIndex + step, 0), rows.length - 1)];
36276
+ if (targetRow === row2) return current;
36277
+ const x = Number.isFinite(Number(options.x)) ? Number(options.x) : Number(rects[current].left) || 0;
36278
+ let best = targetRow[0];
36279
+ for (const candidate of targetRow) {
36280
+ if (Math.abs((Number(rects[candidate].left) || 0) - x) < Math.abs((Number(rects[best].left) || 0) - x)) best = candidate;
36281
+ }
36282
+ return best;
36283
+ }
36284
+ function cardPageSize(viewport, rowHeight, fallback = 10) {
36285
+ const visible = Number(viewport);
36286
+ const height = Number(rowHeight);
36287
+ if (!(visible > 0) || !(height > 0)) return Math.max(1, Math.trunc(fallback) || 10);
36288
+ return Math.max(1, Math.floor(visible / height) - 1);
36289
+ }
36290
+ function viewportHeight(element) {
36291
+ let node = element.parentElement;
36292
+ while (node && node !== element.ownerDocument.body) {
36293
+ const overflow = getComputedStyle(node).overflowY;
36294
+ if (/(auto|scroll|overlay)/.test(overflow) && node.scrollHeight > node.clientHeight) return node.clientHeight;
36295
+ node = node.parentElement;
36296
+ }
36297
+ return element.ownerDocument.defaultView?.innerHeight ?? 0;
36298
+ }
35876
36299
  function groupCardRecords(records, groupBy2, groupOrder = [], fields = [], ungroupedLabel = "Ungrouped") {
35877
36300
  if (!Array.isArray(records)) throw new TypeError("Card view records must be an array");
35878
36301
  if (groupBy2 == null) return records.length ? [{ id: null, label: "", records: [...records] }] : [];