@shortfuse/materialdesignweb 0.7.1 → 0.7.4

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.
@@ -1,5 +1,7 @@
1
1
  /* https://html.spec.whatwg.org/multipage/form-control-infrastructure.html */
2
2
 
3
+ import { cloneAttributeCallback } from '../core/CustomElement.js';
4
+
3
5
  import FormAssociatedMixin from './FormAssociatedMixin.js';
4
6
 
5
7
  /** @typedef {import('../core/CustomElement.js').default} CustomElement */
@@ -12,264 +14,207 @@ import FormAssociatedMixin from './FormAssociatedMixin.js';
12
14
  * @param {ReturnType<import('./StateMixin.js').default>} Base
13
15
  */
14
16
  export default function ControlMixin(Base) {
15
- class Control extends Base.mixin(FormAssociatedMixin) {
16
- /** @return {Iterable<string>} */
17
- static get observedAttributes() {
18
- return [
19
- ...super.observedAttributes,
20
- 'aria-label',
21
- ...this.valueChangingContentAttributes,
22
- ...this.clonedContentAttributes,
23
- ];
24
- }
25
-
26
- static controlTagName = 'input';
27
-
28
- static controlVoidElement = true;
29
-
30
- /** @type {string[]} */
31
- static clonedContentAttributes = [
32
- 'autocomplete', 'name', 'readonly', 'required',
33
- ];
34
-
35
- /** @type {string[]} */
36
- static valueChangingContentAttributes = [];
37
-
38
- static {
39
- // eslint-disable-next-line no-unused-expressions
40
- this.css`
41
-
42
- :host {
43
- display: inline-flex;
44
- }
45
-
46
- /* Remove Firefox inner */
47
- :host(::-moz-focus-inner) {
48
- border: 0;
49
- }
50
-
51
- #label {
52
- display: contents;
53
-
54
- pointer-events: none;
55
- }
56
-
57
- #control {
58
- /* Control is the touch target */
59
- /* Firefox requires at least 1px "visible" for screen reading */
60
- /* Safari will not allow interaction with 0 opacity */
61
- /* Chrome will not focus with visibility:hidden */
62
-
63
- position: absolute;
64
- inset: 50%;
65
- /* --mdw-device-pixel-ratio: 1; */
66
-
67
- block-size: 100%;
68
- min-block-size: 48px;
69
- inline-size:100%;
70
- min-inline-size: 48px;
71
- margin: 0;
72
- border: 0;
73
- padding: 0;
74
-
75
- -webkit-appearance: none;
76
- -moz-appearance: none;
77
- appearance: none;
78
-
79
- cursor: auto;
80
- outline: none;
81
-
82
- pointer-events: auto;
83
-
84
- transform: translateX(-50%) translateY(-50%);
85
-
86
- /* Safari and Chrome will emit two click events if not at top of stack */
87
- /* Allows up to 3 other layers (eg: ripple, outline) */
88
- z-index: 4;
89
-
90
- background-color: transparent;
91
-
92
- border-radius: 0;
93
- color: transparent;
94
- }
95
-
96
- #control::-moz-focus-inner {
97
- border: 0;
98
- }
99
-
100
- `;
101
- }
17
+ return Base
18
+ .mixin(FormAssociatedMixin)
19
+ .extend()
20
+ .observe({
21
+ ariaLabel: 'string',
22
+ })
23
+ .set({
24
+ delegatesFocus: true,
25
+ focusableOnDisabled: false,
26
+ controlTagName: 'input',
27
+ controlVoidElement: true,
28
+ })
29
+ .methods({
30
+ onValueChangingContentAttribute() {
31
+ const control = /** @type {HTMLControlElement} */ (this.refs.control);
102
32
 
103
- /** @param {any[]} args */
104
- constructor(...args) {
105
- super(...args);
106
- /** @type {string} */
107
- this._value = this._control.value;
108
- // Expose this element as focusable
109
- if (!this.hasAttribute('tabindex')) {
110
- this.tabIndex = 0;
111
- }
112
- }
113
-
114
- /** @type {CustomElement['attributeChangedCallback']} */
115
- attributeChangedCallback(name, oldValue, newValue) {
116
- super.attributeChangedCallback(name, oldValue, newValue);
117
- if (this.static.clonedContentAttributes.includes(name)) {
118
- if (newValue == null) {
119
- this._control.removeAttribute(name);
120
- } else {
121
- this._control.setAttribute(name, newValue);
122
- }
123
- }
124
-
125
- if (this.static.valueChangingContentAttributes.includes(name)) {
126
33
  if (!this.hasAttribute('value')) {
127
34
  // Force HTMLInputElement to recalculate default
128
35
  // Unintended effect of incrementally changing attributes (eg: range)
129
- this._control.setAttribute('value', '');
36
+ control.removeAttribute('value'); // Firefox will not run steps unless value is changed (remove first)
37
+ control.setAttribute('value', ''); // Chrome needs to know to reset
130
38
  }
131
39
  // Changing control attribute may change the value (eg: min/max)
132
- this._value = this._control.value;
133
- }
134
- }
135
-
136
- /** @type {HTMLControlElement} */
137
- get _control() { return this.refs.control; }
138
-
139
- /**
140
- * @param {Partial<this>} data
141
- * @return {string}
142
- */
143
- computeAriaLabelledBy({ ariaLabel }) {
144
- if (ariaLabel) return null;
145
- return '#slot';
146
- }
147
-
148
- get stateTargetElement() { return this._control; }
149
-
150
- click() {
40
+ this._value = control.value;
41
+ },
42
+ /** @type {HTMLElement['focus']} */
43
+ focus(...options) {
44
+ this.refs.control.focus(...options);
45
+ },
151
46
  /** Redirect click requests to control itself */
152
- this._control.click();
153
- }
154
-
155
- static {
156
- this.on({
157
- // Wait until controlTagName is settled before templating
158
- composed({ template, html }) {
159
- template.append(html`
160
- <label id=label disabled={disabledState}>
161
- <${this.static.controlTagName} id=control aria-labelledby={computeAriaLabelledBy} aria-label={ariaLabel}
162
- >${this.static.controlVoidElement ? '' : `</${this.static.controlTagName}>`}
163
- </label>
164
- `);
165
- },
166
- disabledStateChanged(oldValue, newValue) {
167
- this._control.setAttribute('aria-disabled', `${newValue}`);
168
- if (!this.focusableOnDisabled) {
169
- this._control.disabled = newValue;
170
- if (newValue) {
171
- this.tabIndex = 0;
172
- } else {
173
- this.removeAttribute('tabindex');
174
- }
47
+ click() {
48
+ console.log('ControlMixin: Click');
49
+ this.refs.control.click();
50
+ },
51
+ })
52
+ .define({
53
+ stateTargetElement() { return this.refs.control; },
54
+ form() { return this.elementInternals.form; },
55
+ validity() { return this.elementInternals.validity; },
56
+ validationMessage() { return this.elementInternals.validationMessage; },
57
+ willValidate() { return this.elementInternals.willValidate; },
58
+ labels() { return this.elementInternals.labels; },
59
+ })
60
+ .methods({
61
+ checkValidity() {
62
+ const control = /** @type {HTMLControlElement} */ (this.refs.control);
63
+ const validityState = control.checkValidity();
64
+ /** @type {Partial<ValidityState>} */
65
+ const newValidity = {};
66
+
67
+ // eslint-disable-next-line guard-for-in
68
+ for (const key in control.validity) {
69
+ // @ts-ignore Skip cast
70
+ newValidity[key] = control.validity[key];
71
+ }
72
+ this.elementInternals.setValidity(newValidity, control.validationMessage);
73
+ this._invalid = !validityState;
74
+ this._validationMessage = control.validationMessage;
75
+ this._badInput = control.validity.badInput;
76
+ return validityState;
77
+ },
78
+ reportValidity() {
79
+ this.checkValidity();
80
+ /** @type {HTMLControlElement} */ (this.refs.control).reportValidity();
81
+ return this.elementInternals.reportValidity();
82
+ },
83
+ /**
84
+ * @param {string} error
85
+ * @return {void}
86
+ */
87
+ setCustomValidity(error) {
88
+ /** @type {HTMLControlElement} */ (this.refs.control).setCustomValidity(error);
89
+ this.checkValidity();
90
+ },
91
+
92
+ })
93
+ .on({
94
+ // Wait until controlTagName is settled before templating
95
+ composed({ template, html }) {
96
+ template.append(html`
97
+ <label id=label disabled={disabledState}>
98
+ <${this.controlTagName} id=control
99
+ aria-labelledby=${({ ariaLabel }) => (ariaLabel ? null : '#slot')}
100
+ aria-label={ariaLabel}
101
+ >${this.controlVoidElement ? '' : `</${this.controlTagName}>`}
102
+ </label>
103
+ `);
104
+ },
105
+ disabledStateChanged(oldValue, newValue) {
106
+ const control = /** @type {HTMLControlElement} */ (this.refs.control);
107
+ control.setAttribute('aria-disabled', `${newValue}`);
108
+ if (!this.focusableOnDisabled) {
109
+ control.disabled = newValue;
110
+ if (newValue) {
111
+ this.tabIndex = 0;
112
+ } else {
113
+ this.removeAttribute('tabindex');
175
114
  }
176
- },
177
- });
178
- this.childEvents({
179
- control: {
180
- input({ currentTarget }) {
181
- const control = /** @type {HTMLControlElement} */ (currentTarget);
182
- if (this.validity.valid) {
115
+ }
116
+ },
117
+ constructed() {
118
+ const control = /** @type {HTMLControlElement} */ (this.refs.control);
119
+ this._value = control.value;
120
+ },
121
+ connected() {
122
+ // Expose this element as focusable
123
+ if (!this.hasAttribute('tabindex')) {
124
+ this.tabIndex = 0;
125
+ }
126
+ },
127
+ attrs: {
128
+ autocomplete: cloneAttributeCallback('autocomplete', 'control'),
129
+ name: cloneAttributeCallback('name', 'control'),
130
+ readonly: cloneAttributeCallback('readonly', 'control'),
131
+ required: cloneAttributeCallback('required', 'control'),
132
+ },
133
+ })
134
+ .childEvents({
135
+ control: {
136
+ input({ currentTarget }) {
137
+ console.debug('ControlMixin: input');
138
+ const control = /** @type {HTMLControlElement} */ (currentTarget);
139
+ if (this.validity.valid) {
183
140
  // Track internally
184
- control.checkValidity();
185
- this._badInput = control.validity.badInput;
186
- } else {
141
+ control.checkValidity();
142
+ this._badInput = control.validity.badInput;
143
+ } else {
187
144
  // Perform check in case user has validated
188
- this.checkValidity();
189
- }
190
- this._value = control.value;
191
- },
192
- change({ currentTarget }) {
193
- const control = /** @type {HTMLControlElement} */ (currentTarget);
194
- this._value = control.value;
195
145
  this.checkValidity();
196
- // Change event is NOT composed. Needs to escape shadow DOM
197
- this.dispatchEvent(new Event('change', { bubbles: true }));
198
- },
146
+ }
147
+ this._value = control.value;
199
148
  },
200
- });
201
- }
202
-
203
- /** @type {HTMLElement['focus']} */
204
- focus(...options) {
205
- super.focus(...options);
206
- this.refs.control.focus(...options);
207
- }
208
-
209
- /**
210
- * @template {typeof Control & ReturnType<import('./RippleMixin.js').default> & ReturnType<import('./FormAssociatedMixin.js').default>} T
211
- * @return {T}
212
- */
213
- get static() {
214
- return /** @type {T} */ (/** @type {unknown} */ (super.static));
215
- }
216
-
217
- get form() { return this.elementInternals.form; }
218
-
219
- // get name() { return this.getAttribute('name'); }
220
- get value() {
221
- return this._value;
222
- }
223
-
224
- set value(v) {
225
- this._valueDirty = true;
226
- this._control.value = v;
227
- this._value = this._control.value;
228
- }
229
-
230
- get validity() { return this.elementInternals.validity; }
231
-
232
- get validationMessage() { return this.elementInternals.validationMessage; }
233
-
234
- get willValidate() { return this.elementInternals.willValidate; }
235
-
236
- checkValidity() {
237
- const validityState = this._control.checkValidity();
238
- /** @type {Partial<ValidityState>} */
239
- const newValidity = {};
240
-
241
- // eslint-disable-next-line guard-for-in
242
- for (const key in this._control.validity) {
243
- // @ts-ignore Skip cast
244
- newValidity[key] = this._control.validity[key];
149
+ change({ currentTarget }) {
150
+ console.debug('ControlMixin: change');
151
+ const control = /** @type {HTMLControlElement} */ (currentTarget);
152
+ this._valueDirty = true;
153
+ this._value = control.value;
154
+ this.checkValidity();
155
+ // Change event is NOT composed. Needs to escape shadow DOM
156
+ this.dispatchEvent(new Event('change', { bubbles: true }));
157
+ },
158
+ },
159
+ })
160
+ .css`
161
+ :host {
162
+ display: inline-flex;
245
163
  }
246
- this.elementInternals.setValidity(newValidity, this._control.validationMessage);
247
- this._invalid = !validityState;
248
- this._validationMessage = this._control.validationMessage;
249
- this._badInput = this._control.validity.badInput;
250
- return validityState;
251
- }
252
-
253
- reportValidity() {
254
- this.checkValidity();
255
- this._control.reportValidity();
256
- return this.elementInternals.reportValidity();
257
- }
258
-
259
- /**
260
- * @param {string} error
261
- * @return {void}
262
- */
263
- setCustomValidity(error) {
264
- this._control.setCustomValidity(error);
265
- this.checkValidity();
266
- }
267
-
268
- get labels() { return this.elementInternals.labels; }
269
- }
270
- Control.prototype.ariaLabel = Control.prop('ariaLabel');
271
- Control.prototype.delegatesFocus = true;
272
- Control.prototype.focusableOnDisabled = false;
273
-
274
- return Control;
164
+
165
+ /* Remove Firefox inner */
166
+ :host(::-moz-focus-inner) {
167
+ border: 0;
168
+ }
169
+
170
+ #label {
171
+ display: contents;
172
+
173
+ pointer-events: none;
174
+ }
175
+
176
+ #control {
177
+ /* Control is the touch target */
178
+ /* Firefox requires at least 1px "visible" for screen reading */
179
+ /* Safari will not allow interaction with 0 opacity */
180
+ /* Chrome will not focus with visibility:hidden */
181
+
182
+ position: absolute;
183
+ inset: 50%;
184
+ /* --mdw-device-pixel-ratio: 1; */
185
+
186
+ block-size: 100%;
187
+ min-block-size: 48px;
188
+ inline-size:100%;
189
+ min-inline-size: 48px;
190
+ margin: 0;
191
+ border: 0;
192
+ padding: 0;
193
+
194
+ -webkit-appearance: none;
195
+ -moz-appearance: none;
196
+ appearance: none;
197
+
198
+ cursor: auto;
199
+ outline: none;
200
+
201
+ pointer-events: auto;
202
+
203
+ transform: translateX(-50%) translateY(-50%);
204
+
205
+ /* Safari and Chrome will emit two click events if not at top of stack */
206
+ /* Allows up to 3 other layers (eg: ripple, outline) */
207
+ z-index: 4;
208
+
209
+ background-color: transparent;
210
+
211
+ border-radius: 0;
212
+ color: transparent;
213
+ }
214
+
215
+ #control::-moz-focus-inner {
216
+ border: 0;
217
+ }
218
+
219
+ `;
275
220
  }