@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,45 @@
|
|
|
1
|
+
import { themeCssProperties } from '../theme-docs.js';
|
|
2
|
+
export const numberFieldDocs = {
|
|
3
|
+
summary: 'A numeric input with an inset field and stepper buttons. The value attribute is the default value.',
|
|
4
|
+
descriptions: {
|
|
5
|
+
value: 'The current number as typed or set by script, as a string.',
|
|
6
|
+
min: 'The lowest accepted value, also the target of the Home key.',
|
|
7
|
+
max: 'The highest accepted value, also the target of the End key.',
|
|
8
|
+
step: 'The amount one step changes the value by, and the granularity values must match.',
|
|
9
|
+
name: 'The name under which the value is submitted with a form.',
|
|
10
|
+
label: 'Visible label text; the label slot replaces it.',
|
|
11
|
+
placeholder: 'Hint shown while the field is empty.',
|
|
12
|
+
helpText: 'Guidance shown below the field; the help-text slot replaces it.',
|
|
13
|
+
errorText: "Shown in place of the browser's validation message once the field is invalid after user interaction; does not itself make the field invalid.",
|
|
14
|
+
required: 'Makes the form invalid while the field is empty.',
|
|
15
|
+
disabled: 'Prevents interaction and leaves the field out of form submission.',
|
|
16
|
+
readOnly: 'Keeps the value visible and submitted but not editable.',
|
|
17
|
+
size: 'The field size.',
|
|
18
|
+
incrementLabel: 'Accessible name of the button that raises the value; defaults to "Increase".',
|
|
19
|
+
decrementLabel: 'Accessible name of the button that lowers the value; defaults to "Decrease".',
|
|
20
|
+
defaultValue: 'The value the field starts with, which a form reset restores; set by the value attribute.',
|
|
21
|
+
},
|
|
22
|
+
events: [
|
|
23
|
+
{ name: 'input', description: 'Fired on every edit and every step (composed).' },
|
|
24
|
+
{ name: 'change', description: 'Fired when an edit is committed, or after each step.' },
|
|
25
|
+
],
|
|
26
|
+
slots: [
|
|
27
|
+
{ name: 'label', description: 'Rich label; replaces the label attribute.' },
|
|
28
|
+
{ name: 'help-text', description: 'Rich help text; replaces the help-text attribute.' },
|
|
29
|
+
],
|
|
30
|
+
parts: [
|
|
31
|
+
{ name: 'base', description: 'Outer wrapper.' },
|
|
32
|
+
{ name: 'label', description: 'The <label> element.' },
|
|
33
|
+
{ name: 'control', description: 'The inset field around the input and buttons.' },
|
|
34
|
+
{ name: 'input', description: 'The native <input type="number">.' },
|
|
35
|
+
{ name: 'decrement', description: 'The button that lowers the value.' },
|
|
36
|
+
{ name: 'increment', description: 'The button that raises the value.' },
|
|
37
|
+
{ name: 'help-text', description: 'Help or error text area.' },
|
|
38
|
+
],
|
|
39
|
+
cssProperties: themeCssProperties,
|
|
40
|
+
keyboard: [
|
|
41
|
+
{ name: 'ArrowUp / ArrowDown', description: 'Steps the value up or down by step.' },
|
|
42
|
+
{ name: 'PageUp / PageDown', description: 'Steps the value up or down by ten times step.' },
|
|
43
|
+
{ name: 'Home / End', description: 'Sets the value to min or max, when set.' },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { afterDispatch, composing, setOptionalAttribute } from '../../core/dom.js';
|
|
2
|
+
import { baseCommitProps, TextControlElement } from '../shared/text-control.js';
|
|
3
|
+
import { BLOCKS_IMPLICIT_SUBMIT, implicitSubmit } from '../text-field/implicit-submit.js';
|
|
4
|
+
import { stepUnit } from '../shared/step.js';
|
|
5
|
+
import { decimalPad, numberFieldDef } from './number-field.def.js';
|
|
6
|
+
export class NeuNumberField extends TextControlElement {
|
|
7
|
+
static def = numberFieldDef;
|
|
8
|
+
static [BLOCKS_IMPLICIT_SUBMIT] = true;
|
|
9
|
+
static extraProperties = ['defaultValue', 'valueAsNumber'];
|
|
10
|
+
commitProps = [...baseCommitProps, 'min', 'max', 'step'];
|
|
11
|
+
get np() {
|
|
12
|
+
return this.props;
|
|
13
|
+
}
|
|
14
|
+
get valueAsNumber() {
|
|
15
|
+
const input = this.input;
|
|
16
|
+
return input ? input.valueAsNumber : this.probe(this.value).valueAsNumber;
|
|
17
|
+
}
|
|
18
|
+
set valueAsNumber(next) {
|
|
19
|
+
this.value = Number.isFinite(next) ? String(next) : '';
|
|
20
|
+
}
|
|
21
|
+
/** The limits and step a native input of this field carries. */
|
|
22
|
+
constrain(input) {
|
|
23
|
+
const p = this.np;
|
|
24
|
+
setOptionalAttribute(input, 'min', Number.isFinite(p.min) ? String(p.min) : '');
|
|
25
|
+
setOptionalAttribute(input, 'max', Number.isFinite(p.max) ? String(p.max) : '');
|
|
26
|
+
input.step = String(stepUnit(p.step));
|
|
27
|
+
setOptionalAttribute(input, 'inputmode', decimalPad(p.min) ? 'decimal' : '');
|
|
28
|
+
}
|
|
29
|
+
probe(value) {
|
|
30
|
+
const probe = document.createElement('input');
|
|
31
|
+
probe.type = 'number';
|
|
32
|
+
this.constrain(probe);
|
|
33
|
+
probe.value = value;
|
|
34
|
+
return probe;
|
|
35
|
+
}
|
|
36
|
+
/** Moves the value, then reports it like a finished native edit: input (composed), then change. */
|
|
37
|
+
move(change) {
|
|
38
|
+
const input = this.input;
|
|
39
|
+
if (!input || input.disabled || input.readOnly)
|
|
40
|
+
return;
|
|
41
|
+
const before = input.value;
|
|
42
|
+
try {
|
|
43
|
+
change(input);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// stepUp and stepDown throw when no step applies; the value stays as it was.
|
|
47
|
+
}
|
|
48
|
+
if (input.value === before)
|
|
49
|
+
return;
|
|
50
|
+
input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
51
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
52
|
+
}
|
|
53
|
+
rendered() {
|
|
54
|
+
super.rendered();
|
|
55
|
+
const steps = [
|
|
56
|
+
['decrement', (input) => input.stepDown()],
|
|
57
|
+
['increment', (input) => input.stepUp()],
|
|
58
|
+
];
|
|
59
|
+
for (const [ref, mover] of steps) {
|
|
60
|
+
const button = this.ref(ref);
|
|
61
|
+
// Keep focus (and the caret) in the input while the button is pressed. A touch leaves focus
|
|
62
|
+
// where it is: focusing the input would open the on-screen keyboard on every tap.
|
|
63
|
+
button?.addEventListener('pointerdown', (event) => {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
if (!button.disabled && event.pointerType !== 'touch')
|
|
66
|
+
this.input?.focus();
|
|
67
|
+
});
|
|
68
|
+
button?.addEventListener('click', () => this.move(mover));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
controlKeyDown(event) {
|
|
72
|
+
if (composing(event))
|
|
73
|
+
return;
|
|
74
|
+
if (event.key === 'Enter') {
|
|
75
|
+
// Like a native input: submit once the keydown has finished dispatching, unless cancelled.
|
|
76
|
+
afterDispatch(event, () => {
|
|
77
|
+
if (!event.defaultPrevented)
|
|
78
|
+
implicitSubmit(this.internals.form);
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
83
|
+
return;
|
|
84
|
+
const { min, max } = this.np;
|
|
85
|
+
let mover;
|
|
86
|
+
if (event.key === 'PageUp')
|
|
87
|
+
mover = (input) => input.stepUp(10);
|
|
88
|
+
else if (event.key === 'PageDown')
|
|
89
|
+
mover = (input) => input.stepDown(10);
|
|
90
|
+
else if (event.key === 'Home' && Number.isFinite(min))
|
|
91
|
+
mover = (input) => (input.value = `${min}`);
|
|
92
|
+
else if (event.key === 'End' && Number.isFinite(max))
|
|
93
|
+
mover = (input) => (input.value = `${max}`);
|
|
94
|
+
if (!mover)
|
|
95
|
+
return;
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
this.move(mover);
|
|
98
|
+
}
|
|
99
|
+
update(changed) {
|
|
100
|
+
const input = this.input;
|
|
101
|
+
if (input) {
|
|
102
|
+
if (['min', 'max', 'step'].some((key) => changed.has(key)))
|
|
103
|
+
this.constrain(input);
|
|
104
|
+
const off = this.isDisabled || this.np.readOnly;
|
|
105
|
+
const buttons = [
|
|
106
|
+
['decrement', 'decrementLabel'],
|
|
107
|
+
['increment', 'incrementLabel'],
|
|
108
|
+
];
|
|
109
|
+
for (const [ref, label] of buttons) {
|
|
110
|
+
const button = this.ref(ref);
|
|
111
|
+
if (!button)
|
|
112
|
+
continue;
|
|
113
|
+
if (changed.has('disabled') || changed.has('readOnly'))
|
|
114
|
+
button.disabled = off;
|
|
115
|
+
if (changed.has(label))
|
|
116
|
+
button.setAttribute('aria-label', this.np[label]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
super.update(changed);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const numberFieldStyles = "\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 { font-size: 0.8125rem; font-weight: 600; line-height: 1rem; color: var(--_neu-fg-muted); }\n.label-text, ::slotted([slot='label']) { display: block; margin-block-end: 0.375rem; }\n.label-text:empty { display: none; }\n.control {\n display: flex; align-items: center; min-block-size: 2.75rem; padding-inline: 0.875rem;\n border-radius: var(--_neu-r); background: var(--_neu-bg); box-shadow: var(--_neu-inset);\n transition: box-shadow var(--_neu-dur) ease;\n}\n:host([size='sm']) .control { min-block-size: 2.25rem; padding-inline: 0.75rem; box-shadow: var(--_neu-inset-sm); }\n:host([size='lg']) .control { min-block-size: 3.25rem; padding-inline: 1rem; }\n:host(:focus-within) .control { box-shadow: var(--_neu-inset), var(--_neu-focus); }\n:host(:state(user-invalid)) .control {\n box-shadow:\n inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) color-mix(in oklab, var(--_neu-danger) 35%, var(--_neu-sd)),\n inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n}\n:host(:disabled) .control, :host([disabled]) .control { box-shadow: var(--_neu-inset-sm); opacity: 0.6; }\n.input {\n flex: 1; min-inline-size: 0; align-self: stretch; padding: 0; border: none; outline: none;\n background: transparent; color: inherit; font: inherit; font-size: 0.9375rem;\n}\n.input::placeholder { color: var(--_neu-fg-muted); opacity: 1; }\n.help { font-size: 0.75rem; color: var(--_neu-fg-muted); }\n.help-text, ::slotted([slot='help-text']) { display: block; margin-block-start: 0.375rem; }\n.help-text:empty { display: none; }\n.error-text { display: none; margin-block-start: 0.375rem; color: var(--_neu-danger); }\n:host(:state(user-invalid)) slot[name='help-text'] { display: none; }\n:host(:state(user-invalid)) .error-text:not(:empty) { display: block; }\n\n.input { appearance: textfield; text-align: center; }\n.input::-webkit-inner-spin-button, .input::-webkit-outer-spin-button { appearance: none; margin: 0; }\n.step {\n display: inline-flex; flex: none; align-items: center; justify-content: center;\n inline-size: 1.75rem; block-size: 1.75rem; padding: 0; border: none; border-radius: 50%;\n background: var(--_neu-bg); color: var(--_neu-fg); box-shadow: var(--_neu-raised-sm);\n font: inherit; cursor: pointer;\n transition: box-shadow var(--_neu-dur) ease;\n}\n.step:active:not(:disabled) { box-shadow: var(--_neu-inset-sm); }\n.step:disabled { box-shadow: none; color: var(--_neu-fg-muted); cursor: not-allowed; }\n.step svg { inline-size: 1rem; block-size: 1rem; }\n";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { fieldStyles } from '../shared/field.styles.js';
|
|
2
|
+
export const numberFieldStyles = `${fieldStyles}
|
|
3
|
+
.input { appearance: textfield; text-align: center; }
|
|
4
|
+
.input::-webkit-inner-spin-button, .input::-webkit-outer-spin-button { appearance: none; margin: 0; }
|
|
5
|
+
.step {
|
|
6
|
+
display: inline-flex; flex: none; align-items: center; justify-content: center;
|
|
7
|
+
inline-size: 1.75rem; block-size: 1.75rem; padding: 0; border: none; border-radius: 50%;
|
|
8
|
+
background: var(--_neu-bg); color: var(--_neu-fg); box-shadow: var(--_neu-raised-sm);
|
|
9
|
+
font: inherit; cursor: pointer;
|
|
10
|
+
transition: box-shadow var(--_neu-dur) ease;
|
|
11
|
+
}
|
|
12
|
+
.step:active:not(:disabled) { box-shadow: var(--_neu-inset-sm); }
|
|
13
|
+
.step:disabled { box-shadow: none; color: var(--_neu-fg-muted); cursor: not-allowed; }
|
|
14
|
+
.step svg { inline-size: 1rem; block-size: 1rem; }
|
|
15
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OptionData } from '../../core/define.js';
|
|
2
|
+
/** An option's label: its text with ASCII whitespace collapsed and trimmed, as a native option's. */
|
|
3
|
+
export declare function optionLabel(text: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* What a neu-option holds, read from its attributes and text, so it works before the element is
|
|
6
|
+
* upgraded (plan decision 11): the value attribute, or the label when there is none.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readOption(el: Element): OptionData;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** An option's label: its text with ASCII whitespace collapsed and trimmed, as a native option's. */
|
|
2
|
+
export function optionLabel(text) {
|
|
3
|
+
return text.replace(/[\t\n\f\r ]+/g, ' ').replace(/^ | $/g, '');
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* What a neu-option holds, read from its attributes and text, so it works before the element is
|
|
7
|
+
* upgraded (plan decision 11): the value attribute, or the label when there is none.
|
|
8
|
+
*/
|
|
9
|
+
export function readOption(el) {
|
|
10
|
+
const label = optionLabel(el.textContent ?? '');
|
|
11
|
+
return { value: el.getAttribute('value') ?? label, label, disabled: el.hasAttribute('disabled') };
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NeuElement } from '../../core/neu-element.js';
|
|
2
|
+
export declare class NeuOption extends NeuElement {
|
|
3
|
+
static def: import("../../ssr.js").ComponentDef<{
|
|
4
|
+
readonly value: {
|
|
5
|
+
readonly type: "string";
|
|
6
|
+
};
|
|
7
|
+
readonly disabled: {
|
|
8
|
+
readonly type: "boolean";
|
|
9
|
+
};
|
|
10
|
+
}>;
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
/** The value attribute, or the label when there is none (like a native option). */
|
|
13
|
+
get value(): string;
|
|
14
|
+
set value(next: string);
|
|
15
|
+
/** The text content, whitespace collapsed. */
|
|
16
|
+
get label(): string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineComponent } from '../../core/define.js';
|
|
2
|
+
import { html } from '../../core/html.js';
|
|
3
|
+
import { optionStyles } from './option.styles.js';
|
|
4
|
+
export const optionDef = defineComponent({
|
|
5
|
+
tag: 'neu-option',
|
|
6
|
+
props: {
|
|
7
|
+
value: { type: 'string' },
|
|
8
|
+
disabled: { type: 'boolean' },
|
|
9
|
+
},
|
|
10
|
+
styles: optionStyles,
|
|
11
|
+
template: () => html `<slot></slot>`,
|
|
12
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const optionDocs = {
|
|
2
|
+
summary: 'One choice of a neu-select. It is data only and never rendered: the select reads its value (the value attribute, or the label when there is none), its text as the label, and disabled.',
|
|
3
|
+
descriptions: {
|
|
4
|
+
value: 'The value the select takes when this option is chosen; the label is used when it is missing.',
|
|
5
|
+
disabled: 'Keeps the option visible but not selectable.',
|
|
6
|
+
},
|
|
7
|
+
events: [],
|
|
8
|
+
slots: [{ name: '', description: 'The label, as plain text.' }],
|
|
9
|
+
parts: [],
|
|
10
|
+
cssProperties: [],
|
|
11
|
+
keyboard: [],
|
|
12
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NeuElement } from '../../core/neu-element.js';
|
|
2
|
+
import { optionDef } from './option.def.js';
|
|
3
|
+
import { optionLabel, readOption } from './option-data.js';
|
|
4
|
+
export class NeuOption extends NeuElement {
|
|
5
|
+
static def = optionDef;
|
|
6
|
+
/** The value attribute, or the label when there is none (like a native option). */
|
|
7
|
+
get value() {
|
|
8
|
+
return readOption(this).value;
|
|
9
|
+
}
|
|
10
|
+
set value(next) {
|
|
11
|
+
this.setAttribute('value', next === null || next === undefined ? '' : String(next));
|
|
12
|
+
}
|
|
13
|
+
/** The text content, whitespace collapsed. */
|
|
14
|
+
get label() {
|
|
15
|
+
return optionLabel(this.textContent ?? '');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** An option is data for its neu-select, which renders the listbox in its own shadow root. */
|
|
2
|
+
export declare const optionStyles = "\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: none !important; }\n";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** How a radio and its group talk. Symbol.for: two copies of the library on one page agree. */
|
|
2
|
+
export declare const SET_CHECKED: unique symbol;
|
|
3
|
+
export declare const SET_GROUP_DISABLED: unique symbol;
|
|
4
|
+
/** Implemented by neu-radio-group: radios ask for a resync when they upgrade or move in. */
|
|
5
|
+
export declare const RESYNC: unique symbol;
|
|
6
|
+
/** The part of a neu-radio its group uses; absent on a radio that is not upgraded yet. */
|
|
7
|
+
export interface RadioLink extends HTMLElement {
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
[SET_CHECKED]?(on: boolean): void;
|
|
10
|
+
[SET_GROUP_DISABLED]?(off: boolean): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** How a radio and its group talk. Symbol.for: two copies of the library on one page agree. */
|
|
2
|
+
export const SET_CHECKED = Symbol.for('neu-ui.radio.setChecked');
|
|
3
|
+
export const SET_GROUP_DISABLED = Symbol.for('neu-ui.radio.setGroupDisabled');
|
|
4
|
+
/** Implemented by neu-radio-group: radios ask for a resync when they upgrade or move in. */
|
|
5
|
+
export const RESYNC = Symbol.for('neu-ui.radioGroup.resync');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NeuElement } from '../../core/neu-element.js';
|
|
2
|
+
import { SET_CHECKED, SET_GROUP_DISABLED } from './radio-link.js';
|
|
3
|
+
export { SET_CHECKED, SET_GROUP_DISABLED };
|
|
4
|
+
export declare class NeuRadio extends NeuElement {
|
|
5
|
+
static def: import("../../ssr.js").ComponentDef<{
|
|
6
|
+
readonly value: {
|
|
7
|
+
readonly type: "string";
|
|
8
|
+
readonly default: "on";
|
|
9
|
+
};
|
|
10
|
+
readonly disabled: {
|
|
11
|
+
readonly type: "boolean";
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
value: string;
|
|
15
|
+
disabled: boolean;
|
|
16
|
+
private on;
|
|
17
|
+
private groupOff;
|
|
18
|
+
constructor();
|
|
19
|
+
get checked(): boolean;
|
|
20
|
+
[SET_CHECKED](on: boolean): void;
|
|
21
|
+
[SET_GROUP_DISABLED](off: boolean): void;
|
|
22
|
+
protected get isDisabled(): boolean;
|
|
23
|
+
protected update(changed: ReadonlySet<string>): void;
|
|
24
|
+
protected connected(): void;
|
|
25
|
+
/** Leaving a group (or the page) must not leave its selection or disabling behind. */
|
|
26
|
+
protected disconnected(): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineComponent } from '../../core/define.js';
|
|
2
|
+
import { html } from '../../core/html.js';
|
|
3
|
+
import { radioStyles } from './radio.styles.js';
|
|
4
|
+
export const radioDef = defineComponent({
|
|
5
|
+
tag: 'neu-radio',
|
|
6
|
+
props: {
|
|
7
|
+
value: { type: 'string', default: 'on' },
|
|
8
|
+
disabled: { type: 'boolean' },
|
|
9
|
+
},
|
|
10
|
+
styles: radioStyles,
|
|
11
|
+
template: () => html `<span part="base" class="base"><span part="control" class="control"><span part="indicator" class="indicator"></span></span><span part="label" class="label"><slot></slot></span></span>`,
|
|
12
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { themeCssProperties } from '../theme-docs.js';
|
|
2
|
+
export const radioDocs = {
|
|
3
|
+
summary: 'One choice inside a neu-radio-group, which owns the value, the keyboard and the form. On its own it does nothing.',
|
|
4
|
+
descriptions: {
|
|
5
|
+
value: 'The value the group takes when this radio is selected; defaults to "on".',
|
|
6
|
+
disabled: 'Prevents this radio from being selected or focused by arrow keys.',
|
|
7
|
+
},
|
|
8
|
+
events: [],
|
|
9
|
+
slots: [{ name: '', description: 'Label; also the accessible name.' }],
|
|
10
|
+
parts: [
|
|
11
|
+
{ name: 'base', description: 'Wrapper around control and label.' },
|
|
12
|
+
{ name: 'control', description: 'The inset ring.' },
|
|
13
|
+
{ name: 'indicator', description: 'The dot shown when selected.' },
|
|
14
|
+
{ name: 'label', description: 'Wrapper around the default slot.' },
|
|
15
|
+
],
|
|
16
|
+
cssProperties: themeCssProperties,
|
|
17
|
+
keyboard: [],
|
|
18
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NeuElement } from '../../core/neu-element.js';
|
|
2
|
+
import { radioDef } from './radio.def.js';
|
|
3
|
+
import { RESYNC, SET_CHECKED, SET_GROUP_DISABLED } from './radio-link.js';
|
|
4
|
+
export { SET_CHECKED, SET_GROUP_DISABLED };
|
|
5
|
+
export class NeuRadio extends NeuElement {
|
|
6
|
+
static def = radioDef;
|
|
7
|
+
on = false;
|
|
8
|
+
groupOff = false;
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this.internals.role = 'radio';
|
|
12
|
+
this.internals.ariaChecked = 'false';
|
|
13
|
+
}
|
|
14
|
+
get checked() {
|
|
15
|
+
return this.on;
|
|
16
|
+
}
|
|
17
|
+
[SET_CHECKED](on) {
|
|
18
|
+
this.on = on;
|
|
19
|
+
this.toggleState('checked', on);
|
|
20
|
+
this.internals.ariaChecked = on ? 'true' : 'false';
|
|
21
|
+
}
|
|
22
|
+
[SET_GROUP_DISABLED](off) {
|
|
23
|
+
if (off === this.groupOff)
|
|
24
|
+
return;
|
|
25
|
+
this.groupOff = off;
|
|
26
|
+
this.syncDisabledState();
|
|
27
|
+
}
|
|
28
|
+
get isDisabled() {
|
|
29
|
+
return super.isDisabled || this.groupOff;
|
|
30
|
+
}
|
|
31
|
+
update(changed) {
|
|
32
|
+
if (changed.has('disabled'))
|
|
33
|
+
this.syncDisabledState();
|
|
34
|
+
}
|
|
35
|
+
connected() {
|
|
36
|
+
this.closest('neu-radio-group')?.[RESYNC]?.();
|
|
37
|
+
}
|
|
38
|
+
/** Leaving a group (or the page) must not leave its selection or disabling behind. */
|
|
39
|
+
disconnected() {
|
|
40
|
+
this[SET_CHECKED](false);
|
|
41
|
+
this[SET_GROUP_DISABLED](false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const radioStyles = "\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: inline-flex; vertical-align: middle; cursor: pointer; user-select: none; }\n:host(:disabled), :host([disabled]) { cursor: not-allowed; }\n:host(:state(checked)) { --_neu-radio-on: 1; }\n.base { display: inline-flex; align-items: center; gap: 0.625rem; }\n.control {\n display: inline-flex; align-items: center; justify-content: center; flex: none;\n inline-size: 1.25rem; block-size: 1.25rem; border-radius: 50%;\n background: var(--_neu-bg); box-shadow: var(--_neu-inset-sm);\n transition: box-shadow var(--_neu-dur) ease;\n}\n.indicator {\n display: block; inline-size: 50%; block-size: 50%; border-radius: 50%;\n background: var(--_neu-accent-c); transform: scale(var(--_neu-radio-on, 0));\n transition: transform var(--_neu-dur) ease;\n}\n:host(:focus-visible) .control { box-shadow: var(--_neu-inset-sm), var(--_neu-focus); }\n:host([disabled]) .base { opacity: 0.55; }\n.label { color: var(--_neu-fg); }\n";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { hostTheme } from '../../core/styles.js';
|
|
2
|
+
export const radioStyles = `${hostTheme}
|
|
3
|
+
:host { display: inline-flex; vertical-align: middle; cursor: pointer; user-select: none; }
|
|
4
|
+
:host(:disabled), :host([disabled]) { cursor: not-allowed; }
|
|
5
|
+
:host(:state(checked)) { --_neu-radio-on: 1; }
|
|
6
|
+
.base { display: inline-flex; align-items: center; gap: 0.625rem; }
|
|
7
|
+
.control {
|
|
8
|
+
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
|
9
|
+
inline-size: 1.25rem; block-size: 1.25rem; border-radius: 50%;
|
|
10
|
+
background: var(--_neu-bg); box-shadow: var(--_neu-inset-sm);
|
|
11
|
+
transition: box-shadow var(--_neu-dur) ease;
|
|
12
|
+
}
|
|
13
|
+
.indicator {
|
|
14
|
+
display: block; inline-size: 50%; block-size: 50%; border-radius: 50%;
|
|
15
|
+
background: var(--_neu-accent-c); transform: scale(var(--_neu-radio-on, 0));
|
|
16
|
+
transition: transform var(--_neu-dur) ease;
|
|
17
|
+
}
|
|
18
|
+
:host(:focus-visible) .control { box-shadow: var(--_neu-inset-sm), var(--_neu-focus); }
|
|
19
|
+
:host([disabled]) .base { opacity: 0.55; }
|
|
20
|
+
.label { color: var(--_neu-fg); }
|
|
21
|
+
`;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { FormControlElement } from '../../core/form-control.js';
|
|
2
|
+
import { RESYNC } from '../radio/radio-link.js';
|
|
3
|
+
export declare class NeuRadioGroup extends FormControlElement {
|
|
4
|
+
static def: import("../../ssr.js").ComponentDef<{
|
|
5
|
+
readonly label: {
|
|
6
|
+
readonly type: "string";
|
|
7
|
+
};
|
|
8
|
+
readonly name: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
};
|
|
11
|
+
readonly value: {
|
|
12
|
+
readonly type: "string";
|
|
13
|
+
};
|
|
14
|
+
readonly required: {
|
|
15
|
+
readonly type: "boolean";
|
|
16
|
+
};
|
|
17
|
+
readonly disabled: {
|
|
18
|
+
readonly type: "boolean";
|
|
19
|
+
};
|
|
20
|
+
readonly orientation: {
|
|
21
|
+
readonly type: "string";
|
|
22
|
+
readonly default: "vertical";
|
|
23
|
+
readonly values: readonly ["vertical", "horizontal"];
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
static extraProperties: string[];
|
|
27
|
+
name: string;
|
|
28
|
+
label: string;
|
|
29
|
+
required: boolean;
|
|
30
|
+
disabled: boolean;
|
|
31
|
+
orientation: 'vertical' | 'horizontal';
|
|
32
|
+
/**
|
|
33
|
+
* The requested value, kept even while no radio matches it (plan decision 8); null requests
|
|
34
|
+
* nothing, so a radio whose value is "" is chosen only when asked for.
|
|
35
|
+
*/
|
|
36
|
+
private current;
|
|
37
|
+
private dirty;
|
|
38
|
+
/** The radio that holds the Tab stop. */
|
|
39
|
+
private stop;
|
|
40
|
+
/** The selected radio as of the last sync, which the form value and validity read. */
|
|
41
|
+
private picked;
|
|
42
|
+
private observer;
|
|
43
|
+
constructor();
|
|
44
|
+
get value(): string;
|
|
45
|
+
set value(next: string);
|
|
46
|
+
get defaultValue(): string;
|
|
47
|
+
set defaultValue(next: string);
|
|
48
|
+
/** The radios of this group: those whose closest group is this one. */
|
|
49
|
+
private radios;
|
|
50
|
+
/** The first radio of this group matching the requested value, if one is requested. */
|
|
51
|
+
private selected;
|
|
52
|
+
private radioAt;
|
|
53
|
+
private onKey;
|
|
54
|
+
/** A choice by the user: no event when the value does not change. */
|
|
55
|
+
private choose;
|
|
56
|
+
[RESYNC](): void;
|
|
57
|
+
/**
|
|
58
|
+
* Brings every radio in line with the group: checked state, disabling, the one Tab stop (the
|
|
59
|
+
* selected radio, else the first enabled one), the server-visible mark, and the form value.
|
|
60
|
+
* Radios that are not upgraded yet are skipped; they ask for a resync when they connect.
|
|
61
|
+
*/
|
|
62
|
+
private sync;
|
|
63
|
+
/** The group's name: the slotted label's text when there is one, else the label attribute. */
|
|
64
|
+
private nameGroup;
|
|
65
|
+
protected rendered(): void;
|
|
66
|
+
protected willUpdate(changed: ReadonlySet<string>): void;
|
|
67
|
+
protected update(changed: ReadonlySet<string>): void;
|
|
68
|
+
protected connected(): void;
|
|
69
|
+
protected disconnected(): void;
|
|
70
|
+
protected formValue(): string | null;
|
|
71
|
+
/** Nothing selected saves nothing, so a radio whose value is "" is not chosen on restore. */
|
|
72
|
+
protected formState(): string | null;
|
|
73
|
+
protected validate(): void;
|
|
74
|
+
protected resetToDefault(): void;
|
|
75
|
+
protected restoreState(state: string): void;
|
|
76
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type RawHtml } from '../../core/html.js';
|
|
2
|
+
/**
|
|
3
|
+
* Escapes a value for a double-quoted CSS string. Besides the characters that end the string,
|
|
4
|
+
* `&`, `<`, `>` and `'` are hex-escaped too, so `html` leaves the text alone: it is raw text in a
|
|
5
|
+
* <style>, where an HTML entity would never be decoded.
|
|
6
|
+
*/
|
|
7
|
+
export declare function cssString(value: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* The rule that shows the selected radio before any script ran (plan decision 7). Only radios that
|
|
10
|
+
* are not defined yet: once upgraded, :state(checked) is the one source, so a duplicate value
|
|
11
|
+
* marks the first match only. cssString leaves nothing for `html` to escape, and the quotes are
|
|
12
|
+
* static text, so the style holds the same bytes on the server and in the client.
|
|
13
|
+
*/
|
|
14
|
+
export declare function markRule(value: string): RawHtml | '';
|
|
15
|
+
export declare const radioGroupDef: import("../../core/define.js").ComponentDef<{
|
|
16
|
+
readonly label: {
|
|
17
|
+
readonly type: "string";
|
|
18
|
+
};
|
|
19
|
+
readonly name: {
|
|
20
|
+
readonly type: "string";
|
|
21
|
+
};
|
|
22
|
+
readonly value: {
|
|
23
|
+
readonly type: "string";
|
|
24
|
+
};
|
|
25
|
+
readonly required: {
|
|
26
|
+
readonly type: "boolean";
|
|
27
|
+
};
|
|
28
|
+
readonly disabled: {
|
|
29
|
+
readonly type: "boolean";
|
|
30
|
+
};
|
|
31
|
+
readonly orientation: {
|
|
32
|
+
readonly type: "string";
|
|
33
|
+
readonly default: "vertical";
|
|
34
|
+
readonly values: readonly ["vertical", "horizontal"];
|
|
35
|
+
};
|
|
36
|
+
}>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineComponent } from '../../core/define.js';
|
|
2
|
+
import { html } from '../../core/html.js';
|
|
3
|
+
import { radioGroupStyles } from './radio-group.styles.js';
|
|
4
|
+
/**
|
|
5
|
+
* Escapes a value for a double-quoted CSS string. Besides the characters that end the string,
|
|
6
|
+
* `&`, `<`, `>` and `'` are hex-escaped too, so `html` leaves the text alone: it is raw text in a
|
|
7
|
+
* <style>, where an HTML entity would never be decoded.
|
|
8
|
+
*/
|
|
9
|
+
export function cssString(value) {
|
|
10
|
+
return value.replace(/[\\"'&<>\r\n\f]/g, (char) => `\\${char.charCodeAt(0).toString(16)} `);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The rule that shows the selected radio before any script ran (plan decision 7). Only radios that
|
|
14
|
+
* are not defined yet: once upgraded, :state(checked) is the one source, so a duplicate value
|
|
15
|
+
* marks the first match only. cssString leaves nothing for `html` to escape, and the quotes are
|
|
16
|
+
* static text, so the style holds the same bytes on the server and in the client.
|
|
17
|
+
*/
|
|
18
|
+
export function markRule(value) {
|
|
19
|
+
return value === ''
|
|
20
|
+
? ''
|
|
21
|
+
: html `::slotted(neu-radio:not(:defined)[value="${cssString(value)}"]){--_neu-radio-on:1}`;
|
|
22
|
+
}
|
|
23
|
+
export const radioGroupDef = defineComponent({
|
|
24
|
+
tag: 'neu-radio-group',
|
|
25
|
+
props: {
|
|
26
|
+
label: { type: 'string' },
|
|
27
|
+
name: { type: 'string' },
|
|
28
|
+
value: { type: 'string' },
|
|
29
|
+
required: { type: 'boolean' },
|
|
30
|
+
disabled: { type: 'boolean' },
|
|
31
|
+
orientation: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
default: 'vertical',
|
|
34
|
+
values: ['vertical', 'horizontal'],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
form: { valueProp: 'value' },
|
|
38
|
+
styles: radioGroupStyles,
|
|
39
|
+
template: (p) => html `<div part="base" class="base"><span part="label" class="label"><slot name="label"><span class="label-text" data-ref="label-text">${p.label}</span></slot></span><span class="items"><slot></slot></span><style data-ref="mark">${markRule(p.value)}</style></div>`,
|
|
40
|
+
});
|