vectis-ui 0.10.3 → 0.11.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/components/VBadge/VBadge.d.ts +1 -1
- package/dist/components/VButton/VButtonGroup.css +1 -1
- package/dist/components/VButton/VButtonGroup.d.ts +4 -4
- package/dist/components/VButton/VButtonGroup.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VIcon/icons/first_page.d.ts +5 -0
- package/dist/components/VIcon/icons/first_page.js +9 -0
- package/dist/components/VIcon/icons/first_page.js.map +1 -0
- package/dist/components/VIcon/icons/index.d.ts +12 -2
- package/dist/components/VIcon/icons/index.js +70 -66
- package/dist/components/VIcon/icons/index.js.map +1 -1
- package/dist/components/VIcon/icons/last_page.d.ts +5 -0
- package/dist/components/VIcon/icons/last_page.js +9 -0
- package/dist/components/VIcon/icons/last_page.js.map +1 -0
- package/dist/components/VIcon/icons/names.d.ts +2 -2
- package/dist/components/VIcon/icons/names.js +1 -1
- package/dist/components/VIcon/icons/names.js.map +1 -1
- package/dist/components/VMenu/VMenu.d.ts +6 -3
- package/dist/components/VMenu/VMenu.js.map +1 -1
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js +34 -31
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.css +1 -1
- package/dist/components/VMenu/VMenuItem.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js +70 -67
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.css +1 -1
- package/dist/components/VMenu/VMenuPanel.d.ts +7 -0
- package/dist/components/VMenu/VMenuPanel.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/context.d.ts +6 -0
- package/dist/components/VMenu/context.js +4 -1
- package/dist/components/VMenu/context.js.map +1 -1
- package/dist/components/VPagination/VPagination.css +1 -1
- package/dist/components/VPagination/VPagination.d.ts +30 -6
- package/dist/components/VPagination/VPagination.js.map +1 -1
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js +125 -75
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggle.d.ts +5 -5
- package/dist/components/VToggle/VToggle.js.map +1 -1
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/fr.js +2 -0
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/types.d.ts +2 -0
- package/dist/icons.js +36 -34
- package/package.json +1 -1
|
@@ -7,28 +7,30 @@ import { useAriaLabel as a } from "../../composables/useAriaLabel.js";
|
|
|
7
7
|
import o from "../VButton/VButtonGroup.js";
|
|
8
8
|
import { chevron_left as s } from "../VIcon/icons/chevron_left.js";
|
|
9
9
|
import { chevron_right as c } from "../VIcon/icons/chevron_right.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
10
|
+
import { first_page as l } from "../VIcon/icons/first_page.js";
|
|
11
|
+
import { last_page as u } from "../VIcon/icons/last_page.js";
|
|
12
|
+
import { more_horiz as d } from "../VIcon/icons/more_horiz.js";
|
|
13
|
+
import { arrowNavigate as f, navigableItems as p } from "../../utils/arrowNav.js";
|
|
14
|
+
import { clamp as m } from "../../utils/number.js";
|
|
15
|
+
import { resolveMatcher as h } from "../../utils/matcher.js";
|
|
16
|
+
import { Fragment as g, computed as _, createBlock as v, createCommentVNode as y, createElementBlock as b, createTextVNode as x, createVNode as S, defineComponent as C, h as w, mergeModels as T, nextTick as ee, openBlock as E, ref as D, renderList as O, toDisplayString as k, unref as A, useModel as j, withCtx as M } from "vue";
|
|
15
17
|
//#region src/components/VPagination/VPagination.vue?vue&type=script&setup=true&lang.ts
|
|
16
|
-
var
|
|
18
|
+
var N = [
|
|
17
19
|
"aria-label",
|
|
18
20
|
"data-align",
|
|
19
21
|
"data-item-variant",
|
|
20
22
|
"data-controls",
|
|
21
23
|
"data-responsive"
|
|
22
|
-
],
|
|
24
|
+
], P = /*@__PURE__*/ C({
|
|
23
25
|
__name: "VPagination",
|
|
24
|
-
props: /*@__PURE__*/
|
|
26
|
+
props: /*@__PURE__*/ T({
|
|
25
27
|
length: { default: 1 },
|
|
26
28
|
totalVisible: { default: void 0 },
|
|
27
29
|
detached: {
|
|
28
30
|
type: Boolean,
|
|
29
31
|
default: !1
|
|
30
32
|
},
|
|
31
|
-
|
|
33
|
+
bordered: {
|
|
32
34
|
type: Boolean,
|
|
33
35
|
default: !1
|
|
34
36
|
},
|
|
@@ -53,6 +55,14 @@ var j = [
|
|
|
53
55
|
nextIcon: { default: () => c },
|
|
54
56
|
prevText: { default: void 0 },
|
|
55
57
|
nextText: { default: void 0 },
|
|
58
|
+
edgeControls: {
|
|
59
|
+
type: Boolean,
|
|
60
|
+
default: !1
|
|
61
|
+
},
|
|
62
|
+
firstIcon: { default: () => l },
|
|
63
|
+
lastIcon: { default: () => u },
|
|
64
|
+
firstText: { default: void 0 },
|
|
65
|
+
lastText: { default: void 0 },
|
|
56
66
|
disabled: {
|
|
57
67
|
type: Boolean,
|
|
58
68
|
default: !1
|
|
@@ -78,14 +88,14 @@ var j = [
|
|
|
78
88
|
modelValue: { default: 1 },
|
|
79
89
|
modelModifiers: {}
|
|
80
90
|
}),
|
|
81
|
-
emits: /*@__PURE__*/
|
|
82
|
-
setup(s, { expose: c, emit:
|
|
83
|
-
let
|
|
84
|
-
function
|
|
85
|
-
return
|
|
91
|
+
emits: /*@__PURE__*/ T(["navigate"], ["update:modelValue"]),
|
|
92
|
+
setup(s, { expose: c, emit: l }) {
|
|
93
|
+
let u = s, C = l, T = e(), P = a(() => u.label ?? T.value.pagination.label), te = _(() => u.prevText ?? T.value.pagination.previous), ne = _(() => u.nextText ?? T.value.pagination.next), F = _(() => u.firstText ?? T.value.pagination.first), I = _(() => u.lastText ?? T.value.pagination.last), L = j(s, "modelValue"), R = _(() => Math.max(Math.trunc(u.length) || 1, 1)), z = _(() => m(Math.round(L.value) || 1, 1, R.value)), B = _(() => h(u.disabledPages));
|
|
94
|
+
function V(e) {
|
|
95
|
+
return u.pageLabel ? u.pageLabel(e) : T.value.pagination.page(e);
|
|
86
96
|
}
|
|
87
|
-
let
|
|
88
|
-
let e =
|
|
97
|
+
let H = _(() => {
|
|
98
|
+
let e = R.value, t = z.value, n = (n) => ({
|
|
89
99
|
kind: "page",
|
|
90
100
|
key: `page-${n}`,
|
|
91
101
|
page: n,
|
|
@@ -98,8 +108,12 @@ var j = [
|
|
|
98
108
|
}, i = (e) => ({
|
|
99
109
|
kind: "gap",
|
|
100
110
|
key: `gap-${e}`
|
|
101
|
-
}), a = Math.trunc(
|
|
111
|
+
}), a = Math.trunc(u.totalVisible ?? e), o = Number.isNaN(a) ? e : Math.max(a, 1);
|
|
102
112
|
if (o >= e) return r(1, e);
|
|
113
|
+
if (o < 5) {
|
|
114
|
+
let n = m(t - Math.floor((o - 1) / 2), 1, e - o + 1);
|
|
115
|
+
return r(n, n + o - 1);
|
|
116
|
+
}
|
|
103
117
|
let s = t - Math.floor((o - 5) / 2), c = s + (o - 5);
|
|
104
118
|
return s <= 3 ? [
|
|
105
119
|
...r(1, o - 2),
|
|
@@ -117,97 +131,129 @@ var j = [
|
|
|
117
131
|
n(e)
|
|
118
132
|
];
|
|
119
133
|
});
|
|
120
|
-
function
|
|
121
|
-
for (let t =
|
|
134
|
+
function U(e) {
|
|
135
|
+
for (let t = z.value + e; t >= 1 && t <= R.value; t += e) if (!B.value(t)) return t;
|
|
122
136
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
function J(e) {
|
|
128
|
-
return C.href?.(e);
|
|
137
|
+
function W(e) {
|
|
138
|
+
let t = e < 0 ? 1 : R.value;
|
|
139
|
+
for (let n = t; n !== z.value; n -= e) if (!B.value(n)) return n;
|
|
129
140
|
}
|
|
141
|
+
let G = _(() => W(-1)), K = _(() => U(-1)), q = _(() => U(1)), J = _(() => W(1));
|
|
130
142
|
function Y(e) {
|
|
131
|
-
|
|
143
|
+
e !== void 0 && (L.value = m(e, 1, R.value));
|
|
144
|
+
}
|
|
145
|
+
function X(e) {
|
|
146
|
+
return u.href?.(e);
|
|
132
147
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
148
|
+
function re(e) {
|
|
149
|
+
return u.href !== void 0 && (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey);
|
|
150
|
+
}
|
|
151
|
+
function Z(e, t) {
|
|
152
|
+
t === void 0 || re(e) || (C("navigate", t, e), Y(t));
|
|
135
153
|
}
|
|
136
|
-
function
|
|
154
|
+
function ie(e) {
|
|
137
155
|
return e.matches(":disabled, [aria-disabled=\"true\"]");
|
|
138
156
|
}
|
|
139
|
-
async function
|
|
157
|
+
async function ae(e, t) {
|
|
140
158
|
let n = e.currentTarget, r = n === document.activeElement;
|
|
141
|
-
|
|
159
|
+
Z(e, t), r && (await ee(), ie(n) && $.value?.querySelector("[aria-current=\"page\"]")?.focus());
|
|
142
160
|
}
|
|
143
161
|
let Q = ({ side: e }) => {
|
|
144
|
-
let
|
|
162
|
+
let { label: a, icon: o, target: s } = {
|
|
163
|
+
first: {
|
|
164
|
+
label: F.value,
|
|
165
|
+
icon: u.firstIcon,
|
|
166
|
+
target: G.value
|
|
167
|
+
},
|
|
168
|
+
prev: {
|
|
169
|
+
label: te.value,
|
|
170
|
+
icon: u.prevIcon,
|
|
171
|
+
target: K.value
|
|
172
|
+
},
|
|
173
|
+
next: {
|
|
174
|
+
label: ne.value,
|
|
175
|
+
icon: u.nextIcon,
|
|
176
|
+
target: q.value
|
|
177
|
+
},
|
|
178
|
+
last: {
|
|
179
|
+
label: I.value,
|
|
180
|
+
icon: u.lastIcon,
|
|
181
|
+
target: J.value
|
|
182
|
+
}
|
|
183
|
+
}[e], c = u.disabled || s === void 0, l = {
|
|
145
184
|
class: "v-pagination-control",
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
185
|
+
"data-side": e,
|
|
186
|
+
variant: u.itemVariant,
|
|
187
|
+
disabled: c,
|
|
188
|
+
href: X(s ?? z.value),
|
|
189
|
+
rel: u.href && !c ? e : void 0,
|
|
190
|
+
onClick: (e) => ae(e, s)
|
|
191
|
+
}, d = () => w(n, {
|
|
192
|
+
...t(o),
|
|
153
193
|
mirrored: !0
|
|
154
194
|
});
|
|
155
|
-
|
|
156
|
-
...
|
|
157
|
-
label:
|
|
158
|
-
}, d)
|
|
159
|
-
|
|
195
|
+
if (u.controls === "icon") return w(i, {
|
|
196
|
+
...l,
|
|
197
|
+
label: a
|
|
198
|
+
}, d);
|
|
199
|
+
let f = e === "first" || e === "prev";
|
|
200
|
+
return w(r, {
|
|
201
|
+
...l,
|
|
160
202
|
tone: "neutral",
|
|
161
|
-
"aria-label":
|
|
203
|
+
"aria-label": a
|
|
162
204
|
}, {
|
|
163
|
-
...
|
|
164
|
-
default: () =>
|
|
205
|
+
...u.controls === "both" ? { [f ? "start" : "end"]: d } : {},
|
|
206
|
+
default: () => w("span", { class: "v-pagination-control-label" }, a)
|
|
165
207
|
});
|
|
166
|
-
}, $ =
|
|
167
|
-
function
|
|
208
|
+
}, $ = D(null);
|
|
209
|
+
function oe(e) {
|
|
168
210
|
let t = $.value;
|
|
169
|
-
t &&
|
|
211
|
+
t && f(e, t, () => p(t, ".v-pagination-page:not(:disabled, [aria-disabled=\"true\"])"));
|
|
170
212
|
}
|
|
171
213
|
return c({
|
|
172
214
|
focus: (e) => ($.value?.querySelector("[aria-current=\"page\"]:is(button, a[href]):not(:disabled)") ?? $.value?.querySelector(":is(button, a[href]):not(:disabled)"))?.focus(e),
|
|
173
215
|
el: $
|
|
174
|
-
}), (e, t) => (
|
|
216
|
+
}), (e, t) => (E(), b("nav", {
|
|
175
217
|
ref_key: "navEl",
|
|
176
218
|
ref: $,
|
|
177
219
|
class: "v-pagination",
|
|
178
|
-
"aria-label":
|
|
220
|
+
"aria-label": A(P),
|
|
179
221
|
"data-align": s.align,
|
|
180
222
|
"data-item-variant": s.itemVariant,
|
|
181
223
|
"data-controls": s.controls || void 0,
|
|
182
224
|
"data-responsive": s.responsive ? "" : void 0,
|
|
183
|
-
onKeydown:
|
|
184
|
-
}, [
|
|
225
|
+
onKeydown: oe
|
|
226
|
+
}, [S(o, {
|
|
185
227
|
class: "v-pagination-items",
|
|
186
228
|
detached: s.detached,
|
|
187
|
-
|
|
229
|
+
bordered: s.bordered,
|
|
188
230
|
size: s.size,
|
|
189
231
|
compact: s.compact || void 0,
|
|
190
232
|
elevated: s.elevated || void 0
|
|
191
233
|
}, {
|
|
192
|
-
default:
|
|
193
|
-
s.controls ? (
|
|
234
|
+
default: M(() => [
|
|
235
|
+
s.controls && s.edgeControls ? (E(), v(Q, {
|
|
194
236
|
key: 0,
|
|
237
|
+
side: "first"
|
|
238
|
+
})) : y("", !0),
|
|
239
|
+
s.controls ? (E(), v(Q, {
|
|
240
|
+
key: 1,
|
|
195
241
|
side: "prev"
|
|
196
|
-
})) :
|
|
197
|
-
(
|
|
242
|
+
})) : y("", !0),
|
|
243
|
+
(E(!0), b(g, null, O(H.value, (e) => (E(), b(g, { key: e.key }, [e.kind === "page" ? (E(), v(r, {
|
|
198
244
|
key: 0,
|
|
199
245
|
class: "v-pagination-page",
|
|
200
|
-
variant: e.page ===
|
|
201
|
-
tone: e.page ===
|
|
202
|
-
disabled: s.disabled ||
|
|
203
|
-
"aria-label":
|
|
204
|
-
"aria-current": e.page ===
|
|
205
|
-
href:
|
|
246
|
+
variant: e.page === z.value ? s.selectedVariant : s.itemVariant,
|
|
247
|
+
tone: e.page === z.value ? s.tone : "neutral",
|
|
248
|
+
disabled: s.disabled || B.value(e.page),
|
|
249
|
+
"aria-label": V(e.page),
|
|
250
|
+
"aria-current": e.page === z.value ? "page" : void 0,
|
|
251
|
+
href: X(e.page),
|
|
206
252
|
"data-edge": e.edge ? "" : void 0,
|
|
207
253
|
"data-distance": !e.edge && e.distance > 0 ? e.distance : void 0,
|
|
208
|
-
onClick: (t) =>
|
|
254
|
+
onClick: (t) => Z(t, e.page)
|
|
209
255
|
}, {
|
|
210
|
-
default:
|
|
256
|
+
default: M(() => [x(k(e.page), 1)]),
|
|
211
257
|
_: 2
|
|
212
258
|
}, 1032, [
|
|
213
259
|
"variant",
|
|
@@ -219,7 +265,7 @@ var j = [
|
|
|
219
265
|
"data-edge",
|
|
220
266
|
"data-distance",
|
|
221
267
|
"onClick"
|
|
222
|
-
])) : (
|
|
268
|
+
])) : (E(), v(r, {
|
|
223
269
|
key: 1,
|
|
224
270
|
class: "v-pagination-ellipsis",
|
|
225
271
|
"aria-hidden": "true",
|
|
@@ -228,25 +274,29 @@ var j = [
|
|
|
228
274
|
tone: "neutral",
|
|
229
275
|
disabled: ""
|
|
230
276
|
}, {
|
|
231
|
-
start:
|
|
277
|
+
start: M(() => [S(n, { name: A(d) }, null, 8, ["name"])]),
|
|
232
278
|
_: 1
|
|
233
279
|
}, 8, ["variant"]))], 64))), 128)),
|
|
234
|
-
s.controls ? (
|
|
235
|
-
key:
|
|
280
|
+
s.controls ? (E(), v(Q, {
|
|
281
|
+
key: 2,
|
|
236
282
|
side: "next"
|
|
237
|
-
})) :
|
|
283
|
+
})) : y("", !0),
|
|
284
|
+
s.controls && s.edgeControls ? (E(), v(Q, {
|
|
285
|
+
key: 3,
|
|
286
|
+
side: "last"
|
|
287
|
+
})) : y("", !0)
|
|
238
288
|
]),
|
|
239
289
|
_: 1
|
|
240
290
|
}, 8, [
|
|
241
291
|
"detached",
|
|
242
|
-
"
|
|
292
|
+
"bordered",
|
|
243
293
|
"size",
|
|
244
294
|
"compact",
|
|
245
295
|
"elevated"
|
|
246
|
-
])], 40,
|
|
296
|
+
])], 40, N));
|
|
247
297
|
}
|
|
248
298
|
});
|
|
249
299
|
//#endregion
|
|
250
|
-
export {
|
|
300
|
+
export { P as default };
|
|
251
301
|
|
|
252
302
|
//# sourceMappingURL=VPagination.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VPagination.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VPagination/VPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * Native buttons activate pages. JavaScript builds bounded page windows and provides roving\n * keyboard navigation.\n */\nimport { computed, h, nextTick, ref } from 'vue'\nimport type { FunctionalComponent } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { more_horiz as moreHorizIcon } from '../VIcon/icons/more_horiz'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\n\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { clamp } from '../../utils/number'\nimport { resolveMatcher } from '../../utils/matcher'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the pages that are not the current one are drawn. */\nexport type PaginationItemVariant = 'ghost' | 'outline'\n\n/** How the current page is drawn. */\nexport type PaginationSelectedVariant = 'solid' | 'soft' | 'ghost'\n\n/** The colour of the current page. */\nexport type PaginationTone = 'accent' | 'neutral' | 'danger'\n\n/** The height of the pills, from the scale every control shares. */\nexport type PaginationSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\n/** Where the row sits in the width it is given. */\nexport type PaginationAlign = 'start' | 'center' | 'end'\n\n/** Whether the previous and next controls are rendered, and what they show. */\nexport type PaginationControls = false | 'icon' | 'text' | 'both'\n\n/**\n * Which pages cannot be reached, given either as a list of page numbers or as a function\n * answering that question for a page.\n */\nexport type PaginationMatcher = number[] | ((page: number) => boolean)\n\ninterface PaginationProps {\n /**\n * How many pages there are in all. It is 1 by default, which renders a single page: the\n * real count almost always has to be given.\n */\n length?: number\n /**\n * How many slots to render, ellipses counted among them, so the row keeps exactly the same\n * width whichever page is current. Left out, every page is rendered.\n */\n totalVisible?: number\n\n /**\n * Separates the buttons instead of joining them into one segmented control. It is the\n * word VButtonGroup and VToggle use for the same question, in the same direction.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined buttons, so the row reads as one frame rather\n * than as segments, on the terms of VButtonGroup's own `seamless`. It has no effect under\n * `detached`.\n */\n seamless?: boolean\n /**\n * How the pages OTHER than the current one, and the controls, are drawn. It is named for the\n * ITEMS because that is what it paints: on VTabs and VDataTable `variant` names the\n * decoration of the frame instead.\n */\n itemVariant?: PaginationItemVariant\n /**\n * How the current page is drawn, in the row's tone: filled, tinted, or the colour of its\n * text alone. The same choice as VToggle's `selectedVariant`.\n */\n selectedVariant?: PaginationSelectedVariant\n /** The colour the current page takes. The other pages and the controls stay neutral. */\n tone?: PaginationTone\n /** The height of the buttons, from the scale shared by every control. */\n size?: PaginationSize\n /** Takes 4px off the height of every button. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButton's own `elevated`. */\n elevated?: boolean\n /**\n * Where the row sits in the space it is given. It only matters in responsive mode,\n * where the row takes the whole width available.\n */\n align?: PaginationAlign\n\n /**\n * The previous and next buttons on either side of the pages: what they show, or `false` to\n * leave them out.\n */\n controls?: PaginationControls\n /** The icon of the previous control: an icon name, or an explicit render. */\n prevIcon?: IconSource\n /** The icon of the next control: an icon name, or an explicit render. */\n nextIcon?: IconSource\n /**\n * The wording of the previous control, used both as its visible text and as what\n * screen readers announce. It falls back to the design system dictionary.\n */\n prevText?: string\n /**\n * The wording of the next control, used both as its visible text and as what screen\n * readers announce. It falls back to the design system dictionary.\n */\n nextText?: string\n\n /** Makes the whole component unusable. */\n disabled?: boolean\n /**\n * Which pages cannot be reached, as a list or as a function. The previous and next\n * controls step OVER them rather than stopping at one.\n */\n disabledPages?: PaginationMatcher\n\n /**\n * Lets the row shed pages as the space narrows, by asking about its own width. It is\n * off by default, because it makes the row take the full width available.\n */\n responsive?: boolean\n\n /**\n * What screen readers announce for the navigation itself. It falls back to the\n * design system dictionary.\n */\n label?: string\n /**\n * How a page is announced. A pill shows a bare number, which alone means nothing to\n * a screen reader: this is what turns it into \"Page 3\". It falls back to the design\n * system dictionary.\n */\n pageLabel?: (page: number) => string\n /** The address of a page. Left out, they are buttons. */\n href?: (page: number) => string\n}\n\nconst props = withDefaults(defineProps<PaginationProps>(), {\n length: 1,\n totalVisible: undefined,\n detached: false,\n seamless: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n align: 'start',\n controls: 'icon',\n prevIcon: () => chevronLeftIcon,\n nextIcon: () => chevronRightIcon,\n prevText: undefined,\n nextText: undefined,\n disabled: false,\n disabledPages: undefined,\n responsive: false,\n label: undefined,\n pageLabel: undefined,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A page was chosen from the row, a pill or a control, with the click that chose it. It comes\n * before the model changes, so under `href` a single-page application calls\n * `preventDefault()` on the event and hands the address to its router instead of letting the\n * browser load it.\n */\n navigate: [page: number, event: MouseEvent]\n}>()\n\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins; that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.pagination.label)\nconst resolvedPrevText = computed(() => props.prevText ?? m.value.pagination.previous)\nconst resolvedNextText = computed(() => props.nextText ?? m.value.pagination.next)\n\n/** The page being shown, counted from 1. It starts on the first. */\nconst page = defineModel<number>({ default: 1 })\n\n/** One slot in the row: either a page, or the ellipsis standing for those left out. */\ntype PaginationItem =\n | { kind: 'page'; key: string; page: number; edge: boolean; distance: number }\n | { kind: 'gap'; key: string }\n\n// A length or a model that is not a whole number (`NaN` while a page size is still loading,\n// a computed `2.5`) is brought back to a page that exists, or no pill would ever match it.\nconst total = computed(() => Math.max(Math.trunc(props.length) || 1, 1))\nconst currentPage = computed(() => clamp(Math.round(page.value) || 1, 1, total.value))\n\nconst isPageDisabled = computed(() => resolveMatcher(props.disabledPages))\n\n// @a11y\n// A pill shows nothing but a number, which on its own tells a screen reader\n// nothing at all; this is what has it announced as \"Page 3\".\nfunction pageLabelFor(n: number): string {\n return props.pageLabel ? props.pageLabel(n) : m.value.pagination.page(n)\n}\n\n/** Works out which slots the row holds. */\nconst items = computed<PaginationItem[]>(() => {\n const count = total.value\n const current = currentPage.value\n\n const pageItem = (n: number): PaginationItem => ({\n kind: 'page',\n key: `page-${n}`,\n page: n,\n edge: n === 1 || n === count,\n distance: Math.min(Math.abs(n - current), 3),\n })\n const pages = (from: number, to: number): PaginationItem[] => {\n const out: PaginationItem[] = []\n for (let n = from; n <= to; n++) out.push(pageItem(n))\n return out\n }\n // Keyed by the side it stands on, never by the page it follows: there is at most one\n // ellipsis per side, and the left one always comes after the first page while the right\n // one always comes before the last. A key derived from the window would change on every\n // navigation, so the right-hand ellipsis would be destroyed and rebuilt each time the\n // window slid, for a node that never changes.\n const gap = (side: 'start' | 'end'): PaginationItem => ({ kind: 'gap', key: `gap-${side}` })\n\n // With no limit given, nothing is left out. Below five slots there would be no room\n // for the first page, an ellipsis, the current page, another ellipsis and the last,\n // so that is the floor.\n const limit = Math.trunc(props.totalVisible ?? count)\n const visible = Number.isNaN(limit) ? count : Math.max(limit, 5)\n if (visible >= count) return pages(1, count)\n\n const start = current - Math.floor((visible - 5) / 2)\n const end = start + (visible - 5)\n\n // The window is stretched to the bound instead, which both avoids that and keeps the number\n // of slots exactly the same.\n if (start <= 3) return [...pages(1, visible - 2), gap('end'), pageItem(count)]\n if (end >= count - 2) return [pageItem(1), gap('start'), ...pages(count - visible + 3, count)]\n return [pageItem(1), gap('start'), ...pages(start, end), gap('end'), pageItem(count)]\n})\n\n/**\n * Where a control leads: the nearest page in that direction that can actually be reached,\n * disabled ones being stepped over rather than stopped at. Answering `undefined` means there is\n * none left, which disables the control; and that single answer also covers being on the first\n * or the last page, with no separate test for the ends.\n */\nfunction step(direction: -1 | 1): number | undefined {\n for (let n = currentPage.value + direction; n >= 1 && n <= total.value; n += direction) {\n if (!isPageDisabled.value(n)) return n\n }\n return undefined\n}\n\nconst prevTarget = computed(() => step(-1))\nconst nextTarget = computed(() => step(1))\nconst prevDisabled = computed(() => props.disabled || prevTarget.value === undefined)\nconst nextDisabled = computed(() => props.disabled || nextTarget.value === undefined)\n\nfunction goTo(n: number | undefined) {\n if (n === undefined) return\n page.value = clamp(n, 1, total.value)\n}\n\n/** The address a pill or a control links to, or `undefined` when the row is made of buttons. */\nfunction hrefFor(n: number): string | undefined {\n return props.href?.(n)\n}\n\n// @core\n/*\n * A link clicked with a modifier held (Ctrl or Cmd for a new tab, Shift for a window, Alt for a\n * download) opens somewhere ELSE: the page on screen stays where it is, so the model must not\n * move. The test the routers themselves make before taking a click over.\n */\nfunction opensElsewhere(event: MouseEvent): boolean {\n return (\n props.href !== undefined &&\n (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)\n )\n}\n\nfunction choose(event: MouseEvent, n: number | undefined) {\n if (n === undefined || opensElsewhere(event)) return\n emit('navigate', n, event)\n goTo(n)\n}\n\nfunction isUnusable(el: HTMLElement): boolean {\n return el.matches(':disabled, [aria-disabled=\"true\"]')\n}\n\n// @a11y\n/*\n * The previous and next controls disable themselves once no page is left in their direction,\n * and a focused button that becomes disabled hands the focus to <body>: a keyboard reader\n * pressing \"Next page\" on the second to last page would be sent back to the top of the document\n * by the next Tab. The focus goes to the page just reached instead, which is always rendered,\n * the current page being the one pill the responsive steps never hide.\n */\nasync function goFromControl(event: MouseEvent, n: number | undefined) {\n const control = event.currentTarget as HTMLElement\n const hadFocus = control === document.activeElement\n choose(event, n)\n if (!hadFocus) return\n await nextTick()\n if (isUnusable(control)) {\n navEl.value?.querySelector<HTMLElement>('[aria-current=\"page\"]')?.focus()\n }\n}\n\n/*\n * The previous and next controls, written once: the two differ only by their direction, and a\n * pair of template blocks drifting apart on the next change is exactly what this avoids. A\n * functional component because the two sit at either end of the row, with the pages in between,\n * where a `v-for` over two descriptors could not put them.\n */\nconst PageControl: FunctionalComponent<{ side: 'prev' | 'next' }> = ({ side }) => {\n const prev = side === 'prev'\n const label = prev ? resolvedPrevText.value : resolvedNextText.value\n const icon = prev ? props.prevIcon : props.nextIcon\n const target = prev ? prevTarget.value : nextTarget.value\n const disabled = prev ? prevDisabled.value : nextDisabled.value\n const common = {\n class: 'v-pagination-control',\n variant: props.itemVariant,\n disabled,\n href: hrefFor(target ?? currentPage.value),\n rel: props.href && !disabled ? side : undefined,\n onClick: (event: MouseEvent) => goFromControl(event, target),\n }\n const glyph = () => h(VIcon, { ...iconProps(icon), mirrored: true })\n if (props.controls === 'icon') return h(VIconButton, { ...common, label }, glyph)\n return h(\n VButton,\n { ...common, tone: 'neutral', 'aria-label': label },\n {\n ...(props.controls === 'both' ? { [prev ? 'start' : 'end']: glyph } : {}),\n default: () => h('span', { class: 'v-pagination-control-label' }, label),\n },\n )\n}\n\nconst navEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/**\n * The arrow keys, through the shared implementation in `utils/arrowNav`. The pills the\n * responsive rules have hidden are left out by the helper, which skips anything not displayed;\n * so they cannot be focused into.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const nav = navEl.value\n if (!nav) return\n arrowNavigate(event, nav, () =>\n navigableItems(nav, '.v-pagination-page:not(:disabled, [aria-disabled=\"true\"])'),\n )\n}\n\n// The pills are rendered by a VButtonGroup inside the nav, so a template ref reaches none of\n// them. `focus` goes to the current page, which every responsive step keeps on screen.\ndefineExpose({\n // Two queries, not one selector list: a list matches in DOM order, which would hand the\n // focus to the previous control ahead of the current page. `a[href]` because an inert link\n // has lost its address, and with it the ability to take the focus.\n /** Moves the focus to the current page, or to the first control that can take it. */\n focus: (options?: FocusOptions) =>\n (\n navEl.value?.querySelector<HTMLElement>(\n '[aria-current=\"page\"]:is(button, a[href]):not(:disabled)',\n ) ?? navEl.value?.querySelector<HTMLElement>(':is(button, a[href]):not(:disabled)')\n )?.focus(options),\n /** The `<nav>` element, which is also where the consumer's attributes land. */\n el: navEl,\n})\n</script>\n\n<template>\n <nav\n ref=\"navEl\"\n class=\"v-pagination\"\n :aria-label=\"ariaLabel\"\n :data-align=\"align\"\n :data-item-variant=\"itemVariant\"\n :data-controls=\"controls || undefined\"\n :data-responsive=\"responsive ? '' : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- The row is a VButtonGroup either way: joined it merges the borders of its DIRECT\n button children, and detached it spaces them, which is all this prop asks of it.\n That is why there is no list markup wrapping the pills, and why the ellipsis is\n itself an inert button rather than a plain span: anything else between two pills\n would break the seam. -->\n <!--\n `|| undefined` keeps the group opinion-free when the row does not ask, `undefined` being\n what means \"no opinion\" there where `false` is an order. The size is always an opinion,\n the pagination having a default of its own.\n -->\n <VButtonGroup\n class=\"v-pagination-items\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n >\n <PageControl v-if=\"controls\" side=\"prev\" />\n\n <template v-for=\"item in items\" :key=\"item.key\">\n <VButton\n v-if=\"item.kind === 'page'\"\n class=\"v-pagination-page\"\n :variant=\"item.page === currentPage ? selectedVariant : itemVariant\"\n :tone=\"item.page === currentPage ? tone : 'neutral'\"\n :disabled=\"disabled || isPageDisabled(item.page)\"\n :aria-label=\"pageLabelFor(item.page)\"\n :aria-current=\"item.page === currentPage ? 'page' : undefined\"\n :href=\"hrefFor(item.page)\"\n :data-edge=\"item.edge ? '' : undefined\"\n :data-distance=\"!item.edge && item.distance > 0 ? item.distance : undefined\"\n @click=\"choose($event, item.page)\"\n >\n {{ item.page }}\n </VButton>\n <!--\n The ellipsis is a disabled icon-only button rather than a span: being a button, it\n keeps the joined row's seam continuous and follows the size and density like\n everything else. Disabling it takes it out of the tab order, and it is hidden from\n screen readers, which have the page numbers themselves; which is also why it is a\n VButton and not a VIconButton, whose required label would name something nobody can\n reach.\n -->\n <VButton\n v-else\n class=\"v-pagination-ellipsis\"\n aria-hidden=\"true\"\n data-icon-only\n :variant=\"itemVariant\"\n tone=\"neutral\"\n disabled\n >\n <template #start>\n <VIcon :name=\"moreHorizIcon\" />\n </template>\n </VButton>\n </template>\n\n <PageControl v-if=\"controls\" side=\"next\" />\n </VButtonGroup>\n </nav>\n</template>\n\n<style>\n@layer vectis.components {\n .v-pagination {\n display: flex;\n }\n\n /*\n * A container of this kind computes its width WITHOUT looking at its content, so the row has\n * to be block-level: as an inline box it would measure zero and hide everything at once. It\n * therefore takes its parent's whole width, which is exactly what makes the hiding follow the\n * space the component was actually given, and what obliges a flex parent to grant it one.\n */\n .v-pagination[data-responsive] {\n container-type: inline-size;\n container-name: v-pagination;\n }\n\n .v-pagination[data-align='center'] {\n justify-content: center;\n }\n\n .v-pagination[data-align='end'] {\n justify-content: flex-end;\n }\n\n /*\n * Qualified by an attribute VButton always renders, which makes both of these beat that\n * button's own padding and its own transition whatever order the two sheets end up in; the\n * VIconButton idiom.\n */\n .v-pagination-page[data-size] {\n /* A one-digit pill is square, and widens by itself past that. The height variable\n is set by the shared size class on this very element, so this single rule covers\n all five sizes and their compact forms. */\n min-inline-size: var(--control-height);\n padding-inline: var(--vectis-space-2);\n\n /*\n * The highlight changes instantly, and this must not be \"restored for consistency\". VButton\n * fades `background-color` over `--vectis-duration-fast`, which is right for a button that\n * stays put and wrong here: truncation SHIFTS its window, so from one page to the next the\n * highlight keeps its slot but changes ELEMENT, and the outgoing and incoming pills are\n * both half-tinted for a few frames; read as a flicker, with nothing in the console.\n */\n transition: none;\n }\n\n /* The ellipsis is a disabled button only as a technical device; to the reader it is\n not a control that has been turned off, so it does not take the forbidden\n cursor. Qualified like the pills: VButton's disabled cursor weighs (0,2,0), and a bare\n class loses to it whatever the sheet order. */\n .v-pagination-ellipsis[data-size] {\n cursor: default;\n }\n\n /*\n * Each threshold is the width NEEDED to show that level, never what remains after hiding:\n * written the other way round the row overflows for the whole interval before the next step\n * takes effect. The most distant neighbours go first.\n */\n @container v-pagination (max-inline-size: 36rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='3'] {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 31rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='2'] {\n display: none;\n }\n\n /* It is never done when the control shows text alone: there would be nothing left to click. */\n .v-pagination[data-responsive][data-controls='both'] .v-pagination-control-label {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 25rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='1'] {\n display: none;\n }\n }\n\n /*\n * A disabled page greys its outline to a different token, hence a variable the second rule\n * changes. VToggle's `--toggle-frame` is the same device.\n */\n .v-pagination[data-item-variant='outline'] {\n --pagination-frame: var(--vectis-color-border-strong);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page:is(:disabled, [aria-disabled='true']) {\n --pagination-frame: var(--vectis-color-border);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page[aria-current='page']:is([data-variant='soft'], [data-variant='ghost']) {\n border-color: var(--pagination-frame);\n }\n\n /*\n * The class is repeated to reach (0,6,0). The variant, hover and active rules reach (0,5,0),\n * and with the forcing turned off any of them that still won would paint its tone over the\n * selection, with HighlightText on top of it.\n */\n @media (forced-colors: active) {\n .v-pagination-page.v-pagination-page.v-pagination-page.v-pagination-page[aria-current='page']:not(\n :disabled,\n [aria-disabled='true']\n ) {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n border-color: Highlight;\n outline-color: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VPagination.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VPagination/VPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * Native buttons activate pages. JavaScript builds bounded page windows and provides roving\n * keyboard navigation.\n */\nimport { computed, h, nextTick, ref } from 'vue'\nimport type { FunctionalComponent } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { first_page as firstPageIcon } from '../VIcon/icons/first_page'\nimport { last_page as lastPageIcon } from '../VIcon/icons/last_page'\nimport { more_horiz as moreHorizIcon } from '../VIcon/icons/more_horiz'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\n\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { clamp } from '../../utils/number'\nimport { resolveMatcher } from '../../utils/matcher'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the pages that are not the current one are drawn. */\nexport type PaginationItemVariant = 'ghost' | 'outline'\n\n/** How the current page is drawn. */\nexport type PaginationSelectedVariant = 'solid' | 'soft' | 'ghost'\n\n/** The colour of the current page. */\nexport type PaginationTone = 'accent' | 'neutral' | 'danger'\n\n/** The height of the pills, from the scale every control shares. */\nexport type PaginationSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\n/** Where the row sits in the width it is given. */\nexport type PaginationAlign = 'start' | 'center' | 'end'\n\n/** Whether the previous and next controls are rendered, and what they show. */\nexport type PaginationControls = false | 'icon' | 'text' | 'both'\n\n/**\n * Which pages cannot be reached, given either as a list of page numbers or as a function\n * answering that question for a page.\n */\nexport type PaginationMatcher = number[] | ((page: number) => boolean)\n\ninterface PaginationProps {\n /**\n * How many pages there are in all. It is 1 by default, which renders a single page: the\n * real count almost always has to be given.\n */\n length?: number\n /**\n * How many slots to render, ellipses counted among them, so the row keeps exactly the same\n * width whichever page is current. Below 5, the row drops the bounds and the ellipses and\n * shows that many consecutive pages around the current one. Left out, every page is rendered.\n */\n totalVisible?: number\n\n /**\n * Separates the buttons instead of joining them into one segmented control. It is the\n * word VButtonGroup and VToggle use for the same question, in the same direction.\n */\n detached?: boolean\n /**\n * Draws a line between the joined buttons, so the row reads as segments rather than as one\n * frame, on the terms of VButtonGroup's own `bordered`. It has no effect under `detached`.\n */\n bordered?: boolean\n /**\n * How the pages OTHER than the current one, and the controls, are drawn. It is named for the\n * ITEMS because that is what it paints: on VTabs and VDataTable `variant` names the\n * decoration of the frame instead.\n */\n itemVariant?: PaginationItemVariant\n /**\n * How the current page is drawn, in the row's tone: filled, tinted, or the colour of its\n * text alone. The same choice as VToggle's `selectedVariant`.\n */\n selectedVariant?: PaginationSelectedVariant\n /** The colour the current page takes. The other pages and the controls stay neutral. */\n tone?: PaginationTone\n /** The height of the buttons, from the scale shared by every control. */\n size?: PaginationSize\n /** Takes 4px off the height of every button. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButton's own `elevated`. */\n elevated?: boolean\n /**\n * Where the row sits in the space it is given. It only matters in responsive mode,\n * where the row takes the whole width available.\n */\n align?: PaginationAlign\n\n /**\n * The previous and next buttons on either side of the pages: what they show, or `false` to\n * leave them out.\n */\n controls?: PaginationControls\n /** The icon of the previous control: an icon name, or an explicit render. */\n prevIcon?: IconSource\n /** The icon of the next control: an icon name, or an explicit render. */\n nextIcon?: IconSource\n /**\n * The wording of the previous control, used both as its visible text and as what\n * screen readers announce. It falls back to the design system dictionary.\n */\n prevText?: string\n /**\n * The wording of the next control, used both as its visible text and as what screen\n * readers announce. It falls back to the design system dictionary.\n */\n nextText?: string\n /**\n * Adds first and last page controls outside the previous and next ones, drawn the way\n * `controls` draws those. It has no effect when `controls` is `false`.\n */\n edgeControls?: boolean\n /** The icon of the first page control: an icon name, or an explicit render. */\n firstIcon?: IconSource\n /** The icon of the last page control: an icon name, or an explicit render. */\n lastIcon?: IconSource\n /**\n * The wording of the first page control, used both as its visible text and as what\n * screen readers announce. It falls back to the design system dictionary.\n */\n firstText?: string\n /**\n * The wording of the last page control, used both as its visible text and as what\n * screen readers announce. It falls back to the design system dictionary.\n */\n lastText?: string\n\n /** Makes the whole component unusable. */\n disabled?: boolean\n /**\n * Which pages cannot be reached, as a list or as a function. The previous and next\n * controls step OVER them rather than stopping at one.\n */\n disabledPages?: PaginationMatcher\n\n /**\n * Lets the row shed pages as the space narrows, by asking about its own width. It is\n * off by default, because it makes the row take the full width available.\n */\n responsive?: boolean\n\n /**\n * What screen readers announce for the navigation itself. It falls back to the\n * design system dictionary.\n */\n label?: string\n /**\n * How a page is announced. A pill shows a bare number, which alone means nothing to\n * a screen reader: this is what turns it into \"Page 3\". It falls back to the design\n * system dictionary.\n */\n pageLabel?: (page: number) => string\n /** The address of a page. Left out, they are buttons. */\n href?: (page: number) => string\n}\n\nconst props = withDefaults(defineProps<PaginationProps>(), {\n length: 1,\n totalVisible: undefined,\n detached: false,\n bordered: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n align: 'start',\n controls: 'icon',\n prevIcon: () => chevronLeftIcon,\n nextIcon: () => chevronRightIcon,\n prevText: undefined,\n nextText: undefined,\n edgeControls: false,\n firstIcon: () => firstPageIcon,\n lastIcon: () => lastPageIcon,\n firstText: undefined,\n lastText: undefined,\n disabled: false,\n disabledPages: undefined,\n responsive: false,\n label: undefined,\n pageLabel: undefined,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A page was chosen from the row, a pill or a control, with the click that chose it. It comes\n * before the model changes, so under `href` a single-page application calls\n * `preventDefault()` on the event and hands the address to its router instead of letting the\n * browser load it.\n */\n navigate: [page: number, event: MouseEvent]\n}>()\n\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins; that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.pagination.label)\nconst resolvedPrevText = computed(() => props.prevText ?? m.value.pagination.previous)\nconst resolvedNextText = computed(() => props.nextText ?? m.value.pagination.next)\nconst resolvedFirstText = computed(() => props.firstText ?? m.value.pagination.first)\nconst resolvedLastText = computed(() => props.lastText ?? m.value.pagination.last)\n\n/** The page being shown, counted from 1. It starts on the first. */\nconst page = defineModel<number>({ default: 1 })\n\n/** One slot in the row: either a page, or the ellipsis standing for those left out. */\ntype PaginationItem =\n | { kind: 'page'; key: string; page: number; edge: boolean; distance: number }\n | { kind: 'gap'; key: string }\n\n// A length or a model that is not a whole number (`NaN` while a page size is still loading,\n// a computed `2.5`) is brought back to a page that exists, or no pill would ever match it.\nconst total = computed(() => Math.max(Math.trunc(props.length) || 1, 1))\nconst currentPage = computed(() => clamp(Math.round(page.value) || 1, 1, total.value))\n\nconst isPageDisabled = computed(() => resolveMatcher(props.disabledPages))\n\n// @a11y\n// A pill shows nothing but a number, which on its own tells a screen reader\n// nothing at all; this is what has it announced as \"Page 3\".\nfunction pageLabelFor(n: number): string {\n return props.pageLabel ? props.pageLabel(n) : m.value.pagination.page(n)\n}\n\n/** Works out which slots the row holds. */\nconst items = computed<PaginationItem[]>(() => {\n const count = total.value\n const current = currentPage.value\n\n const pageItem = (n: number): PaginationItem => ({\n kind: 'page',\n key: `page-${n}`,\n page: n,\n edge: n === 1 || n === count,\n distance: Math.min(Math.abs(n - current), 3),\n })\n const pages = (from: number, to: number): PaginationItem[] => {\n const out: PaginationItem[] = []\n for (let n = from; n <= to; n++) out.push(pageItem(n))\n return out\n }\n // Keyed by the side it stands on, never by the page it follows: there is at most one\n // ellipsis per side, and the left one always comes after the first page while the right\n // one always comes before the last. A key derived from the window would change on every\n // navigation, so the right-hand ellipsis would be destroyed and rebuilt each time the\n // window slid, for a node that never changes.\n const gap = (side: 'start' | 'end'): PaginationItem => ({ kind: 'gap', key: `gap-${side}` })\n\n // With no limit given, nothing is left out. At least the current page is always shown.\n const limit = Math.trunc(props.totalVisible ?? count)\n const visible = Number.isNaN(limit) ? count : Math.max(limit, 1)\n if (visible >= count) return pages(1, count)\n\n // Below five slots there is no room for the first page, an ellipsis, the current page,\n // another ellipsis and the last: the row is a plain window of consecutive pages, held\n // against the bounds so it keeps the same number of slots.\n if (visible < 5) {\n const from = clamp(current - Math.floor((visible - 1) / 2), 1, count - visible + 1)\n return pages(from, from + visible - 1)\n }\n\n const start = current - Math.floor((visible - 5) / 2)\n const end = start + (visible - 5)\n\n // The window is stretched to the bound instead, which both avoids that and keeps the number\n // of slots exactly the same.\n if (start <= 3) return [...pages(1, visible - 2), gap('end'), pageItem(count)]\n if (end >= count - 2) return [pageItem(1), gap('start'), ...pages(count - visible + 3, count)]\n return [pageItem(1), gap('start'), ...pages(start, end), gap('end'), pageItem(count)]\n})\n\n/**\n * Where a control leads: the nearest page in that direction that can actually be reached,\n * disabled ones being stepped over rather than stopped at. Answering `undefined` means there is\n * none left, which disables the control; and that single answer also covers being on the first\n * or the last page, with no separate test for the ends.\n */\nfunction step(direction: -1 | 1): number | undefined {\n for (let n = currentPage.value + direction; n >= 1 && n <= total.value; n += direction) {\n if (!isPageDisabled.value(n)) return n\n }\n return undefined\n}\n\n/**\n * Where a first or last control leads: the reachable page farthest in that direction. It is\n * `undefined` exactly when `step` is, so each edge control disables itself with its neighbour.\n */\nfunction leap(direction: -1 | 1): number | undefined {\n const end = direction < 0 ? 1 : total.value\n for (let n = end; n !== currentPage.value; n -= direction) {\n if (!isPageDisabled.value(n)) return n\n }\n return undefined\n}\n\nconst firstTarget = computed(() => leap(-1))\nconst prevTarget = computed(() => step(-1))\nconst nextTarget = computed(() => step(1))\nconst lastTarget = computed(() => leap(1))\n\nfunction goTo(n: number | undefined) {\n if (n === undefined) return\n page.value = clamp(n, 1, total.value)\n}\n\n/** The address a pill or a control links to, or `undefined` when the row is made of buttons. */\nfunction hrefFor(n: number): string | undefined {\n return props.href?.(n)\n}\n\n// @core\n/*\n * A link clicked with a modifier held (Ctrl or Cmd for a new tab, Shift for a window, Alt for a\n * download) opens somewhere ELSE: the page on screen stays where it is, so the model must not\n * move. The test the routers themselves make before taking a click over.\n */\nfunction opensElsewhere(event: MouseEvent): boolean {\n return (\n props.href !== undefined &&\n (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)\n )\n}\n\nfunction choose(event: MouseEvent, n: number | undefined) {\n if (n === undefined || opensElsewhere(event)) return\n emit('navigate', n, event)\n goTo(n)\n}\n\nfunction isUnusable(el: HTMLElement): boolean {\n return el.matches(':disabled, [aria-disabled=\"true\"]')\n}\n\n// @a11y\n/*\n * The previous and next controls disable themselves once no page is left in their direction,\n * and a focused button that becomes disabled hands the focus to <body>: a keyboard reader\n * pressing \"Next page\" on the second to last page would be sent back to the top of the document\n * by the next Tab. The focus goes to the page just reached instead, which is always rendered,\n * the current page being the one pill the responsive steps never hide.\n */\nasync function goFromControl(event: MouseEvent, n: number | undefined) {\n const control = event.currentTarget as HTMLElement\n const hadFocus = control === document.activeElement\n choose(event, n)\n if (!hadFocus) return\n await nextTick()\n if (isUnusable(control)) {\n navEl.value?.querySelector<HTMLElement>('[aria-current=\"page\"]')?.focus()\n }\n}\n\ntype ControlSide = 'first' | 'prev' | 'next' | 'last'\n\n/*\n * The four controls, written once: they differ only by their direction and reach, and template\n * blocks drifting apart on the next change is exactly what this avoids. A functional component\n * because they sit at either end of the row, with the pages in between, where a `v-for` over\n * descriptors could not put them.\n */\nconst PageControl: FunctionalComponent<{ side: ControlSide }> = ({ side }) => {\n const { label, icon, target } = {\n first: { label: resolvedFirstText.value, icon: props.firstIcon, target: firstTarget.value },\n prev: { label: resolvedPrevText.value, icon: props.prevIcon, target: prevTarget.value },\n next: { label: resolvedNextText.value, icon: props.nextIcon, target: nextTarget.value },\n last: { label: resolvedLastText.value, icon: props.lastIcon, target: lastTarget.value },\n }[side]\n const disabled = props.disabled || target === undefined\n const common = {\n class: 'v-pagination-control',\n 'data-side': side,\n variant: props.itemVariant,\n disabled,\n href: hrefFor(target ?? currentPage.value),\n rel: props.href && !disabled ? side : undefined,\n onClick: (event: MouseEvent) => goFromControl(event, target),\n }\n const glyph = () => h(VIcon, { ...iconProps(icon), mirrored: true })\n if (props.controls === 'icon') return h(VIconButton, { ...common, label }, glyph)\n const backward = side === 'first' || side === 'prev'\n return h(\n VButton,\n { ...common, tone: 'neutral', 'aria-label': label },\n {\n ...(props.controls === 'both' ? { [backward ? 'start' : 'end']: glyph } : {}),\n default: () => h('span', { class: 'v-pagination-control-label' }, label),\n },\n )\n}\n\nconst navEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/**\n * The arrow keys, through the shared implementation in `utils/arrowNav`. The pills the\n * responsive rules have hidden are left out by the helper, which skips anything not displayed;\n * so they cannot be focused into.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const nav = navEl.value\n if (!nav) return\n arrowNavigate(event, nav, () =>\n navigableItems(nav, '.v-pagination-page:not(:disabled, [aria-disabled=\"true\"])'),\n )\n}\n\n// The pills are rendered by a VButtonGroup inside the nav, so a template ref reaches none of\n// them. `focus` goes to the current page, which every responsive step keeps on screen.\ndefineExpose({\n // Two queries, not one selector list: a list matches in DOM order, which would hand the\n // focus to the previous control ahead of the current page. `a[href]` because an inert link\n // has lost its address, and with it the ability to take the focus.\n /** Moves the focus to the current page, or to the first control that can take it. */\n focus: (options?: FocusOptions) =>\n (\n navEl.value?.querySelector<HTMLElement>(\n '[aria-current=\"page\"]:is(button, a[href]):not(:disabled)',\n ) ?? navEl.value?.querySelector<HTMLElement>(':is(button, a[href]):not(:disabled)')\n )?.focus(options),\n /** The `<nav>` element, which is also where the consumer's attributes land. */\n el: navEl,\n})\n</script>\n\n<template>\n <nav\n ref=\"navEl\"\n class=\"v-pagination\"\n :aria-label=\"ariaLabel\"\n :data-align=\"align\"\n :data-item-variant=\"itemVariant\"\n :data-controls=\"controls || undefined\"\n :data-responsive=\"responsive ? '' : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- The row is a VButtonGroup either way: joined it merges the borders of its DIRECT\n button children, and detached it spaces them, which is all this prop asks of it.\n That is why there is no list markup wrapping the pills, and why the ellipsis is\n itself an inert button rather than a plain span: anything else between two pills\n would break the seam. -->\n <!--\n `|| undefined` keeps the group opinion-free when the row does not ask, `undefined` being\n what means \"no opinion\" there where `false` is an order. The size is always an opinion,\n the pagination having a default of its own.\n -->\n <VButtonGroup\n class=\"v-pagination-items\"\n :detached=\"detached\"\n :bordered=\"bordered\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n >\n <PageControl v-if=\"controls && edgeControls\" side=\"first\" />\n <PageControl v-if=\"controls\" side=\"prev\" />\n\n <template v-for=\"item in items\" :key=\"item.key\">\n <VButton\n v-if=\"item.kind === 'page'\"\n class=\"v-pagination-page\"\n :variant=\"item.page === currentPage ? selectedVariant : itemVariant\"\n :tone=\"item.page === currentPage ? tone : 'neutral'\"\n :disabled=\"disabled || isPageDisabled(item.page)\"\n :aria-label=\"pageLabelFor(item.page)\"\n :aria-current=\"item.page === currentPage ? 'page' : undefined\"\n :href=\"hrefFor(item.page)\"\n :data-edge=\"item.edge ? '' : undefined\"\n :data-distance=\"!item.edge && item.distance > 0 ? item.distance : undefined\"\n @click=\"choose($event, item.page)\"\n >\n {{ item.page }}\n </VButton>\n <!--\n The ellipsis is a disabled icon-only button rather than a span: being a button, it\n keeps the joined row's seam continuous and follows the size and density like\n everything else. Disabling it takes it out of the tab order, and it is hidden from\n screen readers, which have the page numbers themselves; which is also why it is a\n VButton and not a VIconButton, whose required label would name something nobody can\n reach.\n -->\n <VButton\n v-else\n class=\"v-pagination-ellipsis\"\n aria-hidden=\"true\"\n data-icon-only\n :variant=\"itemVariant\"\n tone=\"neutral\"\n disabled\n >\n <template #start>\n <VIcon :name=\"moreHorizIcon\" />\n </template>\n </VButton>\n </template>\n\n <PageControl v-if=\"controls\" side=\"next\" />\n <PageControl v-if=\"controls && edgeControls\" side=\"last\" />\n </VButtonGroup>\n </nav>\n</template>\n\n<style>\n@layer vectis.components {\n .v-pagination {\n display: flex;\n }\n\n /*\n * A container of this kind computes its width WITHOUT looking at its content, so the row has\n * to be block-level: as an inline box it would measure zero and hide everything at once. It\n * therefore takes its parent's whole width, which is exactly what makes the hiding follow the\n * space the component was actually given, and what obliges a flex parent to grant it one.\n */\n .v-pagination[data-responsive] {\n container-type: inline-size;\n container-name: v-pagination;\n }\n\n .v-pagination[data-align='center'] {\n justify-content: center;\n }\n\n .v-pagination[data-align='end'] {\n justify-content: flex-end;\n }\n\n /*\n * Qualified by an attribute VButton always renders, which makes both of these beat that\n * button's own padding and its own transition whatever order the two sheets end up in; the\n * VIconButton idiom.\n */\n .v-pagination-page[data-size] {\n /* A one-digit pill is square, and widens by itself past that. The height variable\n is set by the shared size class on this very element, so this single rule covers\n all five sizes and their compact forms. */\n min-inline-size: var(--control-height);\n padding-inline: var(--vectis-space-2);\n\n /*\n * The highlight changes instantly, and this must not be \"restored for consistency\". VButton\n * fades `background-color` over `--vectis-duration-fast`, which is right for a button that\n * stays put and wrong here: truncation SHIFTS its window, so from one page to the next the\n * highlight keeps its slot but changes ELEMENT, and the outgoing and incoming pills are\n * both half-tinted for a few frames; read as a flicker, with nothing in the console.\n */\n transition: none;\n }\n\n /* The ellipsis is a disabled button only as a technical device; to the reader it is\n not a control that has been turned off, so it does not take the forbidden\n cursor. Qualified like the pills: VButton's disabled cursor weighs (0,2,0), and a bare\n class loses to it whatever the sheet order. */\n .v-pagination-ellipsis[data-size] {\n cursor: default;\n }\n\n /*\n * Each threshold is the width NEEDED to show that level, never what remains after hiding:\n * written the other way round the row overflows for the whole interval before the next step\n * takes effect. The most distant neighbours go first.\n */\n @container v-pagination (max-inline-size: 36rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='3'] {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 31rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='2'] {\n display: none;\n }\n\n /* It is never done when the control shows text alone: there would be nothing left to click. */\n .v-pagination[data-responsive][data-controls='both'] .v-pagination-control-label {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 25rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='1'] {\n display: none;\n }\n\n /* The first and last pills stay on screen and lead to the same pages. */\n .v-pagination[data-responsive]\n .v-pagination-control:is([data-side='first'], [data-side='last']) {\n display: none;\n }\n\n /*\n * VButtonGroup joins its segments by DOM position, which a hidden edge control still holds:\n * the previous and next controls would keep the square corners, the pulled margin and the\n * cleared or seamed border of an inner segment. These restore an outer edge, at (0,7,0)\n * against the (0,6,0) of VButtonGroup's heaviest join rule. Controls are neutral and either\n * ghost or outline, so their border is the outline frame or nothing.\n */\n .v-pagination[data-responsive]\n > .v-pagination-items:not([data-detached])\n > [data-side='first']\n + .v-pagination-control[data-side='prev'] {\n margin-inline-start: 0;\n border-start-start-radius: var(--vectis-radius-interactive);\n border-end-start-radius: var(--vectis-radius-interactive);\n border-inline-start-color: var(--pagination-frame, transparent);\n }\n\n .v-pagination[data-responsive]\n > .v-pagination-items:not([data-detached])\n > [data-side='first']\n + .v-pagination-control[data-side='prev']::before {\n content: none;\n }\n\n .v-pagination[data-responsive]\n > .v-pagination-items:not([data-detached])\n > .v-pagination-control[data-side='next']:has(+ [data-side='last']) {\n border-start-end-radius: var(--vectis-radius-interactive);\n border-end-end-radius: var(--vectis-radius-interactive);\n border-inline-end-color: var(--pagination-frame, transparent);\n }\n }\n\n /*\n * A disabled page or control greys its outline to a different token, hence a variable the\n * second rule changes. VToggle's `--toggle-frame` is the same device.\n */\n .v-pagination[data-item-variant='outline'] {\n --pagination-frame: var(--vectis-color-border-strong);\n }\n\n .v-pagination[data-item-variant='outline'] > * > :is(:disabled, [aria-disabled='true']) {\n --pagination-frame: var(--vectis-color-border);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page[aria-current='page']:is([data-variant='soft'], [data-variant='ghost']) {\n border-color: var(--pagination-frame);\n }\n\n /*\n * The class is repeated to reach (0,6,0). The variant, hover and active rules reach (0,5,0),\n * and with the forcing turned off any of them that still won would paint its tone over the\n * selection, with HighlightText on top of it.\n */\n @media (forced-colors: active) {\n .v-pagination-page.v-pagination-page.v-pagination-page.v-pagination-page[aria-current='page']:not(\n :disabled,\n [aria-disabled='true']\n ) {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n border-color: Highlight;\n outline-color: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -34,11 +34,11 @@ interface ToggleProps {
|
|
|
34
34
|
*/
|
|
35
35
|
detached?: boolean;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Draws a line between the joined items, so the row reads as segments rather than as
|
|
38
|
+
* one frame. It has no effect under `detached`, where the items are separate buttons
|
|
39
|
+
* already.
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
bordered?: boolean;
|
|
42
42
|
/** Whether the items run across the page or down it. */
|
|
43
43
|
orientation?: ToggleOrientation;
|
|
44
44
|
/**
|
|
@@ -113,7 +113,7 @@ declare const __VLS_base: import('vue').DefineComponent<__VLS_PublicProps, {
|
|
|
113
113
|
fullWidth: boolean;
|
|
114
114
|
disabled: boolean;
|
|
115
115
|
detached: boolean;
|
|
116
|
-
|
|
116
|
+
bordered: boolean;
|
|
117
117
|
itemVariant: ToggleItemVariant;
|
|
118
118
|
selectedVariant: ToggleSelectedVariant;
|
|
119
119
|
multiple: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VToggle.js","names":[],"sources":["../../../src/components/VToggle/VToggle.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * Reuse segmented native buttons with aria-pressed. JavaScript synchronizes selection and\n * provides group keyboard navigation.\n */\nimport { computed, provide, ref } from 'vue'\n\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport { toggleKey } from './context'\n\nimport { toggleValue } from '../../utils/array'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\n\nimport type { ItemValue } from '../../types'\n\n/**\n * What identifies an item. It is the design system's own `ItemValue`, re-exported\n * under the family's name so a consumer typing a toggle need not reach for another module.\n */\nexport type ToggleValue = ItemValue\n/**\n * What the v-model holds, which follows `multiple`: one value or nothing when a single\n * choice is allowed, and a list of them when several are.\n */\nexport type ToggleModelValue = ToggleValue | ToggleValue[] | null\n/** How the items that are not selected are drawn. */\nexport type ToggleItemVariant = 'ghost' | 'outline'\n/** How a selected item is drawn. */\nexport type ToggleSelectedVariant = 'solid' | 'soft' | 'ghost'\n/** The colour of a selected item. */\nexport type ToggleTone = 'accent' | 'neutral' | 'danger'\n/** The height of the items, from the scale every control shares. */\nexport type ToggleSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the items run across the page or down it. */\nexport type ToggleOrientation = 'horizontal' | 'vertical'\n\ninterface ToggleProps {\n /** Allows several items to be chosen at once, which makes the v-model a list. */\n multiple?: boolean\n /**\n * Refuses to let the last chosen item be given up, so that something is always\n * selected once something has been. It is a guard and nothing more: it selects\n * nothing on its own at the start.\n */\n mandatory?: boolean\n /**\n * Leaves the items as separate buttons with a gap between them. Left out, they are\n * joined into one segmented control.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined items, so the row reads as one frame\n * rather than as segments. It has no effect under `detached`, where the items are\n * separate buttons already.\n */\n seamless?: boolean\n /** Whether the items run across the page or down it. */\n orientation?: ToggleOrientation\n /**\n * Stretches the row across the whole inline size of its parent, every item taking an equal\n * share of it, on the terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * How the UNSELECTED items are drawn. It is named for the ITEMS because that is what it\n * paints: on VTabs and VDataTable `variant` names the decoration of the frame instead.\n */\n itemVariant?: ToggleItemVariant\n /**\n * How the SELECTED item is drawn, in the group's tone: filled, tinted, or the colour\n * of its text alone.\n */\n selectedVariant?: ToggleSelectedVariant\n /** The colour a selected item takes. The others stay neutral. */\n tone?: ToggleTone\n /** The height of the items, from the scale shared by every control. */\n size?: ToggleSize\n /** Takes 4px off the height of every item. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButtonGroup's own `elevated`. */\n elevated?: boolean\n /** Makes the whole group unusable. */\n disabled?: boolean\n /**\n * Draws the selected item's icon in its filled form, a common way of reinforcing\n * that it is the one in effect.\n */\n selectedIconFilled?: boolean\n /**\n * What screen readers announce for the group, such as \"Text alignment\" or \"Filters\". It is\n * strongly recommended: no default could say what a group of buttons is for.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ToggleProps>(), {\n multiple: false,\n mandatory: false,\n detached: false,\n seamless: false,\n orientation: 'horizontal',\n fullWidth: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n disabled: false,\n selectedIconFilled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The items of the group. */\n default(): unknown\n}>()\n\n/**\n * What is selected, and its shape follows `multiple`: a single value (or `null`, which is where\n * it starts) when one item may be chosen, and an array when several may. A null or scalar value\n * passed in `multiple` mode is read as an empty selection.\n */\nconst model = defineModel<ToggleModelValue>({ default: null })\n\nfunction isSelected(value: ToggleValue): boolean {\n return props.multiple\n ? Array.isArray(model.value) && model.value.includes(value)\n : model.value === value\n}\n\n/*\n * With several choices allowed, the list is never changed in place: each write produces a new\n * one. That is what makes a consumer's own watchers and computed values notice; mutating the\n * existing list would leave some of them silent.\n */\nfunction select(value: ToggleValue) {\n if (props.multiple) {\n const current = Array.isArray(model.value) ? model.value : []\n // `every` rather than a length of one: the removal takes out every copy of the value, so\n // a list holding it twice would otherwise be emptied past the guard.\n if (props.mandatory && current.length > 0 && current.every((v) => v === value)) return\n model.value = toggleValue(current, value)\n return\n }\n if (model.value === value) {\n if (!props.mandatory) model.value = null\n return\n }\n model.value = value\n}\n\nprovide(toggleKey, {\n isSelected,\n select,\n get itemVariant() {\n return props.itemVariant\n },\n get selectedVariant() {\n return props.selectedVariant\n },\n get tone() {\n return props.tone\n },\n get selectedIconFilled() {\n return props.selectedIconFilled\n },\n})\n\n// @keyboard @a11y\n/*\n * Every visible item stays a stop in the tab order here; the VPagination model. The single-stop\n * treatment is reserved for the patterns that require it, a row of tabs or a radio group, and a\n * group of buttons is neither.\n */\nfunction onKeydown(event: KeyboardEvent) {\n if (!(event.target as Element).matches('.v-toggle-item')) return\n const group = event.currentTarget as HTMLElement\n arrowNavigate(event, group, () => navigableItems(group, '.v-toggle-item:not(:disabled)'), {\n vertical: props.orientation === 'vertical',\n })\n}\n\nconst groupRef = ref<InstanceType<typeof VButtonGroup> | null>(null)\nconst groupEl = computed(() => groupRef.value?.el ?? null)\n\n// `focus` goes to the item that stands for the current choice, the one a keyboard user would\n// expect to resume from.\ndefineExpose({\n /**\n * Moves the focus to the selected item (the first of them when several are), or to the\n * first item that can take it when nothing is selected.\n */\n focus: (options?: FocusOptions) => {\n const group = groupEl.value\n const enabled = '.v-toggle-item:not(:disabled, [aria-disabled=\"true\"])'\n const item =\n group?.querySelector<HTMLElement>(`${enabled}[aria-pressed=\"true\"]`) ??\n group?.querySelector<HTMLElement>(enabled)\n item?.focus(options)\n },\n /** The `role=\"group\"` row, which is also where the consumer's attributes land. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!--\n Pass default group booleans as undefined so absent opinions do not force false over\n individual toggle items.\n -->\n <VButtonGroup\n ref=\"groupRef\"\n class=\"v-toggle\"\n :orientation=\"orientation\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :full-width=\"fullWidth\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n :disabled=\"disabled || undefined\"\n :label=\"label\"\n :data-item-variant=\"itemVariant\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </VButtonGroup>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The `:not(:where(.v-overlay))` guard weighs nothing and keeps a panel from passing for an\n * item; the pair is one specificity, so the arbitration with the group's seamless rules holds\n * on both shapes. The frame of an `outline` row, which only that row has: the colour its\n * unselected items already paint.\n */\n .v-toggle[data-item-variant='outline'] {\n --toggle-frame: var(--vectis-color-border-strong);\n }\n\n .v-toggle[data-item-variant='outline'] > .v-toggle-item:is(:disabled, [aria-disabled='true']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item:is(:disabled, [aria-disabled='true']):not(:where(.v-overlay *)) {\n --toggle-frame: var(--vectis-color-border);\n }\n\n /*\n * `soft` and `ghost` leave VButton's border transparent, which in an outline row opens a gap\n * in the frame for the whole width of the selected segment. Restoring it on all four sides is\n * what keeps the frame closed wherever the selection sits, in either orientation and in RTL.\n */\n .v-toggle[data-item-variant='outline']\n > .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']):not(\n :where(.v-overlay *)\n ) {\n border-color: var(--toggle-frame);\n }\n\n /*\n * The class is repeated to reach (0,6,0). The variant, hover and active rules reach (0,5,0),\n * and with the forcing turned off any of them that still won would paint its tone over the\n * selection, with HighlightText on top of it.\n */\n @media (forced-colors: active) {\n .v-toggle-item.v-toggle-item.v-toggle-item.v-toggle-item[aria-pressed='true']:not(:disabled) {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n border-color: Highlight;\n outline-color: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VToggle.js","names":[],"sources":["../../../src/components/VToggle/VToggle.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * Reuse segmented native buttons with aria-pressed. JavaScript synchronizes selection and\n * provides group keyboard navigation.\n */\nimport { computed, provide, ref } from 'vue'\n\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport { toggleKey } from './context'\n\nimport { toggleValue } from '../../utils/array'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\n\nimport type { ItemValue } from '../../types'\n\n/**\n * What identifies an item. It is the design system's own `ItemValue`, re-exported\n * under the family's name so a consumer typing a toggle need not reach for another module.\n */\nexport type ToggleValue = ItemValue\n/**\n * What the v-model holds, which follows `multiple`: one value or nothing when a single\n * choice is allowed, and a list of them when several are.\n */\nexport type ToggleModelValue = ToggleValue | ToggleValue[] | null\n/** How the items that are not selected are drawn. */\nexport type ToggleItemVariant = 'ghost' | 'outline'\n/** How a selected item is drawn. */\nexport type ToggleSelectedVariant = 'solid' | 'soft' | 'ghost'\n/** The colour of a selected item. */\nexport type ToggleTone = 'accent' | 'neutral' | 'danger'\n/** The height of the items, from the scale every control shares. */\nexport type ToggleSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the items run across the page or down it. */\nexport type ToggleOrientation = 'horizontal' | 'vertical'\n\ninterface ToggleProps {\n /** Allows several items to be chosen at once, which makes the v-model a list. */\n multiple?: boolean\n /**\n * Refuses to let the last chosen item be given up, so that something is always\n * selected once something has been. It is a guard and nothing more: it selects\n * nothing on its own at the start.\n */\n mandatory?: boolean\n /**\n * Leaves the items as separate buttons with a gap between them. Left out, they are\n * joined into one segmented control.\n */\n detached?: boolean\n /**\n * Draws a line between the joined items, so the row reads as segments rather than as\n * one frame. It has no effect under `detached`, where the items are separate buttons\n * already.\n */\n bordered?: boolean\n /** Whether the items run across the page or down it. */\n orientation?: ToggleOrientation\n /**\n * Stretches the row across the whole inline size of its parent, every item taking an equal\n * share of it, on the terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * How the UNSELECTED items are drawn. It is named for the ITEMS because that is what it\n * paints: on VTabs and VDataTable `variant` names the decoration of the frame instead.\n */\n itemVariant?: ToggleItemVariant\n /**\n * How the SELECTED item is drawn, in the group's tone: filled, tinted, or the colour\n * of its text alone.\n */\n selectedVariant?: ToggleSelectedVariant\n /** The colour a selected item takes. The others stay neutral. */\n tone?: ToggleTone\n /** The height of the items, from the scale shared by every control. */\n size?: ToggleSize\n /** Takes 4px off the height of every item. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButtonGroup's own `elevated`. */\n elevated?: boolean\n /** Makes the whole group unusable. */\n disabled?: boolean\n /**\n * Draws the selected item's icon in its filled form, a common way of reinforcing\n * that it is the one in effect.\n */\n selectedIconFilled?: boolean\n /**\n * What screen readers announce for the group, such as \"Text alignment\" or \"Filters\". It is\n * strongly recommended: no default could say what a group of buttons is for.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ToggleProps>(), {\n multiple: false,\n mandatory: false,\n detached: false,\n bordered: false,\n orientation: 'horizontal',\n fullWidth: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n disabled: false,\n selectedIconFilled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The items of the group. */\n default(): unknown\n}>()\n\n/**\n * What is selected, and its shape follows `multiple`: a single value (or `null`, which is where\n * it starts) when one item may be chosen, and an array when several may. A null or scalar value\n * passed in `multiple` mode is read as an empty selection.\n */\nconst model = defineModel<ToggleModelValue>({ default: null })\n\nfunction isSelected(value: ToggleValue): boolean {\n return props.multiple\n ? Array.isArray(model.value) && model.value.includes(value)\n : model.value === value\n}\n\n/*\n * With several choices allowed, the list is never changed in place: each write produces a new\n * one. That is what makes a consumer's own watchers and computed values notice; mutating the\n * existing list would leave some of them silent.\n */\nfunction select(value: ToggleValue) {\n if (props.multiple) {\n const current = Array.isArray(model.value) ? model.value : []\n // `every` rather than a length of one: the removal takes out every copy of the value, so\n // a list holding it twice would otherwise be emptied past the guard.\n if (props.mandatory && current.length > 0 && current.every((v) => v === value)) return\n model.value = toggleValue(current, value)\n return\n }\n if (model.value === value) {\n if (!props.mandatory) model.value = null\n return\n }\n model.value = value\n}\n\nprovide(toggleKey, {\n isSelected,\n select,\n get itemVariant() {\n return props.itemVariant\n },\n get selectedVariant() {\n return props.selectedVariant\n },\n get tone() {\n return props.tone\n },\n get selectedIconFilled() {\n return props.selectedIconFilled\n },\n})\n\n// @keyboard @a11y\n/*\n * Every visible item stays a stop in the tab order here; the VPagination model. The single-stop\n * treatment is reserved for the patterns that require it, a row of tabs or a radio group, and a\n * group of buttons is neither.\n */\nfunction onKeydown(event: KeyboardEvent) {\n if (!(event.target as Element).matches('.v-toggle-item')) return\n const group = event.currentTarget as HTMLElement\n arrowNavigate(event, group, () => navigableItems(group, '.v-toggle-item:not(:disabled)'), {\n vertical: props.orientation === 'vertical',\n })\n}\n\nconst groupRef = ref<InstanceType<typeof VButtonGroup> | null>(null)\nconst groupEl = computed(() => groupRef.value?.el ?? null)\n\n// `focus` goes to the item that stands for the current choice, the one a keyboard user would\n// expect to resume from.\ndefineExpose({\n /**\n * Moves the focus to the selected item (the first of them when several are), or to the\n * first item that can take it when nothing is selected.\n */\n focus: (options?: FocusOptions) => {\n const group = groupEl.value\n const enabled = '.v-toggle-item:not(:disabled, [aria-disabled=\"true\"])'\n const item =\n group?.querySelector<HTMLElement>(`${enabled}[aria-pressed=\"true\"]`) ??\n group?.querySelector<HTMLElement>(enabled)\n item?.focus(options)\n },\n /** The `role=\"group\"` row, which is also where the consumer's attributes land. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!--\n Pass default group booleans as undefined so absent opinions do not force false over\n individual toggle items.\n -->\n <VButtonGroup\n ref=\"groupRef\"\n class=\"v-toggle\"\n :orientation=\"orientation\"\n :detached=\"detached\"\n :bordered=\"bordered\"\n :full-width=\"fullWidth\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n :disabled=\"disabled || undefined\"\n :label=\"label\"\n :data-item-variant=\"itemVariant\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </VButtonGroup>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The `:not(:where(.v-overlay))` guard weighs nothing and keeps a panel from passing for an\n * item; the pair is one specificity, so the arbitration with the group's cleared edges holds\n * on both shapes. The frame of an `outline` row, which only that row has: the colour its\n * unselected items already paint.\n */\n .v-toggle[data-item-variant='outline'] {\n --toggle-frame: var(--vectis-color-border-strong);\n }\n\n .v-toggle[data-item-variant='outline'] > .v-toggle-item:is(:disabled, [aria-disabled='true']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item:is(:disabled, [aria-disabled='true']):not(:where(.v-overlay *)) {\n --toggle-frame: var(--vectis-color-border);\n }\n\n /*\n * `soft` and `ghost` leave VButton's border transparent, which in an outline row opens a gap\n * in the frame for the whole width of the selected segment. Restoring it on all four sides is\n * what keeps the frame closed wherever the selection sits, in either orientation and in RTL.\n */\n .v-toggle[data-item-variant='outline']\n > .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']):not(\n :where(.v-overlay *)\n ) {\n border-color: var(--toggle-frame);\n }\n\n /*\n * The class is repeated to reach (0,6,0). The variant, hover and active rules reach (0,5,0),\n * and with the forcing turned off any of them that still won would paint its tone over the\n * selection, with HighlightText on top of it.\n */\n @media (forced-colors: active) {\n .v-toggle-item.v-toggle-item.v-toggle-item.v-toggle-item[aria-pressed='true']:not(:disabled) {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n border-color: Highlight;\n outline-color: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -19,7 +19,7 @@ var h = /*@__PURE__*/ s({
|
|
|
19
19
|
type: Boolean,
|
|
20
20
|
default: !1
|
|
21
21
|
},
|
|
22
|
-
|
|
22
|
+
bordered: {
|
|
23
23
|
type: Boolean,
|
|
24
24
|
default: !1
|
|
25
25
|
},
|
|
@@ -106,7 +106,7 @@ var h = /*@__PURE__*/ s({
|
|
|
106
106
|
class: "v-toggle",
|
|
107
107
|
orientation: s.orientation,
|
|
108
108
|
detached: s.detached,
|
|
109
|
-
|
|
109
|
+
bordered: s.bordered,
|
|
110
110
|
"full-width": s.fullWidth,
|
|
111
111
|
size: s.size,
|
|
112
112
|
compact: s.compact || void 0,
|
|
@@ -121,7 +121,7 @@ var h = /*@__PURE__*/ s({
|
|
|
121
121
|
}, 8, [
|
|
122
122
|
"orientation",
|
|
123
123
|
"detached",
|
|
124
|
-
"
|
|
124
|
+
"bordered",
|
|
125
125
|
"full-width",
|
|
126
126
|
"size",
|
|
127
127
|
"compact",
|