@vaadin/radio-group 24.3.0-alpha4 → 24.3.0-alpha6

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.
@@ -0,0 +1,424 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
7
+ import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
8
+ import { KeyboardMixin } from '@vaadin/a11y-base/src/keyboard-mixin.js';
9
+ import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
10
+ import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
11
+ import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
12
+ import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js';
13
+
14
+ /**
15
+ * A mixin providing common radio-group functionality.
16
+ *
17
+ * @polymerMixin
18
+ * @mixes DisabledMixin
19
+ * @mixes FieldMixin
20
+ * @mixes FocusMixin
21
+ * @mixes KeyboardMixin
22
+ */
23
+ export const RadioGroupMixin = (superclass) =>
24
+ class RadioGroupMixinClass extends FieldMixin(FocusMixin(DisabledMixin(KeyboardMixin(superclass)))) {
25
+ static get properties() {
26
+ return {
27
+ /**
28
+ * The value of the radio group.
29
+ *
30
+ * @type {string}
31
+ */
32
+ value: {
33
+ type: String,
34
+ notify: true,
35
+ value: '',
36
+ observer: '__valueChanged',
37
+ },
38
+
39
+ /**
40
+ * When present, the user cannot modify the value of the radio group.
41
+ * The property works similarly to the `disabled` property.
42
+ * While the `disabled` property disables all radio buttons inside the group,
43
+ * the `readonly` property disables only unchecked ones.
44
+ *
45
+ * @type {boolean}
46
+ */
47
+ readonly: {
48
+ type: Boolean,
49
+ value: false,
50
+ reflectToAttribute: true,
51
+ observer: '__readonlyChanged',
52
+ },
53
+
54
+ /**
55
+ * @type {string}
56
+ * @private
57
+ */
58
+ _fieldName: {
59
+ type: String,
60
+ },
61
+ };
62
+ }
63
+
64
+ constructor() {
65
+ super();
66
+
67
+ this.__registerRadioButton = this.__registerRadioButton.bind(this);
68
+ this.__unregisterRadioButton = this.__unregisterRadioButton.bind(this);
69
+ this.__onRadioButtonCheckedChange = this.__onRadioButtonCheckedChange.bind(this);
70
+
71
+ this._tooltipController = new TooltipController(this);
72
+ this._tooltipController.addEventListener('tooltip-changed', (event) => {
73
+ const tooltip = event.detail.node;
74
+ if (tooltip && tooltip.isConnected) {
75
+ // Tooltip element has been added to the DOM
76
+ const inputs = this.__radioButtons.map((radio) => radio.inputElement);
77
+ this._tooltipController.setAriaTarget(inputs);
78
+ } else {
79
+ // Tooltip element is no longer connected
80
+ this._tooltipController.setAriaTarget([]);
81
+ }
82
+ });
83
+ }
84
+
85
+ /**
86
+ * A collection of the group's radio buttons.
87
+ *
88
+ * @return {!Array<!RadioButton>}
89
+ * @private
90
+ */
91
+ get __radioButtons() {
92
+ return this.__filterRadioButtons([...this.children]);
93
+ }
94
+
95
+ /**
96
+ * A currently selected radio button.
97
+ *
98
+ * @return {!RadioButton | undefined}
99
+ * @private
100
+ */
101
+ get __selectedRadioButton() {
102
+ return this.__radioButtons.find((radioButton) => radioButton.checked);
103
+ }
104
+
105
+ /**
106
+ * @return {boolean}
107
+ * @private
108
+ */
109
+ get isHorizontalRTL() {
110
+ return this.__isRTL && this._theme !== 'vertical';
111
+ }
112
+
113
+ /** @protected */
114
+ ready() {
115
+ super.ready();
116
+
117
+ this.ariaTarget = this;
118
+
119
+ // See https://github.com/vaadin/vaadin-web-components/issues/94
120
+ this.setAttribute('role', 'radiogroup');
121
+
122
+ this._fieldName = `${this.localName}-${generateUniqueId()}`;
123
+
124
+ const slot = this.shadowRoot.querySelector('slot:not([name])');
125
+ this._observer = new SlotObserver(slot, ({ addedNodes, removedNodes }) => {
126
+ // Registers the added radio buttons in the reverse order
127
+ // in order for the group to take the value of the most recent button.
128
+ this.__filterRadioButtons(addedNodes).reverse().forEach(this.__registerRadioButton);
129
+
130
+ // Unregisters the removed radio buttons.
131
+ this.__filterRadioButtons(removedNodes).forEach(this.__unregisterRadioButton);
132
+
133
+ const inputs = this.__radioButtons.map((radio) => radio.inputElement);
134
+ this._tooltipController.setAriaTarget(inputs);
135
+ });
136
+
137
+ this.addController(this._tooltipController);
138
+ }
139
+
140
+ /**
141
+ * @param {!Array<!Node>} nodes
142
+ * @return {!Array<!RadioButton>}
143
+ * @private
144
+ */
145
+ __filterRadioButtons(nodes) {
146
+ return nodes.filter((node) => node.nodeType === Node.ELEMENT_NODE && node.localName === 'vaadin-radio-button');
147
+ }
148
+
149
+ /**
150
+ * Override method inherited from `KeyboardMixin`
151
+ * to implement the custom keyboard navigation as a replacement for the native one
152
+ * in order for the navigation to work the same way across different browsers.
153
+ *
154
+ * @param {!KeyboardEvent} event
155
+ * @override
156
+ * @protected
157
+ */
158
+ _onKeyDown(event) {
159
+ super._onKeyDown(event);
160
+
161
+ const radioButton = event
162
+ .composedPath()
163
+ .find((node) => node.nodeType === Node.ELEMENT_NODE && node.localName === 'vaadin-radio-button');
164
+
165
+ if (['ArrowLeft', 'ArrowUp'].includes(event.key)) {
166
+ event.preventDefault();
167
+ this.__selectNextRadioButton(radioButton);
168
+ }
169
+
170
+ if (['ArrowRight', 'ArrowDown'].includes(event.key)) {
171
+ event.preventDefault();
172
+ this.__selectPrevRadioButton(radioButton);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Override an observer from `FieldMixin`.
178
+ *
179
+ * @param {boolean} invalid
180
+ * @protected
181
+ * @override
182
+ */
183
+ _invalidChanged(invalid) {
184
+ super._invalidChanged(invalid);
185
+
186
+ if (invalid) {
187
+ this.setAttribute('aria-invalid', 'true');
188
+ } else {
189
+ this.removeAttribute('aria-invalid');
190
+ }
191
+ }
192
+
193
+ /**
194
+ * @param {number} index
195
+ * @private
196
+ */
197
+ __selectNextRadioButton(radioButton) {
198
+ const index = this.__radioButtons.indexOf(radioButton);
199
+
200
+ this.__selectIncRadioButton(index, this.isHorizontalRTL ? 1 : -1);
201
+ }
202
+
203
+ /**
204
+ * @param {number} index
205
+ * @private
206
+ */
207
+ __selectPrevRadioButton(radioButton) {
208
+ const index = this.__radioButtons.indexOf(radioButton);
209
+
210
+ this.__selectIncRadioButton(index, this.isHorizontalRTL ? -1 : 1);
211
+ }
212
+
213
+ /**
214
+ * @param {number} index
215
+ * @param {number} step
216
+ * @private
217
+ */
218
+ __selectIncRadioButton(index, step) {
219
+ const newIndex = (this.__radioButtons.length + index + step) % this.__radioButtons.length;
220
+ const newRadioButton = this.__radioButtons[newIndex];
221
+
222
+ if (newRadioButton.disabled) {
223
+ this.__selectIncRadioButton(newIndex, step);
224
+ } else {
225
+ newRadioButton.focusElement.focus();
226
+ newRadioButton.focusElement.click();
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Registers the radio button after adding it to the group.
232
+ *
233
+ * @param {!RadioButton} radioButton
234
+ * @private
235
+ */
236
+ __registerRadioButton(radioButton) {
237
+ radioButton.name = this._fieldName;
238
+ radioButton.addEventListener('checked-changed', this.__onRadioButtonCheckedChange);
239
+
240
+ if (this.disabled || this.readonly) {
241
+ radioButton.disabled = true;
242
+ }
243
+
244
+ if (radioButton.checked) {
245
+ this.__selectRadioButton(radioButton);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Unregisters the radio button before removing it from the group.
251
+ *
252
+ * @param {!RadioButton} radioButton
253
+ * @private
254
+ */
255
+ __unregisterRadioButton(radioButton) {
256
+ radioButton.removeEventListener('checked-changed', this.__onRadioButtonCheckedChange);
257
+
258
+ if (radioButton.value === this.value) {
259
+ this.__selectRadioButton(null);
260
+ }
261
+ }
262
+
263
+ /**
264
+ * @param {!CustomEvent} event
265
+ * @private
266
+ */
267
+ __onRadioButtonCheckedChange(event) {
268
+ if (event.target.checked) {
269
+ this.__selectRadioButton(event.target);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Whenever the user sets a non-empty value,
275
+ * the method tries to select the radio button with that value
276
+ * showing a warning if no radio button was found with the given value.
277
+ * If the new value is empty, the method deselects the currently selected radio button.
278
+ * At last, the method toggles the `has-value` attribute considering the new value.
279
+ *
280
+ * @param {string | null | undefined} newValue
281
+ * @param {string | null | undefined} oldValue
282
+ * @private
283
+ */
284
+ __valueChanged(newValue, oldValue) {
285
+ if (oldValue === undefined && newValue === '') {
286
+ return;
287
+ }
288
+
289
+ if (newValue) {
290
+ const newSelectedRadioButton = this.__radioButtons.find((radioButton) => {
291
+ return radioButton.value === newValue;
292
+ });
293
+
294
+ if (newSelectedRadioButton) {
295
+ this.__selectRadioButton(newSelectedRadioButton);
296
+ this.toggleAttribute('has-value', true);
297
+ } else {
298
+ console.warn(`The radio button with the value "${newValue}" was not found.`);
299
+ }
300
+ } else {
301
+ this.__selectRadioButton(null);
302
+ this.removeAttribute('has-value');
303
+ }
304
+
305
+ if (oldValue !== undefined) {
306
+ this.validate();
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Whenever `readonly` property changes on the group element,
312
+ * the method updates the `disabled` property for the radio buttons.
313
+ *
314
+ * @param {boolean} newValue
315
+ * @param {boolean} oldValue
316
+ * @private
317
+ */
318
+ __readonlyChanged(newValue, oldValue) {
319
+ // Prevent updating the `disabled` property for the radio buttons at initialization.
320
+ // Otherwise, the group's radio buttons may end up enabled regardless
321
+ // an intentionally added `disabled` attribute on some of them.
322
+ if (!newValue && oldValue === undefined) {
323
+ return;
324
+ }
325
+
326
+ if (oldValue !== newValue) {
327
+ this.__updateRadioButtonsDisabledProperty();
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Override method inherited from `DisabledMixin`
333
+ * to update the `disabled` property for the radio buttons
334
+ * whenever the property changes on the group element.
335
+ *
336
+ * @param {boolean} newValue
337
+ * @param {boolean} oldValue
338
+ * @override
339
+ * @protected
340
+ */
341
+ _disabledChanged(newValue, oldValue) {
342
+ super._disabledChanged(newValue, oldValue);
343
+
344
+ // Prevent updating the `disabled` property for the radio buttons at initialization.
345
+ // Otherwise, the group's radio buttons may end up enabled regardless
346
+ // an intentionally added `disabled` attribute on some of them.
347
+ if (!newValue && oldValue === undefined) {
348
+ return;
349
+ }
350
+
351
+ if (oldValue !== newValue) {
352
+ this.__updateRadioButtonsDisabledProperty();
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Override method inherited from `FocusMixin`
358
+ * to prevent removing the `focused` attribute
359
+ * when focus moves between radio buttons inside the group.
360
+ *
361
+ * @param {!FocusEvent} event
362
+ * @return {boolean}
363
+ * @protected
364
+ */
365
+ _shouldRemoveFocus(event) {
366
+ return !this.contains(event.relatedTarget);
367
+ }
368
+
369
+ /**
370
+ * Override method inherited from `FocusMixin`
371
+ * to run validation when the group loses focus.
372
+ *
373
+ * @param {boolean} focused
374
+ * @override
375
+ * @protected
376
+ */
377
+ _setFocused(focused) {
378
+ super._setFocused(focused);
379
+
380
+ // Do not validate when focusout is caused by document
381
+ // losing focus, which happens on browser tab switch.
382
+ if (!focused && document.hasFocus()) {
383
+ this.validate();
384
+ }
385
+ }
386
+
387
+ /**
388
+ * @param {RadioButton} radioButton
389
+ * @private
390
+ */
391
+ __selectRadioButton(radioButton) {
392
+ if (radioButton) {
393
+ this.value = radioButton.value;
394
+ } else {
395
+ this.value = '';
396
+ }
397
+
398
+ this.__radioButtons.forEach((button) => {
399
+ button.checked = button === radioButton;
400
+ });
401
+
402
+ if (this.readonly) {
403
+ this.__updateRadioButtonsDisabledProperty();
404
+ }
405
+ }
406
+
407
+ /**
408
+ * If the group is read-only, the method disables the unchecked radio buttons.
409
+ * Otherwise, the method propagates the group's `disabled` property to the radio buttons.
410
+ *
411
+ * @private
412
+ */
413
+ __updateRadioButtonsDisabledProperty() {
414
+ this.__radioButtons.forEach((button) => {
415
+ if (this.readonly) {
416
+ // The native radio button doesn't support the `readonly` attribute
417
+ // so the state can be only imitated, by disabling unchecked radio buttons.
418
+ button.disabled = button !== this.__selectedRadioButton;
419
+ } else {
420
+ button.disabled = this.disabled;
421
+ }
422
+ });
423
+ }
424
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { CSSResult } from 'lit';
7
+
8
+ export const radioGroupStyles: CSSResult;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { css } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
7
+
8
+ export const radioGroupStyles = css`
9
+ :host {
10
+ display: inline-flex;
11
+ }
12
+
13
+ :host::before {
14
+ content: '\\2003';
15
+ width: 0;
16
+ display: inline-block;
17
+ }
18
+
19
+ :host([hidden]) {
20
+ display: none !important;
21
+ }
22
+
23
+ .vaadin-group-field-container {
24
+ display: flex;
25
+ flex-direction: column;
26
+ width: 100%;
27
+ }
28
+
29
+ [part='group-field'] {
30
+ display: flex;
31
+ flex-wrap: wrap;
32
+ }
33
+
34
+ :host(:not([has-label])) [part='label'] {
35
+ display: none;
36
+ }
37
+ `;
@@ -3,37 +3,11 @@
3
3
  * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
7
- import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
8
- import { KeyboardMixin } from '@vaadin/a11y-base/src/keyboard-mixin.js';
9
6
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
10
- import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js';
11
7
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
8
+ import { type RadioGroupEventMap, RadioGroupMixin } from './vaadin-radio-group-mixin.js';
12
9
 
13
- /**
14
- * Fired when the `invalid` property changes.
15
- */
16
- export type RadioGroupInvalidChangedEvent = CustomEvent<{ value: boolean }>;
17
-
18
- /**
19
- * Fired when the `value` property changes.
20
- */
21
- export type RadioGroupValueChangedEvent = CustomEvent<{ value: string }>;
22
-
23
- /**
24
- * Fired whenever the field is validated.
25
- */
26
- export type RadioGroupValidatedEvent = CustomEvent<{ valid: boolean }>;
27
-
28
- export interface RadioGroupCustomEventMap {
29
- 'invalid-changed': RadioGroupInvalidChangedEvent;
30
-
31
- 'value-changed': RadioGroupValueChangedEvent;
32
-
33
- validated: RadioGroupValidatedEvent;
34
- }
35
-
36
- export interface RadioGroupEventMap extends HTMLElementEventMap, RadioGroupCustomEventMap {}
10
+ export * from './vaadin-radio-group-mixin.js';
37
11
 
38
12
  /**
39
13
  * `<vaadin-radio-group>` is a web component that allows the user to choose one item from a group of choices.
@@ -77,22 +51,7 @@ export interface RadioGroupEventMap extends HTMLElementEventMap, RadioGroupCusto
77
51
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
78
52
  * @fires {CustomEvent} validated - Fired whenever the field is validated.
79
53
  */
80
- declare class RadioGroup extends FieldMixin(
81
- FocusMixin(DisabledMixin(KeyboardMixin(ElementMixin(ThemableMixin(HTMLElement))))),
82
- ) {
83
- /**
84
- * The value of the radio group.
85
- */
86
- value: string | null | undefined;
87
-
88
- /**
89
- * When present, the user cannot modify the value of the radio group.
90
- * The property works similarly to the `disabled` property.
91
- * While the `disabled` property disables all the radio buttons inside the group,
92
- * the `readonly` property disables only unchecked ones.
93
- */
94
- readonly: boolean;
95
-
54
+ declare class RadioGroup extends RadioGroupMixin(ElementMixin(ThemableMixin(HTMLElement))) {
96
55
  addEventListener<K extends keyof RadioGroupEventMap>(
97
56
  type: K,
98
57
  listener: (this: RadioGroup, ev: RadioGroupEventMap[K]) => void,