@neumorphism-ui/elements 1.0.0-next.1 → 1.0.0-next.3
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/README.md +12 -1
- package/cdn/neu.min.js +176 -51
- package/custom-elements.json +2555 -358
- package/dist/all.d.ts +8 -0
- package/dist/all.js +8 -0
- package/dist/checkbox.d.ts +3 -0
- package/dist/checkbox.js +4 -0
- package/dist/components/avatar/avatar.d.ts +0 -3
- package/dist/components/avatar/avatar.def.d.ts +0 -3
- package/dist/components/avatar/avatar.def.js +4 -18
- package/dist/components/avatar/avatar.docs.d.ts +2 -0
- package/dist/components/avatar/avatar.docs.js +22 -0
- package/dist/components/avatar/avatar.js +7 -1
- package/dist/components/badge/badge.d.ts +0 -2
- package/dist/components/badge/badge.def.d.ts +0 -2
- package/dist/components/badge/badge.def.js +1 -14
- package/dist/components/badge/badge.docs.d.ts +2 -0
- package/dist/components/badge/badge.docs.js +17 -0
- package/dist/components/button/button.d.ts +0 -2
- package/dist/components/button/button.def.d.ts +0 -5
- package/dist/components/button/button.def.js +3 -25
- package/dist/components/button/button.docs.d.ts +2 -0
- package/dist/components/button/button.docs.js +29 -0
- package/dist/components/button/button.js +3 -36
- package/dist/components/button-group/button-group.d.ts +0 -2
- package/dist/components/button-group/button-group.def.d.ts +0 -2
- package/dist/components/button-group/button-group.def.js +1 -10
- package/dist/components/button-group/button-group.docs.d.ts +2 -0
- package/dist/components/button-group/button-group.docs.js +13 -0
- package/dist/components/checkbox/checkbox.d.ts +40 -0
- package/dist/components/checkbox/checkbox.def.d.ts +27 -0
- package/dist/components/checkbox/checkbox.def.js +21 -0
- package/dist/components/checkbox/checkbox.docs.d.ts +2 -0
- package/dist/components/checkbox/checkbox.docs.js +27 -0
- package/dist/components/checkbox/checkbox.js +40 -0
- package/dist/components/checkbox/checkbox.styles.d.ts +1 -0
- package/dist/components/checkbox/checkbox.styles.js +28 -0
- package/dist/components/divider/divider.d.ts +0 -1
- package/dist/components/divider/divider.def.d.ts +0 -1
- package/dist/components/divider/divider.def.js +0 -9
- package/dist/components/divider/divider.docs.d.ts +2 -0
- package/dist/components/divider/divider.docs.js +12 -0
- package/dist/components/icon/icon.d.ts +0 -3
- package/dist/components/icon/icon.def.d.ts +0 -3
- package/dist/components/icon/icon.def.js +3 -10
- package/dist/components/icon/icon.docs.d.ts +2 -0
- package/dist/components/icon/icon.docs.js +13 -0
- package/dist/components/icon/system-icons.d.ts +10 -0
- package/dist/components/icon/system-icons.js +22 -11
- package/dist/components/icon-button/icon-button.d.ts +0 -5
- package/dist/components/icon-button/icon-button.def.d.ts +0 -5
- package/dist/components/icon-button/icon-button.def.js +3 -17
- package/dist/components/icon-button/icon-button.docs.d.ts +2 -0
- package/dist/components/icon-button/icon-button.docs.js +26 -0
- package/dist/components/number-field/number-field.d.ts +79 -0
- package/dist/components/number-field/number-field.def.d.ts +115 -0
- package/dist/components/number-field/number-field.def.js +40 -0
- package/dist/components/number-field/number-field.docs.d.ts +2 -0
- package/dist/components/number-field/number-field.docs.js +45 -0
- package/dist/components/number-field/number-field.js +121 -0
- package/dist/components/number-field/number-field.styles.d.ts +1 -0
- package/dist/components/number-field/number-field.styles.js +15 -0
- package/dist/components/option/option-data.d.ts +8 -0
- package/dist/components/option/option-data.js +12 -0
- package/dist/components/option/option.d.ts +17 -0
- package/dist/components/option/option.def.d.ts +8 -0
- package/dist/components/option/option.def.js +12 -0
- package/dist/components/option/option.docs.d.ts +2 -0
- package/dist/components/option/option.docs.js +12 -0
- package/dist/components/option/option.js +17 -0
- package/dist/components/option/option.styles.d.ts +2 -0
- package/dist/components/option/option.styles.js +5 -0
- package/dist/components/radio/radio-link.d.ts +11 -0
- package/dist/components/radio/radio-link.js +5 -0
- package/dist/components/radio/radio.d.ts +27 -0
- package/dist/components/radio/radio.def.d.ts +9 -0
- package/dist/components/radio/radio.def.js +12 -0
- package/dist/components/radio/radio.docs.d.ts +2 -0
- package/dist/components/radio/radio.docs.js +18 -0
- package/dist/components/radio/radio.js +43 -0
- package/dist/components/radio/radio.styles.d.ts +1 -0
- package/dist/components/radio/radio.styles.js +21 -0
- package/dist/components/radio-group/radio-group.d.ts +76 -0
- package/dist/components/radio-group/radio-group.def.d.ts +36 -0
- package/dist/components/radio-group/radio-group.def.js +40 -0
- package/dist/components/radio-group/radio-group.docs.d.ts +2 -0
- package/dist/components/radio-group/radio-group.docs.js +37 -0
- package/dist/components/radio-group/radio-group.js +208 -0
- package/dist/components/radio-group/radio-group.styles.d.ts +1 -0
- package/dist/components/radio-group/radio-group.styles.js +13 -0
- package/dist/components/select/select.d.ts +115 -0
- package/dist/components/select/select.def.d.ts +34 -0
- package/dist/components/select/select.def.js +35 -0
- package/dist/components/select/select.docs.d.ts +2 -0
- package/dist/components/select/select.docs.js +65 -0
- package/dist/components/select/select.js +391 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +35 -0
- package/dist/components/shared/checkable.d.ts +45 -0
- package/dist/components/shared/checkable.js +108 -0
- package/dist/components/shared/field.styles.d.ts +1 -0
- package/dist/components/shared/field.styles.js +34 -0
- package/dist/components/shared/press.d.ts +7 -0
- package/dist/components/shared/press.js +42 -0
- package/dist/components/shared/step.d.ts +2 -0
- package/dist/components/shared/step.js +2 -0
- package/dist/components/shared/text-control.d.ts +72 -0
- package/dist/components/shared/text-control.js +191 -0
- package/dist/components/slider/slider.d.ts +67 -0
- package/dist/components/slider/slider.def.d.ts +37 -0
- package/dist/components/slider/slider.def.js +54 -0
- package/dist/components/slider/slider.docs.d.ts +2 -0
- package/dist/components/slider/slider.docs.js +45 -0
- package/dist/components/slider/slider.js +170 -0
- package/dist/components/slider/slider.styles.d.ts +1 -0
- package/dist/components/slider/slider.styles.js +16 -0
- package/dist/components/spinner/spinner.d.ts +0 -1
- package/dist/components/spinner/spinner.def.d.ts +0 -1
- package/dist/components/spinner/spinner.def.js +1 -13
- package/dist/components/spinner/spinner.docs.d.ts +2 -0
- package/dist/components/spinner/spinner.docs.js +17 -0
- package/dist/components/surface/surface.d.ts +0 -4
- package/dist/components/surface/surface.def.d.ts +0 -4
- package/dist/components/surface/surface.def.js +3 -12
- package/dist/components/surface/surface.docs.d.ts +2 -0
- package/dist/components/surface/surface.docs.js +15 -0
- package/dist/components/switch/switch.d.ts +4 -35
- package/dist/components/switch/switch.def.d.ts +0 -5
- package/dist/components/switch/switch.def.js +5 -21
- package/dist/components/switch/switch.docs.d.ts +2 -0
- package/dist/components/switch/switch.docs.js +26 -0
- package/dist/components/switch/switch.js +5 -117
- package/dist/components/text-field/implicit-submit.d.ts +5 -0
- package/dist/components/text-field/implicit-submit.js +8 -3
- package/dist/components/text-field/text-field.d.ts +9 -42
- package/dist/components/text-field/text-field.def.d.ts +0 -12
- package/dist/components/text-field/text-field.def.js +5 -30
- package/dist/components/text-field/text-field.docs.d.ts +2 -0
- package/dist/components/text-field/text-field.docs.js +45 -0
- package/dist/components/text-field/text-field.js +21 -178
- package/dist/components/text-field/text-field.styles.d.ts +1 -1
- package/dist/components/text-field/text-field.styles.js +2 -35
- package/dist/components/textarea/textarea.d.ts +92 -0
- package/dist/components/textarea/textarea.def.d.ts +127 -0
- package/dist/components/textarea/textarea.def.js +44 -0
- package/dist/components/textarea/textarea.docs.d.ts +2 -0
- package/dist/components/textarea/textarea.docs.js +40 -0
- package/dist/components/textarea/textarea.js +101 -0
- package/dist/components/textarea/textarea.styles.d.ts +1 -0
- package/dist/components/textarea/textarea.styles.js +9 -0
- package/dist/components/theme-docs.d.ts +3 -0
- package/dist/components/theme-docs.js +8 -0
- package/dist/core/define.d.ts +20 -2
- package/dist/core/dom.d.ts +4 -0
- package/dist/core/dom.js +7 -0
- package/dist/core/form-control.js +6 -1
- package/dist/core/neu-element.d.ts +2 -2
- package/dist/core/neu-element.js +10 -7
- package/dist/core/props.d.ts +13 -1
- package/dist/core/props.js +13 -3
- package/dist/core/styles.d.ts +0 -2
- package/dist/core/styles.js +0 -7
- package/dist/core/validation.d.ts +6 -2
- package/dist/core/validation.js +15 -8
- package/dist/docs.d.ts +3 -0
- package/dist/docs.js +41 -0
- package/dist/number-field.d.ts +3 -0
- package/dist/number-field.js +4 -0
- package/dist/option.d.ts +3 -0
- package/dist/option.js +4 -0
- package/dist/radio-group.d.ts +4 -0
- package/dist/radio-group.js +6 -0
- package/dist/radio.d.ts +3 -0
- package/dist/radio.js +4 -0
- package/dist/select.d.ts +3 -0
- package/dist/select.js +4 -0
- package/dist/slider.d.ts +3 -0
- package/dist/slider.js +4 -0
- package/dist/ssr.d.ts +10 -6
- package/dist/ssr.js +26 -6
- package/dist/textarea.d.ts +3 -0
- package/dist/textarea.js +4 -0
- package/package.json +45 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { themeCssProperties } from '../theme-docs.js';
|
|
2
|
+
export const radioGroupDocs = {
|
|
3
|
+
summary: 'A group of neu-radio elements with one selected value, arrow-key navigation and form support. The value attribute is the default value.',
|
|
4
|
+
descriptions: {
|
|
5
|
+
label: 'Visible label that names the group.',
|
|
6
|
+
name: 'The name under which the selected value is submitted with a form.',
|
|
7
|
+
value: 'The value of the currently selected radio.',
|
|
8
|
+
required: 'Makes the form invalid until a radio is selected.',
|
|
9
|
+
disabled: 'Prevents interaction with every radio in the group.',
|
|
10
|
+
orientation: 'The direction the radios are laid out in.',
|
|
11
|
+
defaultValue: 'The value selected at the start, which a form reset restores; set by the value attribute.',
|
|
12
|
+
},
|
|
13
|
+
events: [
|
|
14
|
+
{ name: 'input', description: 'Fired when the user selects a radio.' },
|
|
15
|
+
{ name: 'change', description: 'Fired after input when the user selects a radio.' },
|
|
16
|
+
],
|
|
17
|
+
slots: [
|
|
18
|
+
{ name: '', description: 'neu-radio elements.' },
|
|
19
|
+
{ name: 'label', description: 'Rich label; replaces the label attribute.' },
|
|
20
|
+
],
|
|
21
|
+
parts: [
|
|
22
|
+
{ name: 'base', description: 'Outer wrapper.' },
|
|
23
|
+
{ name: 'label', description: 'The group label.' },
|
|
24
|
+
],
|
|
25
|
+
cssProperties: themeCssProperties,
|
|
26
|
+
keyboard: [
|
|
27
|
+
{
|
|
28
|
+
name: 'ArrowDown / ArrowRight',
|
|
29
|
+
description: 'Moves to the next radio and selects it, wrapping and skipping disabled radios; ArrowLeft and ArrowRight swap in right-to-left text.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'ArrowUp / ArrowLeft',
|
|
33
|
+
description: 'Moves to the previous radio and selects it, wrapping and skipping disabled radios; ArrowLeft and ArrowRight swap in right-to-left text.',
|
|
34
|
+
},
|
|
35
|
+
{ name: 'Space', description: 'Selects the focused radio.' },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { dispatch, inSlotted, setText } from '../../core/dom.js';
|
|
2
|
+
import { FormControlElement } from '../../core/form-control.js';
|
|
3
|
+
import { requiredMessage } from '../../core/validation.js';
|
|
4
|
+
import { RESYNC, SET_CHECKED, SET_GROUP_DISABLED } from '../radio/radio-link.js';
|
|
5
|
+
import { markRule, radioGroupDef } from './radio-group.def.js';
|
|
6
|
+
/** Which way each arrow key moves through the radios. */
|
|
7
|
+
const STEP = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 };
|
|
8
|
+
/** A radio's value; without the attribute it is "on", like a native radio. */
|
|
9
|
+
const valueOf = (radio) => radio.getAttribute('value') ?? 'on';
|
|
10
|
+
export class NeuRadioGroup extends FormControlElement {
|
|
11
|
+
static def = radioGroupDef;
|
|
12
|
+
static extraProperties = ['defaultValue'];
|
|
13
|
+
/**
|
|
14
|
+
* The requested value, kept even while no radio matches it (plan decision 8); null requests
|
|
15
|
+
* nothing, so a radio whose value is "" is chosen only when asked for.
|
|
16
|
+
*/
|
|
17
|
+
current;
|
|
18
|
+
dirty = false;
|
|
19
|
+
/** The radio that holds the Tab stop. */
|
|
20
|
+
stop;
|
|
21
|
+
/** The selected radio as of the last sync, which the form value and validity read. */
|
|
22
|
+
picked;
|
|
23
|
+
observer;
|
|
24
|
+
constructor() {
|
|
25
|
+
super();
|
|
26
|
+
this.current = this.getAttribute('value');
|
|
27
|
+
this.internals.role = 'radiogroup';
|
|
28
|
+
this.addEventListener('click', (event) => {
|
|
29
|
+
const radio = this.radioAt(event);
|
|
30
|
+
if (radio && !this.isDisabled && !radio.hasAttribute('disabled'))
|
|
31
|
+
this.choose(radio);
|
|
32
|
+
});
|
|
33
|
+
this.addEventListener('keydown', (event) => this.onKey(event));
|
|
34
|
+
}
|
|
35
|
+
get value() {
|
|
36
|
+
return this.selected() ? this.current : '';
|
|
37
|
+
}
|
|
38
|
+
set value(next) {
|
|
39
|
+
this.dirty = true;
|
|
40
|
+
this.current = next === null || next === undefined ? null : String(next);
|
|
41
|
+
this.sync();
|
|
42
|
+
}
|
|
43
|
+
get defaultValue() {
|
|
44
|
+
return String(this.props.value);
|
|
45
|
+
}
|
|
46
|
+
set defaultValue(next) {
|
|
47
|
+
this.setAttribute('value', String(next));
|
|
48
|
+
}
|
|
49
|
+
/** The radios of this group: those whose closest group is this one. */
|
|
50
|
+
radios() {
|
|
51
|
+
return [...this.querySelectorAll('neu-radio')].filter((radio) => radio.closest('neu-radio-group') === this);
|
|
52
|
+
}
|
|
53
|
+
/** The first radio of this group matching the requested value, if one is requested. */
|
|
54
|
+
selected(radios = this.radios()) {
|
|
55
|
+
return this.current === null
|
|
56
|
+
? undefined
|
|
57
|
+
: radios.find((radio) => valueOf(radio) === this.current);
|
|
58
|
+
}
|
|
59
|
+
radioAt(event) {
|
|
60
|
+
const radio = event.target instanceof Element ? event.target.closest('neu-radio') : null;
|
|
61
|
+
return radio && radio.closest('neu-radio-group') === this ? radio : null;
|
|
62
|
+
}
|
|
63
|
+
onKey(event) {
|
|
64
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey)
|
|
65
|
+
return;
|
|
66
|
+
const radio = this.radioAt(event);
|
|
67
|
+
// A key pressed inside a disabled radio (say, a link in its label) is not a choice.
|
|
68
|
+
if (!radio || this.isDisabled || radio.hasAttribute('disabled'))
|
|
69
|
+
return;
|
|
70
|
+
if (event.key === ' ') {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
this.choose(radio);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let step = STEP[event.key];
|
|
76
|
+
if (!step)
|
|
77
|
+
return;
|
|
78
|
+
// Like native radios, the horizontal arrows follow the reading direction.
|
|
79
|
+
if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight') && this.matches(':dir(rtl)')) {
|
|
80
|
+
step = -step;
|
|
81
|
+
}
|
|
82
|
+
const on = this.radios().filter((candidate) => !candidate.hasAttribute('disabled'));
|
|
83
|
+
const next = on[(on.indexOf(radio) + step + on.length) % on.length];
|
|
84
|
+
if (!next)
|
|
85
|
+
return;
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
next.focus();
|
|
88
|
+
this.choose(next);
|
|
89
|
+
}
|
|
90
|
+
/** A choice by the user: no event when the value does not change. */
|
|
91
|
+
choose(radio) {
|
|
92
|
+
const next = valueOf(radio);
|
|
93
|
+
if (next === this.current && this.selected())
|
|
94
|
+
return;
|
|
95
|
+
this.dirty = true;
|
|
96
|
+
this.current = next;
|
|
97
|
+
this.sync();
|
|
98
|
+
this.markInteracted();
|
|
99
|
+
dispatch(this, 'input');
|
|
100
|
+
dispatch(this, 'change');
|
|
101
|
+
}
|
|
102
|
+
[RESYNC]() {
|
|
103
|
+
this.sync();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Brings every radio in line with the group: checked state, disabling, the one Tab stop (the
|
|
107
|
+
* selected radio, else the first enabled one), the server-visible mark, and the form value.
|
|
108
|
+
* Radios that are not upgraded yet are skipped; they ask for a resync when they connect.
|
|
109
|
+
*/
|
|
110
|
+
sync() {
|
|
111
|
+
const radios = this.radios();
|
|
112
|
+
const picked = (this.picked = this.selected(radios));
|
|
113
|
+
const off = this.isDisabled;
|
|
114
|
+
const ready = radios.filter((radio) => SET_CHECKED in radio);
|
|
115
|
+
for (const radio of ready) {
|
|
116
|
+
radio[SET_GROUP_DISABLED](off);
|
|
117
|
+
radio[SET_CHECKED](radio === picked);
|
|
118
|
+
}
|
|
119
|
+
const on = off ? [] : ready.filter((radio) => !radio.disabled);
|
|
120
|
+
this.stop = on.find((radio) => radio === picked) ?? on[0];
|
|
121
|
+
for (const radio of on) {
|
|
122
|
+
const tabindex = radio === this.stop ? '0' : '-1';
|
|
123
|
+
if (radio.getAttribute('tabindex') !== tabindex)
|
|
124
|
+
radio.setAttribute('tabindex', tabindex);
|
|
125
|
+
}
|
|
126
|
+
const rule = markRule(this.current ?? '');
|
|
127
|
+
setText(this.ref('mark'), rule === '' ? '' : rule.value);
|
|
128
|
+
this.commit();
|
|
129
|
+
}
|
|
130
|
+
/** The group's name: the slotted label's text when there is one, else the label attribute. */
|
|
131
|
+
nameGroup() {
|
|
132
|
+
const slot = this.shadowRoot?.querySelector('slot[name="label"]');
|
|
133
|
+
const text = (slot?.assignedNodes() ?? [])
|
|
134
|
+
.map((node) => node.textContent)
|
|
135
|
+
.join('')
|
|
136
|
+
.replace(/\s+/g, ' ')
|
|
137
|
+
.trim();
|
|
138
|
+
this.internals.ariaLabel = text || String(this.props.label) || null;
|
|
139
|
+
}
|
|
140
|
+
rendered() {
|
|
141
|
+
this.shadowRoot.querySelector('slot[name="label"]').addEventListener('slotchange', () => this.nameGroup());
|
|
142
|
+
}
|
|
143
|
+
willUpdate(changed) {
|
|
144
|
+
if (changed.has('value') && !this.dirty)
|
|
145
|
+
this.current = this.getAttribute('value');
|
|
146
|
+
}
|
|
147
|
+
update(changed) {
|
|
148
|
+
if (changed.has('label')) {
|
|
149
|
+
setText(this.ref('label-text'), String(this.props.label));
|
|
150
|
+
this.nameGroup();
|
|
151
|
+
}
|
|
152
|
+
if (changed.has('required'))
|
|
153
|
+
this.internals.ariaRequired = this.props.required ? 'true' : null;
|
|
154
|
+
if (changed.has('disabled'))
|
|
155
|
+
this.internals.ariaDisabled = this.isDisabled ? 'true' : null;
|
|
156
|
+
if (changed.has('value') || changed.has('required') || changed.has('disabled'))
|
|
157
|
+
this.sync();
|
|
158
|
+
}
|
|
159
|
+
connected() {
|
|
160
|
+
// Radios coming, going or changing value or disabled resync the group. Frameworks rewrite the
|
|
161
|
+
// slotted label's text in place, which fires no slotchange; that renames the group only.
|
|
162
|
+
this.observer ??= new MutationObserver((records) => {
|
|
163
|
+
const label = records.filter((record) => inSlotted(this, record.target));
|
|
164
|
+
if (label.length)
|
|
165
|
+
this.nameGroup();
|
|
166
|
+
if (records.some((r) => r.type !== 'characterData' && !label.includes(r)))
|
|
167
|
+
this.sync();
|
|
168
|
+
});
|
|
169
|
+
this.observer.observe(this, {
|
|
170
|
+
childList: true,
|
|
171
|
+
subtree: true,
|
|
172
|
+
characterData: true,
|
|
173
|
+
attributes: true,
|
|
174
|
+
attributeFilter: ['disabled', 'value'],
|
|
175
|
+
});
|
|
176
|
+
// The label may have changed while the group was detached and nothing was observing.
|
|
177
|
+
this.nameGroup();
|
|
178
|
+
this.sync();
|
|
179
|
+
}
|
|
180
|
+
disconnected() {
|
|
181
|
+
this.observer?.disconnect();
|
|
182
|
+
}
|
|
183
|
+
formValue() {
|
|
184
|
+
return this.picked ? this.current : null;
|
|
185
|
+
}
|
|
186
|
+
/** Nothing selected saves nothing, so a radio whose value is "" is not chosen on restore. */
|
|
187
|
+
formState() {
|
|
188
|
+
return this.formValue();
|
|
189
|
+
}
|
|
190
|
+
validate() {
|
|
191
|
+
if (this.props.required === true && !this.picked) {
|
|
192
|
+
this.internals.setValidity({ valueMissing: true }, requiredMessage('radio', 'Please select one of these options.'), this.stop);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
this.internals.setValidity({});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
resetToDefault() {
|
|
199
|
+
this.dirty = false;
|
|
200
|
+
this.current = this.getAttribute('value');
|
|
201
|
+
this.sync();
|
|
202
|
+
}
|
|
203
|
+
restoreState(state) {
|
|
204
|
+
this.dirty = true;
|
|
205
|
+
this.current = state;
|
|
206
|
+
this.sync();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const radioGroupStyles = "\n:host {\n --_neu-bg: var(--neu-bg, #e0e5ec);\n --_neu-accent-c: var(--neu-accent, #6d5dfc);\n --_neu-d: var(--neu-distance, 6px);\n --_neu-k: var(--neu-intensity, 1);\n --_neu-r: var(--neu-radius, 12px);\n --_neu-danger: var(--neu-danger, #e5484d);\n --_neu-dur: var(--neu-duration, 150ms);\n --_neu-sd: var(--neu-shadow-dark, color-mix(in oklab, var(--_neu-bg), black min(100%, calc(var(--_neu-shade, 18%) * var(--_neu-k)))));\n --_neu-sl: var(--neu-shadow-light, color-mix(in oklab, var(--_neu-bg), white min(100%, calc(var(--_neu-light, 100%) * var(--_neu-k)))));\n --_neu-fg: var(--neu-fg, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-mix, 32%)));\n --_neu-fg-muted: var(--neu-fg-muted, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-muted-mix, 18%)));\n --_neu-accent-soft: color-mix(in oklab, var(--_neu-accent-c) 25%, var(--_neu-bg));\n --_neu-focus: 0 0 0 3px color-mix(in oklab, var(--_neu-accent-c) 45%, transparent);\n --_neu-raised: var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-raised-sm: calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n --_neu-inset: inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-inset-sm: inset calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), inset calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n font-family: var(--neu-font);\n color: var(--_neu-fg);\n -webkit-tap-highlight-color: transparent;\n}\n:host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }\n:host([hidden]) { display: none !important; }\n:host(:focus) { outline: none; }\n*, *::before, *::after { box-sizing: border-box; }\n\n:host { display: block; }\n:host(:disabled), :host([disabled]) { cursor: not-allowed; }\n.base { display: flex; flex-direction: column; }\n.label { display: block; font-size: 0.8125rem; font-weight: 600; color: var(--_neu-fg-muted); }\n.label-text, ::slotted([slot='label']) { display: block; margin-block-end: 0.5rem; }\n.label-text:empty { display: none; }\n.items { display: flex; flex-direction: column; align-items: flex-start; gap: 0.75rem; }\n:host([orientation='horizontal']) .items { flex-direction: row; flex-wrap: wrap; align-items: center; }\n:host(:disabled) .items, :host([disabled]) .items { opacity: 0.55; cursor: not-allowed; }\n:host(:state(user-invalid)) .label { color: var(--_neu-danger); }\n";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { hostTheme } from '../../core/styles.js';
|
|
2
|
+
export const radioGroupStyles = `${hostTheme}
|
|
3
|
+
:host { display: block; }
|
|
4
|
+
:host(:disabled), :host([disabled]) { cursor: not-allowed; }
|
|
5
|
+
.base { display: flex; flex-direction: column; }
|
|
6
|
+
.label { display: block; font-size: 0.8125rem; font-weight: 600; color: var(--_neu-fg-muted); }
|
|
7
|
+
.label-text, ::slotted([slot='label']) { display: block; margin-block-end: 0.5rem; }
|
|
8
|
+
.label-text:empty { display: none; }
|
|
9
|
+
.items { display: flex; flex-direction: column; align-items: flex-start; gap: 0.75rem; }
|
|
10
|
+
:host([orientation='horizontal']) .items { flex-direction: row; flex-wrap: wrap; align-items: center; }
|
|
11
|
+
:host(:disabled) .items, :host([disabled]) .items { opacity: 0.55; cursor: not-allowed; }
|
|
12
|
+
:host(:state(user-invalid)) .label { color: var(--_neu-danger); }
|
|
13
|
+
`;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { FormControlElement } from '../../core/form-control.js';
|
|
2
|
+
export declare class NeuSelect extends FormControlElement {
|
|
3
|
+
static def: import("../../core/define.js").ComponentDef<{
|
|
4
|
+
readonly value: {
|
|
5
|
+
readonly type: "string";
|
|
6
|
+
};
|
|
7
|
+
readonly name: {
|
|
8
|
+
readonly type: "string";
|
|
9
|
+
};
|
|
10
|
+
readonly label: {
|
|
11
|
+
readonly type: "string";
|
|
12
|
+
};
|
|
13
|
+
readonly placeholder: {
|
|
14
|
+
readonly type: "string";
|
|
15
|
+
};
|
|
16
|
+
readonly helpText: {
|
|
17
|
+
readonly type: "string";
|
|
18
|
+
};
|
|
19
|
+
readonly errorText: {
|
|
20
|
+
readonly type: "string";
|
|
21
|
+
};
|
|
22
|
+
readonly required: {
|
|
23
|
+
readonly type: "boolean";
|
|
24
|
+
};
|
|
25
|
+
readonly disabled: {
|
|
26
|
+
readonly type: "boolean";
|
|
27
|
+
};
|
|
28
|
+
readonly size: {
|
|
29
|
+
readonly type: "string";
|
|
30
|
+
readonly default: "md";
|
|
31
|
+
readonly values: readonly ["sm", "md", "lg"];
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
static extraProperties: string[];
|
|
35
|
+
name: string;
|
|
36
|
+
label: string;
|
|
37
|
+
placeholder: string;
|
|
38
|
+
helpText: string;
|
|
39
|
+
errorText: string;
|
|
40
|
+
required: boolean;
|
|
41
|
+
disabled: boolean;
|
|
42
|
+
size: 'sm' | 'md' | 'lg';
|
|
43
|
+
/** The requested value, kept even while no option matches it (plan decision 8). */
|
|
44
|
+
private current;
|
|
45
|
+
private dirty;
|
|
46
|
+
/** The options as last read, which the listbox mirrors by index (`option-<index>`). */
|
|
47
|
+
private opts;
|
|
48
|
+
private observer;
|
|
49
|
+
private opened;
|
|
50
|
+
/** The light-DOM option behind the active one, so a rebuild keeps it active. */
|
|
51
|
+
private activeSource;
|
|
52
|
+
/** Whether the listbox was open when the pointer went down on the combobox. */
|
|
53
|
+
private pressedOpen;
|
|
54
|
+
private typed;
|
|
55
|
+
private typedTimer;
|
|
56
|
+
constructor();
|
|
57
|
+
get value(): string;
|
|
58
|
+
set value(next: string);
|
|
59
|
+
get defaultValue(): string;
|
|
60
|
+
set defaultValue(next: string);
|
|
61
|
+
/** Reads the neu-option children from their attributes and text; they need not be upgraded. */
|
|
62
|
+
private read;
|
|
63
|
+
/** The value as of the last read: the requested one while an option matches it, else ''. */
|
|
64
|
+
private chosen;
|
|
65
|
+
/**
|
|
66
|
+
* Reads the options again, then brings the display, an open listbox and the form in line.
|
|
67
|
+
* setText writes only when the text differs, so a server-rendered display is left alone.
|
|
68
|
+
*/
|
|
69
|
+
private sync;
|
|
70
|
+
/** The first enabled option from `from` going `dir`, `from` included; -1 when there is none. */
|
|
71
|
+
private step;
|
|
72
|
+
/**
|
|
73
|
+
* The enabled option nearest to `from` (clamped to the list) going `dir`, else turning back:
|
|
74
|
+
* a move or jump past either end stops at the last enabled option that way.
|
|
75
|
+
*/
|
|
76
|
+
private near;
|
|
77
|
+
private activeIndex;
|
|
78
|
+
/** Builds the listbox from the options and places it; the active option stays if it can. */
|
|
79
|
+
private render;
|
|
80
|
+
/** Makes option `i` the active one (none for -1); `scroll` brings it into view. */
|
|
81
|
+
private activate;
|
|
82
|
+
/** Puts the listbox under the combobox at its width, or above it when that has more room. */
|
|
83
|
+
private readonly place;
|
|
84
|
+
private open;
|
|
85
|
+
/**
|
|
86
|
+
* Focus that lands outside the select while the listbox is open. The combobox's focusout
|
|
87
|
+
* covers most moves; this covers focus set elsewhere after the window was left (the focusout
|
|
88
|
+
* then kept the listbox open, and Firefox sends no second one).
|
|
89
|
+
*/
|
|
90
|
+
private readonly focusLanded;
|
|
91
|
+
/** The one way the listbox closes: by key, by choice, by light dismiss (toggle), by focus leaving or when disabled. */
|
|
92
|
+
private close;
|
|
93
|
+
/** A choice by the user: enabled options only, and events only when the value changes. */
|
|
94
|
+
private choose;
|
|
95
|
+
/**
|
|
96
|
+
* Jumps to the next option whose label starts with the characters typed within TYPEAHEAD_MS.
|
|
97
|
+
* A buffer of one letter (typed once or repeated) searches that letter from the option after
|
|
98
|
+
* the active one, so repeating it cycles; any other buffer refines the search and starts at the
|
|
99
|
+
* active option, which may still match.
|
|
100
|
+
*/
|
|
101
|
+
private typeahead;
|
|
102
|
+
private onKey;
|
|
103
|
+
/** Index of the listbox option an event happened on, or -1. */
|
|
104
|
+
private optionAt;
|
|
105
|
+
protected rendered(): void;
|
|
106
|
+
protected willUpdate(changed: ReadonlySet<string>): void;
|
|
107
|
+
protected update(changed: ReadonlySet<string>): void;
|
|
108
|
+
protected connected(): void;
|
|
109
|
+
protected disconnected(): void;
|
|
110
|
+
/** Like a native select: an option whose value is "" submits name=; no option, nothing. */
|
|
111
|
+
protected formValue(): string | null;
|
|
112
|
+
protected validate(): void;
|
|
113
|
+
protected resetToDefault(): void;
|
|
114
|
+
protected restoreState(state: string): void;
|
|
115
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type OptionData } from '../../core/define.js';
|
|
2
|
+
/** The option a select shows for `value`: the first one whose value matches, as in the client. */
|
|
3
|
+
export declare function pickOption<T extends OptionData>(options: readonly T[] | undefined, value: string): T | undefined;
|
|
4
|
+
export declare const selectDef: import("../../core/define.js").ComponentDef<{
|
|
5
|
+
readonly value: {
|
|
6
|
+
readonly type: "string";
|
|
7
|
+
};
|
|
8
|
+
readonly name: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
};
|
|
11
|
+
readonly label: {
|
|
12
|
+
readonly type: "string";
|
|
13
|
+
};
|
|
14
|
+
readonly placeholder: {
|
|
15
|
+
readonly type: "string";
|
|
16
|
+
};
|
|
17
|
+
readonly helpText: {
|
|
18
|
+
readonly type: "string";
|
|
19
|
+
};
|
|
20
|
+
readonly errorText: {
|
|
21
|
+
readonly type: "string";
|
|
22
|
+
};
|
|
23
|
+
readonly required: {
|
|
24
|
+
readonly type: "boolean";
|
|
25
|
+
};
|
|
26
|
+
readonly disabled: {
|
|
27
|
+
readonly type: "boolean";
|
|
28
|
+
};
|
|
29
|
+
readonly size: {
|
|
30
|
+
readonly type: "string";
|
|
31
|
+
readonly default: "md";
|
|
32
|
+
readonly values: readonly ["sm", "md", "lg"];
|
|
33
|
+
};
|
|
34
|
+
}>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineComponent } from '../../core/define.js';
|
|
2
|
+
import { html, unsafeHTML } from '../../core/html.js';
|
|
3
|
+
import { chevronDownBody, svgWrap } from '../icon/system-icons.js';
|
|
4
|
+
import { selectStyles } from './select.styles.js';
|
|
5
|
+
/** The option a select shows for `value`: the first one whose value matches, as in the client. */
|
|
6
|
+
export function pickOption(options, value) {
|
|
7
|
+
return options?.find((option) => option.value === value);
|
|
8
|
+
}
|
|
9
|
+
const chevron = unsafeHTML(svgWrap(chevronDownBody));
|
|
10
|
+
export const selectDef = defineComponent({
|
|
11
|
+
tag: 'neu-select',
|
|
12
|
+
props: {
|
|
13
|
+
value: { type: 'string' },
|
|
14
|
+
name: { type: 'string' },
|
|
15
|
+
label: { type: 'string' },
|
|
16
|
+
placeholder: { type: 'string' },
|
|
17
|
+
helpText: { type: 'string' },
|
|
18
|
+
errorText: { type: 'string' },
|
|
19
|
+
required: { type: 'boolean' },
|
|
20
|
+
disabled: { type: 'boolean' },
|
|
21
|
+
size: { type: 'string', default: 'md', values: ['sm', 'md', 'lg'] },
|
|
22
|
+
},
|
|
23
|
+
delegatesFocus: true,
|
|
24
|
+
form: { valueProp: 'value' },
|
|
25
|
+
optionsFrom: 'neu-option',
|
|
26
|
+
styles: selectStyles,
|
|
27
|
+
// The client renders without ctx and update() fills the display, so both roots end up the same.
|
|
28
|
+
template: (p, ctx) => {
|
|
29
|
+
const picked = pickOption(ctx?.options, p.value);
|
|
30
|
+
// Options the server could not read (e.g. a custom component's children) leave a set value's
|
|
31
|
+
// display empty rather than showing the placeholder; update() fills it after hydration.
|
|
32
|
+
const unread = !picked && p.value !== '' && ctx?.options?.length === 0;
|
|
33
|
+
return html `<div part="base" class="base"><span part="label" class="label" id="label" data-ref="label"><slot name="label"><span class="label-text" data-ref="label-text">${p.label}</span></slot></span><div part="combobox" class="combobox" id="combobox" role="combobox"${p.disabled ? html ` aria-disabled="true"` : html ` tabindex="0"`} aria-haspopup="listbox" aria-expanded="false" aria-controls="listbox" aria-labelledby="label" aria-describedby="help"${p.required ? html ` aria-required="true"` : ''} data-ref="combobox"><span part="display" class="display${picked || unread ? '' : ' placeholder'}" data-ref="display">${picked ? picked.label : unread ? '' : p.placeholder}</span><span part="chevron" class="chevron">${chevron}</span></div><div part="listbox" class="listbox" id="listbox" role="listbox" aria-labelledby="label" popover="auto" data-ref="listbox"></div><div part="help-text" class="help" id="help"><slot name="help-text"><span class="help-text" data-ref="help-text">${p.helpText}</span></slot><span class="error-text" data-ref="error-text">${p.errorText}</span></div></div>`;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { themeCssProperties } from '../theme-docs.js';
|
|
2
|
+
export const selectDocs = {
|
|
3
|
+
summary: 'A single-value select (the APG select-only combobox): a raised trigger and a popover listbox built from the neu-option children, which are data only. The value attribute is the default value.',
|
|
4
|
+
descriptions: {
|
|
5
|
+
value: 'The value of the currently chosen option.',
|
|
6
|
+
name: 'The name under which the value is submitted with a form.',
|
|
7
|
+
label: 'Visible label text that names the select.',
|
|
8
|
+
placeholder: 'Text shown while no option is chosen.',
|
|
9
|
+
helpText: 'Guidance shown below the select; the help-text slot replaces it.',
|
|
10
|
+
errorText: "Shown in place of the browser's validation message once the select is invalid after user interaction; does not itself make the select invalid.",
|
|
11
|
+
required: 'Makes the form invalid until an option is chosen.',
|
|
12
|
+
disabled: 'Prevents interaction and leaves the select out of form submission.',
|
|
13
|
+
size: 'The select size.',
|
|
14
|
+
defaultValue: 'The value chosen at the start, which a form reset restores; set by the value attribute.',
|
|
15
|
+
},
|
|
16
|
+
events: [
|
|
17
|
+
{ name: 'input', description: 'Fired when the user chooses an option with a new value.' },
|
|
18
|
+
{ name: 'change', description: 'Fired after input when the user chooses a new value.' },
|
|
19
|
+
{ name: 'neu-open', description: 'Fired when the listbox opens.' },
|
|
20
|
+
{ name: 'neu-close', description: 'Fired when the listbox closes.' },
|
|
21
|
+
],
|
|
22
|
+
slots: [
|
|
23
|
+
{ name: 'label', description: 'Rich label; replaces the label attribute.' },
|
|
24
|
+
{ name: 'help-text', description: 'Rich help text; replaces the help-text attribute.' },
|
|
25
|
+
],
|
|
26
|
+
parts: [
|
|
27
|
+
{ name: 'base', description: 'Outer wrapper.' },
|
|
28
|
+
{ name: 'label', description: 'The label text.' },
|
|
29
|
+
{ name: 'combobox', description: 'The raised trigger that holds focus.' },
|
|
30
|
+
{ name: 'display', description: 'The selected label, or the placeholder.' },
|
|
31
|
+
{ name: 'chevron', description: 'The chevron icon.' },
|
|
32
|
+
{ name: 'listbox', description: 'The popover list of options.' },
|
|
33
|
+
{ name: 'option', description: 'One option in the listbox.' },
|
|
34
|
+
{ name: 'help-text', description: 'Help or error text area.' },
|
|
35
|
+
],
|
|
36
|
+
cssProperties: themeCssProperties,
|
|
37
|
+
keyboard: [
|
|
38
|
+
{ name: 'ArrowDown / ArrowUp / Enter / Space', description: 'When closed: opens the listbox.' },
|
|
39
|
+
{
|
|
40
|
+
name: 'Home / End',
|
|
41
|
+
description: 'When closed: opens the listbox on the first or last option.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Printable characters',
|
|
45
|
+
description: 'When closed: opens the listbox on the first matching option.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'ArrowDown / ArrowUp',
|
|
49
|
+
description: 'When open: moves to the next or previous option.',
|
|
50
|
+
},
|
|
51
|
+
{ name: 'Home / End', description: 'When open: moves to the first or last option.' },
|
|
52
|
+
{ name: 'PageDown / PageUp', description: 'When open: moves ten options.' },
|
|
53
|
+
{ name: 'Enter / Space', description: 'When open: chooses the active option and closes.' },
|
|
54
|
+
{ name: 'Escape', description: 'When open: closes without changing the value.' },
|
|
55
|
+
{
|
|
56
|
+
name: 'Tab',
|
|
57
|
+
description: 'When open: chooses the active option, closes and moves focus on.',
|
|
58
|
+
},
|
|
59
|
+
{ name: 'Alt+ArrowUp', description: 'When open: chooses the active option and closes.' },
|
|
60
|
+
{
|
|
61
|
+
name: 'Printable characters',
|
|
62
|
+
description: 'When open: moves to the matching option; characters typed within 500 ms add up.',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|