-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: extract checkbox-group logic to reusable mixin (#7258)
- Loading branch information
1 parent
17fa7b2
commit f38b8b3
Showing
6 changed files
with
342 additions
and
281 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
packages/checkbox-group/src/vaadin-checkbox-group-mixin.d.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/** | ||
* @license | ||
* Copyright (c) 2018 - 2024 Vaadin Ltd. | ||
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ | ||
*/ | ||
import type { Constructor } from '@open-wc/dedupe-mixin'; | ||
import type { DisabledMixinClass } from '@vaadin/a11y-base/src/disabled-mixin.js'; | ||
import type { FocusMixinClass } from '@vaadin/a11y-base/src/focus-mixin.js'; | ||
import type { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js'; | ||
import type { FieldMixinClass } from '@vaadin/field-base/src/field-mixin.js'; | ||
import type { LabelMixinClass } from '@vaadin/field-base/src/label-mixin.js'; | ||
import type { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.js'; | ||
|
||
/** | ||
* A mixin providing common checkbox-group functionality. | ||
*/ | ||
export declare function CheckboxGroupMixin<T extends Constructor<HTMLElement>>( | ||
base: T, | ||
): Constructor<CheckboxGroupMixinClass> & | ||
Constructor<ControllerMixinClass> & | ||
Constructor<DisabledMixinClass> & | ||
Constructor<FieldMixinClass> & | ||
Constructor<FocusMixinClass> & | ||
Constructor<LabelMixinClass> & | ||
Constructor<ValidateMixinClass> & | ||
T; | ||
|
||
export declare class CheckboxGroupMixinClass { | ||
/** | ||
* An array containing values of the currently checked checkboxes. | ||
* | ||
* The array is immutable so toggling checkboxes always results in | ||
* creating a new array. | ||
*/ | ||
value: string[]; | ||
} |
281 changes: 281 additions & 0 deletions
281
packages/checkbox-group/src/vaadin-checkbox-group-mixin.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
/** | ||
* @license | ||
* Copyright (c) 2018 - 2024 Vaadin Ltd. | ||
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ | ||
*/ | ||
import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js'; | ||
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js'; | ||
import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js'; | ||
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js'; | ||
import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js'; | ||
|
||
/** | ||
* A mixin providing common checkbox-group functionality. | ||
* | ||
* @polymerMixin | ||
* @mixes DisabledMixin | ||
* @mixes FieldMixin | ||
* @mixes FocusMixin | ||
* @mixes KeyboardMixin | ||
*/ | ||
export const CheckboxGroupMixin = (superclass) => | ||
class CheckboxGroupMixinClass extends FieldMixin(FocusMixin(DisabledMixin(superclass))) { | ||
static get properties() { | ||
return { | ||
/** | ||
* An array containing values of the currently checked checkboxes. | ||
* | ||
* The array is immutable so toggling checkboxes always results in | ||
* creating a new array. | ||
* | ||
* @type {!Array<!string>} | ||
*/ | ||
value: { | ||
type: Array, | ||
value: () => [], | ||
notify: true, | ||
observer: '__valueChanged', | ||
}, | ||
}; | ||
} | ||
|
||
constructor() { | ||
super(); | ||
|
||
this.__registerCheckbox = this.__registerCheckbox.bind(this); | ||
this.__unregisterCheckbox = this.__unregisterCheckbox.bind(this); | ||
this.__onCheckboxCheckedChanged = this.__onCheckboxCheckedChanged.bind(this); | ||
|
||
this._tooltipController = new TooltipController(this); | ||
this._tooltipController.addEventListener('tooltip-changed', (event) => { | ||
const tooltip = event.detail.node; | ||
if (tooltip && tooltip.isConnected) { | ||
// Tooltip element has been added to the DOM | ||
const inputs = this.__checkboxes.map((checkbox) => checkbox.inputElement); | ||
this._tooltipController.setAriaTarget(inputs); | ||
} else { | ||
// Tooltip element is no longer connected | ||
this._tooltipController.setAriaTarget([]); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* A collection of the checkboxes. | ||
* | ||
* @return {!Array<!Checkbox>} | ||
* @private | ||
*/ | ||
get __checkboxes() { | ||
return this.__filterCheckboxes([...this.children]); | ||
} | ||
|
||
/** @protected */ | ||
ready() { | ||
super.ready(); | ||
|
||
this.ariaTarget = this; | ||
|
||
// See https://github.com/vaadin/vaadin-web-components/issues/94 | ||
this.setAttribute('role', 'group'); | ||
|
||
const slot = this.shadowRoot.querySelector('slot:not([name])'); | ||
this._observer = new SlotObserver(slot, ({ addedNodes, removedNodes }) => { | ||
const addedCheckboxes = this.__filterCheckboxes(addedNodes); | ||
const removedCheckboxes = this.__filterCheckboxes(removedNodes); | ||
|
||
addedCheckboxes.forEach(this.__registerCheckbox); | ||
removedCheckboxes.forEach(this.__unregisterCheckbox); | ||
|
||
const inputs = this.__checkboxes.map((checkbox) => checkbox.inputElement); | ||
this._tooltipController.setAriaTarget(inputs); | ||
|
||
this.__warnOfCheckboxesWithoutValue(addedCheckboxes); | ||
}); | ||
|
||
this.addController(this._tooltipController); | ||
} | ||
|
||
/** | ||
* Override method inherited from `ValidateMixin` | ||
* to validate the value array. | ||
* | ||
* @override | ||
* @return {boolean} | ||
*/ | ||
checkValidity() { | ||
return !this.required || this.value.length > 0; | ||
} | ||
|
||
/** | ||
* @param {!Array<!Node>} nodes | ||
* @return {!Array<!Checkbox>} | ||
* @private | ||
*/ | ||
__filterCheckboxes(nodes) { | ||
return nodes.filter((node) => node.nodeType === Node.ELEMENT_NODE && node.localName === 'vaadin-checkbox'); | ||
} | ||
|
||
/** | ||
* @param {!Array<!Checkbox>} checkboxes | ||
* @private | ||
*/ | ||
__warnOfCheckboxesWithoutValue(checkboxes) { | ||
const hasCheckboxesWithoutValue = checkboxes.some((checkbox) => { | ||
const { value } = checkbox; | ||
|
||
return !checkbox.hasAttribute('value') && (!value || value === 'on'); | ||
}); | ||
|
||
if (hasCheckboxesWithoutValue) { | ||
console.warn('Please provide the value attribute to all the checkboxes inside the checkbox group.'); | ||
} | ||
} | ||
|
||
/** | ||
* Registers the checkbox after adding it to the group. | ||
* | ||
* @param {!Checkbox} checkbox | ||
* @private | ||
*/ | ||
__registerCheckbox(checkbox) { | ||
checkbox.addEventListener('checked-changed', this.__onCheckboxCheckedChanged); | ||
|
||
if (this.disabled) { | ||
checkbox.disabled = true; | ||
} | ||
|
||
if (checkbox.checked) { | ||
this.__addCheckboxToValue(checkbox.value); | ||
} else if (this.value.includes(checkbox.value)) { | ||
checkbox.checked = true; | ||
} | ||
} | ||
|
||
/** | ||
* Unregisters the checkbox before removing it from the group. | ||
* | ||
* @param {!Checkbox} checkbox | ||
* @private | ||
*/ | ||
__unregisterCheckbox(checkbox) { | ||
checkbox.removeEventListener('checked-changed', this.__onCheckboxCheckedChanged); | ||
|
||
if (checkbox.checked) { | ||
this.__removeCheckboxFromValue(checkbox.value); | ||
} | ||
} | ||
|
||
/** | ||
* Override method inherited from `DisabledMixin` | ||
* to propagate the `disabled` property to the checkboxes. | ||
* | ||
* @param {boolean} newValue | ||
* @param {boolean} oldValue | ||
* @override | ||
* @protected | ||
*/ | ||
_disabledChanged(newValue, oldValue) { | ||
super._disabledChanged(newValue, oldValue); | ||
|
||
// Prevent updating the `disabled` property for the checkboxes at initialization. | ||
// Otherwise, the checkboxes may end up enabled regardless the `disabled` attribute | ||
// intentionally added by the user on some of them. | ||
if (!newValue && oldValue === undefined) { | ||
return; | ||
} | ||
|
||
if (oldValue !== newValue) { | ||
this.__checkboxes.forEach((checkbox) => { | ||
checkbox.disabled = newValue; | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* @param {string} value | ||
* @private | ||
*/ | ||
__addCheckboxToValue(value) { | ||
if (!this.value.includes(value)) { | ||
this.value = [...this.value, value]; | ||
} | ||
} | ||
|
||
/** | ||
* @param {string} value | ||
* @private | ||
*/ | ||
__removeCheckboxFromValue(value) { | ||
if (this.value.includes(value)) { | ||
this.value = this.value.filter((v) => v !== value); | ||
} | ||
} | ||
|
||
/** | ||
* @param {!CustomEvent} event | ||
* @private | ||
*/ | ||
__onCheckboxCheckedChanged(event) { | ||
const checkbox = event.target; | ||
|
||
if (checkbox.checked) { | ||
this.__addCheckboxToValue(checkbox.value); | ||
} else { | ||
this.__removeCheckboxFromValue(checkbox.value); | ||
} | ||
} | ||
|
||
/** | ||
* @param {string | null | undefined} value | ||
* @param {string | null | undefined} oldValue | ||
* @private | ||
*/ | ||
__valueChanged(value, oldValue) { | ||
// Setting initial value to empty array, skip validation | ||
if (value.length === 0 && oldValue === undefined) { | ||
return; | ||
} | ||
|
||
this.toggleAttribute('has-value', value.length > 0); | ||
|
||
this.__checkboxes.forEach((checkbox) => { | ||
checkbox.checked = value.includes(checkbox.value); | ||
}); | ||
|
||
if (oldValue !== undefined) { | ||
this.validate(); | ||
} | ||
} | ||
|
||
/** | ||
* Override method inherited from `FocusMixin` | ||
* to prevent removing the `focused` attribute | ||
* when focus moves between checkboxes inside the group. | ||
* | ||
* @param {!FocusEvent} event | ||
* @return {boolean} | ||
* @protected | ||
*/ | ||
_shouldRemoveFocus(event) { | ||
return !this.contains(event.relatedTarget); | ||
} | ||
|
||
/** | ||
* Override method inherited from `FocusMixin` | ||
* to run validation when the group loses focus. | ||
* | ||
* @param {boolean} focused | ||
* @override | ||
* @protected | ||
*/ | ||
_setFocused(focused) { | ||
super._setFocused(focused); | ||
|
||
// Do not validate when focusout is caused by document | ||
// losing focus, which happens on browser tab switch. | ||
if (!focused && document.hasFocus()) { | ||
this.validate(); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.