@fusion.dev/helix-headless 0.1.0 → 0.3.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/custom-elements.json +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- package/vue.d.ts +1 -1
|
@@ -8,152 +8,22 @@ import { AriaForwardController, customElement, HelixFormElement, MixinController
|
|
|
8
8
|
import { nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { html } from 'lit/static-html.js';
|
|
11
|
-
/* Side-effect import, because this renders a picker in its own shadow root and
|
|
12
|
-
a custom element registers on import. The path is relative, so this registers
|
|
13
|
-
the base picker and the styled file's identical import registers the styled
|
|
14
|
-
one; `tier` picks which name the template asks for. */
|
|
15
11
|
import '../../color-picker/index.js';
|
|
16
12
|
import { parseColor, toCss } from '../../color-picker/color.js';
|
|
17
|
-
/**
|
|
18
|
-
* @summary A color, typed in any syntax or picked from a panel.
|
|
19
|
-
*
|
|
20
|
-
* @description
|
|
21
|
-
* **The picker is `helix-color-picker`, not a second one written here.** It owns the
|
|
22
|
-
* square, the ramps, the eyedropper and the syntax toggle, and it is worth
|
|
23
|
-
* having on its own: a settings pane wants a picker flat on the page with
|
|
24
|
-
* nothing to open. This element is the field, the overlay, and the form.
|
|
25
|
-
* Exactly the arrangement Date Input has with `helix-date-picker`.
|
|
26
|
-
*
|
|
27
|
-
* **Typed as well as picked, and the typing is the reason this is not
|
|
28
|
-
* `<input type="color">`.** The native control gives a free picker and takes
|
|
29
|
-
* nearly all styling with it, which is a fair trade until a product needs the
|
|
30
|
-
* panel to match everything around it. What is kept from the native one is the
|
|
31
|
-
* part worth keeping: a real text field, so a reader who has the hex pastes it
|
|
32
|
-
* and never opens anything.
|
|
33
|
-
*
|
|
34
|
-
* **Every syntax is read; the one `format` names is written.** `#f00`,
|
|
35
|
-
* `#ff0000`, `rgb(255 0 0)`, `rgba(255,0,0,.5)`, `rgb(255 0 0 / 50%)` and
|
|
36
|
-
* `hsl(0 100% 50%)` are all colors, and a field that refuses one because it
|
|
37
|
-
* prefers another is arguing with the person using it. What the field *shows* is
|
|
38
|
-
* the value itself, for the reason Date Input gives about its dates: half-typed
|
|
39
|
-
* input does not survive a round trip through a formatter.
|
|
40
|
-
*
|
|
41
|
-
* The custom property below is written by this component and never set by a
|
|
42
|
-
* consumer: it carries a value this element resolved and CSS cannot, which is
|
|
43
|
-
* what lets a stylesheet on this tier paint the swatch without parsing anything
|
|
44
|
-
* itself.
|
|
45
|
-
*
|
|
46
|
-
* @since 0.1.0
|
|
47
|
-
* @status experimental
|
|
48
|
-
* @category forms
|
|
49
|
-
* @aliases ColorField, ColourInput, InputColor
|
|
50
|
-
* @documentation https://helix.fusion.dev/components/color-input
|
|
51
|
-
*
|
|
52
|
-
* @slot end - Content at the trailing end, before the trigger.
|
|
53
|
-
* @slot feedback - What is wrong with it.
|
|
54
|
-
* @slot hint - Help under the field.
|
|
55
|
-
* @slot start - Content at the leading end of the field, inside its border.
|
|
56
|
-
* @slot trigger - The mark on the field saying a panel opens.
|
|
57
|
-
*
|
|
58
|
-
* @event {Event} change - Announced once the value has moved.
|
|
59
|
-
* @event {HelixBeforeChangeEvent} helix-before-change - Fired before the color changes. Cancelable: refusing it takes the decision and nothing moves.
|
|
60
|
-
* @event {HelixBeforeCloseEvent} helix-before-close - Fired before it closes. Cancelable: refusing it takes the decision and nothing moves.
|
|
61
|
-
* @event {HelixBeforeOpenEvent} helix-before-open - Fired before the panel opens. Cancelable: refusing it takes the decision and nothing moves.
|
|
62
|
-
* @event {Event} input - Announced once the value has moved.
|
|
63
|
-
* @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
|
|
64
|
-
*
|
|
65
|
-
* @csspart control - The text input carrying the value. Absent in `swatch`.
|
|
66
|
-
* @csspart feedback - What is wrong with it.
|
|
67
|
-
* @csspart field - The box holding the text and the trigger.
|
|
68
|
-
* @csspart hint - Help under the field, while nothing is wrong.
|
|
69
|
-
* @csspart label - The question, over the field.
|
|
70
|
-
* @csspart picker - The color picker inside it.
|
|
71
|
-
* @csspart picker-actions - The group holding the eyedropper and the syntax toggle.
|
|
72
|
-
* @csspart picker-channel - One ramp.
|
|
73
|
-
* @csspart picker-channel-control - The range input.
|
|
74
|
-
* @csspart picker-channel-feedback - What is wrong with it.
|
|
75
|
-
* @csspart picker-channel-field - The row: the control and its readout.
|
|
76
|
-
* @csspart picker-channel-hint - Help under the track, while nothing is wrong.
|
|
77
|
-
* @csspart picker-channel-label - The question, over the track.
|
|
78
|
-
* @csspart picker-channel-marker - One mark.
|
|
79
|
-
* @csspart picker-channel-marker-label - The value a mark stands at, when `marker-labels` is set.
|
|
80
|
-
* @csspart picker-channel-markers - The row of marks along the track.
|
|
81
|
-
* @csspart picker-channel-output - The current value, shown beside it.
|
|
82
|
-
* @csspart picker-channels - The ramps beside it.
|
|
83
|
-
* @csspart picker-controls - The row under the square.
|
|
84
|
-
* @csspart picker-eyedropper - The control that samples a color from the screen.
|
|
85
|
-
* @csspart picker-eyedropper-button - The inner control.
|
|
86
|
-
* @csspart picker-footer - The row holding the value and the tools.
|
|
87
|
-
* @csspart picker-format - The control that changes the syntax the value is written in.
|
|
88
|
-
* @csspart picker-format-button - The inner control.
|
|
89
|
-
* @csspart picker-grid - The saturation and brightness square.
|
|
90
|
-
* @csspart picker-grid-brightness - The range input for its vertical axis.
|
|
91
|
-
* @csspart picker-grid-handle - The mark showing where in the square the color sits.
|
|
92
|
-
* @csspart picker-grid-saturation - The range input for the square's horizontal axis.
|
|
93
|
-
* @csspart picker-icon - A glyph this component draws.
|
|
94
|
-
* @csspart picker-sample - The chip of the current color, which copies it when pressed.
|
|
95
|
-
* @csspart picker-swatch - One preset.
|
|
96
|
-
* @csspart picker-swatches - The row of presets.
|
|
97
|
-
* @csspart picker-value - The color as text, which can be typed or pasted.
|
|
98
|
-
* @csspart preview - The chip showing the color, inside the trigger.
|
|
99
|
-
* @csspart surface - The floating panel.
|
|
100
|
-
* @csspart trigger - The control that opens the panel.
|
|
101
|
-
*
|
|
102
|
-
* @cssproperty --color-input-swatch - The chosen color, to paint over the transparency check.
|
|
103
|
-
*/
|
|
104
13
|
let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControllerOpen(MixinControllerValue(MixinField(HelixFormElement))) {
|
|
105
14
|
constructor() {
|
|
106
15
|
super(...arguments);
|
|
107
|
-
/* A consumer's `aria-describedby` names ids in their own tree and the control
|
|
108
|
-
is in this shadow root, so the reference has to be carried down as an
|
|
109
|
-
element rather than copied as an id. See `AriaForwardController`: measured
|
|
110
|
-
in Chrome's own tree, the host held the description and the control the
|
|
111
|
-
reader is actually on was exposed without it. */
|
|
112
16
|
this.aria = new AriaForwardController(this, {
|
|
113
17
|
target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
|
|
114
18
|
});
|
|
115
|
-
/**
|
|
116
|
-
* Whether the color carries an alpha channel.
|
|
117
|
-
*/
|
|
118
19
|
this.alpha = false;
|
|
119
|
-
/**
|
|
120
|
-
* What the browser may autofill in the text field, as a platform token.
|
|
121
|
-
*
|
|
122
|
-
* **Defaults to `off`, which is not the platform's default and is the
|
|
123
|
-
* point.** The field holds a color, so a browser offering to fill it with a
|
|
124
|
-
* saved address is competing for the same keystrokes as the picker. A
|
|
125
|
-
* consumer who wants autofill can still ask.
|
|
126
|
-
*/
|
|
127
20
|
this.autocomplete = 'off';
|
|
128
|
-
/**
|
|
129
|
-
* Whether the reader may change the format from the panel.
|
|
130
|
-
*/
|
|
131
21
|
this.noFormat = false;
|
|
132
|
-
/**
|
|
133
|
-
* What the field says before a color is in it.
|
|
134
|
-
*/
|
|
135
22
|
this.placeholder = '#000000';
|
|
136
|
-
/**
|
|
137
|
-
* Colors offered under the picker, as a comma-separated list.
|
|
138
|
-
*/
|
|
139
23
|
this.swatches = '';
|
|
140
|
-
/**
|
|
141
|
-
* Accessible name for the control that opens the panel.
|
|
142
|
-
*/
|
|
143
24
|
this.triggerLabel = 'Choose a color';
|
|
144
|
-
/** The overlay, placed against the field for the reason Date Input gives. */
|
|
145
25
|
this.panel = new PopoverController(this, {
|
|
146
26
|
anchor: () => this.renderRoot?.querySelector('[part="field"]'),
|
|
147
|
-
/* The surface *and* the trigger.
|
|
148
|
-
*
|
|
149
|
-
* A control is not always one element: the button that opens this panel is
|
|
150
|
-
* part of it, and a press on it is not a press outside. Scoped to the
|
|
151
|
-
* surface alone, `pointerdown` on the trigger closes the panel and the
|
|
152
|
-
* trigger's own `toggleOpen` reopens it on the `click` that follows, so the
|
|
153
|
-
* panel can never be closed by the button that opened it.
|
|
154
|
-
*
|
|
155
|
-
* `DismissController` documents this exact failure and takes a list for
|
|
156
|
-
* it. */
|
|
157
27
|
boundary: () => [
|
|
158
28
|
this.renderRoot?.querySelector('[part="surface"]'),
|
|
159
29
|
this.renderRoot?.querySelector('[part="trigger"]'),
|
|
@@ -163,23 +33,7 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
|
|
|
163
33
|
placement: 'bottom-start',
|
|
164
34
|
surface: () => this.renderRoot?.querySelector('[part="surface"]'),
|
|
165
35
|
});
|
|
166
|
-
/**
|
|
167
|
-
* The last value that was a color.
|
|
168
|
-
*
|
|
169
|
-
* Half-typed input reaches here on every keystroke and most of it is not a
|
|
170
|
-
* color, so the picker is given the last thing that was one rather than a
|
|
171
|
-
* fallback. Typing `#00ff00` one character at a time goes `#`, `#0`, `#00`,
|
|
172
|
-
* `#00f`, `#00ff`, `#00ff0`, `#00ff00`: measured, the square went red, red,
|
|
173
|
-
* red, blue, blue, **red**, green, because two of those are colors and the
|
|
174
|
-
* ones between are not. Holding the last good one takes the flicker out and
|
|
175
|
-
* leaves the square on blue until green arrives.
|
|
176
|
-
*
|
|
177
|
-
* Not state, because it is only ever read on a render the value already
|
|
178
|
-
* caused. The same reasoning Date Input records for its calendar, which will
|
|
179
|
-
* not follow a date until the field says something complete.
|
|
180
|
-
*/
|
|
181
36
|
this.settled = '';
|
|
182
|
-
/** Relayed under this element's name, so a consumer hears one control. */
|
|
183
37
|
this.handleInput = (event) => {
|
|
184
38
|
event.stopPropagation();
|
|
185
39
|
this.requestValue(event.target.value);
|
|
@@ -190,21 +44,12 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
|
|
|
190
44
|
this.requestValue(event.detail.value);
|
|
191
45
|
this.restore();
|
|
192
46
|
};
|
|
193
|
-
/**
|
|
194
|
-
* The picker is about to take the pointer over, so the panel gets out of the
|
|
195
|
-
* way and comes back after.
|
|
196
|
-
*
|
|
197
|
-
* `helix-color-picker` says what it is doing rather than reaching for a panel it
|
|
198
|
-
* cannot know it is in. A floating panel left up during an eyedropper is one
|
|
199
|
-
* the reader cannot dismiss and may well be trying to sample from underneath.
|
|
200
|
-
*/
|
|
201
47
|
this.handleSampleStart = () => {
|
|
202
48
|
this.requestOpen(false);
|
|
203
49
|
};
|
|
204
50
|
this.handleSampleEnd = () => {
|
|
205
51
|
this.requestOpen(true);
|
|
206
52
|
};
|
|
207
|
-
/** Escape shuts the panel without moving the color. */
|
|
208
53
|
this.handleKeydown = (event) => {
|
|
209
54
|
if (event.key === 'Escape' && this.open) {
|
|
210
55
|
event.stopPropagation();
|
|
@@ -213,14 +58,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
|
|
|
213
58
|
}
|
|
214
59
|
};
|
|
215
60
|
}
|
|
216
|
-
/**
|
|
217
|
-
* What the label points at and validity hangs off.
|
|
218
|
-
*
|
|
219
|
-
* The input where there is one, the trigger where there is not. `setValidity`
|
|
220
|
-
* needs an element to anchor the message to and a label needs something
|
|
221
|
-
* focusable to name, and in `swatch` the trigger is the only control in the
|
|
222
|
-
* field. It carries `id="control"` there for the same reason.
|
|
223
|
-
*/
|
|
224
61
|
get anchor() {
|
|
225
62
|
if (typeof this.renderRoot?.querySelector !== 'function') {
|
|
226
63
|
return null;
|
|
@@ -233,22 +70,13 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
|
|
|
233
70
|
}
|
|
234
71
|
return this.renderRoot.querySelector('[part="control"]');
|
|
235
72
|
}
|
|
236
|
-
/** What the picker should show, which is only ever a color. */
|
|
237
73
|
get picked() {
|
|
238
74
|
return parseColor(this.value) ? this.value : this.settled;
|
|
239
75
|
}
|
|
240
|
-
/** What the trigger's swatch paints, which is only ever a color. */
|
|
241
76
|
get painted() {
|
|
242
77
|
const parsed = parseColor(this.value);
|
|
243
78
|
return parsed ? toCss(parsed) : 'transparent';
|
|
244
79
|
}
|
|
245
|
-
/*
|
|
246
|
-
* Puts the field back where the answer differs from what was typed.
|
|
247
|
-
*
|
|
248
|
-
* `requestValue` announces and decides; this is the other half, which Input
|
|
249
|
-
* and Date Input both need for the same reason: the native control has already
|
|
250
|
-
* changed, so wherever the answer does not follow it, it has to be restored.
|
|
251
|
-
*/
|
|
252
80
|
restore() {
|
|
253
81
|
const control = this.control;
|
|
254
82
|
if (control && control.value !== this.value) {
|
|
@@ -256,7 +84,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
|
|
|
256
84
|
}
|
|
257
85
|
this.sync();
|
|
258
86
|
}
|
|
259
|
-
/** Reports the value and the validity the host stands behind. */
|
|
260
87
|
sync() {
|
|
261
88
|
this.internals.setFormValue(this.value);
|
|
262
89
|
const anchor = this.anchor;
|
|
@@ -264,10 +91,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
|
|
|
264
91
|
return;
|
|
265
92
|
}
|
|
266
93
|
const control = this.control;
|
|
267
|
-
/* Checked here rather than left to the input, which is `text` and has no
|
|
268
|
-
idea what a color is. `type="color"` is what was given up to own the
|
|
269
|
-
panel, and it would not have helped: it has no invalid state, because it
|
|
270
|
-
cannot hold one. */
|
|
271
94
|
if (this.value !== '' && parseColor(this.value) === null) {
|
|
272
95
|
this.internals.setValidity({ badInput: true }, 'Enter a color, like #ff0000, rgb(255 0 0) or hsl(0 100% 50%).', anchor);
|
|
273
96
|
}
|
|
@@ -275,36 +98,20 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
|
|
|
275
98
|
this.internals.setValidity(control.validity, control.validationMessage, control);
|
|
276
99
|
}
|
|
277
100
|
else {
|
|
278
|
-
/* A swatch has no native validity of its own, so `required` is the only
|
|
279
|
-
thing left to check and this element has to check it: an empty value on
|
|
280
|
-
a required field is the one state a block of color can be wrong in. */
|
|
281
101
|
this.internals.setValidity(this.required && this.value === '' ? { valueMissing: true } : {}, this.required && this.value === '' ? 'Choose a color.' : '', anchor);
|
|
282
102
|
}
|
|
283
103
|
this.refreshFeedback();
|
|
284
104
|
}
|
|
285
105
|
firstUpdated(changed) {
|
|
286
106
|
super.firstUpdated(changed);
|
|
287
|
-
// Whatever the markup arrived with, so a form knows before anything is typed.
|
|
288
107
|
this.sync();
|
|
289
108
|
}
|
|
290
109
|
updated(changed) {
|
|
291
110
|
super.updated(changed);
|
|
292
|
-
/* Every update, not only the ones that name `value`.
|
|
293
|
-
`value` is an accessor over `valueState ?? defaultValue`, so a consumer
|
|
294
|
-
moving `default-value` on an untouched control moves what the form
|
|
295
|
-
submits without ever putting `value` in the changed map, and the reported
|
|
296
|
-
entry goes stale. Native `<input>` updates in exactly that case, which is
|
|
297
|
-
the behavior the mixin's own notes take as the reference. */
|
|
298
111
|
this.sync();
|
|
299
112
|
if (changed.has('value') && parseColor(this.value)) {
|
|
300
113
|
this.settled = this.value;
|
|
301
114
|
}
|
|
302
|
-
/* Driven from here rather than from the toggle, because `open` is a property
|
|
303
|
-
a consumer may set directly and the panel has to follow either way. The
|
|
304
|
-
controller is idempotent, so arriving twice for one open costs nothing.
|
|
305
|
-
Without this the surface stays `display: none`, a `popover` element being
|
|
306
|
-
hidden until something shows it, and the square inside measures zero,
|
|
307
|
-
which is the one measurement a drag cannot work without. */
|
|
308
115
|
if (this.open) {
|
|
309
116
|
this.panel.show();
|
|
310
117
|
}
|
|
@@ -312,33 +119,9 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
|
|
|
312
119
|
this.panel.hide();
|
|
313
120
|
}
|
|
314
121
|
}
|
|
315
|
-
/** Puts the caret in the field, the way a label pointing at one would. */
|
|
316
122
|
focus(options) {
|
|
317
123
|
this.anchor?.focus(options);
|
|
318
124
|
}
|
|
319
|
-
/*
|
|
320
|
-
* The picker is told `no-value` because the field above it is that input.
|
|
321
|
-
*
|
|
322
|
-
* Standing on a page, helix-color-picker states its own color as text: a square and
|
|
323
|
-
* two ramps can build one and cannot state one, and a reader who already has a
|
|
324
|
-
* hex should not have to find it by dragging. Here that row would be a second
|
|
325
|
-
* editable field for the same value, which is two places to type with no
|
|
326
|
-
* answer to which one is authoritative or which has the caret.
|
|
327
|
-
*
|
|
328
|
-
* Not the other way round, this element dropping its own field and letting the
|
|
329
|
-
* panel's be the only one: the value could then not be read at a glance or
|
|
330
|
-
* pasted without opening a panel.
|
|
331
|
-
*/
|
|
332
|
-
/**
|
|
333
|
-
* The picker the field opens, behind a seam the styled tier can move.
|
|
334
|
-
*
|
|
335
|
-
* A method rather than a fragment inline, so a tier above can render its own,
|
|
336
|
-
* wrap this one, pass it a property the base does not know about, or leave it
|
|
337
|
-
* out, without reimplementing the whole template to do any of them. The tag is
|
|
338
|
-
* interpolated rather than written, so `tier` resolves it: this renders the
|
|
339
|
-
* headless element, and a styled subclass that sets `tier` renders the styled one
|
|
340
|
-
* from the same template.
|
|
341
|
-
*/
|
|
342
125
|
renderPicker() {
|
|
343
126
|
return html `<${this.tag('color-picker')}
|
|
344
127
|
part="picker"
|
|
@@ -1,31 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading and writing the color syntaxes a field is allowed to hold.
|
|
3
|
-
*
|
|
4
|
-
* **The working model is HSB**, hue and saturation and brightness, because that
|
|
5
|
-
* is what the panel is: a square of saturation against brightness with a hue
|
|
6
|
-
* ramp beside it. HSL would put the square's corners somewhere a reader does not
|
|
7
|
-
* expect, since full saturation in HSL is a mid-lightness band rather than a
|
|
8
|
-
* corner.
|
|
9
|
-
*
|
|
10
|
-
* **Every syntax here is read; three are written.** A field that refuses
|
|
11
|
-
* `rgb(255 0 0)` because it prefers hex is a field arguing with the person using
|
|
12
|
-
* it, so the space-separated forms, the comma forms, `rgba` and `hsla` are all
|
|
13
|
-
* taken. What is written back is whichever of hex, rgb or hsl the component is
|
|
14
|
-
* set to, so a value stays in the shape the consumer asked for however it was
|
|
15
|
-
* entered.
|
|
16
|
-
*
|
|
17
|
-
* **What is written is the comma form.** Both are valid, and the commas are what
|
|
18
|
-
* a reader recognizes, what most code around them holds, and what pastes into
|
|
19
|
-
* anything without a second thought. A syntax a value is handed *out* in is not
|
|
20
|
-
* the place to be early. `rgba` and `hsla` come with them, since a comma form
|
|
21
|
-
* with a fourth argument is spelled that way.
|
|
22
|
-
*
|
|
23
|
-
* Its own module rather than sitting in the element, because it is arithmetic
|
|
24
|
-
* with no DOM in it: this is the half worth testing directly.
|
|
25
|
-
*/
|
|
26
|
-
/** Keeps a number inside a range, since every channel here has one. */
|
|
27
1
|
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
28
|
-
/** `50%` and `0.5` mean the same thing wherever an alpha is accepted. */
|
|
29
2
|
const toAlpha = (token) => {
|
|
30
3
|
if (token === undefined) {
|
|
31
4
|
return 100;
|
|
@@ -36,7 +9,6 @@ const toAlpha = (token) => {
|
|
|
36
9
|
}
|
|
37
10
|
return clamp(token.includes('%') ? number : number * 100, 0, 100);
|
|
38
11
|
};
|
|
39
|
-
/** A channel written either as `255` or as `100%`. */
|
|
40
12
|
const toByte = (token) => {
|
|
41
13
|
const number = Number.parseFloat(token);
|
|
42
14
|
return clamp(token.includes('%') ? (number / 100) * 255 : number, 0, 255);
|
|
@@ -68,7 +40,6 @@ const rgbToHsb = (red, green, blue, alpha) => {
|
|
|
68
40
|
saturation: Math.round(high === 0 ? 0 : (span / high) * 100),
|
|
69
41
|
};
|
|
70
42
|
};
|
|
71
|
-
/** HSB to bytes, which is the road every written syntax goes down. */
|
|
72
43
|
export const toRgb = (color) => {
|
|
73
44
|
const hue = (((color.hue % 360) + 360) % 360) / 60;
|
|
74
45
|
const saturation = color.saturation / 100;
|
|
@@ -91,7 +62,6 @@ export const toRgb = (color) => {
|
|
|
91
62
|
red: Math.round((red + base) * 255),
|
|
92
63
|
};
|
|
93
64
|
};
|
|
94
|
-
/** HSB to HSL, which is a different pair of numbers for the same color. */
|
|
95
65
|
const toHsl = (color) => {
|
|
96
66
|
const saturation = color.saturation / 100;
|
|
97
67
|
const brightness = color.brightness / 100;
|
|
@@ -104,19 +74,6 @@ const toHsl = (color) => {
|
|
|
104
74
|
: (brightness - lightness) / Math.min(lightness, 1 - lightness)) * 100),
|
|
105
75
|
};
|
|
106
76
|
};
|
|
107
|
-
/**
|
|
108
|
-
* Splits a comma-separated list of colors, ignoring the commas inside them.
|
|
109
|
-
*
|
|
110
|
-
* A plain `split(',')` cannot hold the legacy syntaxes:
|
|
111
|
-
* `#ef4444, rgb(249 115 22), rgba(34,197,94,.5)` comes apart into six fragments,
|
|
112
|
-
* four of them not colors, and the row quietly renders three swatches instead of
|
|
113
|
-
* four. The modern space-separated forms survive it, which is what makes such a
|
|
114
|
-
* list look like it works.
|
|
115
|
-
*
|
|
116
|
-
* Depth rather than a regular expression, because the thing that matters is
|
|
117
|
-
* whether a comma is inside a function call and that is exactly what a counter
|
|
118
|
-
* answers. No color syntax nests, so a single counter is enough.
|
|
119
|
-
*/
|
|
120
77
|
export const splitColors = (value) => {
|
|
121
78
|
const found = [];
|
|
122
79
|
let depth = 0;
|
|
@@ -139,14 +96,6 @@ export const splitColors = (value) => {
|
|
|
139
96
|
found.push(current.trim());
|
|
140
97
|
return found.filter(Boolean);
|
|
141
98
|
};
|
|
142
|
-
/**
|
|
143
|
-
* Any of the syntaxes to the working model, or `null` where it is not a color.
|
|
144
|
-
*
|
|
145
|
-
* Named colors are not here. Resolving `rebeccapurple` means either shipping
|
|
146
|
-
* the table of a hundred and forty or asking the browser, and the browser is not
|
|
147
|
-
* present on the server: a field that accepted a name client-side and rejected
|
|
148
|
-
* it during a server render would be worse than one that never took it.
|
|
149
|
-
*/
|
|
150
99
|
export const parseColor = (value) => {
|
|
151
100
|
const text = value.trim().toLowerCase();
|
|
152
101
|
if (text === '') {
|
|
@@ -167,16 +116,10 @@ export const parseColor = (value) => {
|
|
|
167
116
|
const carriesAlpha = digits.length === 4 || digits.length === 8;
|
|
168
117
|
return rgbToHsb(channel(0), channel(1), channel(2), carriesAlpha ? Math.round((channel(3) / 255) * 100) : 100);
|
|
169
118
|
}
|
|
170
|
-
/* Commas and the slash both become spaces, which collapses every legal
|
|
171
|
-
spelling into one list: `rgba(255,0,0,0.5)` and `rgb(255 0 0 / 50%)` are the
|
|
172
|
-
same four tokens by the time they are read. */
|
|
173
119
|
const call = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(text);
|
|
174
120
|
if (!call?.[1] || call[2] === undefined) {
|
|
175
121
|
return null;
|
|
176
122
|
}
|
|
177
|
-
/* Destructured with a fallback rather than indexed, because an index into an
|
|
178
|
-
array is `string | undefined` here and three of these are read twice. Four
|
|
179
|
-
names cost less than six assertions. */
|
|
180
123
|
const [first = '', second = '', third = '', fourth] = call[2]
|
|
181
124
|
.replace(/[,/]/g, ' ')
|
|
182
125
|
.split(/\s+/)
|
|
@@ -194,7 +137,6 @@ export const parseColor = (value) => {
|
|
|
194
137
|
if (Number.isNaN(hue) || Number.isNaN(saturation) || Number.isNaN(lightness)) {
|
|
195
138
|
return null;
|
|
196
139
|
}
|
|
197
|
-
// HSL to HSB, the inverse of `toHsl` above.
|
|
198
140
|
const brightness = lightness + saturation * Math.min(lightness, 1 - lightness);
|
|
199
141
|
return {
|
|
200
142
|
alpha,
|
|
@@ -203,18 +145,8 @@ export const parseColor = (value) => {
|
|
|
203
145
|
saturation: Math.round((brightness === 0 ? 0 : 2 * (1 - lightness / brightness)) * 100),
|
|
204
146
|
};
|
|
205
147
|
};
|
|
206
|
-
/**
|
|
207
|
-
* The working model back out, in the syntax asked for.
|
|
208
|
-
*
|
|
209
|
-
* The alpha is written only where the component carries one. A consumer who did
|
|
210
|
-
* not ask for transparency should not have to strip `ff` off every hex or read
|
|
211
|
-
* past a fourth argument they never wanted.
|
|
212
|
-
*/
|
|
213
148
|
export const formatColor = (color, format, withAlpha) => {
|
|
214
149
|
const alpha = clamp(color.alpha, 0, 100) / 100;
|
|
215
|
-
/* Two decimal places at most, and no trailing zeros: an alpha is a fraction
|
|
216
|
-
here and `0.5` is what a stylesheet holds, not `0.50` and not
|
|
217
|
-
`0.5019607843137255`. */
|
|
218
150
|
const opacity = Number(alpha.toFixed(2));
|
|
219
151
|
if (format === 'hsl') {
|
|
220
152
|
const { hue, lightness, saturation } = toHsl(color);
|
|
@@ -230,7 +162,6 @@ export const formatColor = (color, format, withAlpha) => {
|
|
|
230
162
|
const hex = `#${byte(red)}${byte(green)}${byte(blue)}`;
|
|
231
163
|
return withAlpha ? hex + byte(Math.round(alpha * 255)) : hex;
|
|
232
164
|
};
|
|
233
|
-
/** The color as CSS, for the swatches and the tracks. Always with its alpha. */
|
|
234
165
|
export const toCss = (color) => {
|
|
235
166
|
const { blue, green, red } = toRgb(color);
|
|
236
167
|
return `rgb(${red} ${green} ${blue} / ${clamp(color.alpha, 0, 100)}%)`;
|