@nexxtmove/ui 2.7.0 → 2.8.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/editor/molecules/EditorColor/EditorColor.vue.d.ts +7 -5
- package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +4 -1
- package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +3 -1
- package/dist/index.js +46 -46
- package/package.json +1 -1
- package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +80 -10
- package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +105 -18
- package/src/components/editor/molecules/EditorColor/EditorColor.vue +7 -5
- package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +39 -0
- package/src/components/editor/molecules/EditorColor/_ColorField.vue +20 -15
- package/src/components/organisms/ColorPicker/ColorPicker.test.ts +58 -0
- package/src/components/organisms/ColorPicker/ColorPicker.vue +12 -3
|
@@ -74,11 +74,13 @@ export interface NexxtEditorColorProps {
|
|
|
74
74
|
/**
|
|
75
75
|
* Offer an opacity slider in the picker. A colour picked below full opacity is
|
|
76
76
|
* written as an 8-digit hex (`bg-[#1777ea80]`); a fully opaque one keeps the
|
|
77
|
-
* 6-digit form.
|
|
78
|
-
* class carries no opacity
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
77
|
+
* 6-digit form. While a theme role is selected the slider stands at 100%, as a
|
|
78
|
+
* role class carries no opacity; moving it writes the role's hex with that
|
|
79
|
+
* opacity, so the role is no longer selected. The same goes for a role stop of
|
|
80
|
+
* a gradient, which without `showAlpha` stays opaque. Off by default, in which
|
|
81
|
+
* case a stored 8-digit hex is shown opaque and only replaced once a new colour
|
|
82
|
+
* is picked. Turning it off while a translucent colour is set does write at
|
|
83
|
+
* once: the picker drops the alpha and the model gets the opaque 6-digit hex.
|
|
82
84
|
*/
|
|
83
85
|
showAlpha?: boolean;
|
|
84
86
|
/**
|
|
@@ -26,7 +26,10 @@ export interface ColorFieldProps<P extends string> {
|
|
|
26
26
|
suggestions?: string[];
|
|
27
27
|
/** Theme roles offered above the suggestions; an entry with an unreadable hex is left out. */
|
|
28
28
|
themeRoles?: EditorColorThemeRole[];
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Offer an opacity slider, also while a theme role is selected: it then stands
|
|
31
|
+
* at 100%, and moving it turns the role into its hex with that opacity.
|
|
32
|
+
*/
|
|
30
33
|
showAlpha?: boolean;
|
|
31
34
|
/**
|
|
32
35
|
* The fills offered: `['solid']` (the default), `['solid', 'gradient']` or
|
|
@@ -16,7 +16,9 @@ export interface NexxtColorPickerProps {
|
|
|
16
16
|
/**
|
|
17
17
|
* Offer an opacity slider and accept colours below full opacity. Turning it
|
|
18
18
|
* off while the colour is translucent writes the opaque colour to the model
|
|
19
|
-
* at once.
|
|
19
|
+
* at once. The gradient mode always has the slider; without `showAlpha` it is
|
|
20
|
+
* locked at 100% for a stop bound to a preset, with it a change unbinds the
|
|
21
|
+
* stop from its preset.
|
|
20
22
|
*/
|
|
21
23
|
showAlpha?: boolean;
|
|
22
24
|
formats?: ColorFormat[];
|
package/dist/index.js
CHANGED
|
@@ -2872,7 +2872,7 @@ var vn = { funda: { render: _n } }, $ = /* @__PURE__ */ p({
|
|
|
2872
2872
|
set: (e) => {
|
|
2873
2873
|
O.value = e === "gradient" ? "gradient" : "solid";
|
|
2874
2874
|
}
|
|
2875
|
-
}), oe = i(() => T.showAlpha || R.value), se = i(() => R.value && !!V.selected.value?.preset), ce = i(() => `flex flex-col ${R.value ? "gap-3" : "gap-4"} rounded-xl bg-white p-4 ring ring-gray-200`), le = (e, t) => {
|
|
2875
|
+
}), oe = i(() => T.showAlpha || R.value), se = i(() => R.value && !T.showAlpha && !!V.selected.value?.preset), ce = i(() => `flex flex-col ${R.value ? "gap-3" : "gap-4"} rounded-xl bg-white p-4 ring ring-gray-200`), le = (e, t) => {
|
|
2876
2876
|
V.setStopColor(t.slice(0, 7), e);
|
|
2877
2877
|
}, ue = () => {
|
|
2878
2878
|
W.value = null, V.resetSession(), u("close");
|
|
@@ -7940,19 +7940,19 @@ var Ol = /* @__PURE__ */ p({
|
|
|
7940
7940
|
}),
|
|
7941
7941
|
emits: ["update:modelValue"],
|
|
7942
7942
|
setup(e) {
|
|
7943
|
-
let t = B(e, "modelValue"), n = i(() => Mt(t.value, e.accepts ?? [e.property])), r = i(() => n.value && "hex" in n.value ? n.value.hex : ""), o = i(() => n.value && "role" in n.value ? n.value.role : void 0), s = i(() => e.themeRoles.filter((e) => At.test(e.hex))), c = i(() => s.value.find(({ role: e }) => e === o.value)), u = (e) => `${e.slice(0, 7)}ff`, d = i(
|
|
7943
|
+
let t = B(e, "modelValue"), n = i(() => Mt(t.value, e.accepts ?? [e.property])), r = i(() => n.value && "hex" in n.value ? n.value.hex : ""), o = i(() => n.value && "role" in n.value ? n.value.role : void 0), s = i(() => e.themeRoles.filter((e) => At.test(e.hex))), c = i(() => s.value.find(({ role: e }) => e === o.value)), u = (e) => `${e.slice(0, 7)}ff`, d = i({
|
|
7944
7944
|
get: () => c.value ? u(c.value.hex) : r.value,
|
|
7945
7945
|
set: (n) => {
|
|
7946
7946
|
let r = n.replace(Jl, "$1");
|
|
7947
7947
|
jt(r) && (t.value = `${e.property}-[${r}]`);
|
|
7948
7948
|
}
|
|
7949
|
-
}),
|
|
7949
|
+
}), p = (n) => {
|
|
7950
7950
|
t.value = `${e.property}-theme-${n}`;
|
|
7951
|
-
},
|
|
7952
|
-
K(() => [t.value,
|
|
7953
|
-
t ?
|
|
7951
|
+
}, m = i(() => e.modes.includes("gradient") && e.property === "bg" ? e.modes.includes("solid") ? ["solid", "gradient"] : ["gradient"] : ["solid"]), h = i(() => m.value.includes("gradient")), g = i(() => !m.value.includes("solid")), _ = i(() => h.value ? un(t.value) : null), v = (t) => pn(t) ? "gradient" : Mt(t, e.accepts ?? [e.property]) ? "solid" : null, y = A(h.value ? v(t.value) ?? "solid" : "solid");
|
|
7952
|
+
K(() => [t.value, h.value], ([e, t]) => {
|
|
7953
|
+
t ? y.value = v(e) ?? y.value : y.value = "solid";
|
|
7954
7954
|
});
|
|
7955
|
-
let
|
|
7955
|
+
let b = i(() => h.value ? g.value ? "gradient" : y.value : "solid"), x = A(0), S = A(null), C = (e) => s.value.find((t) => t.role === e), w = (e) => {
|
|
7956
7956
|
if (!e) return;
|
|
7957
7957
|
let t = [];
|
|
7958
7958
|
for (let { color: n, position: r } of e.stops) {
|
|
@@ -7963,7 +7963,7 @@ var Ol = /* @__PURE__ */ p({
|
|
|
7963
7963
|
});
|
|
7964
7964
|
continue;
|
|
7965
7965
|
}
|
|
7966
|
-
let e =
|
|
7966
|
+
let e = C(n.role);
|
|
7967
7967
|
if (!e) return;
|
|
7968
7968
|
t.push({
|
|
7969
7969
|
color: e.hex.slice(0, 7).toLowerCase(),
|
|
@@ -7981,13 +7981,13 @@ var Ol = /* @__PURE__ */ p({
|
|
|
7981
7981
|
centerY: e.type === "radial" ? ni(e.y) : 50,
|
|
7982
7982
|
stops: t
|
|
7983
7983
|
};
|
|
7984
|
-
},
|
|
7984
|
+
}, T = i(() => w(_.value)), E = i(() => g.value && h.value && !T.value ? w(un(j())) : void 0), O = (e, t) => {
|
|
7985
7985
|
let n = kt.find((e) => e === t);
|
|
7986
7986
|
return n ? { role: n } : { hex: ln(jt(e) ? e : `#${e}`) };
|
|
7987
|
-
},
|
|
7987
|
+
}, k = (e) => {
|
|
7988
7988
|
if (!e) return;
|
|
7989
7989
|
let n = e.stops.map(({ color: e, position: t, preset: n }) => ({
|
|
7990
|
-
color:
|
|
7990
|
+
color: O(e, n?.key),
|
|
7991
7991
|
position: t
|
|
7992
7992
|
}));
|
|
7993
7993
|
t.value = fn(e.type === "linear" ? {
|
|
@@ -8000,7 +8000,7 @@ var Ol = /* @__PURE__ */ p({
|
|
|
8000
8000
|
y: e.centerY,
|
|
8001
8001
|
stops: n
|
|
8002
8002
|
});
|
|
8003
|
-
},
|
|
8003
|
+
}, j = () => {
|
|
8004
8004
|
let e = c.value, t = r.value;
|
|
8005
8005
|
if (e) {
|
|
8006
8006
|
let t = e.hex.slice(0, 7).toLowerCase();
|
|
@@ -8011,75 +8011,75 @@ var Ol = /* @__PURE__ */ p({
|
|
|
8011
8011
|
return `bg-linear-180 from-[${e}] to-[${e.slice(0, 7)}00]`;
|
|
8012
8012
|
}
|
|
8013
8013
|
return ql;
|
|
8014
|
-
},
|
|
8014
|
+
}, M = (t) => {
|
|
8015
8015
|
let n = ln(t), r = n.slice(0, 7);
|
|
8016
8016
|
return e.showAlpha || !jt(r) ? n : r;
|
|
8017
|
-
},
|
|
8018
|
-
let n = e.stops[
|
|
8019
|
-
return n ? "role" in n.color ? `${t}-theme-${n.color.role}` : `${t}-[${
|
|
8020
|
-
},
|
|
8021
|
-
if (n ===
|
|
8017
|
+
}, N = (e) => e.stops.every(({ color: e }) => "hex" in e || !!C(e.role)), ee = (e, t) => {
|
|
8018
|
+
let n = e.stops[x.value] ?? e.stops[0];
|
|
8019
|
+
return n ? "role" in n.color ? `${t}-theme-${n.color.role}` : `${t}-[${M(n.color.hex)}]` : null;
|
|
8020
|
+
}, P = (n) => {
|
|
8021
|
+
if (n === b.value || g.value) return;
|
|
8022
8022
|
if (n === "gradient") {
|
|
8023
|
-
|
|
8024
|
-
let e =
|
|
8025
|
-
|
|
8023
|
+
y.value = "gradient";
|
|
8024
|
+
let e = S.value;
|
|
8025
|
+
x.value = e?.stop ?? 0, t.value = e?.value ?? j();
|
|
8026
8026
|
return;
|
|
8027
8027
|
}
|
|
8028
|
-
|
|
8029
|
-
let r =
|
|
8028
|
+
y.value = "solid";
|
|
8029
|
+
let r = T.value ? _.value : null;
|
|
8030
8030
|
if (!r) return;
|
|
8031
|
-
|
|
8031
|
+
S.value = {
|
|
8032
8032
|
value: fn(r),
|
|
8033
|
-
stop:
|
|
8033
|
+
stop: x.value
|
|
8034
8034
|
};
|
|
8035
|
-
let i =
|
|
8035
|
+
let i = ee(r, e.property);
|
|
8036
8036
|
i && (t.value = i);
|
|
8037
|
-
},
|
|
8038
|
-
|
|
8037
|
+
}, F = (e, t) => {
|
|
8038
|
+
b.value === "gradient" ? t({
|
|
8039
8039
|
key: e.role,
|
|
8040
8040
|
label: e.label
|
|
8041
|
-
}, e.hex) :
|
|
8042
|
-
},
|
|
8043
|
-
|
|
8041
|
+
}, e.hex) : p(e.role);
|
|
8042
|
+
}, I = (e, t) => b.value === "gradient" ? e === t : e === c.value?.role, L = () => {
|
|
8043
|
+
S.value = null;
|
|
8044
8044
|
};
|
|
8045
8045
|
return K(() => e.property, (e) => {
|
|
8046
8046
|
if (o.value) t.value = `${e}-theme-${o.value}`;
|
|
8047
8047
|
else if (r.value) t.value = `${e}-[${r.value}]`;
|
|
8048
8048
|
else if (e !== "bg") {
|
|
8049
|
-
let n = un(t.value), r = n &&
|
|
8049
|
+
let n = un(t.value), r = n && N(n) ? ee(n, e) : null;
|
|
8050
8050
|
r && (t.value = r);
|
|
8051
8051
|
}
|
|
8052
8052
|
}), (t, n) => (D(), a(la, {
|
|
8053
|
-
modelValue:
|
|
8054
|
-
"onUpdate:modelValue": n[0] ||= (e) =>
|
|
8055
|
-
"selected-stop":
|
|
8056
|
-
"onUpdate:selectedStop": n[1] ||= (e) =>
|
|
8057
|
-
modes:
|
|
8053
|
+
modelValue: d.value,
|
|
8054
|
+
"onUpdate:modelValue": n[0] ||= (e) => d.value = e,
|
|
8055
|
+
"selected-stop": x.value,
|
|
8056
|
+
"onUpdate:selectedStop": n[1] ||= (e) => x.value = e,
|
|
8057
|
+
modes: m.value,
|
|
8058
8058
|
"gradient-messages": e.gradientMessages,
|
|
8059
|
-
mode:
|
|
8060
|
-
"gradient-value":
|
|
8061
|
-
"gradient-start":
|
|
8059
|
+
mode: b.value,
|
|
8060
|
+
"gradient-value": T.value,
|
|
8061
|
+
"gradient-start": E.value,
|
|
8062
8062
|
"full-width": "",
|
|
8063
8063
|
"match-width": "",
|
|
8064
8064
|
label: e.label,
|
|
8065
8065
|
describedby: e.describedby,
|
|
8066
8066
|
formats: ["hex"],
|
|
8067
|
-
"show-alpha":
|
|
8067
|
+
"show-alpha": e.showAlpha,
|
|
8068
8068
|
placeholder: e.placeholder,
|
|
8069
8069
|
suggestions: { hex: e.suggestions },
|
|
8070
8070
|
"suggestions-label": e.suggestionsLabel,
|
|
8071
8071
|
"alpha-label": e.alphaLabel,
|
|
8072
8072
|
"value-label": c.value?.label,
|
|
8073
|
-
"onUpdate:mode":
|
|
8074
|
-
"onUpdate:gradientValue":
|
|
8075
|
-
onClose:
|
|
8073
|
+
"onUpdate:mode": P,
|
|
8074
|
+
"onUpdate:gradientValue": k,
|
|
8075
|
+
onClose: L
|
|
8076
8076
|
}, l({ _: 2 }, [s.value.length ? {
|
|
8077
8077
|
name: "presets",
|
|
8078
8078
|
fn: q(({ preset: t, selectPreset: n }) => [f(ha, {
|
|
8079
8079
|
roles: s.value,
|
|
8080
8080
|
label: e.themeLabel,
|
|
8081
|
-
"is-pressed": (e) =>
|
|
8082
|
-
onPick: (e) =>
|
|
8081
|
+
"is-pressed": (e) => I(e.role, t),
|
|
8082
|
+
onPick: (e) => F(e, n)
|
|
8083
8083
|
}, null, 8, [
|
|
8084
8084
|
"roles",
|
|
8085
8085
|
"label",
|
package/package.json
CHANGED
|
@@ -215,16 +215,23 @@ export const ThemeRolesWithSuggestions: Story = {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
|
-
* With `showAlpha` the picker offers an opacity slider
|
|
219
|
-
*
|
|
220
|
-
*
|
|
218
|
+
* With `showAlpha` the picker offers an opacity slider, also for a theme role.
|
|
219
|
+
* A role has no opacity, so its slider stands at 100%; moving it writes the
|
|
220
|
+
* role's hex with an 8-digit alpha, and the role is no longer selected. Picking
|
|
221
|
+
* the role again makes it opaque. The suggestions are those of
|
|
222
|
+
* `ThemeRolesWithSuggestions`; two of them share the hex of a role, and picking
|
|
223
|
+
* one writes that hex as a literal, so the role is no longer selected.
|
|
221
224
|
*/
|
|
222
225
|
export const WithAlpha: Story = {
|
|
223
|
-
args: {
|
|
226
|
+
args: {
|
|
227
|
+
showAlpha: true,
|
|
228
|
+
themeRoles: THEME_ROLES,
|
|
229
|
+
suggestions: ['#0F766E', '#F0F4F8', '#1F2937'],
|
|
230
|
+
},
|
|
224
231
|
render: (args) => ({
|
|
225
232
|
components: { EditorColor },
|
|
226
233
|
setup() {
|
|
227
|
-
const color = ref<EditorColorValue>('bg-
|
|
234
|
+
const color = ref<EditorColorValue>('bg-theme-main')
|
|
228
235
|
return { args, color }
|
|
229
236
|
},
|
|
230
237
|
template: `
|
|
@@ -241,19 +248,37 @@ export const WithAlpha: Story = {
|
|
|
241
248
|
|
|
242
249
|
// The slider is named by its visible label and reads the opacity out as a percentage.
|
|
243
250
|
const alpha = await canvas.findByRole('slider', { name: 'Dekking' })
|
|
244
|
-
expect(alpha).toHaveAttribute('aria-valuetext', '
|
|
251
|
+
expect(alpha).toHaveAttribute('aria-valuetext', '100%')
|
|
252
|
+
expect(canvas.getByRole('button', { name: 'Hoofdkleur' })).toHaveAttribute(
|
|
253
|
+
'aria-pressed',
|
|
254
|
+
'true',
|
|
255
|
+
)
|
|
245
256
|
|
|
257
|
+
// Opening wrote nothing, and a key that leaves the slider at 100% neither.
|
|
246
258
|
alpha.focus()
|
|
247
259
|
await userEvent.keyboard('{ArrowRight}')
|
|
260
|
+
await userEvent.keyboard('{End}')
|
|
261
|
+
expect(canvas.getByText('bg-theme-main')).toBeInTheDocument()
|
|
248
262
|
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
for (let step = 0; step < 10; step++) await userEvent.keyboard('{ArrowLeft}')
|
|
264
|
+
|
|
265
|
+
// 90% of 255 rounds to 0xe6; the picker writes hex in lower case.
|
|
266
|
+
await waitFor(() => {
|
|
267
|
+
expect(canvas.getByText('bg-[#1777eae6]')).toBeInTheDocument()
|
|
268
|
+
expect(canvas.getByRole('button', { name: 'Hoofdkleur' })).toHaveAttribute(
|
|
269
|
+
'aria-pressed',
|
|
270
|
+
'false',
|
|
271
|
+
)
|
|
272
|
+
})
|
|
251
273
|
|
|
252
274
|
await userEvent.click(canvas.getByRole('button', { name: 'Hoofdkleur' }))
|
|
253
275
|
|
|
254
276
|
await waitFor(() => {
|
|
255
277
|
expect(canvas.getByText('bg-theme-main')).toBeInTheDocument()
|
|
256
|
-
expect(canvas.
|
|
278
|
+
expect(canvas.getByRole('slider', { name: 'Dekking' })).toHaveAttribute(
|
|
279
|
+
'aria-valuetext',
|
|
280
|
+
'100%',
|
|
281
|
+
)
|
|
257
282
|
})
|
|
258
283
|
},
|
|
259
284
|
}
|
|
@@ -293,9 +318,13 @@ export const WithAlphaRolePicked: Story = {
|
|
|
293
318
|
|
|
294
319
|
await userEvent.click(canvas.getByRole('button', { name: 'Hoofdkleur' }))
|
|
295
320
|
|
|
321
|
+
// The role wins: it is stored as a role, and the slider is back at 100%.
|
|
296
322
|
await waitFor(() => {
|
|
297
323
|
expect(canvas.getByText('bg-theme-main')).toBeInTheDocument()
|
|
298
|
-
expect(canvas.
|
|
324
|
+
expect(canvas.getByRole('slider', { name: 'Dekking' })).toHaveAttribute(
|
|
325
|
+
'aria-valuetext',
|
|
326
|
+
'100%',
|
|
327
|
+
)
|
|
299
328
|
})
|
|
300
329
|
},
|
|
301
330
|
}
|
|
@@ -501,6 +530,47 @@ export const GradientRadial: Story = {
|
|
|
501
530
|
},
|
|
502
531
|
}
|
|
503
532
|
|
|
533
|
+
/**
|
|
534
|
+
* With `showAlpha` a role stop is not locked: its opacity slider stands at 100%
|
|
535
|
+
* without the hint, and a change turns the stop into the role's hex with alpha.
|
|
536
|
+
* Without `showAlpha` the lock stays, as `GradientRadial` shows.
|
|
537
|
+
*/
|
|
538
|
+
export const GradientRoleAlpha: Story = {
|
|
539
|
+
// Axe runs on the open panel, after the play function.
|
|
540
|
+
parameters: { a11y: { test: 'error' } },
|
|
541
|
+
args: { modes: ['solid', 'gradient'], showAlpha: true, themeRoles: THEME_ROLES },
|
|
542
|
+
render: gradientRender('bg-linear-90 from-theme-main to-[#ffffff]'),
|
|
543
|
+
play: async ({ canvasElement }) => {
|
|
544
|
+
const panel = await openAndMeasure(canvasElement)
|
|
545
|
+
const [swatch] = within(panel).getAllByRole('button', { name: 'Kleur kiezen' })
|
|
546
|
+
await userEvent.click(swatch!)
|
|
547
|
+
const dialog = await within(panel).findByRole('dialog', { name: 'Kleur van kleurstop 1' })
|
|
548
|
+
const opacity = within(dialog).getByRole('slider', { name: 'Dekking' })
|
|
549
|
+
expect(opacity).not.toHaveAttribute('aria-disabled')
|
|
550
|
+
expect(opacity).toHaveAttribute('aria-valuetext', '100%')
|
|
551
|
+
expect(opacity).not.toHaveAccessibleDescription()
|
|
552
|
+
expect(dialog).not.toHaveTextContent('Huisstijlkleuren zijn altijd dekkend.')
|
|
553
|
+
expect(within(dialog).getByRole('button', { name: 'Hoofdkleur' })).toHaveAttribute(
|
|
554
|
+
'aria-pressed',
|
|
555
|
+
'true',
|
|
556
|
+
)
|
|
557
|
+
// Opening the panel and the popover wrote nothing.
|
|
558
|
+
expect(canvasElement).toHaveTextContent('bg-linear-90 from-theme-main to-[#ffffff]')
|
|
559
|
+
|
|
560
|
+
opacity.focus()
|
|
561
|
+
await userEvent.keyboard('{ArrowLeft}')
|
|
562
|
+
|
|
563
|
+
// 99% of 255 rounds to 0xfc.
|
|
564
|
+
await waitFor(() => {
|
|
565
|
+
expect(canvasElement).toHaveTextContent('bg-linear-90 from-[#1777eafc] to-[#ffffff]')
|
|
566
|
+
expect(within(dialog).getByRole('button', { name: 'Hoofdkleur' })).toHaveAttribute(
|
|
567
|
+
'aria-pressed',
|
|
568
|
+
'false',
|
|
569
|
+
)
|
|
570
|
+
})
|
|
571
|
+
},
|
|
572
|
+
}
|
|
573
|
+
|
|
504
574
|
/**
|
|
505
575
|
* Three stops, the most the gradient takes: adding is unavailable, with the
|
|
506
576
|
* reason, and removing is possible. The middle stop is a theme role and the
|
|
@@ -397,20 +397,25 @@ describe('EditorColor', () => {
|
|
|
397
397
|
})
|
|
398
398
|
|
|
399
399
|
// The way to detach a colour from the theme without changing how it looks.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
400
|
+
// The opacity slider is shown for a role as well, so this holds with it too.
|
|
401
|
+
it.each([false, true])(
|
|
402
|
+
'writes a literal when the suggestion with the hex of the selected role is picked (showAlpha %s)',
|
|
403
|
+
async (showAlpha) => {
|
|
404
|
+
const wrapper = mount(EditorColor, {
|
|
405
|
+
props: {
|
|
406
|
+
modelValue: 'bg-theme-main',
|
|
407
|
+
themeRoles: [{ role: 'main', label: 'Hoofdkleur', hex: '#1777ea' }],
|
|
408
|
+
suggestions: ['#1777ea'],
|
|
409
|
+
showAlpha,
|
|
410
|
+
},
|
|
411
|
+
})
|
|
408
412
|
|
|
409
|
-
|
|
410
|
-
|
|
413
|
+
await open(wrapper)
|
|
414
|
+
await wrapper.find('button[aria-label="Kies kleur #1777ea"]').trigger('click')
|
|
411
415
|
|
|
412
|
-
|
|
413
|
-
|
|
416
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['bg-[#1777ea]']])
|
|
417
|
+
},
|
|
418
|
+
)
|
|
414
419
|
|
|
415
420
|
it('follows the model from a role to a literal and back', async () => {
|
|
416
421
|
const wrapper = mount(EditorColor, {
|
|
@@ -634,20 +639,101 @@ describe('EditorColor', () => {
|
|
|
634
639
|
expect(pickerValue(wrapper)).toBe('#1777EAff')
|
|
635
640
|
})
|
|
636
641
|
|
|
637
|
-
it('
|
|
642
|
+
it('keeps the slider at 100% while a theme role is selected, and opening writes nothing', async () => {
|
|
638
643
|
const wrapper = mount(EditorColor, {
|
|
639
644
|
props: { modelValue: 'bg-theme-main', themeRoles: THEME_ROLES, showAlpha: true },
|
|
640
645
|
})
|
|
641
646
|
|
|
642
|
-
expect(wrapper.findComponent(NexxtColorPicker).props('showAlpha')).toBe(
|
|
647
|
+
expect(wrapper.findComponent(NexxtColorPicker).props('showAlpha')).toBe(true)
|
|
648
|
+
|
|
649
|
+
await open(wrapper)
|
|
650
|
+
|
|
651
|
+
expect(alphaThumb(wrapper).attributes('aria-valuetext')).toBe('100%')
|
|
652
|
+
expect(pressedLabels(wrapper)).toEqual(['Hoofdkleur'])
|
|
653
|
+
expect(trigger(wrapper).text()).toBe('Hoofdkleur')
|
|
654
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
// The slider stays mounted while the model turns into a role from outside,
|
|
658
|
+
// so the picker's colour changes under it: that must not come back as a write.
|
|
659
|
+
it('writes nothing when a role becomes active under an open slider', async () => {
|
|
660
|
+
const wrapper = mount(EditorColor, {
|
|
661
|
+
props: { modelValue: 'bg-[#00FF0080]', themeRoles: THEME_ROLES, showAlpha: true },
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
await open(wrapper)
|
|
665
|
+
expect(alphaThumb(wrapper).attributes('aria-valuetext')).toBe('50%')
|
|
666
|
+
|
|
667
|
+
await wrapper.setProps({ modelValue: 'bg-theme-main' })
|
|
668
|
+
await flushPromises()
|
|
669
|
+
|
|
670
|
+
expect(alphaThumb(wrapper).attributes('aria-valuetext')).toBe('100%')
|
|
671
|
+
expect(hexField(wrapper).element.value).toBe('#1777ea')
|
|
672
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it.each(['ArrowRight', 'End', 'PageUp'])(
|
|
676
|
+
'keeps the role on %s, a key that leaves the slider at full opacity',
|
|
677
|
+
async (key) => {
|
|
678
|
+
const wrapper = mount(EditorColor, {
|
|
679
|
+
props: { modelValue: 'bg-theme-main', themeRoles: THEME_ROLES, showAlpha: true },
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
await open(wrapper)
|
|
683
|
+
await alphaThumb(wrapper).trigger('keydown', { key })
|
|
684
|
+
|
|
685
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
686
|
+
},
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
it('detaches the role into its hex with alpha when the slider moves, and keeps a literal at 100%', async () => {
|
|
690
|
+
const role: EditorColorValue = 'bg-theme-main'
|
|
691
|
+
const wrapper = mount(EditorColor, {
|
|
692
|
+
props: {
|
|
693
|
+
modelValue: role,
|
|
694
|
+
themeRoles: THEME_ROLES,
|
|
695
|
+
showAlpha: true,
|
|
696
|
+
'onUpdate:modelValue': (value: EditorColorValue) =>
|
|
697
|
+
wrapper.setProps({ modelValue: value }),
|
|
698
|
+
},
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
await open(wrapper)
|
|
702
|
+
await alphaThumb(wrapper).trigger('keydown', { key: 'ArrowLeft' })
|
|
703
|
+
await flushPromises()
|
|
704
|
+
|
|
705
|
+
// 99% of 255 rounds to 0xfc; the role's hex comes in lower case.
|
|
706
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['bg-[#1777eafc]']])
|
|
707
|
+
expect(pressedLabels(wrapper)).toEqual([])
|
|
708
|
+
expect(trigger(wrapper).text()).toBe('#1777eafc')
|
|
709
|
+
expect(alphaThumb(wrapper).attributes('aria-valuetext')).toBe('99%')
|
|
710
|
+
|
|
711
|
+
// Back at full opacity it is the role's colour, but no longer the role.
|
|
712
|
+
await alphaThumb(wrapper).trigger('keydown', { key: 'ArrowRight' })
|
|
713
|
+
await flushPromises()
|
|
714
|
+
|
|
715
|
+
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['bg-[#1777ea]'])
|
|
716
|
+
expect(pressedLabels(wrapper)).toEqual([])
|
|
717
|
+
expect(trigger(wrapper).text()).toBe('#1777ea')
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
// A role stop has no opacity without the slider; that lock is covered in
|
|
721
|
+
// EditorColorGradient.test.ts. Here the solid mode keeps working unchanged.
|
|
722
|
+
it('leaves the slider out for a role without showAlpha', async () => {
|
|
723
|
+
const wrapper = mount(EditorColor, {
|
|
724
|
+
props: { modelValue: 'bg-theme-main', themeRoles: THEME_ROLES },
|
|
725
|
+
})
|
|
643
726
|
|
|
644
727
|
await open(wrapper)
|
|
645
728
|
|
|
646
729
|
expect(alphaThumb(wrapper).exists()).toBe(false)
|
|
730
|
+
expect(pickerValue(wrapper)).toBe('#1777EAff')
|
|
731
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
647
732
|
})
|
|
648
733
|
|
|
649
|
-
//
|
|
650
|
-
//
|
|
734
|
+
// A role has no opacity, so it wins over the slider: picked below 100% it is
|
|
735
|
+
// stored as the role, and the slider jumps back to 100% without writing the
|
|
736
|
+
// old colour back over it.
|
|
651
737
|
it('writes only the role when one is picked over a translucent literal', async () => {
|
|
652
738
|
// Typed up front: the handler refers to `wrapper`, which stops inference
|
|
653
739
|
// from narrowing an inline literal to the model's type.
|
|
@@ -669,12 +755,13 @@ describe('EditorColor', () => {
|
|
|
669
755
|
await flushPromises()
|
|
670
756
|
|
|
671
757
|
expect(wrapper.emitted('update:modelValue')).toEqual([['bg-theme-main']])
|
|
672
|
-
expect(alphaThumb(wrapper).
|
|
758
|
+
expect(alphaThumb(wrapper).attributes('aria-valuetext')).toBe('100%')
|
|
759
|
+
expect(pressedLabels(wrapper)).toEqual(['Hoofdkleur'])
|
|
673
760
|
expect(trigger(wrapper).text()).toBe('Hoofdkleur')
|
|
674
761
|
expect(hexField(wrapper).element.value).toBe('#1777ea')
|
|
675
762
|
})
|
|
676
763
|
|
|
677
|
-
it('
|
|
764
|
+
it('shows a literal that replaces the role with its opacity', async () => {
|
|
678
765
|
const wrapper = mount(EditorColor, {
|
|
679
766
|
props: { modelValue: 'bg-theme-main', themeRoles: THEME_ROLES, showAlpha: true },
|
|
680
767
|
})
|
|
@@ -92,11 +92,13 @@ export interface NexxtEditorColorProps {
|
|
|
92
92
|
/**
|
|
93
93
|
* Offer an opacity slider in the picker. A colour picked below full opacity is
|
|
94
94
|
* written as an 8-digit hex (`bg-[#1777ea80]`); a fully opaque one keeps the
|
|
95
|
-
* 6-digit form.
|
|
96
|
-
* class carries no opacity
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
95
|
+
* 6-digit form. While a theme role is selected the slider stands at 100%, as a
|
|
96
|
+
* role class carries no opacity; moving it writes the role's hex with that
|
|
97
|
+
* opacity, so the role is no longer selected. The same goes for a role stop of
|
|
98
|
+
* a gradient, which without `showAlpha` stays opaque. Off by default, in which
|
|
99
|
+
* case a stored 8-digit hex is shown opaque and only replaced once a new colour
|
|
100
|
+
* is picked. Turning it off while a translucent colour is set does write at
|
|
101
|
+
* once: the picker drops the alpha and the model gets the opaque 6-digit hex.
|
|
100
102
|
*/
|
|
101
103
|
showAlpha?: boolean
|
|
102
104
|
/**
|
|
@@ -584,6 +584,45 @@ describe('NexxtEditorColor gradient', () => {
|
|
|
584
584
|
expect(w.find('[role="dialog"] [aria-disabled="true"][role="slider"]').exists()).toBe(false)
|
|
585
585
|
})
|
|
586
586
|
|
|
587
|
+
it('unlocks the opacity of a role stop with showAlpha, and a change makes it a hex stop', async () => {
|
|
588
|
+
const { w, values } = await setup('bg-linear-90 from-theme-main to-[#ffffff]', {
|
|
589
|
+
showAlpha: true,
|
|
590
|
+
})
|
|
591
|
+
await openPopover(w, 0)
|
|
592
|
+
const dialog = w.find('[role="dialog"]')
|
|
593
|
+
const thumb = dialog.find('[role="slider"][aria-labelledby]')
|
|
594
|
+
expect(thumb.attributes('aria-disabled')).toBeUndefined()
|
|
595
|
+
expect(thumb.attributes('aria-describedby')).toBeUndefined()
|
|
596
|
+
expect(thumb.attributes('aria-valuenow')).toBe('100')
|
|
597
|
+
expect(dialog.text()).not.toContain('Huisstijlkleuren')
|
|
598
|
+
expect(dialog.find('button[aria-label="Hoofdkleur"]').attributes('aria-pressed')).toBe('true')
|
|
599
|
+
// Opening the panel and the popover wrote nothing.
|
|
600
|
+
expect(values).toEqual([])
|
|
601
|
+
|
|
602
|
+
await thumb.trigger('keydown', { key: 'ArrowRight' })
|
|
603
|
+
await settle()
|
|
604
|
+
expect(values).toEqual([])
|
|
605
|
+
|
|
606
|
+
await thumb.trigger('keydown', { key: 'ArrowLeft' })
|
|
607
|
+
await settle()
|
|
608
|
+
// 99% of 255 rounds to 0xfc.
|
|
609
|
+
expect(values).toEqual(['bg-linear-90 from-[#1777eafc] to-[#ffffff]'])
|
|
610
|
+
expect(
|
|
611
|
+
w.find('[role="dialog"] button[aria-label="Hoofdkleur"]').attributes('aria-pressed'),
|
|
612
|
+
).toBe('false')
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('keeps the opacity of a role stop locked without showAlpha', async () => {
|
|
616
|
+
const { w, values } = await setup('bg-linear-90 from-theme-main to-[#ffffff]')
|
|
617
|
+
await openPopover(w, 0)
|
|
618
|
+
const thumb = w.find('[role="dialog"] [role="slider"][aria-disabled="true"]')
|
|
619
|
+
expect(thumb.exists()).toBe(true)
|
|
620
|
+
|
|
621
|
+
await thumb.trigger('keydown', { key: 'ArrowLeft' })
|
|
622
|
+
await settle()
|
|
623
|
+
expect(values).toEqual([])
|
|
624
|
+
})
|
|
625
|
+
|
|
587
626
|
it('offers the neighbouring stop first among the suggestions', async () => {
|
|
588
627
|
const { w } = await setup('bg-linear-90 from-[#111111] to-[#222222]', {
|
|
589
628
|
suggestions: ['#ff0000'],
|
|
@@ -31,7 +31,10 @@ export interface ColorFieldProps<P extends string> {
|
|
|
31
31
|
suggestions?: string[]
|
|
32
32
|
/** Theme roles offered above the suggestions; an entry with an unreadable hex is left out. */
|
|
33
33
|
themeRoles?: EditorColorThemeRole[]
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Offer an opacity slider, also while a theme role is selected: it then stands
|
|
36
|
+
* at 100%, and moving it turns the role into its hex with that opacity.
|
|
37
|
+
*/
|
|
35
38
|
showAlpha?: boolean
|
|
36
39
|
/**
|
|
37
40
|
* The fills offered: `['solid']` (the default), `['solid', 'gradient']` or
|
|
@@ -122,25 +125,27 @@ const offeredRoles = computed(() => themeRoles.filter((entry) => ROLE_HEX_PATTER
|
|
|
122
125
|
const activeRole = computed(() => offeredRoles.value.find(({ role }) => role === storedRole.value))
|
|
123
126
|
|
|
124
127
|
/**
|
|
128
|
+
* A role's hex as the picker gets it: opaque, in the 8-digit form.
|
|
129
|
+
*
|
|
125
130
|
* Picking the role's own colour -- from the suggestions, or typed -- is how a
|
|
126
131
|
* colour is detached from the theme without changing how it looks. The picker
|
|
127
|
-
* reports nothing when it is told to pick the string it already has, so
|
|
128
|
-
*
|
|
129
|
-
*
|
|
132
|
+
* reports nothing when it is told to pick the string it already has, so the hex
|
|
133
|
+
* is handed over with an `ff` pair: the picker writes an opaque colour as six
|
|
134
|
+
* digits and an alpha pair only below full opacity, so it never writes this
|
|
135
|
+
* string back, and every pick is a change. That holds with the opacity slider
|
|
136
|
+
* shown as well.
|
|
137
|
+
*
|
|
138
|
+
* A role has no opacity, so an alpha pair in the role's own hex is replaced by
|
|
139
|
+
* `ff`: the slider of an active role stands at 100%. Moving it below that writes
|
|
140
|
+
* the role's hex with the alpha pair, which detaches the role the same way;
|
|
141
|
+
* back at 100% it stays a literal (`OPAQUE_ALPHA_PATTERN`).
|
|
130
142
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
143
|
+
* Opening the panel, or a role becoming active, writes nothing: the picker's
|
|
144
|
+
* primitives only report a change the user makes, and a key that leaves the
|
|
145
|
+
* slider where it is (ArrowRight at 100%) is no change.
|
|
134
146
|
*/
|
|
135
147
|
const pickerHex = (roleHex: string) => `${roleHex.slice(0, 7)}ff`
|
|
136
148
|
|
|
137
|
-
/**
|
|
138
|
-
* A role class has no opacity, so the slider is only offered for a literal.
|
|
139
|
-
* Hiding it is also what keeps the trick in `pickerHex` working: without the
|
|
140
|
-
* alpha channel the picker reads the role's hex as opaque.
|
|
141
|
-
*/
|
|
142
|
-
const alphaEnabled = computed(() => showAlpha && !activeRole.value)
|
|
143
|
-
|
|
144
149
|
// An unparseable value -- empty, a token class such as `bg-red-500`, or copy
|
|
145
150
|
// from before this component -- leaves the picker empty and its placeholder
|
|
146
151
|
// visible, rather than being reported as a problem: the first pick writes a
|
|
@@ -401,7 +406,7 @@ watch(
|
|
|
401
406
|
:label="label"
|
|
402
407
|
:describedby="describedby"
|
|
403
408
|
:formats="['hex']"
|
|
404
|
-
:show-alpha="
|
|
409
|
+
:show-alpha="showAlpha"
|
|
405
410
|
:placeholder="placeholder"
|
|
406
411
|
:suggestions="{ hex: suggestions }"
|
|
407
412
|
:suggestions-label="suggestionsLabel"
|
|
@@ -616,6 +616,64 @@ describe('NexxtColorPicker', () => {
|
|
|
616
616
|
wrapper.unmount()
|
|
617
617
|
})
|
|
618
618
|
|
|
619
|
+
/** A two-stop gradient whose first stop is bound to a preset, with that stop's popover open. */
|
|
620
|
+
const openPresetStop = async (showAlpha: boolean) => {
|
|
621
|
+
const wrapper = mount(NexxtColorPicker, {
|
|
622
|
+
props: {
|
|
623
|
+
modes: ['gradient'],
|
|
624
|
+
showAlpha,
|
|
625
|
+
gradientValue: {
|
|
626
|
+
type: 'linear',
|
|
627
|
+
angle: 90,
|
|
628
|
+
centerX: 50,
|
|
629
|
+
centerY: 50,
|
|
630
|
+
stops: [
|
|
631
|
+
{ color: '#111111', position: 0, preset: { key: 'main', label: 'Hoofdkleur' } },
|
|
632
|
+
{ color: '#ffffff', position: 100 },
|
|
633
|
+
],
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
attachTo: document.body,
|
|
637
|
+
})
|
|
638
|
+
await open(wrapper)
|
|
639
|
+
await wrapper.find('button[aria-label="Kleur kiezen"]').trigger('click')
|
|
640
|
+
await flushPromises()
|
|
641
|
+
await nextTick()
|
|
642
|
+
const thumb = wrapper.find('[role="dialog"] [role="slider"][aria-labelledby]')
|
|
643
|
+
return { wrapper, thumb }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
it('locks the opacity of a preset stop without showAlpha', async () => {
|
|
647
|
+
const { wrapper, thumb } = await openPresetStop(false)
|
|
648
|
+
expect(thumb.attributes('aria-disabled')).toBe('true')
|
|
649
|
+
expect(wrapper.text()).toContain('Huisstijlkleuren zijn altijd dekkend.')
|
|
650
|
+
|
|
651
|
+
await thumb.trigger('keydown', { key: 'ArrowLeft' })
|
|
652
|
+
await flushPromises()
|
|
653
|
+
expect(wrapper.emitted('update:gradientValue')).toBeUndefined()
|
|
654
|
+
wrapper.unmount()
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
it('unbinds a preset stop when its opacity changes with showAlpha', async () => {
|
|
658
|
+
const { wrapper, thumb } = await openPresetStop(true)
|
|
659
|
+
expect(thumb.attributes('aria-disabled')).toBeUndefined()
|
|
660
|
+
expect(thumb.attributes('aria-valuenow')).toBe('100')
|
|
661
|
+
expect(wrapper.text()).not.toContain('Huisstijlkleuren zijn altijd dekkend.')
|
|
662
|
+
expect(wrapper.emitted('update:gradientValue')).toBeUndefined()
|
|
663
|
+
|
|
664
|
+
await thumb.trigger('keydown', { key: 'ArrowLeft' })
|
|
665
|
+
await flushPromises()
|
|
666
|
+
// 99% of 255 rounds to 0xfc; the preset is gone from the stop.
|
|
667
|
+
expect(wrapper.emitted('update:gradientValue')?.at(-1)?.[0]).toMatchObject({
|
|
668
|
+
stops: [{ color: '#111111fc', position: 0 }, { color: '#ffffff' }],
|
|
669
|
+
})
|
|
670
|
+
expect(wrapper.emitted('update:gradientValue')?.at(-1)?.[0]).not.toHaveProperty(
|
|
671
|
+
'stops.0.preset',
|
|
672
|
+
)
|
|
673
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
674
|
+
wrapper.unmount()
|
|
675
|
+
})
|
|
676
|
+
|
|
619
677
|
it('emits close when the panel closes', async () => {
|
|
620
678
|
const wrapper = mount(NexxtColorPicker, {
|
|
621
679
|
props: { modes: ['solid', 'gradient'] },
|
|
@@ -61,7 +61,9 @@ export interface NexxtColorPickerProps {
|
|
|
61
61
|
/**
|
|
62
62
|
* Offer an opacity slider and accept colours below full opacity. Turning it
|
|
63
63
|
* off while the colour is translucent writes the opaque colour to the model
|
|
64
|
-
* at once.
|
|
64
|
+
* at once. The gradient mode always has the slider; without `showAlpha` it is
|
|
65
|
+
* locked at 100% for a stop bound to a preset, with it a change unbinds the
|
|
66
|
+
* stop from its preset.
|
|
65
67
|
*/
|
|
66
68
|
showAlpha?: boolean
|
|
67
69
|
formats?: ColorFormat[]
|
|
@@ -283,8 +285,15 @@ const modeModel = computed({
|
|
|
283
285
|
/** The opacity slider is always there in the gradient mode. */
|
|
284
286
|
const alphaOn = computed(() => props.showAlpha || gradientMode.value)
|
|
285
287
|
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Without `showAlpha` a stop bound to a preset is always opaque: its slider
|
|
290
|
+
* stays, at 100 and disabled. With `showAlpha` the slider stays usable, as it is
|
|
291
|
+
* for the solid colour, and a change unbinds the stop from its preset
|
|
292
|
+
* (`commitColor`), so the stop becomes a hex with that opacity.
|
|
293
|
+
*/
|
|
294
|
+
const alphaLocked = computed(
|
|
295
|
+
() => gradientMode.value && !props.showAlpha && !!editor.selected.value?.preset,
|
|
296
|
+
)
|
|
288
297
|
|
|
289
298
|
const panelClass = computed(
|
|
290
299
|
() =>
|