@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 +12 -0
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +30 -15
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +1 -1
- package/frontend-vue/tests/form-section-nav-groups.test.ts +75 -14
- package/package.json +1 -1
- package/skills/dms/SKILL.md +2 -1
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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<
|
|
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
|
|
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.
|
|
59
|
-
|
|
60
|
-
|
|
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[
|
|
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="
|
|
110
|
+
:class="GROUP_LABEL_CLASSES[layout]"
|
|
96
111
|
/>
|
|
97
112
|
<ul
|
|
98
|
-
:class="
|
|
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
|
|
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
|
|
4
|
-
*
|
|
5
|
-
* row of chips
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
package/skills/dms/SKILL.md
CHANGED
|
@@ -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
|
|
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`
|