@antelopejs/dms 0.7.6 → 0.7.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.7.7
4
+
5
+ [compare changes](https://github.com/AntelopeJS/dms/compare/v0.7.6...v0.7.7)
6
+
7
+ ### 🚀 Enhancements
8
+
9
+ - **dms:** Group the jump navigation chips by navGroup ([#200](https://github.com/AntelopeJS/dms/pull/200))
10
+
11
+ ### ❤️ Contributors
12
+
13
+ - Antony Rizzitelli <rizzitelli.antony@pm.me>
14
+
3
15
  ## v0.7.6
4
16
 
5
17
  [compare changes](https://github.com/AntelopeJS/dms/compare/v0.7.5...v0.7.6)
@@ -25,21 +25,36 @@ const emit = defineEmits<{ select: [sectionId: string] }>();
25
25
  const { t } = useI18n();
26
26
  const { processI18n } = useTranslation();
27
27
 
28
- // The side list reads its sections in titled runs (`navGroup`); the row of
29
- // chips has no room for headings and lists them all in one run.
30
- const GROUPS_CLASSES: Record<FormSectionNavVariant, string> = {
28
+ /**
29
+ * How the entries are laid out: the side list, the row of chips, or the
30
+ * chips cut into labelled runs once a section names a `navGroup`.
31
+ */
32
+ type FormSectionNavLayout = FormSectionNavVariant | "chipGroups";
33
+
34
+ // A row of chips with no group keeps its one scrolling line; with groups, the
35
+ // runs wrap, each led by its label.
36
+ const GROUPS_CLASSES: Record<FormSectionNavLayout, string> = {
31
37
  list: "flex flex-col gap-4",
32
38
  chips: "",
39
+ chipGroups: "flex flex-wrap items-center gap-x-5 gap-y-2.5",
33
40
  };
34
41
 
35
- const GROUP_CLASSES: Record<FormSectionNavVariant, string> = {
42
+ const GROUP_CLASSES: Record<FormSectionNavLayout, string> = {
36
43
  list: "grid gap-1",
37
44
  chips: "contents",
45
+ chipGroups: "flex flex-wrap items-center gap-x-2 gap-y-1.5",
38
46
  };
39
47
 
40
- const VARIANT_CLASSES: Record<FormSectionNavVariant, string> = {
48
+ const LIST_CLASSES: Record<FormSectionNavLayout, string> = {
41
49
  list: "flex flex-col gap-0.5",
42
50
  chips: "flex gap-1.5 overflow-x-auto pb-1",
51
+ chipGroups: "flex flex-wrap gap-1.5",
52
+ };
53
+
54
+ const GROUP_LABEL_CLASSES: Record<FormSectionNavLayout, string> = {
55
+ list: "px-2.5",
56
+ chips: "",
57
+ chipGroups: "shrink-0",
43
58
  };
44
59
 
45
60
  const ITEM_CLASSES: Record<FormSectionNavVariant, string> = {
@@ -55,9 +70,12 @@ const ACTIVE_CLASSES: Record<FormSectionNavVariant, string> = {
55
70
 
56
71
  const groupIdPrefix = useId();
57
72
  const groups = computed<FormSectionNavGroup[]>(() =>
58
- props.variant === "list"
59
- ? groupSectionsForNav(props.sections)
60
- : [{ sections: props.sections }],
73
+ groupSectionsForNav(props.sections),
74
+ );
75
+ const layout = computed<FormSectionNavLayout>(() =>
76
+ props.variant === "chips" && groups.value.some((group) => group.label)
77
+ ? "chipGroups"
78
+ : props.variant,
61
79
  );
62
80
 
63
81
  function groupHeadingId(index: number): string {
@@ -79,23 +97,20 @@ function onSelect(event: MouseEvent, sectionId: string): void {
79
97
  </script>
80
98
 
81
99
  <template>
82
- <nav
83
- :aria-label="t('dms.form.sections.nav')"
84
- :class="GROUPS_CLASSES[props.variant]"
85
- >
100
+ <nav :aria-label="t('dms.form.sections.nav')" :class="GROUPS_CLASSES[layout]">
86
101
  <div
87
102
  v-for="(group, groupIndex) in groups"
88
103
  :key="group.sections[0]?.id ?? groupIndex"
89
- :class="GROUP_CLASSES[props.variant]"
104
+ :class="GROUP_CLASSES[layout]"
90
105
  >
91
106
  <DmsEyebrow
92
107
  v-if="group.label"
93
108
  :id="groupHeadingId(groupIndex)"
94
109
  :label="processI18n(group.label)"
95
- class="px-2.5"
110
+ :class="GROUP_LABEL_CLASSES[layout]"
96
111
  />
97
112
  <ul
98
- :class="VARIANT_CLASSES[props.variant]"
113
+ :class="LIST_CLASSES[layout]"
99
114
  :aria-labelledby="group.label ? groupHeadingId(groupIndex) : undefined"
100
115
  >
101
116
  <li v-for="section in group.sections" :key="section.id">
@@ -54,7 +54,7 @@ export interface FormSection {
54
54
  label: string;
55
55
  description?: string;
56
56
  icon?: string;
57
- /** Heading of the side navigation the section's entry sits under. */
57
+ /** Heading of the navigation (side list or chips) the section's entry sits under. */
58
58
  navGroup?: string;
59
59
  fieldIds: string[];
60
60
  }
@@ -1,8 +1,8 @@
1
1
  // @vitest-environment jsdom
2
2
  /**
3
- * A long sectioned form groups its side navigation under headings: the
4
- * sections naming the same `navGroup` one after the other share one. The
5
- * row of chips lists every section without them.
3
+ * A long sectioned form groups its navigation under headings: the sections
4
+ * naming the same `navGroup` one after the other share one, in the side list
5
+ * and in the row of chips alike. Without a group, the chips stay one row.
6
6
  */
7
7
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
8
  import { type App, createApp, defineComponent, h, nextTick } from "vue";
@@ -37,7 +37,17 @@ const layout = layoutSections(
37
37
  let app: App | undefined;
38
38
  let host: HTMLDivElement;
39
39
 
40
- async function mount(variant: "list" | "chips") {
40
+ const plainLayout = layoutSections(
41
+ [field("a"), field("b")],
42
+ [
43
+ { id: "a", label: "A", fieldIds: ["a"] },
44
+ { id: "b", label: "B", fieldIds: ["b"] },
45
+ ],
46
+ );
47
+
48
+ type NavProps = Record<string, unknown>;
49
+
50
+ async function mount(variant: "list" | "chips", props: NavProps = {}) {
41
51
  app = createApp({
42
52
  render: () =>
43
53
  h(FormSectionNav, {
@@ -45,6 +55,7 @@ async function mount(variant: "list" | "chips") {
45
55
  sections: layout.sections,
46
56
  states: {},
47
57
  anchorId: (id: string) => `form-${id}`,
58
+ ...props,
48
59
  }),
49
60
  });
50
61
  app.component("UIcon", defineComponent({ render: () => h("i") }));
@@ -82,14 +93,7 @@ describe("groupSectionsForNav", () => {
82
93
  });
83
94
 
84
95
  it("keeps a form without groups in one run", () => {
85
- const plain = layoutSections(
86
- [field("a"), field("b")],
87
- [
88
- { id: "a", label: "A", fieldIds: ["a"] },
89
- { id: "b", label: "B", fieldIds: ["b"] },
90
- ],
91
- );
92
- expect(groupSectionsForNav(plain.sections)).toHaveLength(1);
96
+ expect(groupSectionsForNav(plainLayout.sections)).toHaveLength(1);
93
97
  });
94
98
  });
95
99
 
@@ -106,11 +110,68 @@ describe("the side navigation", () => {
106
110
  expect(headings).toEqual(["t($billing)", "Legal"]);
107
111
  expect(lists[1]!.querySelectorAll("li")).toHaveLength(2);
108
112
  });
113
+ });
109
114
 
110
- it("lists the chips in one row, without headings", async () => {
115
+ function labelledRuns(): [string | undefined, string[]][] {
116
+ return [...host.querySelectorAll("ul")].map((list) => {
117
+ const id = list.getAttribute("aria-labelledby");
118
+ const label = id
119
+ ? document.getElementById(id)?.textContent?.trim()
120
+ : undefined;
121
+ const chips = [...list.querySelectorAll("a")].map(
122
+ (chip) => chip.textContent?.trim() ?? "",
123
+ );
124
+ return [label, chips];
125
+ });
126
+ }
127
+
128
+ describe("the row of chips", () => {
129
+ it("leads each run of chips with its group's label", async () => {
111
130
  await mount("chips");
131
+ expect(labelledRuns()).toEqual([
132
+ [undefined, ["Plan"]],
133
+ ["t($billing)", ["Dunning", "Tax"]],
134
+ ["Legal", ["Terms"]],
135
+ ]);
136
+ });
137
+
138
+ it("wraps the runs instead of scrolling one line", async () => {
139
+ await mount("chips");
140
+ const nav = host.querySelector("nav")!;
141
+ expect(nav.classList.contains("flex-wrap")).toBe(true);
142
+ for (const list of host.querySelectorAll("ul")) {
143
+ expect(list.classList.contains("overflow-x-auto")).toBe(false);
144
+ }
145
+ });
146
+
147
+ it("keeps a form without groups in one scrolling row", async () => {
148
+ await mount("chips", { sections: plainLayout.sections });
112
149
  expect(host.querySelectorAll("ul")).toHaveLength(1);
113
- expect(host.querySelectorAll("li")).toHaveLength(4);
150
+ expect(host.querySelectorAll("li")).toHaveLength(2);
114
151
  expect(host.querySelector("[aria-labelledby]")).toBeNull();
152
+ expect(host.querySelector("p")).toBeNull();
153
+ expect(host.querySelector("ul")!.className).toBe(
154
+ "flex gap-1.5 overflow-x-auto pb-1",
155
+ );
156
+ });
157
+
158
+ it("links each chip to its section and reports the one chosen", async () => {
159
+ const selected: string[] = [];
160
+ await mount("chips", {
161
+ activeId: "tax",
162
+ onSelect: (id: string) => selected.push(id),
163
+ });
164
+ const chips = [...host.querySelectorAll("a")];
165
+ expect(chips.map((chip) => chip.getAttribute("href"))).toEqual([
166
+ "#form-plan",
167
+ "#form-dunning",
168
+ "#form-tax",
169
+ "#form-terms",
170
+ ]);
171
+ expect(
172
+ host.querySelector("[aria-current='location']")?.textContent?.trim(),
173
+ ).toBe("Tax");
174
+ chips[3]!.click();
175
+ expect(selected).toEqual(["terms"]);
115
176
  });
116
177
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/AntelopeJS/dms.git"
@@ -58,7 +58,8 @@ For the framework underneath (the core, `ajs` CLI, interfaces as versioned contr
58
58
  A `TableView` over a shared controller hides a column on one page with
59
59
  `columns: { hidden: ["workspace"] }` (as `isVisible: false`, for that table view only) — no view
60
60
  just to hide it.
61
- A long sectioned `Form` groups its side navigation with `FormSection.navGroup` ("Billing", "Legal").
61
+ A long sectioned `Form` groups its navigation with `FormSection.navGroup` ("Billing", "Legal"),
62
+ in the side list and in the `sectionNav: "jump"` chips alike (a form whose side column holds a preview).
62
63
  `TopListCard({ actions })` puts link buttons in its head ("Catalogue →"), and `Tab({ fit: true })`
63
64
  keeps a pill tab list to its tabs' width.
64
65
  A `KeyValueList` row takes `copy: true` (a copy button for its shown value) or `copyValue`