@muonic/muon 0.0.2-experimental-117-75fdff7.0 → 0.0.2-experimental-120-a646376.0

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.
@@ -159,4 +159,16 @@ export class Cta extends ScopedElementsMixin(MuonElement) {
159
159
  ${this._wrapperElement(internal)}
160
160
  `;
161
161
  }
162
+
163
+ get submitTemplate() {
164
+ this.setAttribute('type', 'submit');
165
+
166
+ return this.standardTemplate;
167
+ }
168
+
169
+ get resetTemplate() {
170
+ this.setAttribute('type', 'reset');
171
+
172
+ return this.standardTemplate;
173
+ }
162
174
  }
@@ -0,0 +1 @@
1
+ export { Form } from './src/form-component.js';
@@ -0,0 +1,186 @@
1
+ import { html, MuonElement } from '@muonic/muon';
2
+ import scrollTo from '@muon/utils/scroll';
3
+
4
+ /**
5
+ * A form.
6
+ *
7
+ * @element form
8
+ */
9
+
10
+ export class Form extends MuonElement {
11
+
12
+ constructor() {
13
+ super();
14
+ this._submit = this._submit.bind(this);
15
+ this._reset = this._reset.bind(this);
16
+ }
17
+
18
+ connectedCallback() {
19
+ super.connectedCallback();
20
+
21
+ queueMicrotask(() => {
22
+ this.__checkForFormEl();
23
+ if (this._nativeForm) {
24
+ this.__registerEvents();
25
+ // hack to stop browser validation pop up
26
+ this._nativeForm.setAttribute('novalidate', true);
27
+ // hack to force implicit submission (https://github.com/WICG/webcomponents/issues/187)
28
+ const input = document.createElement('input');
29
+ input.type = 'submit';
30
+ input.hidden = true;
31
+ this._nativeForm.appendChild(input);
32
+ }
33
+ });
34
+ }
35
+
36
+ disconnectedCallback() {
37
+ super.disconnectedCallback();
38
+ this.__teardownEvents();
39
+ }
40
+
41
+ __registerEvents() {
42
+ this._nativeForm?.addEventListener('submit', this._submit);
43
+ this._submitButton?.addEventListener('click', this._submit);
44
+ this._nativeForm?.addEventListener('reset', this._reset);
45
+ }
46
+
47
+ __teardownEvents() {
48
+ this._nativeForm?.removeEventListener('submit', this._submit);
49
+ this._submitButton?.removeEventListener('click', this._submit);
50
+ this._nativeForm?.removeEventListener('reset', this._reset);
51
+ }
52
+
53
+ __checkForFormEl() {
54
+ if (!this._nativeForm) {
55
+ throw new Error(
56
+ 'No form node found. Did you put a <form> element inside?'
57
+ );
58
+ }
59
+ }
60
+
61
+ _reset() {
62
+ this.__checkForFormEl();
63
+
64
+ if (
65
+ !this._resetButton.disabled ||
66
+ !this._resetButton.loading
67
+ ) {
68
+ this._nativeForm.reset();
69
+ }
70
+ }
71
+
72
+ _submit(event) {
73
+ event.preventDefault();
74
+ event.stopPropagation();
75
+
76
+ this.__checkForFormEl();
77
+
78
+ if (
79
+ !this._submitButton ||
80
+ this._submitButton.disabled ||
81
+ this._submitButton.loading
82
+ ) {
83
+ return undefined; // should this be false?
84
+ }
85
+
86
+ const validity = this.validate();
87
+
88
+ if (validity.isValid) {
89
+ this.dispatchEvent(new Event('submit', { cancelable: true }));
90
+ } else {
91
+ const invalidElements = validity.validationStates.filter((state) => {
92
+ return !state.isValid;
93
+ });
94
+
95
+ scrollTo({ element: invalidElements[0].formElement });
96
+ }
97
+
98
+ return validity.isValid;
99
+ }
100
+
101
+ get _nativeForm() {
102
+ return this.querySelector('form');
103
+ }
104
+
105
+ get _submitButton() {
106
+ return this.querySelector('button:not([hidden])[type="submit"]') ||
107
+ this.querySelector('input:not([hidden])[type="submit"]') ||
108
+ this.querySelector('*:not([hidden])[type="submit"]');
109
+ }
110
+
111
+ get _resetButton() {
112
+ return this.querySelector('button:not([hidden])[type="reset"]') ||
113
+ this.querySelector('input:not([hidden])[type="reset"]') ||
114
+ this.querySelector('*:not([hidden])[type="reset"]');
115
+ }
116
+
117
+ _findInputElement(element) {
118
+ if (element.parentElement._inputElement) {
119
+ return element.parentElement;
120
+ }
121
+ // Due to any layout container elements - @TODO - need better logic
122
+ if (element.parentElement.parentElement._inputElement) {
123
+ return element.parentElement.parentElement;
124
+ }
125
+
126
+ return element;
127
+ }
128
+
129
+ validate() {
130
+ let isValid = true;
131
+ // @TODO: Check how this works with form associated
132
+ const validationStates = Array.from(this._nativeForm.elements).reduce((acc, element) => {
133
+ element = this._findInputElement(element);
134
+ const { name } = element;
135
+ const hasBeenSet = acc.filter((el) => el.name === name).length > 0;
136
+
137
+ // For checkboxes and radio button - don't set multiple times (needs checking for native inputs)
138
+ // Ignore buttons (including hidden reset)
139
+ if (
140
+ hasBeenSet ||
141
+ element === this._submitButton ||
142
+ element === this._resetButton ||
143
+ element.type === 'submit'
144
+ ) {
145
+ return acc;
146
+ }
147
+
148
+ if (element.reportValidity) {
149
+ element.reportValidity();
150
+ }
151
+
152
+ const { validity } = element;
153
+
154
+ if (validity) {
155
+ const { value } = element;
156
+ const { valid, validationMessage } = validity;
157
+
158
+ isValid = Boolean(isValid & validity.valid);
159
+
160
+ acc.push({
161
+ name,
162
+ value,
163
+ isValid: valid,
164
+ error: validationMessage,
165
+ validity: validity,
166
+ formElement: element
167
+ });
168
+ }
169
+
170
+ return acc;
171
+ }, []);
172
+
173
+ return {
174
+ isValid,
175
+ validationStates
176
+ };
177
+ }
178
+
179
+ get standardTemplate() {
180
+ return html`
181
+ <div class="form">
182
+ <slot></slot>
183
+ </div>
184
+ `;
185
+ }
186
+ }
@@ -0,0 +1,35 @@
1
+ import { Form } from '@muonic/muon/components/form';
2
+ import setup from '@muonic/muon/storybook/stories';
3
+
4
+ const details = setup('form', Form);
5
+
6
+ export default details.defaultValues;
7
+
8
+ const innerDetail = () => `
9
+ <form>
10
+ <muon-inputter helper="Useful information to help populate this field." validation='["isRequired"]' name="username">
11
+ <label slot="label">Name</label>
12
+ <input type="text" placeholder="e.g. Placeholder" name="username"/>
13
+ </muon-inputter>
14
+
15
+ <muon-inputter value="" helper="How can we help you?" validation="[&quot;isRequired&quot;,&quot;isEmail&quot;]" autocomplete="email">
16
+ <label slot="label">Email</label>
17
+ <input type="email" placeholder="e.g. my@email.com" autocomplete="email" name="useremail">
18
+ <div slot="tip-details">By providing clarification on why this information is necessary.</div>
19
+ </muon-inputter>
20
+
21
+ <label for="user-id">User ID<label>
22
+ <input type="text" id="user-id" name="user-id" required/>
23
+
24
+ <muon-inputter heading="What options do you like?" helper="How can we help you?" validation='["isRequired"]' value="b">
25
+ <input type="checkbox" name="checkboxes" value="a" id="check-01">
26
+ <label for="check-01">Option A</label>
27
+ <input type="checkbox" name="checkboxes" value="b" id="check-02">
28
+ <label for="check-02">Option B</label>
29
+ <div slot="tip-details">By providing clarification on why this information is necessary.</div>
30
+ </muon-inputter>
31
+ <input type="reset" />
32
+ <muon-cta type="submit">Submit</muon-cta>
33
+ <form>`;
34
+
35
+ export const Standard = (args) => details.template(args, innerDetail);
@@ -0,0 +1,36 @@
1
+ import { dedupeMixin } from '@muonic/muon';
2
+
3
+ /**
4
+ * A mixin to associate the component to the enclosing native form.
5
+ *
6
+ * @mixin FormElementMixin
7
+ */
8
+
9
+ export const FormAssociateMixin = dedupeMixin((superClass) =>
10
+ class FormAssociateMixinClass extends superClass {
11
+
12
+ static get properties() {
13
+ return {
14
+ _internals: {
15
+ type: Object,
16
+ state: true
17
+ }
18
+ };
19
+ }
20
+
21
+ static get formAssociated() {
22
+ return true;
23
+ }
24
+
25
+ constructor() {
26
+ super();
27
+ this._internals = this.attachInternals();
28
+ }
29
+
30
+ updated(changedProperties) {
31
+ if (changedProperties.has('value')) {
32
+ this._internals.setFormValue(this.value);
33
+ }
34
+ }
35
+ }
36
+ );
@@ -12,7 +12,8 @@ export const FormElementMixin = dedupeMixin((superClass) =>
12
12
  static get properties() {
13
13
  return {
14
14
  name: {
15
- type: String
15
+ type: String,
16
+ reflect: true
16
17
  },
17
18
 
18
19
  value: {
@@ -28,6 +29,11 @@ export const FormElementMixin = dedupeMixin((superClass) =>
28
29
  type: String
29
30
  },
30
31
 
32
+ _inputElement: {
33
+ type: Boolean,
34
+ state: true
35
+ },
36
+
31
37
  _id: {
32
38
  type: String,
33
39
  state: true
@@ -55,6 +61,7 @@ export const FormElementMixin = dedupeMixin((superClass) =>
55
61
  this.value = '';
56
62
  this.labelID = '';
57
63
  this.heading = '';
64
+ this._inputElement = true;
58
65
  this._id = `${this._randomId}-input`;
59
66
  }
60
67
 
@@ -90,6 +97,9 @@ export const FormElementMixin = dedupeMixin((superClass) =>
90
97
 
91
98
  firstUpdated() {
92
99
  super.firstUpdated();
100
+ if (!this.name) {
101
+ this.name = this._slottedInputs?.[0]?.name ?? '';
102
+ }
93
103
  if (!this._isMultiple) {
94
104
  if (this.labelID?.length > 0) {
95
105
  this._slottedInputs.forEach((slot) => {
@@ -101,7 +111,7 @@ export const FormElementMixin = dedupeMixin((superClass) =>
101
111
  this._slottedLabel?.setAttribute('for', this._id);
102
112
  }
103
113
  }
104
- this.__syncValue();
114
+ this.__syncValue(true);
105
115
 
106
116
  this._boundChangeEvent = (changeEvent) => {
107
117
  this._onChange(changeEvent);
@@ -114,6 +124,7 @@ export const FormElementMixin = dedupeMixin((superClass) =>
114
124
  this._boundInputEvent = (inputEvent) => {
115
125
  this._onInput(inputEvent);
116
126
  };
127
+
117
128
  this._slottedInputs.forEach((input) => {
118
129
  input.addEventListener('change', this._boundChangeEvent);
119
130
  input.addEventListener('blur', this._boundBlurEvent);
@@ -121,13 +132,20 @@ export const FormElementMixin = dedupeMixin((superClass) =>
121
132
  });
122
133
  }
123
134
 
135
+ focus() {
136
+ this.updateComplete.then(() => {
137
+ this._slottedInputs[0].focus();
138
+ });
139
+ }
140
+
124
141
  /**
125
142
  * A method to sync the value property of the component with value of slotted input elements.
126
143
  *
127
- * @returns { void }
144
+ * @param {boolean} firstSync - If first time syncing values.
145
+ * @returns {void}
128
146
  * @private
129
147
  */
130
- __syncValue() {
148
+ __syncValue(firstSync) {
131
149
  if (this._isMultiple) { //Check when component has slotted multi-input
132
150
  if (!this.value && this.__checkedInput) {
133
151
  // If component has null value and slotted input has checked value(s),
@@ -141,6 +159,9 @@ export const FormElementMixin = dedupeMixin((superClass) =>
141
159
  return values.includes(input.value) && !input.checked;
142
160
  }).forEach((input) => {
143
161
  input.checked = true;
162
+ if (firstSync) {
163
+ input.defaultChecked = true;
164
+ }
144
165
  });
145
166
  }
146
167
  } else { //When component has single-input slot
@@ -153,6 +174,10 @@ export const FormElementMixin = dedupeMixin((superClass) =>
153
174
  // If component has not null value and slotted input has null value,
154
175
  // assign the value of the component to value of the slotted input.
155
176
  this._slottedInputs[0].value = this.value;
177
+
178
+ if (firstSync) {
179
+ this._slottedInputs[0].defaultValue = this.value;
180
+ }
156
181
  }
157
182
  }
158
183
  }
@@ -67,6 +67,15 @@ export const ValidationMixin = dedupeMixin((superClass) =>
67
67
  return !this._pristine;
68
68
  }
69
69
 
70
+ reportValidity() {
71
+ return this.validity;
72
+ }
73
+
74
+ get validity() {
75
+ this._pristine = false;
76
+ return this.validate();
77
+ }
78
+
70
79
  /**
71
80
  * A method to validate the value of the form element.
72
81
  *
@@ -95,7 +104,7 @@ export const ValidationMixin = dedupeMixin((superClass) =>
95
104
  }
96
105
 
97
106
  this._validationState = validationState;
98
- this.__updateAllValidity(this.__validationMessage);
107
+ this.__updateAllValidity(this.validationMessage);
99
108
  return this._slottedInputs[0].validity;
100
109
  }
101
110
 
@@ -186,9 +195,8 @@ export const ValidationMixin = dedupeMixin((superClass) =>
186
195
  * A method to get a validation message combind from the validity states.
187
196
  *
188
197
  * @returns {string} - Validation message.
189
- * @private
190
198
  */
191
- get __validationMessage() {
199
+ get validationMessage() {
192
200
  return this._validationState?.filter((state) => {
193
201
  return state?.value;
194
202
  }).map((state) => {
@@ -204,12 +212,12 @@ export const ValidationMixin = dedupeMixin((superClass) =>
204
212
  * @override
205
213
  */
206
214
  get _addValidationMessage() {
207
- if (this.showMessage && this.isDirty && this.__validationMessage) {
215
+ if (this.showMessage && this.isDirty && this.validationMessage) {
208
216
  return html`
209
217
  <div class="validation">
210
218
  ${this._addValidationIcon}
211
219
  <div class="message">
212
- ${this.__validationMessage}
220
+ ${this.validationMessage}
213
221
  </div>
214
222
  </div>`;
215
223
  }
@@ -225,7 +233,7 @@ export const ValidationMixin = dedupeMixin((superClass) =>
225
233
  * @override
226
234
  */
227
235
  get _addValidationListMessage() {
228
- if (this.showMessage && this.isDirty && this.__validationMessage) {
236
+ if (this.showMessage && this.isDirty && this.validationMessage) {
229
237
  const failedValidationStates = this._validationState?.filter((state) => {
230
238
  return state?.value;
231
239
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muonic/muon",
3
- "version": "0.0.2-experimental-117-75fdff7.0",
3
+ "version": "0.0.2-experimental-120-a646376.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -49,12 +49,12 @@
49
49
  "web-component-analyzer": "1.1.6"
50
50
  },
51
51
  "devDependencies": {
52
- "@open-wc/testing": "3.1.2",
53
- "@web/dev-server-esbuild": "0.2.16",
54
- "@web/test-runner": "0.13.27",
52
+ "@open-wc/testing": "3.1.5",
53
+ "@web/dev-server-esbuild": "0.3.0",
54
+ "@web/test-runner": "0.13.30",
55
55
  "@web/test-runner-browserstack": "0.5.0",
56
- "@web/test-runner-playwright": "0.8.8",
57
- "sinon": "13.0.1"
56
+ "@web/test-runner-playwright": "0.8.9",
57
+ "sinon": "14.0.0"
58
58
  },
59
59
  "engines": {
60
60
  "node": ">=16.13.0"
@@ -208,3 +208,39 @@ snapshots["cta implements with disabled"] =
208
208
  `;
209
209
  /* end snapshot cta implements with disabled */
210
210
 
211
+ snapshots["cta implements template `submit`"] =
212
+ `<div
213
+ aria-label="This is a button"
214
+ class="cta submit"
215
+ >
216
+ <span class="label-holder">
217
+ <slot>
218
+ </slot>
219
+ </span>
220
+ <cta-icon
221
+ class="icon"
222
+ name="arrow-right"
223
+ >
224
+ </cta-icon>
225
+ </div>
226
+ `;
227
+ /* end snapshot cta implements template `submit` */
228
+
229
+ snapshots["cta implements template `reset`"] =
230
+ `<div
231
+ aria-label="This is a button"
232
+ class="cta reset"
233
+ >
234
+ <span class="label-holder">
235
+ <slot>
236
+ </slot>
237
+ </span>
238
+ <cta-icon
239
+ class="icon"
240
+ name="arrow-right"
241
+ >
242
+ </cta-icon>
243
+ </div>
244
+ `;
245
+ /* end snapshot cta implements template `reset` */
246
+
@@ -178,9 +178,35 @@ describe('cta', () => {
178
178
  expect(el.type).to.equal('standard', '`type` property has default value `standard`');
179
179
  expect(el.getAttribute('role')).to.not.exist; // eslint-disable-line no-unused-expressions
180
180
  expect(el.getAttribute('tabindex')).to.not.exist; // eslint-disable-line no-unused-expressions
181
- expect(cta.nodeName).to.equal('BUTTON', 'cta is a `div` element');
181
+ expect(cta.nodeName).to.equal('BUTTON', 'cta is a `button` element');
182
182
  expect(cta.href).to.equal(false, 'cta has NO href');
183
183
  expect(cta.getAttribute('tabindex')).to.equal('0', 'has tab index');
184
184
  expect(cta.getAttribute('disabled')).to.equal('', 'cta is disabled');
185
185
  });
186
+
187
+ it('implements template `submit`', async () => {
188
+ // this is to force it to act like it is in a form (aka you need a native button element)
189
+ const el = await fixture(html`<${tag} type="submit">This is a button</${tag}>`);
190
+ await defaultChecks(el);
191
+
192
+ const shadowRoot = el.shadowRoot;
193
+ const cta = shadowRoot.querySelector('.cta');
194
+
195
+ expect(el.type).to.equal('submit', '`type` property has default value `submit`');
196
+ expect(cta.href).to.equal(false, 'cta has NO href');
197
+ expect(cta.getAttribute('disabled')).to.equal(null, 'cta is disabled');
198
+ });
199
+
200
+ it('implements template `reset`', async () => {
201
+ // this is to force it to act like it is in a form (aka you need a native button element)
202
+ const el = await fixture(html`<${tag} type="reset">This is a button</${tag}>`);
203
+ await defaultChecks(el);
204
+
205
+ const shadowRoot = el.shadowRoot;
206
+ const cta = shadowRoot.querySelector('.cta');
207
+
208
+ expect(el.type).to.equal('reset', '`type` property has default value `reset`');
209
+ expect(cta.href).to.equal(false, 'cta has NO href');
210
+ expect(cta.getAttribute('disabled')).to.equal(null, 'cta is disabled');
211
+ });
186
212
  });
@@ -0,0 +1,139 @@
1
+ /* @web/test-runner snapshot v1 */
2
+ export const snapshots = {};
3
+
4
+ snapshots["form implements standard self [with form]"] =
5
+ `<div class="form">
6
+ <slot>
7
+ </slot>
8
+ </div>
9
+ `;
10
+ /* end snapshot form implements standard self [with form] */
11
+
12
+ snapshots["form form with submit button"] =
13
+ `<div class="form">
14
+ <slot>
15
+ </slot>
16
+ </div>
17
+ `;
18
+ /* end snapshot form form with submit button */
19
+
20
+ snapshots["form form with submit input"] =
21
+ `<div class="form">
22
+ <slot>
23
+ </slot>
24
+ </div>
25
+ `;
26
+ /* end snapshot form form with submit input */
27
+
28
+ snapshots["form form with reset button"] =
29
+ `<div class="form">
30
+ <slot>
31
+ </slot>
32
+ </div>
33
+ `;
34
+ /* end snapshot form form with reset button */
35
+
36
+ snapshots["form form with reset input"] =
37
+ `<div class="form">
38
+ <slot>
39
+ </slot>
40
+ </div>
41
+ `;
42
+ /* end snapshot form form with reset input */
43
+
44
+ snapshots["form form submitting"] =
45
+ `<div class="form">
46
+ <slot>
47
+ </slot>
48
+ </div>
49
+ `;
50
+ /* end snapshot form form submitting */
51
+
52
+ snapshots["form form button disabled submitting"] =
53
+ `<div class="form">
54
+ <slot>
55
+ </slot>
56
+ </div>
57
+ `;
58
+ /* end snapshot form form button disabled submitting */
59
+
60
+ snapshots["form form submitting validation"] =
61
+ `<div class="form">
62
+ <slot>
63
+ </slot>
64
+ </div>
65
+ `;
66
+ /* end snapshot form form submitting validation */
67
+
68
+ snapshots["form form submitting with input"] =
69
+ `<div class="form">
70
+ <slot>
71
+ </slot>
72
+ </div>
73
+ `;
74
+ /* end snapshot form form submitting with input */
75
+
76
+ snapshots["form form submitting with inputter"] =
77
+ `<div class="form">
78
+ <slot>
79
+ </slot>
80
+ </div>
81
+ `;
82
+ /* end snapshot form form submitting with inputter */
83
+
84
+ snapshots["form form submitting with inputter parent"] =
85
+ `<div class="form">
86
+ <slot>
87
+ </slot>
88
+ </div>
89
+ `;
90
+ /* end snapshot form form submitting with inputter parent */
91
+
92
+ snapshots["form form with reset cta"] =
93
+ `<div class="form">
94
+ <slot>
95
+ </slot>
96
+ </div>
97
+ `;
98
+ /* end snapshot form form with reset cta */
99
+
100
+ snapshots["form form with submit cta"] =
101
+ `<div class="form">
102
+ <slot>
103
+ </slot>
104
+ </div>
105
+ `;
106
+ /* end snapshot form form with submit cta */
107
+
108
+ snapshots["form form button loading submitting"] =
109
+ `<div class="form">
110
+ <slot>
111
+ </slot>
112
+ </div>
113
+ `;
114
+ /* end snapshot form form button loading submitting */
115
+
116
+ snapshots["form form cta loading submitting"] =
117
+ `<div class="form">
118
+ <slot>
119
+ </slot>
120
+ </div>
121
+ `;
122
+ /* end snapshot form form cta loading submitting */
123
+
124
+ snapshots["form form cta loading reset"] =
125
+ `<div class="form">
126
+ <slot>
127
+ </slot>
128
+ </div>
129
+ `;
130
+ /* end snapshot form form cta loading reset */
131
+
132
+ snapshots["form form submitting with inputter [validate]"] =
133
+ `<div class="form">
134
+ <slot>
135
+ </slot>
136
+ </div>
137
+ `;
138
+ /* end snapshot form form submitting with inputter [validate] */
139
+
@@ -0,0 +1,324 @@
1
+ /* eslint-disable no-undef */
2
+ import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
3
+ import sinon from 'sinon';
4
+ import { defaultChecks } from '../../helpers';
5
+ import { Form } from '@muonic/muon/components/form';
6
+ import { Inputter } from '@muonic/muon/components/inputter';
7
+ import { Cta } from '@muonic/muon/components/cta';
8
+
9
+ const tagName = defineCE(Form);
10
+ const tag = unsafeStatic(tagName);
11
+
12
+ const inputterTagName = defineCE(Inputter);
13
+ const inputterTag = unsafeStatic(inputterTagName);
14
+
15
+ const ctaTagName = defineCE(Cta);
16
+ const ctaTag = unsafeStatic(ctaTagName);
17
+
18
+ describe('form', () => {
19
+ afterEach(() => {
20
+ sinon.restore();
21
+ });
22
+
23
+ it('implements standard self [no form]', () => {
24
+ const el = new Form();
25
+ const message = 'No form node found. Did you put a <form> element inside?';
26
+ expect(() => el.__checkForFormEl()).to.throw(message, 'no `form` added');
27
+ expect(() => el._reset()).to.throw(message, 'no `form` added for reset');
28
+ });
29
+
30
+ it('implements standard self [with form]', async () => {
31
+ const el = await fixture(html`<${tag}><form></form></${tag}>`);
32
+ await defaultChecks(el);
33
+
34
+ const shadowRoot = el.shadowRoot;
35
+ const form = shadowRoot.querySelector('.form');
36
+ const slot = form.querySelector('slot');
37
+ const hiddenSubmit = el.querySelector('input[hidden][type="submit"]');
38
+
39
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
40
+ expect(el._submitButton).to.equal(null, 'no `submit` button added');
41
+ expect(el._resetButton).to.equal(null, 'no `reset` button added');
42
+ expect(form).to.not.be.null; // eslint-disable-line no-unused-expressions
43
+ expect(slot).to.not.be.null; // eslint-disable-line no-unused-expressions
44
+ expect(hiddenSubmit).to.not.be.null; // eslint-disable-line no-unused-expressions
45
+ expect(el._nativeForm).to.not.be.null; // eslint-disable-line no-unused-expressions
46
+ });
47
+
48
+ it('form with submit button', async () => {
49
+ const el = await fixture(html`<${tag}><form><button type="submit">submit</button></form></${tag}>`);
50
+ await defaultChecks(el);
51
+
52
+ const shadowRoot = el.shadowRoot;
53
+ const form = shadowRoot.querySelector('.form');
54
+ const slot = form.querySelector('slot');
55
+ const hiddenSubmit = el.querySelector('input[hidden][type="submit"]');
56
+
57
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
58
+ expect(el._submitButton.nodeName).to.equal('BUTTON', 'has submit button');
59
+ expect(el._submitButton.type).to.equal('submit', 'has submit type button');
60
+ expect(el._resetButton).to.equal(null, 'no `reset` button added');
61
+ expect(form).to.not.be.null; // eslint-disable-line no-unused-expressions
62
+ expect(slot).to.not.be.null; // eslint-disable-line no-unused-expressions
63
+ expect(hiddenSubmit).to.not.be.null; // eslint-disable-line no-unused-expressions
64
+ });
65
+
66
+ it('form with submit cta', async () => {
67
+ const el = await fixture(html`<${tag}><form><${ctaTag} type="submit">submit</${ctaTag}></form></${tag}>`);
68
+ await defaultChecks(el);
69
+
70
+ const shadowRoot = el.shadowRoot;
71
+ const form = shadowRoot.querySelector('.form');
72
+ const slot = form.querySelector('slot');
73
+ const hiddenSubmit = el.querySelector('input[hidden][type="submit"]');
74
+
75
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
76
+ expect(el._submitButton.nodeName.toLowerCase()).to.equal(ctaTagName, 'has submit cta');
77
+ expect(el._submitButton.type).to.equal('submit', 'has submit type button');
78
+ expect(el._resetButton).to.equal(null, 'no `reset` cta added');
79
+ expect(form).to.not.be.null; // eslint-disable-line no-unused-expressions
80
+ expect(slot).to.not.be.null; // eslint-disable-line no-unused-expressions
81
+ expect(hiddenSubmit).to.not.be.null; // eslint-disable-line no-unused-expressions
82
+ });
83
+
84
+ it('form with submit input', async () => {
85
+ const el = await fixture(html`<${tag}><form><input type="submit">submit</input></form></${tag}>`);
86
+ await defaultChecks(el);
87
+
88
+ const shadowRoot = el.shadowRoot;
89
+ const form = shadowRoot.querySelector('.form');
90
+ const slot = form.querySelector('slot');
91
+ const hiddenSubmit = el.querySelector('input[hidden][type="submit"]');
92
+
93
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
94
+ expect(el._submitButton.nodeName).to.equal('INPUT', 'has submit input');
95
+ expect(el._submitButton.type).to.equal('submit', 'has submit type input');
96
+ expect(el._resetButton).to.equal(null, 'no `reset` button added');
97
+ expect(form).to.not.be.null; // eslint-disable-line no-unused-expressions
98
+ expect(slot).to.not.be.null; // eslint-disable-line no-unused-expressions
99
+ expect(hiddenSubmit).to.not.be.null; // eslint-disable-line no-unused-expressions
100
+ });
101
+
102
+ it('form with reset button', async () => {
103
+ const el = await fixture(html`<${tag}><form><button type="reset">reset</button></form></${tag}>`);
104
+ await defaultChecks(el);
105
+
106
+ const shadowRoot = el.shadowRoot;
107
+ const form = shadowRoot.querySelector('.form');
108
+ const slot = form.querySelector('slot');
109
+ const hiddenSubmit = el.querySelector('input[hidden][type="submit"]');
110
+
111
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
112
+ expect(el._resetButton.nodeName).to.equal('BUTTON', 'has reset button');
113
+ expect(el._resetButton.type).to.equal('reset', 'has reset type button');
114
+ expect(el._submitButton).to.equal(null, 'no `submit` button added');
115
+ expect(form).to.not.be.null; // eslint-disable-line no-unused-expressions
116
+ expect(slot).to.not.be.null; // eslint-disable-line no-unused-expressions
117
+ expect(hiddenSubmit).to.not.be.null; // eslint-disable-line no-unused-expressions
118
+ });
119
+
120
+ it('form with reset cta', async () => {
121
+ const el = await fixture(html`<${tag}><form><${ctaTag} type="reset">reset</${ctaTag}></form></${tag}>`);
122
+ await defaultChecks(el);
123
+
124
+ const shadowRoot = el.shadowRoot;
125
+ const form = shadowRoot.querySelector('.form');
126
+ const slot = form.querySelector('slot');
127
+ const hiddenSubmit = el.querySelector('input[hidden][type="submit"]');
128
+
129
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
130
+ expect(el._resetButton.nodeName.toLowerCase()).to.equal(ctaTagName, 'has reset cta');
131
+ expect(el._resetButton.type).to.equal('reset', 'has reset type button');
132
+ expect(el._submitButton).to.equal(null, 'no `submit` cta added');
133
+ expect(form).to.not.be.null; // eslint-disable-line no-unused-expressions
134
+ expect(slot).to.not.be.null; // eslint-disable-line no-unused-expressions
135
+ expect(hiddenSubmit).to.not.be.null; // eslint-disable-line no-unused-expressions
136
+ });
137
+
138
+ it('form with reset input', async () => {
139
+ const el = await fixture(html`<${tag}><form><input type="reset">reset</input></form></${tag}>`);
140
+ await defaultChecks(el);
141
+
142
+ const shadowRoot = el.shadowRoot;
143
+ const form = shadowRoot.querySelector('.form');
144
+ const slot = form.querySelector('slot');
145
+ const hiddenSubmit = el.querySelector('input[hidden][type="submit"]');
146
+
147
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
148
+ expect(el._resetButton.nodeName).to.equal('INPUT', 'has reset input');
149
+ expect(el._resetButton.type).to.equal('reset', 'has reset type input');
150
+ expect(el._submitButton).to.equal(null, 'no `submit` button added');
151
+ expect(form).to.not.be.null; // eslint-disable-line no-unused-expressions
152
+ expect(slot).to.not.be.null; // eslint-disable-line no-unused-expressions
153
+ expect(hiddenSubmit).to.not.be.null; // eslint-disable-line no-unused-expressions
154
+ });
155
+
156
+ it('form submitting', async () => {
157
+ const submitSpy = sinon.spy();
158
+ const el = await fixture(html`<${tag} @submit=${submitSpy}><form><button type="submit">submit</button></form></${tag}>`);
159
+ const submitBtn = el.querySelector('button');
160
+
161
+ await defaultChecks(el);
162
+
163
+ submitBtn.click();
164
+
165
+ expect(submitSpy.callCount).to.equal(1);
166
+ });
167
+
168
+ it('form submitting with input', async () => {
169
+ const submitSpy = sinon.spy();
170
+ const el = await fixture(html`<${tag} @submit=${submitSpy}><form><label for="foo">Bar</label><input id="foo" type="text" required /><button type="submit">submit</button></form></${tag}>`);
171
+ const submitBtn = el.querySelector('button');
172
+
173
+ await defaultChecks(el);
174
+
175
+ submitBtn.click();
176
+
177
+ expect(submitSpy.callCount).to.equal(0);
178
+ });
179
+
180
+ it('form submitting with inputter [validate]', async () => {
181
+ const submitSpy = sinon.spy();
182
+ const el = await fixture(html`
183
+ <${tag} @submit=${submitSpy}>
184
+ <form>
185
+ <${inputterTag} validation='["isRequired"]' value="foo" name="bar">
186
+ <label slot="label" for="foo">Bar</label>
187
+ <input id="foo" type="text" />
188
+ </${inputterTag}>
189
+ <button type="submit">submit</button>
190
+ </form>
191
+ </${tag}>
192
+ `);
193
+
194
+ const inputter = document.querySelector(inputterTagName);
195
+
196
+ await defaultChecks(el);
197
+
198
+ expect(el.validate()).to.deep.equal(
199
+ {
200
+ isValid: true,
201
+ validationStates:
202
+ [
203
+ {
204
+ name: 'bar',
205
+ value: 'foo',
206
+ error: undefined,
207
+ isValid: true,
208
+ formElement: inputter,
209
+ validity: inputter.validity
210
+ }
211
+ ]
212
+ }
213
+ );
214
+ });
215
+
216
+ it('form submitting with inputter', async () => {
217
+ const submitSpy = sinon.spy();
218
+ const el = await fixture(html`
219
+ <${tag} @submit=${submitSpy}>
220
+ <form>
221
+ <${inputterTag} validation='["isRequired"]'>
222
+ <label slot="label" for="foo">Bar</label>
223
+ <input id="foo" type="text" />
224
+ </${inputterTag}>
225
+ <button type="submit">submit</button>
226
+ </form>
227
+ </${tag}>
228
+ `);
229
+ const submitBtn = el.querySelector('button');
230
+
231
+ await defaultChecks(el);
232
+
233
+ submitBtn.click();
234
+
235
+ expect(submitSpy.callCount).to.equal(0);
236
+ });
237
+
238
+ it('form submitting with inputter parent', async () => {
239
+ const submitSpy = sinon.spy();
240
+ const el = await fixture(html`
241
+ <${tag} @submit=${submitSpy}>
242
+ <form>
243
+ <${inputterTag} heading="foo" validation='["isRequired"]'>
244
+ <div>
245
+ <input type="checkbox" name="checkboxes" value="a" id="check-01">
246
+ <label for="check-01">Option A</label>
247
+ <input type="checkbox" name="checkboxes" value="b" id="check-02">
248
+ <label for="check-02">Option B</label>
249
+ </div>
250
+ </${inputterTag}>
251
+ <button type="submit">submit</button>
252
+ </form>
253
+ </${tag}>
254
+ `);
255
+ const submitBtn = el.querySelector('button');
256
+
257
+ await defaultChecks(el);
258
+
259
+ submitBtn.click();
260
+
261
+ expect(submitSpy.callCount).to.equal(0);
262
+ });
263
+
264
+ it('form button disabled submitting', async () => {
265
+ const submitSpy = sinon.spy();
266
+ const el = await fixture(html`<${tag} @submit=${submitSpy}><form><button disabled type="submit">submit</button></form></${tag}>`);
267
+ const submitBtn = el.querySelector('button');
268
+
269
+ await defaultChecks(el);
270
+
271
+ submitBtn.click();
272
+
273
+ expect(submitSpy.callCount).to.equal(0);
274
+ });
275
+
276
+ it('form cta loading submitting', async () => {
277
+ const submitSpy = sinon.spy();
278
+ const el = await fixture(html`<${tag} @submit=${submitSpy}><form><${ctaTag} loading type="submit">submit</${ctaTag}></form></${tag}>`);
279
+ const submitBtn = el.querySelector(ctaTagName);
280
+
281
+ await defaultChecks(el);
282
+
283
+ submitBtn.click();
284
+
285
+ expect(submitSpy.callCount).to.equal(0);
286
+ });
287
+
288
+ it('form with reset button', async () => {
289
+ const el = await fixture(html`<${tag}><form><label for="foo">Bar</label><input id="foo" type="text" value="foo" /><button type="reset">reset</button></form></${tag}>`);
290
+ const input = el.querySelector('input');
291
+ const resetBtn = el.querySelector('button');
292
+
293
+ await defaultChecks(el);
294
+
295
+ expect(input.value).to.equal('foo', 'default input value');
296
+
297
+ input.value = '';
298
+
299
+ expect(input.value).to.equal('', 'changed input value');
300
+
301
+ resetBtn.click();
302
+
303
+ expect(input.value).to.equal('foo', 'reset input value');
304
+ });
305
+
306
+ it('form cta loading reset', async () => {
307
+ const submitSpy = sinon.spy();
308
+ const el = await fixture(html`<${tag} @submit=${submitSpy}><form><label for="foo">Bar</label><input id="foo" type="text" value="foo" /><${ctaTag} loading type="reset">submit</${ctaTag}></form></${tag}>`);
309
+ const input = el.querySelector('input');
310
+ const resetBtn = el.querySelector(ctaTagName);
311
+
312
+ await defaultChecks(el);
313
+
314
+ expect(input.value).to.equal('foo', 'default input value');
315
+
316
+ input.value = '';
317
+
318
+ expect(input.value).to.equal('', 'changed input value');
319
+
320
+ resetBtn.click();
321
+
322
+ expect(input.value).to.equal('', 'no reset input value');
323
+ });
324
+ });
@@ -0,0 +1,31 @@
1
+ export default ({
2
+ element, // the element to scroll to
3
+ focusOn = element // optional element that gets focus
4
+ } = {}) => {
5
+ const motionQuery = window.matchMedia('(prefers-reduced-motion)');
6
+
7
+ setTimeout(function () { // Firefox needs this in order to allow the event queue to clear before scrolling
8
+ if (!motionQuery.matches) {
9
+ element.scrollIntoView({
10
+ behavior: 'smooth'
11
+ });
12
+ }
13
+ }, 0);
14
+
15
+ focusOn.focus({
16
+ preventScroll: !motionQuery.matches
17
+ });
18
+
19
+ // https://css-tricks.com/smooth-scrolling-accessibility/
20
+ // "In order for this to work on non-focusable target elements (section, div, span, h1-6, ect),
21
+ // we have to set tabindex="-1" on them"
22
+
23
+ if (focusOn !== document.activeElement) { // Checking if the target was focused
24
+ const tabIndex = focusOn.getAttribute('tabindex');
25
+ focusOn.setAttribute('tabindex', '-1'); // Adding tabindex for elements not focusable
26
+ focusOn.focus({
27
+ preventScroll: !motionQuery.matches
28
+ }); // Setting focus
29
+ focusOn.setAttribute('tabindex', tabIndex); //resetting tabindex
30
+ }
31
+ };