-
Notifications
You must be signed in to change notification settings - Fork 846
/
switch.ts
254 lines (221 loc) · 7.09 KB
/
switch.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../focus/md-focus-ring.js';
import '../../ripple/ripple.js';
import {html, isServer, LitElement, nothing, TemplateResult} from 'lit';
import {property, query} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
import {mixinDelegatesAria} from '../../internal/aria/delegate.js';
import {
afterDispatch,
setupDispatchHooks,
} from '../../internal/events/dispatch-hooks.js';
import {
dispatchActivationClick,
isActivationClick,
} from '../../internal/events/form-label-activation.js';
import {redispatchEvent} from '../../internal/events/redispatch-event.js';
import {
createValidator,
getValidityAnchor,
mixinConstraintValidation,
} from '../../labs/behaviors/constraint-validation.js';
import {mixinElementInternals} from '../../labs/behaviors/element-internals.js';
import {
getFormState,
getFormValue,
mixinFormAssociated,
} from '../../labs/behaviors/form-associated.js';
import {CheckboxValidator} from '../../labs/behaviors/validators/checkbox-validator.js';
// Separate variable needed for closure.
const switchBaseClass = mixinDelegatesAria(
mixinConstraintValidation(
mixinFormAssociated(mixinElementInternals(LitElement)),
),
);
/**
* @fires input {InputEvent} Fired whenever `selected` changes due to user
* interaction (bubbles and composed).
* @fires change {Event} Fired whenever `selected` changes due to user
* interaction (bubbles).
*/
export class Switch extends switchBaseClass {
/** @nocollapse */
static override shadowRootOptions: ShadowRootInit = {
mode: 'open',
delegatesFocus: true,
};
/**
* Puts the switch in the selected state and sets the form submission value to
* the `value` property.
*/
@property({type: Boolean}) selected = false;
/**
* Shows both the selected and deselected icons.
*/
@property({type: Boolean}) icons = false;
/**
* Shows only the selected icon, and not the deselected icon. If `true`,
* overrides the behavior of the `icons` property.
*/
@property({type: Boolean, attribute: 'show-only-selected-icon'})
showOnlySelectedIcon = false;
/**
* When true, require the switch to be selected when participating in
* form submission.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#validation
*/
@property({type: Boolean}) required = false;
/**
* The value associated with this switch on form submission. `null` is
* submitted when `selected` is `false`.
*/
@property() value = 'on';
@query('input') private readonly input!: HTMLInputElement | null;
constructor() {
super();
if (isServer) {
return;
}
// This click listener does not currently need dispatch hooks since it does
// not check `event.defaultPrevented`.
this.addEventListener('click', (event: MouseEvent) => {
if (!isActivationClick(event) || !this.input) {
return;
}
this.focus();
dispatchActivationClick(this.input);
});
// Add the aria keyboard interaction pattern for switch and the Enter key.
// See https://www.w3.org/WAI/ARIA/apg/patterns/switch/.
setupDispatchHooks(this, 'keydown');
this.addEventListener('keydown', (event: KeyboardEvent) => {
afterDispatch(event, () => {
const ignoreEvent = event.defaultPrevented || event.key !== 'Enter';
if (ignoreEvent || this.disabled || !this.input) {
return;
}
this.input.click();
});
});
}
protected override render(): TemplateResult {
return html`
<div class="switch ${classMap(this.getRenderClasses())}">
<input
id="switch"
class="touch"
type="checkbox"
role="switch"
aria-label=${(this as ARIAMixin).ariaLabel || nothing}
?checked=${this.selected}
?disabled=${this.disabled}
?required=${this.required}
@input=${this.handleInput}
@change=${this.handleChange} />
<md-focus-ring part="focus-ring" for="switch"></md-focus-ring>
<span class="track"> ${this.renderHandle()} </span>
</div>
`;
}
private getRenderClasses(): ClassInfo {
return {
'selected': this.selected,
'unselected': !this.selected,
'disabled': this.disabled,
};
}
private renderHandle() {
const classes = {
'with-icon': this.showOnlySelectedIcon ? this.selected : this.icons,
};
return html`
${this.renderTouchTarget()}
<span class="handle-container">
<md-ripple for="switch" ?disabled="${this.disabled}"></md-ripple>
<span class="handle ${classMap(classes)}">
${this.shouldShowIcons() ? this.renderIcons() : html``}
</span>
</span>
`;
}
private renderIcons() {
return html`
<div class="icons">
${this.renderOnIcon()}
${this.showOnlySelectedIcon ? html`` : this.renderOffIcon()}
</div>
`;
}
/**
* https://fonts.google.com/icons?selected=Material%20Symbols%20Outlined%3Acheck%3AFILL%400%3Bwght%40500%3BGRAD%400%3Bopsz%4024
*/
private renderOnIcon() {
return html`
<slot class="icon icon--on" name="on-icon">
<svg viewBox="0 0 24 24">
<path
d="M9.55 18.2 3.65 12.3 5.275 10.675 9.55 14.95 18.725 5.775 20.35 7.4Z" />
</svg>
</slot>
`;
}
/**
* https://fonts.google.com/icons?selected=Material%20Symbols%20Outlined%3Aclose%3AFILL%400%3Bwght%40500%3BGRAD%400%3Bopsz%4024
*/
private renderOffIcon() {
return html`
<slot class="icon icon--off" name="off-icon">
<svg viewBox="0 0 24 24">
<path
d="M6.4 19.2 4.8 17.6 10.4 12 4.8 6.4 6.4 4.8 12 10.4 17.6 4.8 19.2 6.4 13.6 12 19.2 17.6 17.6 19.2 12 13.6Z" />
</svg>
</slot>
`;
}
private renderTouchTarget() {
return html`<span class="touch"></span>`;
}
private shouldShowIcons(): boolean {
return this.icons || this.showOnlySelectedIcon;
}
private handleInput(event: Event) {
const target = event.target as HTMLInputElement;
this.selected = target.checked;
// <input> 'input' event bubbles and is composed, don't re-dispatch it.
}
private handleChange(event: Event) {
// <input> 'change' event is not composed, re-dispatch it.
redispatchEvent(this, event);
}
// Writable mixin properties for lit-html binding, needed for lit-analyzer
declare disabled: boolean;
declare name: string;
override [getFormValue]() {
return this.selected ? this.value : null;
}
override [getFormState]() {
return String(this.selected);
}
override formResetCallback() {
// The selected property does not reflect, so the original attribute set by
// the user is used to determine the default value.
this.selected = this.hasAttribute('selected');
}
override formStateRestoreCallback(state: string) {
this.selected = state === 'true';
}
[createValidator]() {
return new CheckboxValidator(() => ({
checked: this.selected,
required: this.required,
}));
}
[getValidityAnchor]() {
return this.input;
}
}