ngwr 11.0.0 → 11.2.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/action-sheet/styles/_index.scss +4 -1
- package/anchor/styles/_index.scss +2 -1
- package/calendar/styles/_index.scss +11 -2
- package/cascader/styles/_index.scss +3 -1
- package/circular-text/styles/_index.scss +11 -0
- package/command-palette/styles/_index.scss +14 -1
- package/context-menu/styles/_index.scss +2 -1
- package/falling-text/styles/_index.scss +2 -1
- package/fesm2022/ngwr-action-sheet-testing.mjs +313 -0
- package/fesm2022/ngwr-action-sheet-testing.mjs.map +1 -0
- package/fesm2022/ngwr-bar-chart-testing.mjs +89 -0
- package/fesm2022/ngwr-bar-chart-testing.mjs.map +1 -0
- package/fesm2022/ngwr-blur-text-testing.mjs +203 -0
- package/fesm2022/ngwr-blur-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-blur-text.mjs +15 -1
- package/fesm2022/ngwr-blur-text.mjs.map +1 -1
- package/fesm2022/ngwr-border-glow-testing.mjs +325 -0
- package/fesm2022/ngwr-border-glow-testing.mjs.map +1 -0
- package/fesm2022/ngwr-border-glow.mjs +2 -2
- package/fesm2022/ngwr-breadcrumbs.mjs +2 -2
- package/fesm2022/ngwr-calendar-heatmap-testing.mjs +95 -0
- package/fesm2022/ngwr-calendar-heatmap-testing.mjs.map +1 -0
- package/fesm2022/ngwr-calendar-testing.mjs +368 -0
- package/fesm2022/ngwr-calendar-testing.mjs.map +1 -0
- package/fesm2022/ngwr-calendar.mjs +43 -3
- package/fesm2022/ngwr-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-card.mjs +2 -2
- package/fesm2022/ngwr-cascader.mjs +14 -2
- package/fesm2022/ngwr-cascader.mjs.map +1 -1
- package/fesm2022/ngwr-checkbox.mjs +8 -0
- package/fesm2022/ngwr-checkbox.mjs.map +1 -1
- package/fesm2022/ngwr-circular-text-testing.mjs +230 -0
- package/fesm2022/ngwr-circular-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-circular-text.mjs +2 -2
- package/fesm2022/ngwr-circular-text.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker-testing.mjs +432 -0
- package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -0
- package/fesm2022/ngwr-color-picker.mjs +29 -3
- package/fesm2022/ngwr-color-picker.mjs.map +1 -1
- package/fesm2022/ngwr-context-menu.mjs +25 -5
- package/fesm2022/ngwr-context-menu.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker-testing.mjs +10 -79
- package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-decrypt-text-testing.mjs +261 -0
- package/fesm2022/ngwr-decrypt-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-decrypt-text.mjs +6 -1
- package/fesm2022/ngwr-decrypt-text.mjs.map +1 -1
- package/fesm2022/ngwr-donut-chart-testing.mjs +93 -0
- package/fesm2022/ngwr-donut-chart-testing.mjs.map +1 -0
- package/fesm2022/ngwr-drag-drop.mjs +2 -2
- package/fesm2022/ngwr-drawer.mjs +36 -7
- package/fesm2022/ngwr-drawer.mjs.map +1 -1
- package/fesm2022/ngwr-dropdown.mjs +32 -2
- package/fesm2022/ngwr-dropdown.mjs.map +1 -1
- package/fesm2022/ngwr-event-calendar-testing.mjs +446 -0
- package/fesm2022/ngwr-event-calendar-testing.mjs.map +1 -0
- package/fesm2022/ngwr-event-calendar.mjs +43 -18
- package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-falling-text-testing.mjs +218 -0
- package/fesm2022/ngwr-falling-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-falling-text.mjs +2 -2
- package/fesm2022/ngwr-falling-text.mjs.map +1 -1
- package/fesm2022/ngwr-fuzzy-text-testing.mjs +134 -0
- package/fesm2022/ngwr-fuzzy-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-gauge-testing.mjs +86 -0
- package/fesm2022/ngwr-gauge-testing.mjs.map +1 -0
- package/fesm2022/ngwr-glitch-text-testing.mjs +181 -0
- package/fesm2022/ngwr-glitch-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-glitch-text.mjs +2 -2
- package/fesm2022/ngwr-gradient-text-testing.mjs +223 -0
- package/fesm2022/ngwr-gradient-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-gradient-text.mjs +2 -2
- package/fesm2022/ngwr-i18n-en.mjs +14 -0
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +14 -0
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-image-cropper-testing.mjs +156 -0
- package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -0
- package/fesm2022/ngwr-image-cropper.mjs +10 -3
- package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
- package/fesm2022/ngwr-input-number.mjs +73 -6
- package/fesm2022/ngwr-input-number.mjs.map +1 -1
- package/fesm2022/ngwr-knob-testing.mjs +220 -0
- package/fesm2022/ngwr-knob-testing.mjs.map +1 -0
- package/fesm2022/ngwr-lightbox-testing.mjs +223 -0
- package/fesm2022/ngwr-lightbox-testing.mjs.map +1 -0
- package/fesm2022/ngwr-lightbox.mjs +9 -2
- package/fesm2022/ngwr-lightbox.mjs.map +1 -1
- package/fesm2022/ngwr-line-chart-testing.mjs +119 -0
- package/fesm2022/ngwr-line-chart-testing.mjs.map +1 -0
- package/fesm2022/ngwr-list.mjs +2 -2
- package/fesm2022/ngwr-loading-bar.mjs +2 -2
- package/fesm2022/ngwr-marquee-testing.mjs +336 -0
- package/fesm2022/ngwr-marquee-testing.mjs.map +1 -0
- package/fesm2022/ngwr-marquee.mjs +2 -2
- package/fesm2022/ngwr-meter-group-testing.mjs +80 -0
- package/fesm2022/ngwr-meter-group-testing.mjs.map +1 -0
- package/fesm2022/ngwr-overlay.mjs +28 -1
- package/fesm2022/ngwr-overlay.mjs.map +1 -1
- package/fesm2022/ngwr-popover.mjs +14 -0
- package/fesm2022/ngwr-popover.mjs.map +1 -1
- package/fesm2022/ngwr-rotating-text-testing.mjs +220 -0
- package/fesm2022/ngwr-rotating-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-rotating-text.mjs +2 -2
- package/fesm2022/ngwr-rotating-text.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +70 -11
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-shiny-text-testing.mjs +237 -0
- package/fesm2022/ngwr-shiny-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-shiny-text.mjs +2 -2
- package/fesm2022/ngwr-slider.mjs +44 -6
- package/fesm2022/ngwr-slider.mjs.map +1 -1
- package/fesm2022/ngwr-sparkline-testing.mjs +76 -0
- package/fesm2022/ngwr-sparkline-testing.mjs.map +1 -0
- package/fesm2022/ngwr-speed-dial-testing.mjs +279 -0
- package/fesm2022/ngwr-speed-dial-testing.mjs.map +1 -0
- package/fesm2022/ngwr-splash-cursor-testing.mjs +89 -0
- package/fesm2022/ngwr-splash-cursor-testing.mjs.map +1 -0
- package/fesm2022/ngwr-split-text-testing.mjs +224 -0
- package/fesm2022/ngwr-split-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-split-text.mjs +15 -1
- package/fesm2022/ngwr-split-text.mjs.map +1 -1
- package/fesm2022/ngwr-splitter-testing.mjs +245 -0
- package/fesm2022/ngwr-splitter-testing.mjs.map +1 -0
- package/fesm2022/ngwr-spotlight-card-testing.mjs +368 -0
- package/fesm2022/ngwr-spotlight-card-testing.mjs.map +1 -0
- package/fesm2022/ngwr-spotlight-card.mjs +2 -2
- package/fesm2022/ngwr-star-border-testing.mjs +172 -0
- package/fesm2022/ngwr-star-border-testing.mjs.map +1 -0
- package/fesm2022/ngwr-star-border.mjs +2 -2
- package/fesm2022/ngwr-table.mjs +13 -3
- package/fesm2022/ngwr-table.mjs.map +1 -1
- package/fesm2022/ngwr-tabs.mjs +15 -1
- package/fesm2022/ngwr-tabs.mjs.map +1 -1
- package/fesm2022/ngwr-tilt-card-testing.mjs +190 -0
- package/fesm2022/ngwr-tilt-card-testing.mjs.map +1 -0
- package/fesm2022/ngwr-tilt-card.mjs +21 -3
- package/fesm2022/ngwr-tilt-card.mjs.map +1 -1
- package/fesm2022/ngwr-toast.mjs +36 -7
- package/fesm2022/ngwr-toast.mjs.map +1 -1
- package/fesm2022/ngwr-tour-testing.mjs +173 -0
- package/fesm2022/ngwr-tour-testing.mjs.map +1 -0
- package/fesm2022/ngwr-tour.mjs +2 -2
- package/fesm2022/ngwr-tour.mjs.map +1 -1
- package/fesm2022/ngwr-tree.mjs +10 -3
- package/fesm2022/ngwr-tree.mjs.map +1 -1
- package/fesm2022/ngwr-typewriter-testing.mjs +228 -0
- package/fesm2022/ngwr-typewriter-testing.mjs.map +1 -0
- package/fesm2022/ngwr-typewriter.mjs +2 -2
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/fesm2022/ngwr-virtual-scroll.mjs +2 -2
- package/fesm2022/ngwr-waves-testing.mjs +128 -0
- package/fesm2022/ngwr-waves-testing.mjs.map +1 -0
- package/fesm2022/ngwr-waves.mjs +2 -2
- package/fesm2022/ngwr-window-testing.mjs +361 -0
- package/fesm2022/ngwr-window-testing.mjs.map +1 -0
- package/fesm2022/ngwr-window.mjs +84 -10
- package/fesm2022/ngwr-window.mjs.map +1 -1
- package/file-upload/styles/_index.scss +7 -2
- package/i18n/en.json +16 -2
- package/i18n/ru.json +16 -2
- package/llms-full.txt +147 -0
- package/llms.txt +27 -8
- package/mcp/providers.js +29 -0
- package/mcp/tools.js +1 -29
- package/package.json +145 -1
- package/result/styles/_index.scss +3 -1
- package/segmented/styles/_index.scss +5 -1
- package/select/styles/_index.scss +3 -1
- package/skills/ngwr/SKILL.md +115 -0
- package/skills/ngwr/references/catalog.md +212 -0
- package/skills/ngwr/references/setup.md +73 -0
- package/stepper/styles/_index.scss +18 -2
- package/table/styles/_index.scss +3 -1
- package/theme/styles/_colors.scss +15 -2
- package/theme/styles/_dark.scss +24 -12
- package/tree/styles/_index.scss +4 -2
- package/types/ngwr-action-sheet-testing.d.ts +260 -0
- package/types/ngwr-bar-chart-testing.d.ts +77 -0
- package/types/ngwr-blur-text-testing.d.ts +180 -0
- package/types/ngwr-blur-text.d.ts +12 -0
- package/types/ngwr-border-glow-testing.d.ts +219 -0
- package/types/ngwr-calendar-heatmap-testing.d.ts +79 -0
- package/types/ngwr-calendar-testing.d.ts +277 -0
- package/types/ngwr-calendar.d.ts +10 -0
- package/types/ngwr-checkbox.d.ts +13 -0
- package/types/ngwr-circular-text-testing.d.ts +196 -0
- package/types/ngwr-color-picker-testing.d.ts +291 -0
- package/types/ngwr-color-picker.d.ts +13 -1
- package/types/ngwr-context-menu.d.ts +15 -1
- package/types/ngwr-date-picker-testing.d.ts +6 -66
- package/types/ngwr-decrypt-text-testing.d.ts +222 -0
- package/types/ngwr-donut-chart-testing.d.ts +79 -0
- package/types/ngwr-drawer.d.ts +12 -0
- package/types/ngwr-dropdown.d.ts +9 -0
- package/types/ngwr-event-calendar-testing.d.ts +301 -0
- package/types/ngwr-event-calendar.d.ts +9 -6
- package/types/ngwr-falling-text-testing.d.ts +186 -0
- package/types/ngwr-fuzzy-text-testing.d.ts +130 -0
- package/types/ngwr-gauge-testing.d.ts +71 -0
- package/types/ngwr-glitch-text-testing.d.ts +172 -0
- package/types/ngwr-gradient-text-testing.d.ts +173 -0
- package/types/ngwr-image-cropper-testing.d.ts +134 -0
- package/types/ngwr-image-cropper.d.ts +1 -0
- package/types/ngwr-input-number.d.ts +33 -1
- package/types/ngwr-knob-testing.d.ts +150 -0
- package/types/ngwr-lightbox-testing.d.ts +154 -0
- package/types/ngwr-lightbox.d.ts +6 -0
- package/types/ngwr-line-chart-testing.d.ts +93 -0
- package/types/ngwr-marquee-testing.d.ts +280 -0
- package/types/ngwr-meter-group-testing.d.ts +67 -0
- package/types/ngwr-overlay.d.ts +19 -1
- package/types/ngwr-rotating-text-testing.d.ts +191 -0
- package/types/ngwr-select.d.ts +32 -0
- package/types/ngwr-shiny-text-testing.d.ts +165 -0
- package/types/ngwr-slider.d.ts +15 -0
- package/types/ngwr-sparkline-testing.d.ts +69 -0
- package/types/ngwr-speed-dial-testing.d.ts +195 -0
- package/types/ngwr-splash-cursor-testing.d.ts +95 -0
- package/types/ngwr-split-text-testing.d.ts +209 -0
- package/types/ngwr-split-text.d.ts +12 -0
- package/types/ngwr-splitter-testing.d.ts +174 -0
- package/types/ngwr-spotlight-card-testing.d.ts +303 -0
- package/types/ngwr-star-border-testing.d.ts +135 -0
- package/types/ngwr-table.d.ts +10 -1
- package/types/ngwr-tilt-card-testing.d.ts +158 -0
- package/types/ngwr-tilt-card.d.ts +2 -0
- package/types/ngwr-toast.d.ts +14 -5
- package/types/ngwr-tour-testing.d.ts +143 -0
- package/types/ngwr-tree.d.ts +7 -0
- package/types/ngwr-typewriter-testing.d.ts +201 -0
- package/types/ngwr-version.d.ts +1 -1
- package/types/ngwr-waves-testing.d.ts +121 -0
- package/types/ngwr-window-testing.d.ts +263 -0
- package/types/ngwr-window.d.ts +27 -10
- package/window/styles/_index.scss +44 -16
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
|
|
2
|
+
import { WrSegmentedHarness } from 'ngwr/segmented/testing';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @license
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
8
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* One labelled numeric field — internal. A `TestElement` cannot be queried into, so
|
|
12
|
+
* pairing an input with the label printed under it means a harness anchored on the
|
|
13
|
+
* `<label>` that holds both.
|
|
14
|
+
*/
|
|
15
|
+
class FieldHarness extends ComponentHarness {
|
|
16
|
+
static hostSelector = '.wr-color-picker__field';
|
|
17
|
+
async getLabel() {
|
|
18
|
+
return (await this.locatorFor('.wr-color-picker__field-label')()).text();
|
|
19
|
+
}
|
|
20
|
+
async getInput() {
|
|
21
|
+
return this.locatorFor('input')();
|
|
22
|
+
}
|
|
23
|
+
async getValue() {
|
|
24
|
+
return (await this.getInput()).getProperty('value');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** An inline `left` / `top` percentage as a number. */
|
|
28
|
+
async function percent(thumb, side) {
|
|
29
|
+
return Number.parseFloat(await thumb.getCssValue(side));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Test harness for `<wr-color-picker>` — the inline picker, and the one the
|
|
33
|
+
* `[wrColorPickerTrigger]` directive mounts in an overlay.
|
|
34
|
+
*
|
|
35
|
+
* **What this harness will not do: drag.** The SV canvas and the hue / alpha
|
|
36
|
+
* sliders are pointer surfaces that read `getBoundingClientRect()` and divide by
|
|
37
|
+
* its width. A unit test has no layout, so every box is 0×0 and the division is
|
|
38
|
+
* `NaN` — a `setHue()` built on a synthetic pointer event would report success and
|
|
39
|
+
* write nothing, or write garbage. The numeric fields reach the same state and are
|
|
40
|
+
* also the only keyboard path the component has, so they are the whole write API
|
|
41
|
+
* here. {@link getThumbs} reads where the thumbs ARE, from the percentages the
|
|
42
|
+
* component writes inline, which is how a spec checks a surface followed a value.
|
|
43
|
+
*
|
|
44
|
+
* **And it will not read the preview swatch.** The preview paints the current
|
|
45
|
+
* colour as a CSS background, and jsdom normalises that to `rgb(…)` — dropping the
|
|
46
|
+
* alpha channel entirely, so `#ff880080` and `#ff8800ff` come back identical. A
|
|
47
|
+
* `getColor()` reading it would answer one thing in a unit test and another in a
|
|
48
|
+
* browser. {@link getHex} is the colour: the field holds the canonical 8-digit
|
|
49
|
+
* string, alpha included, whatever `format` writes into `value`.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const picker = await loader.getHarness(WrColorPickerHarness);
|
|
54
|
+
*
|
|
55
|
+
* await picker.setHex('#3969e2');
|
|
56
|
+
* expect(await picker.getHex()).toBe('#3969e2ff');
|
|
57
|
+
*
|
|
58
|
+
* await picker.setTab('rgb');
|
|
59
|
+
* expect(await picker.getRgb()).toEqual({ r: 57, g: 105, b: 226 });
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @see https://ngwr.dev/guides/testing
|
|
63
|
+
*/
|
|
64
|
+
class WrColorPickerHarness extends ComponentHarness {
|
|
65
|
+
static hostSelector = 'wr-color-picker';
|
|
66
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
67
|
+
static with(options = {}) {
|
|
68
|
+
return new HarnessPredicate(WrColorPickerHarness, options)
|
|
69
|
+
.addOption('color', options.color, (harness, color) => HarnessPredicate.stringMatches(harness.readHex(), color))
|
|
70
|
+
.addOption('tab', options.tab, async (harness, tab) => (await harness.getTab()) === tab)
|
|
71
|
+
.addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);
|
|
72
|
+
}
|
|
73
|
+
hexField = this.locatorForOptional('.wr-color-picker__inputs--hex input');
|
|
74
|
+
tabs = this.locatorFor(WrSegmentedHarness.with({ selector: '.wr-color-picker__tabs' }));
|
|
75
|
+
/**
|
|
76
|
+
* The tab switcher, as a {@link WrSegmentedHarness}.
|
|
77
|
+
*
|
|
78
|
+
* Handed back rather than re-queried: the switcher IS a `<wr-segmented>`, which
|
|
79
|
+
* already knows that the roving tab stop is not the selection and how the sliding
|
|
80
|
+
* thumb reports itself. Re-reading its buttons here would be a second, worse copy.
|
|
81
|
+
*/
|
|
82
|
+
async getTabs() {
|
|
83
|
+
return this.tabs();
|
|
84
|
+
}
|
|
85
|
+
/** Which set of numeric fields is showing. */
|
|
86
|
+
async getTab() {
|
|
87
|
+
const label = await (await this.tabs()).getSelectedLabel();
|
|
88
|
+
return (label ?? 'HEX').toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
/** Show a different set of numeric fields. An already-active tab is left alone. */
|
|
91
|
+
async setTab(tab) {
|
|
92
|
+
if ((await this.getTab()) === tab)
|
|
93
|
+
return;
|
|
94
|
+
await (await this.tabs()).select({ label: tab.toUpperCase() });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The canonical hex string, as the HEX field shows it — 8 digits while `alpha` is
|
|
98
|
+
* on, 6 while it is off.
|
|
99
|
+
*
|
|
100
|
+
* This is the picker's colour, and deliberately not the same thing as its `value`:
|
|
101
|
+
* `format` decides whether `value` is written as hex, `rgb()` or `hsl()`, while the
|
|
102
|
+
* field always shows hex. A spec that cares about the emitted string should read
|
|
103
|
+
* the bound signal; one that cares about the COLOUR should read this.
|
|
104
|
+
*
|
|
105
|
+
* Throws off the HEX tab, where the field does not exist — see {@link setTab}.
|
|
106
|
+
*/
|
|
107
|
+
async getHex() {
|
|
108
|
+
const field = await this.hexField();
|
|
109
|
+
if (!field) {
|
|
110
|
+
throw new Error(`WrColorPickerHarness.getHex(): the HEX field is not rendered — the picker is showing the ` +
|
|
111
|
+
`${(await this.getTab()).toUpperCase()} tab. Call setTab('hex') first.`);
|
|
112
|
+
}
|
|
113
|
+
return field.getProperty('value');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Type a colour into the HEX field.
|
|
117
|
+
*
|
|
118
|
+
* Typed rather than assigned, because that is the path the component is built
|
|
119
|
+
* around: it re-parses on every keystroke and applies the first spelling that
|
|
120
|
+
* parses, so a partial `#ff8` commits a colour on its way to `#ff8800`. An
|
|
121
|
+
* unparseable string leaves the committed colour ALONE and lives in the field
|
|
122
|
+
* until {@link blurHex} snaps it back — assert that rather than expecting a throw.
|
|
123
|
+
*/
|
|
124
|
+
async setHex(value) {
|
|
125
|
+
const field = await this.requireHexField('setHex');
|
|
126
|
+
await field.clear();
|
|
127
|
+
await field.sendKeys(value);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Blur the HEX field — re-syncing it with the committed colour and emitting
|
|
131
|
+
* `touch`.
|
|
132
|
+
*
|
|
133
|
+
* The re-sync is the visible half: a field left holding text that never parsed
|
|
134
|
+
* goes back to the canonical hex here, and nowhere else.
|
|
135
|
+
*/
|
|
136
|
+
async blurHex() {
|
|
137
|
+
await (await this.requireHexField('blurHex')).blur();
|
|
138
|
+
}
|
|
139
|
+
/** Whether the HEX field currently holds focus. */
|
|
140
|
+
async isHexFocused() {
|
|
141
|
+
return (await this.requireHexField('isHexFocused')).isFocused();
|
|
142
|
+
}
|
|
143
|
+
/** The three RGB channels as the RGB tab shows them. Throws off that tab. */
|
|
144
|
+
async getRgb() {
|
|
145
|
+
const [r, g, b] = await this.channelValues('rgb', ['R', 'G', 'B']);
|
|
146
|
+
return { r, g, b };
|
|
147
|
+
}
|
|
148
|
+
/** Write one RGB channel, 0–255. Out-of-range numbers are clamped by the component. */
|
|
149
|
+
async setRgbChannel(channel, value) {
|
|
150
|
+
await this.writeChannel('rgb', channel.toUpperCase(), value);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Hue, saturation and lightness as the HSL tab shows them — degrees and whole
|
|
154
|
+
* percents, already rounded for display. Throws off that tab.
|
|
155
|
+
*/
|
|
156
|
+
async getHsl() {
|
|
157
|
+
const [h, s, l] = await this.channelValues('hsl', ['H', 'S%', 'L%']);
|
|
158
|
+
return { h, s, l };
|
|
159
|
+
}
|
|
160
|
+
/** Write one HSL channel — `h` in degrees, `s` / `l` in percent. */
|
|
161
|
+
async setHslChannel(channel, value) {
|
|
162
|
+
await this.writeChannel('hsl', channel === 'h' ? 'H' : `${channel.toUpperCase()}%`, value);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Alpha as the whole percent the numeric tabs show, or `null` when the picker was
|
|
166
|
+
* built without alpha.
|
|
167
|
+
*
|
|
168
|
+
* Only the RGB and HSL tabs carry the field — the HEX tab spells alpha into the
|
|
169
|
+
* hex string instead — so this throws on the HEX tab rather than answering `null`,
|
|
170
|
+
* which would read as "this picker has no alpha".
|
|
171
|
+
*/
|
|
172
|
+
async getAlphaPercent() {
|
|
173
|
+
const tab = await this.getTab();
|
|
174
|
+
if (tab === 'hex') {
|
|
175
|
+
throw new Error('WrColorPickerHarness.getAlphaPercent(): the HEX tab has no alpha field — the alpha is the last two ' +
|
|
176
|
+
"digits of getHex(). Call setTab('rgb') or setTab('hsl') to read it as a percent.");
|
|
177
|
+
}
|
|
178
|
+
if (!(await this.hasAlpha()))
|
|
179
|
+
return null;
|
|
180
|
+
const [alpha] = await this.channelValues(tab, ['A%']);
|
|
181
|
+
return alpha;
|
|
182
|
+
}
|
|
183
|
+
/** Write alpha as a whole percent, 0–100. */
|
|
184
|
+
async setAlphaPercent(value) {
|
|
185
|
+
const tab = await this.getTab();
|
|
186
|
+
if (tab === 'hex') {
|
|
187
|
+
throw new Error("WrColorPickerHarness.setAlphaPercent(): the HEX tab has no alpha field. Call setTab('rgb') first, " +
|
|
188
|
+
'or write the two alpha digits through setHex().');
|
|
189
|
+
}
|
|
190
|
+
await this.writeChannel(tab, 'A%', value);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Whether the picker offers alpha at all.
|
|
194
|
+
*
|
|
195
|
+
* From the host modifier, which is the one answer that holds on every tab: the
|
|
196
|
+
* alpha SLIDER is always rendered when it is on, while the `A%` field only exists
|
|
197
|
+
* on the two numeric tabs.
|
|
198
|
+
*/
|
|
199
|
+
async hasAlpha() {
|
|
200
|
+
return (await this.host()).hasClass('wr-color-picker--alpha');
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether the picker refuses interaction.
|
|
204
|
+
*
|
|
205
|
+
* Read from the modifier the host paints rather than from a field's `disabled`
|
|
206
|
+
* property, because the fields come and go with the tab while the state does not —
|
|
207
|
+
* and the drag surfaces, which have no `disabled` of their own, are gated by the
|
|
208
|
+
* same signal.
|
|
209
|
+
*/
|
|
210
|
+
async isDisabled() {
|
|
211
|
+
return (await this.host()).hasClass('wr-color-picker--disabled');
|
|
212
|
+
}
|
|
213
|
+
/** The preset colours offered under the fields, as the strings the consumer passed. */
|
|
214
|
+
async getSwatches() {
|
|
215
|
+
const swatches = await this.locatorForAll('.wr-color-picker__swatch')();
|
|
216
|
+
return Promise.all(swatches.map(async (swatch) => (await swatch.getAttribute('aria-label')) ?? ''));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Click a preset.
|
|
220
|
+
*
|
|
221
|
+
* Matched on the string the consumer passed, which is what the button publishes as
|
|
222
|
+
* its accessible name — the rendered background is a CSS colour and would come back
|
|
223
|
+
* normalised and alpha-stripped. Throws when nothing matches, naming what is offered.
|
|
224
|
+
*/
|
|
225
|
+
async pickSwatch(color) {
|
|
226
|
+
const swatches = await this.locatorForAll('.wr-color-picker__swatch')();
|
|
227
|
+
for (const swatch of swatches) {
|
|
228
|
+
if ((await swatch.getAttribute('aria-label')) === color) {
|
|
229
|
+
await swatch.click();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
throw new Error(`WrColorPickerHarness.pickSwatch(): no swatch for "${color}". The picker offers: ` +
|
|
234
|
+
`${(await this.getSwatches()).join(', ')}.`);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Where the three thumbs sit, in percent along their surfaces.
|
|
238
|
+
*
|
|
239
|
+
* The component writes these as inline `left` / `top` percentages, so they are
|
|
240
|
+
* readable with no layout at all — which makes them the only evidence a spec has
|
|
241
|
+
* that the surfaces followed a colour set through the fields. A measured position
|
|
242
|
+
* would be zero for all three.
|
|
243
|
+
*/
|
|
244
|
+
async getThumbs() {
|
|
245
|
+
const sv = await this.locatorFor('.wr-color-picker__sv .wr-color-picker__thumb')();
|
|
246
|
+
const hue = await this.locatorFor('.wr-color-picker__slider--hue .wr-color-picker__thumb')();
|
|
247
|
+
const alpha = await this.locatorForOptional('.wr-color-picker__slider--alpha .wr-color-picker__thumb')();
|
|
248
|
+
return {
|
|
249
|
+
sv: { x: await percent(sv, 'left'), y: await percent(sv, 'top') },
|
|
250
|
+
hue: await percent(hue, 'left'),
|
|
251
|
+
alpha: alpha ? await percent(alpha, 'left') : null,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** The HEX field's value without the tab check — for filters, which must not throw. */
|
|
255
|
+
async readHex() {
|
|
256
|
+
const field = await this.hexField();
|
|
257
|
+
return field ? field.getProperty('value') : '';
|
|
258
|
+
}
|
|
259
|
+
async requireHexField(method) {
|
|
260
|
+
const field = await this.hexField();
|
|
261
|
+
if (!field) {
|
|
262
|
+
throw new Error(`WrColorPickerHarness.${method}(): the HEX field is not rendered — the picker is showing the ` +
|
|
263
|
+
`${(await this.getTab()).toUpperCase()} tab. Call setTab('hex') first.`);
|
|
264
|
+
}
|
|
265
|
+
return field;
|
|
266
|
+
}
|
|
267
|
+
async channelValues(tab, labels) {
|
|
268
|
+
await this.requireTab(tab, 'read');
|
|
269
|
+
const inputs = await this.inputsByLabel();
|
|
270
|
+
return labels.map(label => {
|
|
271
|
+
const input = inputs.get(label);
|
|
272
|
+
if (!input)
|
|
273
|
+
throw new Error(`WrColorPickerHarness: the ${tab.toUpperCase()} tab has no "${label}" field.`);
|
|
274
|
+
return Number(input.value);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async writeChannel(tab, label, value) {
|
|
278
|
+
await this.requireTab(tab, 'write');
|
|
279
|
+
const inputs = await this.inputsByLabel();
|
|
280
|
+
const found = inputs.get(label);
|
|
281
|
+
if (!found) {
|
|
282
|
+
throw new Error(`WrColorPickerHarness: the ${tab.toUpperCase()} tab has no "${label}" field. ${label === 'A%' ? 'This picker was built with `[alpha]="false"`.' : ''}`);
|
|
283
|
+
}
|
|
284
|
+
// Set and dispatch once, rather than clearing and typing. Each of these inputs
|
|
285
|
+
// is bound to `[value]`, and the component commits on every `input` event — so
|
|
286
|
+
// typing 128 would land 1, then 12, then 128, and CLEARING would land 0 first,
|
|
287
|
+
// because `Number('')` is 0 rather than NaN. A spec that asked for one colour
|
|
288
|
+
// would get four, the first of them black.
|
|
289
|
+
await found.element.setInputValue(String(value));
|
|
290
|
+
await found.element.dispatchEvent('input');
|
|
291
|
+
}
|
|
292
|
+
async requireTab(tab, verb) {
|
|
293
|
+
const current = await this.getTab();
|
|
294
|
+
if (current !== tab) {
|
|
295
|
+
throw new Error(`WrColorPickerHarness: cannot ${verb} the ${tab.toUpperCase()} channels while the picker is showing ` +
|
|
296
|
+
`the ${current.toUpperCase()} tab — those fields are not rendered. Call setTab('${tab}') first.`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/** Every numeric input of the current tab, keyed by the label printed under it. */
|
|
300
|
+
async inputsByLabel() {
|
|
301
|
+
const found = new Map();
|
|
302
|
+
for (const field of await this.locatorForAll(FieldHarness)()) {
|
|
303
|
+
const label = await field.getLabel();
|
|
304
|
+
found.set(label, { element: await field.getInput(), value: await field.getValue() });
|
|
305
|
+
}
|
|
306
|
+
return found;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @license
|
|
312
|
+
*
|
|
313
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
314
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
315
|
+
*/
|
|
316
|
+
/**
|
|
317
|
+
* Test harness for `[wrColorPickerTrigger]` — the element that opens a
|
|
318
|
+
* `<wr-color-picker>` in an overlay.
|
|
319
|
+
*
|
|
320
|
+
* The trigger stays in your fixture; the picker it opens does not. So this harness
|
|
321
|
+
* is loaded from the fixture loader like any element, and {@link getPicker} reaches
|
|
322
|
+
* ACROSS into the overlay container for you — scoped by the panel id the trigger
|
|
323
|
+
* publishes through `aria-controls`, never by `.wr-color-picker-overlay`, which
|
|
324
|
+
* answers with whichever trigger opened first.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* const trigger = await loader.getHarness(WrColorPickerTriggerHarness);
|
|
329
|
+
* const picker = await trigger.open();
|
|
330
|
+
*
|
|
331
|
+
* await picker.setHex('#3969e2');
|
|
332
|
+
* await trigger.sendEscape();
|
|
333
|
+
* expect(await trigger.isOpen()).toBe(false);
|
|
334
|
+
* ```
|
|
335
|
+
*
|
|
336
|
+
* @see https://ngwr.dev/guides/testing
|
|
337
|
+
*/
|
|
338
|
+
class WrColorPickerTriggerHarness extends ComponentHarness {
|
|
339
|
+
static hostSelector = '[wrColorPickerTrigger]';
|
|
340
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
341
|
+
static with(options = {}) {
|
|
342
|
+
return new HarnessPredicate(WrColorPickerTriggerHarness, options)
|
|
343
|
+
.addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
|
|
344
|
+
.addOption('open', options.open, async (harness, open) => (await harness.isOpen()) === open);
|
|
345
|
+
}
|
|
346
|
+
/** The trigger's own text, trimmed. */
|
|
347
|
+
async getText() {
|
|
348
|
+
return (await this.host()).text();
|
|
349
|
+
}
|
|
350
|
+
/** Whether the picker is showing, from the trigger's `aria-expanded`. */
|
|
351
|
+
async isOpen() {
|
|
352
|
+
return (await (await this.host()).getAttribute('aria-expanded')) === 'true';
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Whether the trigger refuses to open.
|
|
356
|
+
*
|
|
357
|
+
* From `aria-disabled`, which the directive writes for its own `disabled` input.
|
|
358
|
+
* The host is usually a `<button>`, but the directive does not touch its `disabled`
|
|
359
|
+
* PROPERTY — a consumer who binds that as well turns the element off at the DOM
|
|
360
|
+
* level, which is a stronger thing and reads here too.
|
|
361
|
+
*/
|
|
362
|
+
async isDisabled() {
|
|
363
|
+
const host = await this.host();
|
|
364
|
+
return (await host.getAttribute('aria-disabled')) === 'true' || (await host.getProperty('disabled'));
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The picker this trigger opened, or `null` while it is closed.
|
|
368
|
+
*
|
|
369
|
+
* Scoped by the id in `aria-controls`, so two triggers open at once — which
|
|
370
|
+
* nothing in the component prevents, since only an outside click closes one —
|
|
371
|
+
* answer with their own panels rather than with whichever came first.
|
|
372
|
+
*/
|
|
373
|
+
async getPicker() {
|
|
374
|
+
const id = await (await this.host()).getAttribute('aria-controls');
|
|
375
|
+
if (!id)
|
|
376
|
+
return null;
|
|
377
|
+
return this.documentRootLocatorFactory().locatorForOptional(WrColorPickerHarness.with({ ancestor: `#${id}` }))();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Open the picker and hand it back. An already-open trigger is left alone.
|
|
381
|
+
*
|
|
382
|
+
* Throws on a disabled trigger rather than answering `null` three lines later:
|
|
383
|
+
* `toggle()` returns early while `disabled` is set, so the click lands and nothing
|
|
384
|
+
* happens at all.
|
|
385
|
+
*/
|
|
386
|
+
async open() {
|
|
387
|
+
if (!(await this.isOpen())) {
|
|
388
|
+
if (await this.isDisabled()) {
|
|
389
|
+
throw new Error(`WrColorPickerTriggerHarness.open(): "${await this.getText()}" is disabled — the directive turns its ` +
|
|
390
|
+
'own toggle away, so the click would land and change nothing.');
|
|
391
|
+
}
|
|
392
|
+
await (await this.host()).click();
|
|
393
|
+
}
|
|
394
|
+
const picker = await this.getPicker();
|
|
395
|
+
if (!picker) {
|
|
396
|
+
throw new Error('WrColorPickerTriggerHarness.open(): the trigger reports itself open but published no panel to find. ' +
|
|
397
|
+
'That is a broken `aria-controls` pairing, not a timing problem.');
|
|
398
|
+
}
|
|
399
|
+
return picker;
|
|
400
|
+
}
|
|
401
|
+
/** Close the picker. An already-closed trigger is left alone. */
|
|
402
|
+
async close() {
|
|
403
|
+
if (!(await this.isOpen()))
|
|
404
|
+
return;
|
|
405
|
+
await (await this.host()).click();
|
|
406
|
+
}
|
|
407
|
+
/** Click the trigger once, whichever way that takes it. */
|
|
408
|
+
async toggle() {
|
|
409
|
+
await (await this.host()).click();
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Press Escape.
|
|
413
|
+
*
|
|
414
|
+
* Sent to the TRIGGER, and it still reaches the picker: the overlay listens through
|
|
415
|
+
* the CDK's keyboard dispatcher, which keeps one document listener and routes to
|
|
416
|
+
* the top-most overlay — so this does not depend on where focus happens to be.
|
|
417
|
+
*/
|
|
418
|
+
async sendEscape() {
|
|
419
|
+
await (await this.host()).sendKeys(TestKey.ESCAPE);
|
|
420
|
+
}
|
|
421
|
+
/** The role the trigger claims to open — `dialog`. */
|
|
422
|
+
async getHasPopup() {
|
|
423
|
+
return (await this.host()).getAttribute('aria-haspopup');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Generated bundle index. Do not edit.
|
|
429
|
+
*/
|
|
430
|
+
|
|
431
|
+
export { WrColorPickerHarness, WrColorPickerTriggerHarness };
|
|
432
|
+
//# sourceMappingURL=ngwr-color-picker-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-color-picker-testing.mjs","sources":["../../../projects/lib/color-picker/testing/wr-color-picker-harness.ts","../../../projects/lib/color-picker/testing/wr-color-picker-trigger-harness.ts","../../../projects/lib/color-picker/testing/ngwr-color-picker-testing.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate, type TestElement } from '@angular/cdk/testing';\n\nimport { WrSegmentedHarness } from 'ngwr/segmented/testing';\n\nimport type { WrColorPickerHarnessFilters, WrColorPickerTab, WrColorPickerThumbs } from './interfaces';\n\n/**\n * One labelled numeric field — internal. A `TestElement` cannot be queried into, so\n * pairing an input with the label printed under it means a harness anchored on the\n * `<label>` that holds both.\n */\nclass FieldHarness extends ComponentHarness {\n static hostSelector = '.wr-color-picker__field';\n\n async getLabel(): Promise<string> {\n return (await this.locatorFor('.wr-color-picker__field-label')()).text();\n }\n\n async getInput(): Promise<TestElement> {\n return this.locatorFor('input')();\n }\n\n async getValue(): Promise<string> {\n return (await this.getInput()).getProperty<string>('value');\n }\n}\n\n/** An inline `left` / `top` percentage as a number. */\nasync function percent(thumb: TestElement, side: 'left' | 'top'): Promise<number> {\n return Number.parseFloat(await thumb.getCssValue(side));\n}\n\n/**\n * Test harness for `<wr-color-picker>` — the inline picker, and the one the\n * `[wrColorPickerTrigger]` directive mounts in an overlay.\n *\n * **What this harness will not do: drag.** The SV canvas and the hue / alpha\n * sliders are pointer surfaces that read `getBoundingClientRect()` and divide by\n * its width. A unit test has no layout, so every box is 0×0 and the division is\n * `NaN` — a `setHue()` built on a synthetic pointer event would report success and\n * write nothing, or write garbage. The numeric fields reach the same state and are\n * also the only keyboard path the component has, so they are the whole write API\n * here. {@link getThumbs} reads where the thumbs ARE, from the percentages the\n * component writes inline, which is how a spec checks a surface followed a value.\n *\n * **And it will not read the preview swatch.** The preview paints the current\n * colour as a CSS background, and jsdom normalises that to `rgb(…)` — dropping the\n * alpha channel entirely, so `#ff880080` and `#ff8800ff` come back identical. A\n * `getColor()` reading it would answer one thing in a unit test and another in a\n * browser. {@link getHex} is the colour: the field holds the canonical 8-digit\n * string, alpha included, whatever `format` writes into `value`.\n *\n * @example\n * ```ts\n * const picker = await loader.getHarness(WrColorPickerHarness);\n *\n * await picker.setHex('#3969e2');\n * expect(await picker.getHex()).toBe('#3969e2ff');\n *\n * await picker.setTab('rgb');\n * expect(await picker.getRgb()).toEqual({ r: 57, g: 105, b: 226 });\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrColorPickerHarness extends ComponentHarness {\n static hostSelector = 'wr-color-picker';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrColorPickerHarnessFilters = {}): HarnessPredicate<WrColorPickerHarness> {\n return new HarnessPredicate(WrColorPickerHarness, options)\n .addOption('color', options.color, (harness, color) => HarnessPredicate.stringMatches(harness.readHex(), color))\n .addOption('tab', options.tab, async (harness, tab) => (await harness.getTab()) === tab)\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);\n }\n\n private readonly hexField = this.locatorForOptional('.wr-color-picker__inputs--hex input');\n private readonly tabs = this.locatorFor(WrSegmentedHarness.with({ selector: '.wr-color-picker__tabs' }));\n\n /**\n * The tab switcher, as a {@link WrSegmentedHarness}.\n *\n * Handed back rather than re-queried: the switcher IS a `<wr-segmented>`, which\n * already knows that the roving tab stop is not the selection and how the sliding\n * thumb reports itself. Re-reading its buttons here would be a second, worse copy.\n */\n async getTabs(): Promise<WrSegmentedHarness> {\n return this.tabs();\n }\n\n /** Which set of numeric fields is showing. */\n async getTab(): Promise<WrColorPickerTab> {\n const label = await (await this.tabs()).getSelectedLabel();\n return (label ?? 'HEX').toLowerCase() as WrColorPickerTab;\n }\n\n /** Show a different set of numeric fields. An already-active tab is left alone. */\n async setTab(tab: WrColorPickerTab): Promise<void> {\n if ((await this.getTab()) === tab) return;\n await (await this.tabs()).select({ label: tab.toUpperCase() });\n }\n\n /**\n * The canonical hex string, as the HEX field shows it — 8 digits while `alpha` is\n * on, 6 while it is off.\n *\n * This is the picker's colour, and deliberately not the same thing as its `value`:\n * `format` decides whether `value` is written as hex, `rgb()` or `hsl()`, while the\n * field always shows hex. A spec that cares about the emitted string should read\n * the bound signal; one that cares about the COLOUR should read this.\n *\n * Throws off the HEX tab, where the field does not exist — see {@link setTab}.\n */\n async getHex(): Promise<string> {\n const field = await this.hexField();\n if (!field) {\n throw new Error(\n `WrColorPickerHarness.getHex(): the HEX field is not rendered — the picker is showing the ` +\n `${(await this.getTab()).toUpperCase()} tab. Call setTab('hex') first.`\n );\n }\n return field.getProperty<string>('value');\n }\n\n /**\n * Type a colour into the HEX field.\n *\n * Typed rather than assigned, because that is the path the component is built\n * around: it re-parses on every keystroke and applies the first spelling that\n * parses, so a partial `#ff8` commits a colour on its way to `#ff8800`. An\n * unparseable string leaves the committed colour ALONE and lives in the field\n * until {@link blurHex} snaps it back — assert that rather than expecting a throw.\n */\n async setHex(value: string): Promise<void> {\n const field = await this.requireHexField('setHex');\n await field.clear();\n await field.sendKeys(value);\n }\n\n /**\n * Blur the HEX field — re-syncing it with the committed colour and emitting\n * `touch`.\n *\n * The re-sync is the visible half: a field left holding text that never parsed\n * goes back to the canonical hex here, and nowhere else.\n */\n async blurHex(): Promise<void> {\n await (await this.requireHexField('blurHex')).blur();\n }\n\n /** Whether the HEX field currently holds focus. */\n async isHexFocused(): Promise<boolean> {\n return (await this.requireHexField('isHexFocused')).isFocused();\n }\n\n /** The three RGB channels as the RGB tab shows them. Throws off that tab. */\n async getRgb(): Promise<{ r: number; g: number; b: number }> {\n const [r, g, b] = await this.channelValues('rgb', ['R', 'G', 'B']);\n return { r, g, b };\n }\n\n /** Write one RGB channel, 0–255. Out-of-range numbers are clamped by the component. */\n async setRgbChannel(channel: 'r' | 'g' | 'b', value: number): Promise<void> {\n await this.writeChannel('rgb', channel.toUpperCase(), value);\n }\n\n /**\n * Hue, saturation and lightness as the HSL tab shows them — degrees and whole\n * percents, already rounded for display. Throws off that tab.\n */\n async getHsl(): Promise<{ h: number; s: number; l: number }> {\n const [h, s, l] = await this.channelValues('hsl', ['H', 'S%', 'L%']);\n return { h, s, l };\n }\n\n /** Write one HSL channel — `h` in degrees, `s` / `l` in percent. */\n async setHslChannel(channel: 'h' | 's' | 'l', value: number): Promise<void> {\n await this.writeChannel('hsl', channel === 'h' ? 'H' : `${channel.toUpperCase()}%`, value);\n }\n\n /**\n * Alpha as the whole percent the numeric tabs show, or `null` when the picker was\n * built without alpha.\n *\n * Only the RGB and HSL tabs carry the field — the HEX tab spells alpha into the\n * hex string instead — so this throws on the HEX tab rather than answering `null`,\n * which would read as \"this picker has no alpha\".\n */\n async getAlphaPercent(): Promise<number | null> {\n const tab = await this.getTab();\n if (tab === 'hex') {\n throw new Error(\n 'WrColorPickerHarness.getAlphaPercent(): the HEX tab has no alpha field — the alpha is the last two ' +\n \"digits of getHex(). Call setTab('rgb') or setTab('hsl') to read it as a percent.\"\n );\n }\n if (!(await this.hasAlpha())) return null;\n const [alpha] = await this.channelValues(tab, ['A%']);\n return alpha;\n }\n\n /** Write alpha as a whole percent, 0–100. */\n async setAlphaPercent(value: number): Promise<void> {\n const tab = await this.getTab();\n if (tab === 'hex') {\n throw new Error(\n \"WrColorPickerHarness.setAlphaPercent(): the HEX tab has no alpha field. Call setTab('rgb') first, \" +\n 'or write the two alpha digits through setHex().'\n );\n }\n await this.writeChannel(tab, 'A%', value);\n }\n\n /**\n * Whether the picker offers alpha at all.\n *\n * From the host modifier, which is the one answer that holds on every tab: the\n * alpha SLIDER is always rendered when it is on, while the `A%` field only exists\n * on the two numeric tabs.\n */\n async hasAlpha(): Promise<boolean> {\n return (await this.host()).hasClass('wr-color-picker--alpha');\n }\n\n /**\n * Whether the picker refuses interaction.\n *\n * Read from the modifier the host paints rather than from a field's `disabled`\n * property, because the fields come and go with the tab while the state does not —\n * and the drag surfaces, which have no `disabled` of their own, are gated by the\n * same signal.\n */\n async isDisabled(): Promise<boolean> {\n return (await this.host()).hasClass('wr-color-picker--disabled');\n }\n\n /** The preset colours offered under the fields, as the strings the consumer passed. */\n async getSwatches(): Promise<string[]> {\n const swatches = await this.locatorForAll('.wr-color-picker__swatch')();\n return Promise.all(swatches.map(async swatch => (await swatch.getAttribute('aria-label')) ?? ''));\n }\n\n /**\n * Click a preset.\n *\n * Matched on the string the consumer passed, which is what the button publishes as\n * its accessible name — the rendered background is a CSS colour and would come back\n * normalised and alpha-stripped. Throws when nothing matches, naming what is offered.\n */\n async pickSwatch(color: string): Promise<void> {\n const swatches = await this.locatorForAll('.wr-color-picker__swatch')();\n for (const swatch of swatches) {\n if ((await swatch.getAttribute('aria-label')) === color) {\n await swatch.click();\n return;\n }\n }\n throw new Error(\n `WrColorPickerHarness.pickSwatch(): no swatch for \"${color}\". The picker offers: ` +\n `${(await this.getSwatches()).join(', ')}.`\n );\n }\n\n /**\n * Where the three thumbs sit, in percent along their surfaces.\n *\n * The component writes these as inline `left` / `top` percentages, so they are\n * readable with no layout at all — which makes them the only evidence a spec has\n * that the surfaces followed a colour set through the fields. A measured position\n * would be zero for all three.\n */\n async getThumbs(): Promise<WrColorPickerThumbs> {\n const sv = await this.locatorFor('.wr-color-picker__sv .wr-color-picker__thumb')();\n const hue = await this.locatorFor('.wr-color-picker__slider--hue .wr-color-picker__thumb')();\n const alpha = await this.locatorForOptional('.wr-color-picker__slider--alpha .wr-color-picker__thumb')();\n\n return {\n sv: { x: await percent(sv, 'left'), y: await percent(sv, 'top') },\n hue: await percent(hue, 'left'),\n alpha: alpha ? await percent(alpha, 'left') : null,\n };\n }\n\n /** The HEX field's value without the tab check — for filters, which must not throw. */\n private async readHex(): Promise<string> {\n const field = await this.hexField();\n return field ? field.getProperty<string>('value') : '';\n }\n\n private async requireHexField(method: string): Promise<TestElement> {\n const field = await this.hexField();\n if (!field) {\n throw new Error(\n `WrColorPickerHarness.${method}(): the HEX field is not rendered — the picker is showing the ` +\n `${(await this.getTab()).toUpperCase()} tab. Call setTab('hex') first.`\n );\n }\n return field;\n }\n\n private async channelValues(tab: WrColorPickerTab, labels: readonly string[]): Promise<number[]> {\n await this.requireTab(tab, 'read');\n const inputs = await this.inputsByLabel();\n\n return labels.map(label => {\n const input = inputs.get(label);\n if (!input) throw new Error(`WrColorPickerHarness: the ${tab.toUpperCase()} tab has no \"${label}\" field.`);\n return Number(input.value);\n });\n }\n\n private async writeChannel(tab: WrColorPickerTab, label: string, value: number): Promise<void> {\n await this.requireTab(tab, 'write');\n const inputs = await this.inputsByLabel();\n const found = inputs.get(label);\n if (!found) {\n throw new Error(\n `WrColorPickerHarness: the ${tab.toUpperCase()} tab has no \"${label}\" field. ${\n label === 'A%' ? 'This picker was built with `[alpha]=\"false\"`.' : ''\n }`\n );\n }\n\n // Set and dispatch once, rather than clearing and typing. Each of these inputs\n // is bound to `[value]`, and the component commits on every `input` event — so\n // typing 128 would land 1, then 12, then 128, and CLEARING would land 0 first,\n // because `Number('')` is 0 rather than NaN. A spec that asked for one colour\n // would get four, the first of them black.\n await found.element.setInputValue(String(value));\n await found.element.dispatchEvent('input');\n }\n\n private async requireTab(tab: WrColorPickerTab, verb: string): Promise<void> {\n const current = await this.getTab();\n if (current !== tab) {\n throw new Error(\n `WrColorPickerHarness: cannot ${verb} the ${tab.toUpperCase()} channels while the picker is showing ` +\n `the ${current.toUpperCase()} tab — those fields are not rendered. Call setTab('${tab}') first.`\n );\n }\n }\n\n /** Every numeric input of the current tab, keyed by the label printed under it. */\n private async inputsByLabel(): Promise<Map<string, { element: TestElement; value: string }>> {\n const found = new Map<string, { element: TestElement; value: string }>();\n\n for (const field of await this.locatorForAll(FieldHarness)()) {\n const label = await field.getLabel();\n found.set(label, { element: await field.getInput(), value: await field.getValue() });\n }\n return found;\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';\n\nimport type { WrColorPickerTriggerHarnessFilters } from './interfaces';\nimport { WrColorPickerHarness } from './wr-color-picker-harness';\n\n/**\n * Test harness for `[wrColorPickerTrigger]` — the element that opens a\n * `<wr-color-picker>` in an overlay.\n *\n * The trigger stays in your fixture; the picker it opens does not. So this harness\n * is loaded from the fixture loader like any element, and {@link getPicker} reaches\n * ACROSS into the overlay container for you — scoped by the panel id the trigger\n * publishes through `aria-controls`, never by `.wr-color-picker-overlay`, which\n * answers with whichever trigger opened first.\n *\n * @example\n * ```ts\n * const trigger = await loader.getHarness(WrColorPickerTriggerHarness);\n * const picker = await trigger.open();\n *\n * await picker.setHex('#3969e2');\n * await trigger.sendEscape();\n * expect(await trigger.isOpen()).toBe(false);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrColorPickerTriggerHarness extends ComponentHarness {\n static hostSelector = '[wrColorPickerTrigger]';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrColorPickerTriggerHarnessFilters = {}): HarnessPredicate<WrColorPickerTriggerHarness> {\n return new HarnessPredicate(WrColorPickerTriggerHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))\n .addOption('open', options.open, async (harness, open) => (await harness.isOpen()) === open);\n }\n\n /** The trigger's own text, trimmed. */\n async getText(): Promise<string> {\n return (await this.host()).text();\n }\n\n /** Whether the picker is showing, from the trigger's `aria-expanded`. */\n async isOpen(): Promise<boolean> {\n return (await (await this.host()).getAttribute('aria-expanded')) === 'true';\n }\n\n /**\n * Whether the trigger refuses to open.\n *\n * From `aria-disabled`, which the directive writes for its own `disabled` input.\n * The host is usually a `<button>`, but the directive does not touch its `disabled`\n * PROPERTY — a consumer who binds that as well turns the element off at the DOM\n * level, which is a stronger thing and reads here too.\n */\n async isDisabled(): Promise<boolean> {\n const host = await this.host();\n return (await host.getAttribute('aria-disabled')) === 'true' || (await host.getProperty<boolean>('disabled'));\n }\n\n /**\n * The picker this trigger opened, or `null` while it is closed.\n *\n * Scoped by the id in `aria-controls`, so two triggers open at once — which\n * nothing in the component prevents, since only an outside click closes one —\n * answer with their own panels rather than with whichever came first.\n */\n async getPicker(): Promise<WrColorPickerHarness | null> {\n const id = await (await this.host()).getAttribute('aria-controls');\n if (!id) return null;\n\n return this.documentRootLocatorFactory().locatorForOptional(WrColorPickerHarness.with({ ancestor: `#${id}` }))();\n }\n\n /**\n * Open the picker and hand it back. An already-open trigger is left alone.\n *\n * Throws on a disabled trigger rather than answering `null` three lines later:\n * `toggle()` returns early while `disabled` is set, so the click lands and nothing\n * happens at all.\n */\n async open(): Promise<WrColorPickerHarness> {\n if (!(await this.isOpen())) {\n if (await this.isDisabled()) {\n throw new Error(\n `WrColorPickerTriggerHarness.open(): \"${await this.getText()}\" is disabled — the directive turns its ` +\n 'own toggle away, so the click would land and change nothing.'\n );\n }\n await (await this.host()).click();\n }\n\n const picker = await this.getPicker();\n if (!picker) {\n throw new Error(\n 'WrColorPickerTriggerHarness.open(): the trigger reports itself open but published no panel to find. ' +\n 'That is a broken `aria-controls` pairing, not a timing problem.'\n );\n }\n return picker;\n }\n\n /** Close the picker. An already-closed trigger is left alone. */\n async close(): Promise<void> {\n if (!(await this.isOpen())) return;\n await (await this.host()).click();\n }\n\n /** Click the trigger once, whichever way that takes it. */\n async toggle(): Promise<void> {\n await (await this.host()).click();\n }\n\n /**\n * Press Escape.\n *\n * Sent to the TRIGGER, and it still reaches the picker: the overlay listens through\n * the CDK's keyboard dispatcher, which keeps one document listener and routes to\n * the top-most overlay — so this does not depend on where focus happens to be.\n */\n async sendEscape(): Promise<void> {\n await (await this.host()).sendKeys(TestKey.ESCAPE);\n }\n\n /** The role the trigger claims to open — `dialog`. */\n async getHasPopup(): Promise<string | null> {\n return (await this.host()).getAttribute('aria-haspopup');\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -449,7 +449,17 @@ class WrColorPicker {
|
|
|
449
449
|
return parts.join(' ');
|
|
450
450
|
}, /* @ts-ignore */
|
|
451
451
|
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
452
|
-
/**
|
|
452
|
+
/**
|
|
453
|
+
* The one write we just made, held only until the sync effect has seen it.
|
|
454
|
+
*
|
|
455
|
+
* It exists so a drag keeps its continuous HSV values instead of being
|
|
456
|
+
* re-quantised through its own hex round-trip. It used to be set and never
|
|
457
|
+
* cleared, which turned "ignore this write" into "ignore this STRING,
|
|
458
|
+
* forever": after the picker emitted `#ff0000` once, an app writing
|
|
459
|
+
* `#ff0000` back — a reset button, a form patch, an undo — was silently
|
|
460
|
+
* dropped for the rest of the component's life, and the surface kept
|
|
461
|
+
* whatever colour the user had dragged to.
|
|
462
|
+
*/
|
|
453
463
|
lastEmitted = null;
|
|
454
464
|
constructor() {
|
|
455
465
|
// External writes (via `[formField]` / `[(value)]`) flow the incoming hex
|
|
@@ -457,8 +467,12 @@ class WrColorPicker {
|
|
|
457
467
|
// `lastEmitted` so dragging keeps its continuous (unquantised) values.
|
|
458
468
|
effect(() => {
|
|
459
469
|
const value = this.value();
|
|
460
|
-
if (value === this.lastEmitted)
|
|
470
|
+
if (value === this.lastEmitted) {
|
|
471
|
+
// Ours, and now observed. Release the guard so the NEXT write of the
|
|
472
|
+
// same string — which can only come from outside — is honoured.
|
|
473
|
+
this.lastEmitted = null;
|
|
461
474
|
return;
|
|
475
|
+
}
|
|
462
476
|
const rgb = parseColor(value) ?? { r: 0, g: 0, b: 0, a: 1 };
|
|
463
477
|
const hsv = rgbToHsv(rgb);
|
|
464
478
|
this.h.set(hsv.h);
|
|
@@ -606,6 +620,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
|
|
|
606
620
|
* Use of this source code is governed by an MIT-style license that can be
|
|
607
621
|
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
608
622
|
*/
|
|
623
|
+
let triggerUid = 0;
|
|
609
624
|
/**
|
|
610
625
|
* Opens a `<wr-color-picker>` in an overlay anchored to the host element.
|
|
611
626
|
*
|
|
@@ -655,6 +670,8 @@ class WrColorPickerTrigger {
|
|
|
655
670
|
isOpen = signal(false, /* @ts-ignore */
|
|
656
671
|
...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
657
672
|
overlayRef = null;
|
|
673
|
+
/** @internal Public so the host binding can read it. */
|
|
674
|
+
panelId = `wr-color-picker-panel-${++triggerUid}`;
|
|
658
675
|
constructor() {
|
|
659
676
|
this.destroyRef.onDestroy(() => this.dispose());
|
|
660
677
|
}
|
|
@@ -688,6 +705,7 @@ class WrColorPickerTrigger {
|
|
|
688
705
|
scrollStrategy: this.scrollStrategies.reposition(),
|
|
689
706
|
panelClass: 'wr-color-picker-overlay',
|
|
690
707
|
});
|
|
708
|
+
this.overlayRef.overlayElement.id = this.panelId;
|
|
691
709
|
const portal = new ComponentPortal(WrColorPicker, this.vcr);
|
|
692
710
|
const ref = this.overlayRef.attach(portal);
|
|
693
711
|
ref.setInput('alpha', this.alpha());
|
|
@@ -735,7 +753,7 @@ class WrColorPickerTrigger {
|
|
|
735
753
|
}
|
|
736
754
|
}
|
|
737
755
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: WrColorPickerTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
738
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.1", type: WrColorPickerTrigger, isStandalone: true, selector: "[wrColorPickerTrigger]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, alpha: { classPropertyName: "alpha", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, swatches: { classPropertyName: "swatches", publicName: "swatches", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", opened: "opened", closed: "closed" }, host: { listeners: { "click": "toggle()" }, properties: { "attr.aria-haspopup": "\"dialog\"", "attr.aria-expanded": "isOpen()" } }, ngImport: i0 });
|
|
756
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.1", type: WrColorPickerTrigger, isStandalone: true, selector: "[wrColorPickerTrigger]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, alpha: { classPropertyName: "alpha", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, swatches: { classPropertyName: "swatches", publicName: "swatches", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", opened: "opened", closed: "closed" }, host: { listeners: { "click": "toggle()" }, properties: { "attr.aria-haspopup": "\"dialog\"", "attr.aria-expanded": "isOpen()", "attr.aria-controls": "isOpen() ? panelId : null", "attr.aria-disabled": "disabled() ? true : null" } }, ngImport: i0 });
|
|
739
757
|
}
|
|
740
758
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: WrColorPickerTrigger, decorators: [{
|
|
741
759
|
type: Directive,
|
|
@@ -747,6 +765,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
|
|
|
747
765
|
// button gave no hint that it opens anything, and never said it was open.
|
|
748
766
|
'[attr.aria-haspopup]': '"dialog"',
|
|
749
767
|
'[attr.aria-expanded]': 'isOpen()',
|
|
768
|
+
// Which panel, and only while there is one — the same pairing `[wrPopconfirm]`
|
|
769
|
+
// publishes. Without it nothing on the page connects the trigger to the picker
|
|
770
|
+
// it opened, so a second trigger's panel is indistinguishable from this one's.
|
|
771
|
+
'[attr.aria-controls]': 'isOpen() ? panelId : null',
|
|
772
|
+
// The directive's own `disabled` never reached the DOM: it gates `toggle()` and
|
|
773
|
+
// is forwarded to the inner picker, so the button looked live to a screen
|
|
774
|
+
// reader and did nothing when pressed.
|
|
775
|
+
'[attr.aria-disabled]': 'disabled() ? true : null',
|
|
750
776
|
},
|
|
751
777
|
}]
|
|
752
778
|
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], alpha: [{ type: i0.Input, args: [{ isSignal: true, alias: "alpha", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], swatches: [{ type: i0.Input, args: [{ isSignal: true, alias: "swatches", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|