@vaadin/radio-group 24.3.0-alpha1 → 24.3.0-alpha11

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.
@@ -3,18 +3,15 @@
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 './vaadin-radio-button.js';
6
7
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
7
- import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
8
- import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
9
- import { KeyboardMixin } from '@vaadin/a11y-base/src/keyboard-mixin.js';
10
8
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
11
9
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
12
- import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
13
- import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
14
- import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
15
- import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js';
16
- import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
17
- import { RadioButton } from './vaadin-radio-button.js';
10
+ import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
11
+ import { RadioGroupMixin } from './vaadin-radio-group-mixin.js';
12
+ import { radioGroupStyles } from './vaadin-radio-group-styles.js';
13
+
14
+ registerStyles('vaadin-radio-group', radioGroupStyles, { moduleId: 'vaadin-radio-group-styles' });
18
15
 
19
16
  /**
20
17
  * `<vaadin-radio-group>` is a web component that allows the user to choose one item from a group of choices.
@@ -61,51 +58,16 @@ import { RadioButton } from './vaadin-radio-button.js';
61
58
  * @customElement
62
59
  * @extends HTMLElement
63
60
  * @mixes ThemableMixin
64
- * @mixes DisabledMixin
65
61
  * @mixes ElementMixin
66
- * @mixes FocusMixin
67
- * @mixes FieldMixin
68
- * @mixes KeyboardMixin
62
+ * @mixes RadioGroupMixin
69
63
  */
70
- class RadioGroup extends FieldMixin(
71
- FocusMixin(DisabledMixin(KeyboardMixin(ElementMixin(ThemableMixin(PolymerElement))))),
72
- ) {
64
+ class RadioGroup extends RadioGroupMixin(ElementMixin(ThemableMixin(PolymerElement))) {
73
65
  static get is() {
74
66
  return 'vaadin-radio-group';
75
67
  }
76
68
 
77
69
  static get template() {
78
70
  return html`
79
- <style>
80
- :host {
81
- display: inline-flex;
82
- }
83
-
84
- :host::before {
85
- content: '\\2003';
86
- width: 0;
87
- display: inline-block;
88
- }
89
-
90
- :host([hidden]) {
91
- display: none !important;
92
- }
93
-
94
- .vaadin-group-field-container {
95
- display: flex;
96
- flex-direction: column;
97
- width: 100%;
98
- }
99
-
100
- [part='group-field'] {
101
- display: flex;
102
- flex-wrap: wrap;
103
- }
104
-
105
- :host(:not([has-label])) [part='label'] {
106
- display: none;
107
- }
108
- </style>
109
71
  <div class="vaadin-group-field-container">
110
72
  <div part="label">
111
73
  <slot name="label"></slot>
@@ -128,425 +90,6 @@ class RadioGroup extends FieldMixin(
128
90
  <slot name="tooltip"></slot>
129
91
  `;
130
92
  }
131
-
132
- static get properties() {
133
- return {
134
- /**
135
- * The value of the radio group.
136
- *
137
- * @type {string}
138
- */
139
- value: {
140
- type: String,
141
- notify: true,
142
- value: '',
143
- observer: '__valueChanged',
144
- },
145
-
146
- /**
147
- * When present, the user cannot modify the value of the radio group.
148
- * The property works similarly to the `disabled` property.
149
- * While the `disabled` property disables all radio buttons inside the group,
150
- * the `readonly` property disables only unchecked ones.
151
- *
152
- * @type {boolean}
153
- */
154
- readonly: {
155
- type: Boolean,
156
- value: false,
157
- reflectToAttribute: true,
158
- observer: '__readonlyChanged',
159
- },
160
-
161
- /**
162
- * @type {string}
163
- * @private
164
- */
165
- _fieldName: {
166
- type: String,
167
- },
168
-
169
- /**
170
- * Whether the field is dirty.
171
- *
172
- * The field is automatically marked as dirty once the user triggers
173
- * a `change` event. Additionally, the field can be manually marked
174
- * as dirty by setting the property to `true`.
175
- */
176
- dirty: {
177
- type: Boolean,
178
- value: false,
179
- notify: true,
180
- },
181
- };
182
- }
183
-
184
- constructor() {
185
- super();
186
-
187
- this.__registerRadioButton = this.__registerRadioButton.bind(this);
188
- this.__unregisterRadioButton = this.__unregisterRadioButton.bind(this);
189
- this.__onRadioButtonChange = this.__onRadioButtonChange.bind(this);
190
- this.__onRadioButtonCheckedChange = this.__onRadioButtonCheckedChange.bind(this);
191
-
192
- this._tooltipController = new TooltipController(this);
193
- this._tooltipController.addEventListener('tooltip-changed', (event) => {
194
- const tooltip = event.detail.node;
195
- if (tooltip && tooltip.isConnected) {
196
- // Tooltip element has been added to the DOM
197
- const inputs = this.__radioButtons.map((radio) => radio.inputElement);
198
- this._tooltipController.setAriaTarget(inputs);
199
- } else {
200
- // Tooltip element is no longer connected
201
- this._tooltipController.setAriaTarget([]);
202
- }
203
- });
204
- }
205
-
206
- /**
207
- * A collection of the group's radio buttons.
208
- *
209
- * @return {!Array<!RadioButton>}
210
- * @private
211
- */
212
- get __radioButtons() {
213
- return this.__filterRadioButtons([...this.children]);
214
- }
215
-
216
- /**
217
- * A currently selected radio button.
218
- *
219
- * @return {!RadioButton | undefined}
220
- * @private
221
- */
222
- get __selectedRadioButton() {
223
- return this.__radioButtons.find((radioButton) => radioButton.checked);
224
- }
225
-
226
- /**
227
- * @return {boolean}
228
- * @private
229
- */
230
- get isHorizontalRTL() {
231
- return this.__isRTL && this._theme !== 'vertical';
232
- }
233
-
234
- /** @protected */
235
- ready() {
236
- super.ready();
237
-
238
- this.ariaTarget = this;
239
-
240
- // See https://github.com/vaadin/vaadin-web-components/issues/94
241
- this.setAttribute('role', 'radiogroup');
242
-
243
- this._fieldName = `${this.localName}-${generateUniqueId()}`;
244
-
245
- const slot = this.shadowRoot.querySelector('slot:not([name])');
246
- this._observer = new SlotObserver(slot, ({ addedNodes, removedNodes }) => {
247
- // Registers the added radio buttons in the reverse order
248
- // in order for the group to take the value of the most recent button.
249
- this.__filterRadioButtons(addedNodes).reverse().forEach(this.__registerRadioButton);
250
-
251
- // Unregisters the removed radio buttons.
252
- this.__filterRadioButtons(removedNodes).forEach(this.__unregisterRadioButton);
253
-
254
- const inputs = this.__radioButtons.map((radio) => radio.inputElement);
255
- this._tooltipController.setAriaTarget(inputs);
256
- });
257
-
258
- this.addController(this._tooltipController);
259
- }
260
-
261
- /**
262
- * @param {!Array<!Node>} nodes
263
- * @return {!Array<!RadioButton>}
264
- * @private
265
- */
266
- __filterRadioButtons(nodes) {
267
- return nodes.filter((child) => child instanceof RadioButton);
268
- }
269
-
270
- /**
271
- * Override method inherited from `KeyboardMixin`
272
- * to implement the custom keyboard navigation as a replacement for the native one
273
- * in order for the navigation to work the same way across different browsers.
274
- *
275
- * @param {!KeyboardEvent} event
276
- * @override
277
- * @protected
278
- */
279
- _onKeyDown(event) {
280
- super._onKeyDown(event);
281
-
282
- const radioButton = event.composedPath().find((node) => node instanceof RadioButton);
283
-
284
- if (['ArrowLeft', 'ArrowUp'].includes(event.key)) {
285
- event.preventDefault();
286
- this.__selectNextRadioButton(radioButton);
287
- }
288
-
289
- if (['ArrowRight', 'ArrowDown'].includes(event.key)) {
290
- event.preventDefault();
291
- this.__selectPrevRadioButton(radioButton);
292
- }
293
- }
294
-
295
- /**
296
- * Override an observer from `FieldMixin`.
297
- *
298
- * @param {boolean} invalid
299
- * @protected
300
- * @override
301
- */
302
- _invalidChanged(invalid) {
303
- super._invalidChanged(invalid);
304
-
305
- if (invalid) {
306
- this.setAttribute('aria-invalid', 'true');
307
- } else {
308
- this.removeAttribute('aria-invalid');
309
- }
310
- }
311
-
312
- /**
313
- * @param {number} index
314
- * @private
315
- */
316
- __selectNextRadioButton(radioButton) {
317
- const index = this.__radioButtons.indexOf(radioButton);
318
-
319
- this.__selectIncRadioButton(index, this.isHorizontalRTL ? 1 : -1);
320
- }
321
-
322
- /**
323
- * @param {number} index
324
- * @private
325
- */
326
- __selectPrevRadioButton(radioButton) {
327
- const index = this.__radioButtons.indexOf(radioButton);
328
-
329
- this.__selectIncRadioButton(index, this.isHorizontalRTL ? -1 : 1);
330
- }
331
-
332
- /**
333
- * @param {number} index
334
- * @param {number} step
335
- * @private
336
- */
337
- __selectIncRadioButton(index, step) {
338
- const newIndex = (this.__radioButtons.length + index + step) % this.__radioButtons.length;
339
- const newRadioButton = this.__radioButtons[newIndex];
340
-
341
- if (newRadioButton.disabled) {
342
- this.__selectIncRadioButton(newIndex, step);
343
- } else {
344
- newRadioButton.focusElement.focus();
345
- newRadioButton.focusElement.click();
346
- }
347
- }
348
-
349
- /**
350
- * Registers the radio button after adding it to the group.
351
- *
352
- * @param {!RadioButton} radioButton
353
- * @private
354
- */
355
- __registerRadioButton(radioButton) {
356
- radioButton.name = this._fieldName;
357
- radioButton.addEventListener('change', this.__onRadioButtonChange);
358
- radioButton.addEventListener('checked-changed', this.__onRadioButtonCheckedChange);
359
-
360
- if (this.disabled || this.readonly) {
361
- radioButton.disabled = true;
362
- }
363
-
364
- if (radioButton.checked) {
365
- this.__selectRadioButton(radioButton);
366
- }
367
- }
368
-
369
- /**
370
- * Unregisters the radio button before removing it from the group.
371
- *
372
- * @param {!RadioButton} radioButton
373
- * @private
374
- */
375
- __unregisterRadioButton(radioButton) {
376
- radioButton.removeEventListener('change', this.__onRadioButtonChange);
377
- radioButton.removeEventListener('checked-changed', this.__onRadioButtonCheckedChange);
378
-
379
- if (radioButton.value === this.value) {
380
- this.__selectRadioButton(null);
381
- }
382
- }
383
-
384
- /** @private */
385
- __onRadioButtonChange() {
386
- this.dirty = true;
387
- }
388
-
389
- /**
390
- * @param {!CustomEvent} event
391
- * @private
392
- */
393
- __onRadioButtonCheckedChange(event) {
394
- if (event.target.checked) {
395
- this.__selectRadioButton(event.target);
396
- }
397
- }
398
-
399
- /**
400
- * Whenever the user sets a non-empty value,
401
- * the method tries to select the radio button with that value
402
- * showing a warning if no radio button was found with the given value.
403
- * If the new value is empty, the method deselects the currently selected radio button.
404
- * At last, the method toggles the `has-value` attribute considering the new value.
405
- *
406
- * @param {string | null | undefined} newValue
407
- * @param {string | null | undefined} oldValue
408
- * @private
409
- */
410
- __valueChanged(newValue, oldValue) {
411
- if (oldValue === undefined && newValue === '') {
412
- return;
413
- }
414
-
415
- if (newValue) {
416
- const newSelectedRadioButton = this.__radioButtons.find((radioButton) => {
417
- return radioButton.value === newValue;
418
- });
419
-
420
- if (newSelectedRadioButton) {
421
- this.__selectRadioButton(newSelectedRadioButton);
422
- this.toggleAttribute('has-value', true);
423
- } else {
424
- console.warn(`The radio button with the value "${newValue}" was not found.`);
425
- }
426
- } else {
427
- this.__selectRadioButton(null);
428
- this.removeAttribute('has-value');
429
- }
430
-
431
- if (oldValue !== undefined) {
432
- this.validate();
433
- }
434
- }
435
-
436
- /**
437
- * Whenever `readonly` property changes on the group element,
438
- * the method updates the `disabled` property for the radio buttons.
439
- *
440
- * @param {boolean} newValue
441
- * @param {boolean} oldValue
442
- * @private
443
- */
444
- __readonlyChanged(newValue, oldValue) {
445
- // Prevent updating the `disabled` property for the radio buttons at initialization.
446
- // Otherwise, the group's radio buttons may end up enabled regardless
447
- // an intentionally added `disabled` attribute on some of them.
448
- if (!newValue && oldValue === undefined) {
449
- return;
450
- }
451
-
452
- if (oldValue !== newValue) {
453
- this.__updateRadioButtonsDisabledProperty();
454
- }
455
- }
456
-
457
- /**
458
- * Override method inherited from `DisabledMixin`
459
- * to update the `disabled` property for the radio buttons
460
- * whenever the property changes on the group element.
461
- *
462
- * @param {boolean} newValue
463
- * @param {boolean} oldValue
464
- * @override
465
- * @protected
466
- */
467
- _disabledChanged(newValue, oldValue) {
468
- super._disabledChanged(newValue, oldValue);
469
-
470
- // Prevent updating the `disabled` property for the radio buttons at initialization.
471
- // Otherwise, the group's radio buttons may end up enabled regardless
472
- // an intentionally added `disabled` attribute on some of them.
473
- if (!newValue && oldValue === undefined) {
474
- return;
475
- }
476
-
477
- if (oldValue !== newValue) {
478
- this.__updateRadioButtonsDisabledProperty();
479
- }
480
- }
481
-
482
- /**
483
- * Override method inherited from `FocusMixin`
484
- * to prevent removing the `focused` attribute
485
- * when focus moves between radio buttons inside the group.
486
- *
487
- * @param {!FocusEvent} event
488
- * @return {boolean}
489
- * @protected
490
- */
491
- _shouldRemoveFocus(event) {
492
- return !this.contains(event.relatedTarget);
493
- }
494
-
495
- /**
496
- * Override method inherited from `FocusMixin`
497
- * to run validation when the group loses focus.
498
- *
499
- * @param {boolean} focused
500
- * @override
501
- * @protected
502
- */
503
- _setFocused(focused) {
504
- super._setFocused(focused);
505
-
506
- // Do not validate when focusout is caused by document
507
- // losing focus, which happens on browser tab switch.
508
- if (!focused && document.hasFocus()) {
509
- this.validate();
510
- }
511
- }
512
-
513
- /**
514
- * @param {RadioButton} radioButton
515
- * @private
516
- */
517
- __selectRadioButton(radioButton) {
518
- if (radioButton) {
519
- this.value = radioButton.value;
520
- } else {
521
- this.value = '';
522
- }
523
-
524
- this.__radioButtons.forEach((button) => {
525
- button.checked = button === radioButton;
526
- });
527
-
528
- if (this.readonly) {
529
- this.__updateRadioButtonsDisabledProperty();
530
- }
531
- }
532
-
533
- /**
534
- * If the group is read-only, the method disables the unchecked radio buttons.
535
- * Otherwise, the method propagates the group's `disabled` property to the radio buttons.
536
- *
537
- * @private
538
- */
539
- __updateRadioButtonsDisabledProperty() {
540
- this.__radioButtons.forEach((button) => {
541
- if (this.readonly) {
542
- // The native radio button doesn't support the `readonly` attribute
543
- // so the state can be only imitated, by disabling unchecked radio buttons.
544
- button.disabled = button !== this.__selectedRadioButton;
545
- } else {
546
- button.disabled = this.disabled;
547
- }
548
- });
549
- }
550
93
  }
551
94
 
552
95
  defineCustomElement(RadioGroup);
@@ -9,8 +9,8 @@ registerStyles(
9
9
  'vaadin-radio-button',
10
10
  css`
11
11
  :host {
12
- color: var(--lumo-body-text-color);
13
- font-size: var(--lumo-font-size-m);
12
+ color: var(--vaadin-radio-button-label-color, var(--lumo-body-text-color));
13
+ font-size: var(--vaadin-radio-button-label-font-size, var(--lumo-font-size-m));
14
14
  font-family: var(--lumo-font-family);
15
15
  line-height: var(--lumo-line-height-s);
16
16
  -webkit-font-smoothing: antialiased;
@@ -22,11 +22,16 @@ registerStyles(
22
22
  cursor: default;
23
23
  outline: none;
24
24
  --_radio-button-size: var(--vaadin-radio-button-size, calc(var(--lumo-size-m) / 2));
25
+ --_focus-ring-color: var(--vaadin-focus-ring-color, var(--lumo-primary-color-50pct));
26
+ --_focus-ring-width: var(--vaadin-focus-ring-width, 2px);
27
+ --_selection-color: var(--vaadin-selection-color, var(--lumo-primary-color));
25
28
  }
26
29
 
27
30
  :host([has-label]) ::slotted(label) {
28
- padding-block: var(--lumo-space-xs);
29
- padding-inline: var(--lumo-space-xs) var(--lumo-space-s);
31
+ padding: var(
32
+ --vaadin-radio-button-label-padding,
33
+ var(--lumo-space-xs) var(--lumo-space-s) var(--lumo-space-xs) var(--lumo-space-xs)
34
+ );
30
35
  }
31
36
 
32
37
  [part='radio'] {
@@ -35,7 +40,7 @@ registerStyles(
35
40
  margin: var(--lumo-space-xs);
36
41
  position: relative;
37
42
  border-radius: 50%;
38
- background-color: var(--lumo-contrast-20pct);
43
+ background: var(--vaadin-radio-button-background, var(--lumo-contrast-20pct));
39
44
  transition: transform 0.2s cubic-bezier(0.12, 0.32, 0.54, 2), background-color 0.15s;
40
45
  will-change: transform;
41
46
  cursor: var(--lumo-clickable-cursor);
@@ -64,7 +69,8 @@ registerStyles(
64
69
  pointer-events: none;
65
70
  width: 0;
66
71
  height: 0;
67
- border: 3px solid var(--lumo-primary-contrast-color);
72
+ border: var(--vaadin-radio-button-dot-size, 3px) solid
73
+ var(--vaadin-radio-button-dot-color, var(--lumo-primary-contrast-color));
68
74
  border-radius: 50%;
69
75
  position: absolute;
70
76
  top: 50%;
@@ -80,7 +86,7 @@ registerStyles(
80
86
  }
81
87
 
82
88
  :host([checked]) [part='radio'] {
83
- background-color: var(--lumo-primary-color);
89
+ background-color: var(--_selection-color);
84
90
  }
85
91
 
86
92
  :host([checked]) [part='radio']::after {
@@ -88,7 +94,7 @@ registerStyles(
88
94
  }
89
95
 
90
96
  :host(:not([checked]):not([disabled]):hover) [part='radio'] {
91
- background-color: var(--lumo-contrast-30pct);
97
+ background: var(--vaadin-radio-button-background-hover, var(--lumo-contrast-30pct));
92
98
  }
93
99
 
94
100
  :host([active]) [part='radio'] {
@@ -107,7 +113,7 @@ registerStyles(
107
113
  }
108
114
 
109
115
  :host([focus-ring]) [part='radio'] {
110
- box-shadow: 0 0 0 1px var(--lumo-base-color), 0 0 0 3px var(--lumo-primary-color-50pct),
116
+ box-shadow: 0 0 0 1px var(--lumo-base-color), 0 0 0 calc(var(--_focus-ring-width) + 1px) var(--_focus-ring-color),
111
117
  inset 0 0 0 var(--_input-border-width, 0) var(--_input-border-color);
112
118
  }
113
119