Skip to content

Commit

Permalink
refactor!: use new mixins for checkbox
Browse files Browse the repository at this point in the history
  • Loading branch information
vursen committed Sep 15, 2021
1 parent 968ff6f commit 2e76c04
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 239 deletions.
271 changes: 59 additions & 212 deletions packages/checkbox/src/vaadin-checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
import { GestureEventListeners } from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import { ControlStateMixin } from '@vaadin/vaadin-control-state-mixin/vaadin-control-state-mixin.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ActiveMixin } from '@vaadin/field-base/src/active-mixin.js';
import { AriaLabelMixin } from '@vaadin/field-base/src/aria-label-mixin.js';
import { CheckedMixin } from '@vaadin/field-base/src/checked-mixin.js';
import { InputSlotMixin } from '@vaadin/field-base/src/input-slot-mixin.js';
import { SlotLabelMixin } from '@vaadin/field-base/src/slot-label-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';

/**
Expand Down Expand Up @@ -46,12 +49,21 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
* @fires {CustomEvent} indeterminate-changed - Fired when the `indeterminate` property changes.
*
* @extends HTMLElement
* @mixes ElementMixin
* @mixes ControlStateMixin
* @mixes ThemableMixin
* @mixes GestureEventListeners
* @mixes ElementMixin
* @mixes ActiveMixin
* @mixes AriaLabelMixin
* @mixes InputSlotMixin
* @mixes CheckedMixin
* @mixes SlotLabelMixin
*/
class CheckboxElement extends ElementMixin(ControlStateMixin(ThemableMixin(GestureEventListeners(PolymerElement)))) {
class Checkbox extends SlotLabelMixin(
CheckedMixin(InputSlotMixin(AriaLabelMixin(ActiveMixin(ElementMixin(ThemableMixin(PolymerElement))))))
) {
static get is() {
return 'vaadin-checkbox';
}

static get template() {
return html`
<style>
Expand All @@ -63,19 +75,13 @@ class CheckboxElement extends ElementMixin(ControlStateMixin(ThemableMixin(Gestu
display: none !important;
}
label {
[part='container'] {
display: inline-flex;
align-items: baseline;
outline: none;
}
[part='checkbox'] {
position: relative;
display: inline-block;
flex: none;
}
input[type='checkbox'] {
/* visually hidden */
::slotted(input) {
position: absolute;
top: 0;
left: 0;
Expand All @@ -85,239 +91,80 @@ class CheckboxElement extends ElementMixin(ControlStateMixin(ThemableMixin(Gestu
opacity: 0;
cursor: inherit;
margin: 0;
}
:host([disabled]) {
-webkit-tap-highlight-color: transparent;
z-index: 100;
}
</style>
<label>
<span part="checkbox">
<input
type="checkbox"
checked="{{checked::change}}"
disabled$="[[disabled]]"
indeterminate="{{indeterminate::change}}"
role="presentation"
tabindex="-1"
/>
</span>
<span part="label">
<slot></slot>
</span>
</label>
<div part="container">
<div part="checkbox">
<slot name="input"></slot>
</div>
<div part="label">
<slot name="label"></slot>
</div>
<div style="display: none !important">
<slot id="noop"></slot>
</div>
</div>
`;
}

static get is() {
return 'vaadin-checkbox';
}

static get properties() {
return {
/**
* True if the checkbox is checked.
* @type {boolean}
*/
checked: {
type: Boolean,
value: false,
notify: true,
observer: '_checkedChanged',
reflectToAttribute: true
},

/**
* Indeterminate state of the checkbox when it's neither checked nor unchecked, but undetermined.
* True if the checkbox is in the indeterminate state which means
* it is not possible to say whether the checkbox is checked or unchecked.
* The state resets once the checkbox gets checked or unchecked.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#Indeterminate_state_checkboxes
*
* @type {boolean}
*/
indeterminate: {
type: Boolean,
notify: true,
observer: '_indeterminateChanged',
reflectToAttribute: true,
value: false
value: false,
reflectToAttribute: true
},

/**
* The value given to the data submitted with the checkbox's name to the server when the control is inside a form.
* The value of the checkbox.
*/
value: {
type: String,
value: 'on'
},

/** @private */
_nativeCheckbox: {
type: Object
value: 'on',
observer: '_valueChanged',
reflectToAttribute: true
}
};
}

constructor() {
super();
/**
* @type {string}
* Name of the element.
*/
this.name;
}

get name() {
return this.checked ? this._storedName : '';
}

set name(name) {
this._storedName = name;
}

ready() {
super.ready();

this.setAttribute('role', 'checkbox');

this._nativeCheckbox = this.shadowRoot.querySelector('input[type="checkbox"]');

this.addEventListener('click', this._handleClick.bind(this));

this._addActiveListeners();

const attrName = this.getAttribute('name');
if (attrName) {
this.name = attrName;
}

this.shadowRoot
.querySelector('[part~="label"]')
.querySelector('slot')
.addEventListener('slotchange', this._updateLabelAttribute.bind(this));

this._updateLabelAttribute();
}

/** @private */
_updateLabelAttribute() {
const label = this.shadowRoot.querySelector('[part~="label"]');
const assignedNodes = label.firstElementChild.assignedNodes();
if (this._isAssignedNodesEmpty(assignedNodes)) {
label.setAttribute('empty', '');
} else {
label.removeAttribute('empty');
}
}

/** @private */
_isAssignedNodesEmpty(nodes) {
// The assigned nodes considered to be empty if there is no slotted content or only one empty text node
return (
nodes.length === 0 ||
(nodes.length == 1 && nodes[0].nodeType == Node.TEXT_NODE && nodes[0].textContent.trim() === '')
);
}

/** @private */
_checkedChanged(checked) {
if (this.indeterminate) {
this.setAttribute('aria-checked', 'mixed');
} else {
this.setAttribute('aria-checked', Boolean(checked));
}
}

/** @private */
_indeterminateChanged(indeterminate) {
if (indeterminate) {
this.setAttribute('aria-checked', 'mixed');
} else {
this.setAttribute('aria-checked', this.checked);
}
static get delegateProps() {
return [...super.delegateProps, 'indeterminate'];
}

/** @private */
_addActiveListeners() {
// DOWN
this._addEventListenerToNode(this, 'down', (e) => {
if (this.__interactionsAllowed(e)) {
this.setAttribute('active', '');
}
});

// UP
this._addEventListenerToNode(this, 'up', () => this.removeAttribute('active'));

// KEYDOWN
this.addEventListener('keydown', (e) => {
if (this.__interactionsAllowed(e) && e.keyCode === 32) {
e.preventDefault();
this.setAttribute('active', '');
}
});

// KEYUP
this.addEventListener('keyup', (e) => {
if (this.__interactionsAllowed(e) && e.keyCode === 32) {
e.preventDefault();
this._toggleChecked();
this.removeAttribute('active');
constructor() {
super();

if (this.indeterminate) {
this.indeterminate = false;
}
}
});
this._setType('checkbox');
}

/**
* @return {!HTMLInputElement}
* @override
* @protected
*/
get focusElement() {
return this.shadowRoot.querySelector('input');
get _sourceSlot() {
return this.$.noop;
}

/**
* True if users' interactions (mouse or keyboard)
* should toggle the checkbox
* @protected
* @override
*/
__interactionsAllowed(e) {
if (this.disabled) {
return false;
}
_toggleChecked() {
this.indeterminate = false;

// https://github.com/vaadin/vaadin-checkbox/issues/63
if (e.target.localName === 'a') {
return false;
}
super._toggleChecked();

return true;
}

/** @private */
_handleClick(e) {
if (this.__interactionsAllowed(e)) {
if (!this.indeterminate) {
if (e.composedPath()[0] !== this._nativeCheckbox) {
e.preventDefault();
this._toggleChecked();
}
} else {
/*
* Required for IE 11 and Edge.
* See issue here: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7344418/
*/
this.indeterminate = false;
e.preventDefault();
this._toggleChecked();
}
}
}

/** @protected */
_toggleChecked() {
this.checked = !this.checked;
this.dispatchEvent(new CustomEvent('change', { composed: false, bubbles: true }));
}

Expand All @@ -328,6 +175,6 @@ class CheckboxElement extends ElementMixin(ControlStateMixin(ThemableMixin(Gestu
*/
}

customElements.define(CheckboxElement.is, CheckboxElement);
customElements.define(Checkbox.is, Checkbox);

export { CheckboxElement };
export { Checkbox };
1 change: 0 additions & 1 deletion packages/checkbox/theme/lumo/vaadin-checkbox-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ registerStyles(
border-radius: var(--lumo-border-radius-s);
background-color: var(--lumo-contrast-20pct);
transition: transform 0.2s cubic-bezier(0.12, 0.32, 0.54, 2), background-color 0.15s;
pointer-events: none;
line-height: 1.2;
cursor: var(--lumo-clickable-cursor);
}
Expand Down
1 change: 0 additions & 1 deletion packages/checkbox/theme/material/vaadin-checkbox-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ registerStyles(
position: relative;
border-radius: 2px;
box-shadow: inset 0 0 0 2px var(--material-secondary-text-color);
pointer-events: none;
line-height: 1.275;
background-color: transparent;
}
Expand Down
23 changes: 5 additions & 18 deletions packages/field-base/src/aria-label-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,14 @@ import { InputMixin } from './input-mixin.js';

const AriaLabelMixinImplementation = (superclass) =>
class AriaLabelMixinClass extends InputMixin(LabelMixin(superclass)) {
constructor() {
super();

this.__preventDuplicateLabelClick = this.__preventDuplicateLabelClick.bind(this);
}

/** @protected */
connectedCallback() {
super.connectedCallback();

if (this._labelNode) {
this._labelNode.addEventListener('click', this.__preventDuplicateLabelClick);
}
}

/** @protected */
disconnectedCallback() {
super.disconnectedCallback();
ready() {
super.ready();

if (this._labelNode) {
this._labelNode.removeEventListener('click', this.__preventDuplicateLabelClick);
this._labelNode.addEventListener('click', (event) => {
this.__preventDuplicateLabelClick(event);
});
}
}

Expand Down
Loading

0 comments on commit 2e76c04

Please sign in to comment.