@brightspace-ui/core 3.117.0 → 3.118.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.
@@ -2,16 +2,15 @@
2
2
 
3
3
  There are several components and mixins available to help make working with forms easier.
4
4
 
5
- ## Validating and Submitting Forms Using `<d2l-form>` and `<d2l-form-native>`
5
+ ## Validating and Submitting Forms Using `<d2l-form>`
6
6
 
7
- These two components behave much like a native `<form>` element, grouping nested form elements together and controlling how their data is validated and submitted. Unlike native `<form>`s, they support our custom form elements.
7
+ Much like a native `<form>` element, `<d2l-form>` groups nested form elements together and controls how their data is validated and submitted. Unlike native `<form>`s, it supports our custom form elements.
8
8
 
9
9
  - [d2l-form](docs/form.md#form-d2l-form): supports aggregation of nested forms for validation and submission
10
- - [d2l-form-native](docs/form.md#native-form-d2l-form-native): emulates how a native `<form>` submits, but supports our custom form elements
11
10
 
12
11
  ## Custom Elements in Forms
13
12
 
14
- To allow custom elements to participate in `<d2l-form>` and `<d2l-form-native>` submission and validation, use the [FormElementMixin](docs/form-element-mixin.md).
13
+ To allow custom elements to participate in `<d2l-form>` submission and validation, use the [FormElementMixin](docs/form-element-mixin.md).
15
14
 
16
15
  If your custom element uses other nested *custom* form elements internally, read about [form element nesting](docs/form-element-nesting.md).
17
16
 
@@ -1,14 +1,14 @@
1
1
  # FormElementMixin
2
2
 
3
- The `FormElementMixin` allows the user to turn a custom web component into a form element.
3
+ The `FormElementMixin` allows custom web components to participate as a form element.
4
4
 
5
5
  This means that the component will be able to:
6
6
  1. Perform live self-validation when being edited.
7
- 1. Participate in validation and submission when added to `d2l-form` or `d2l-form-native`.
7
+ 1. Participate in validation and submission when added to `d2l-form`.
8
8
 
9
- All custom form elements must provide a form value that will be submitted during `d2l-form` or `d2l-form-native` submission.
10
- However, participating in validation is optional. Some custom form elements may not need validation because they have no
11
- invalid state.
9
+ All custom form elements must provide a form value that will be submitted during `d2l-form` submission.
10
+
11
+ Participating in validation is, however, optional. Some custom form elements may not need validation because they have no invalid state.
12
12
 
13
13
  ## Form Value
14
14
 
@@ -24,7 +24,6 @@ invalid state.
24
24
  - When an `Object` is provided, all keys-value pairs will be submitted. To avoid collision it is recommended that you prefix each key with the component's `name` property value.
25
25
  1. A single value: `'my-value'`
26
26
  - When a single value is provided a key-value pair will be submitted with the component's `name` property value as the key. If `name` does not have a value, the value will not be submitted.
27
- - **Note:** When using `d2l-form-native` all values will be converted to `String`s. Complex value may be used with `d2l-form` but should only be used if absolutely required to maintain compatibility with `d2l-form-native`.
28
27
 
29
28
  ## Validation
30
29
 
@@ -62,7 +61,6 @@ invalid state.
62
61
 
63
62
  ## Usage
64
63
 
65
-
66
64
  ```javascript
67
65
  import { FormElementMixin } from '@brightspace-ui/core/form/form-element-mixin.js';
68
66
 
@@ -199,7 +199,7 @@ render() {
199
199
 
200
200
  **8. Support Validate:**
201
201
 
202
- The last step is to ensure that calling validate on the parent will result in the nested components being validated. This is not required for self-validation but is important to ensure your custom form element works properly when used inside a [`d2l-form`](../form.md) or [`d2l-form-native`](../form-native.md).
202
+ The last step is to ensure that calling validate on the parent will result in the nested components being validated. This is not required for self-validation but is important to ensure your custom form element works properly when used inside a [`d2l-form`].
203
203
 
204
204
  ```javascript
205
205
  async validate() {
@@ -1,4 +1,4 @@
1
- # Form Components
1
+ # Form [d2l-form]
2
2
 
3
3
  <!-- docs: demo -->
4
4
  ```html
@@ -34,12 +34,6 @@
34
34
  </d2l-form>
35
35
  ```
36
36
 
37
- There are two form components that can be used with our custom elements: `d2l-form` and `d2l-form-native`. These are useful in the following scenarios:
38
- - `d2l-form`: when submitting form data via your own API calls OR when nesting multiple forms within each other
39
- - `d2l-form-native`: when emulating native form element submission
40
-
41
- ## Form [d2l-form]
42
-
43
37
  The `d2l-form` component can be used to build sections containing interactive controls that are validated and submitted as a group.
44
38
 
45
39
  It differs from the native HTML `form` element in 4 ways:
@@ -48,8 +42,6 @@ It differs from the native HTML `form` element in 4 ways:
48
42
  1. `d2l-form` elements can be nested. If a parent form is validated or submitted it will also trigger the corresponding action for descendent `d2l-form`s unless they explicitly opt-out using `no-nesting`. This means that a `d2l-form` will only pass validation if it and all of its nested descendants pass validation.
49
43
  1. Submission is not handled directly by `d2l-form`. Instead, all form data will be aggregated and passed back to the caller via an event. The caller is then responsible for submitting the data.
50
44
 
51
- If you're looking to emulate native form element submission, `d2l-form-native` may be more appropriate.
52
-
53
45
  <!-- docs: demo code properties name:d2l-form sandboxTitle:'Form' autoSize:false display:block size:large -->
54
46
  ```html
55
47
  <script type="module">
@@ -174,68 +166,3 @@ In the above example, calling `submit` on form `#a` will cause forms `#a` and `#
174
166
  - `d2l-form#a` will be submitted because submit was called on it directly.
175
167
  - `d2l-form#b` will be submitted because it is nested directly within `#a`'s slot.
176
168
  - `d2l-form#root`, `d2l-form#c` and the `d2l-form` within the shadow root of `#d` will _**not**_ be submitted because they are ancestors of `#a` rather than descendants.
177
-
178
- ## Native Form [d2l-form-native]
179
-
180
- The `d2l-form-native` component can be used to build sections containing interactive controls that are validated and submitted as a group.
181
-
182
- It differs from the native HTML `form` element in 2 ways:
183
- 1. It supports custom form elements made using the [`FormElementMixin`](./form-element-mixin.md) in addition to native form elements like `input`, `select` and `textarea`.
184
- 1. Upon validation, it will display an error summary that contains error messages for any elements that failed validation.
185
-
186
- If you're looking to submit form data via your own API calls or nest multiple forms within each other, `d2l-form` may be more appropriate.
187
-
188
- <!-- docs: demo code properties name:d2l-form-native sandboxTitle:'Native Form' autoSize:false display:block size:medium -->
189
- ```html
190
- <script type="module">
191
- import '@brightspace-ui/core/components/form/form-native.js';
192
- import '@brightspace-ui/core/components/inputs/input-text.js';
193
-
194
- const button = document.querySelector('button');
195
- const form = document.querySelector('d2l-form-native');
196
- button.addEventListener('click', () => {
197
- form.submit();
198
- });
199
-
200
- function handleSubmission(e) {
201
- const { formData } = e.detail;
202
- const email = formData.get('email');
203
- const pets = formData.get('pets');
204
- console.log('Form submission data: email = ' + email + ', pets = ' + pets);
205
- }
206
- form.addEventListener('formdata', (e) => handleSubmission(e));
207
- </script>
208
- <d2l-form-native>
209
- <d2l-input-text required label="Email" name="email" type="email"></d2l-input-text>
210
- <select class="d2l-input-select" name="pets" required>
211
- <option value="">--Please choose an option--</option>
212
- <option value="porpoise">Porpoise</option>
213
- <option value="house hippo">House Hippo</option>
214
- <option value="spiker monkey">Spider Monkey</option>
215
- <option value="capybara">Capybara</option>
216
- </select>
217
- <button name="action" value="save" type="submit">Save</button>
218
- </d2l-form-native>
219
- ```
220
-
221
- <!-- docs: start hidden content -->
222
- ### Properties
223
-
224
- | Property | Type | Description |
225
- |---|---|---|
226
- | `action` | String | The URL that processes the form submission. |
227
- | `enctype` | default: `"application/x-www-form-urlencoded"`<br>`"multipart/form-data"`<br>`"text/plain"` | If the value of the method attribute is post, enctype is the MIME type of the form submission. |
228
- | `method` | default: `"get"`<br>`"post"` | The URL that processes the form submission. |
229
- | `target` | default: `"_self"`<br>`"_blank"`<br>`"_parent"`<br>`"_top"` | Indicates where to display the response after submitting the form. |
230
- | `track-changes` | Boolean, default: `false` | Indicates that the form should interrupt and warn on navigation if the user has unsaved changes. |
231
-
232
- ### Events
233
- - `submit`: Dispatched when the form is submitted. Cancelling this event will prevent form submission.
234
- - `formdata`: Dispatched after the entry list representing the form's data is constructed. This happens when the form is submitted just prior to submission. The form data can be obtained from the `detail`'s `formData` property.
235
- - `d2l-form-dirty`: Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not.
236
- <!-- docs: end hidden content -->
237
-
238
- ### Methods
239
- - `submit()`: Submits the form to the server. This will first perform validation on all elements within the form. Submission will only happen if validation succeeds.
240
- - `requestSubmit(submitter)`: Requests that the form be submitted using the specified submit button and its corresponding configuration. A `button`'s value is only submitted if that button is both part of the form and the `submitter`.
241
- - `async validate()`: Validates the form without submitting even if validation succeeds. This returns a `Map` mapping from an element to the list of error messages associated with it.
@@ -1,7 +1,13 @@
1
+ import './form-errory-summary.js';
2
+ import '../tooltip/tooltip.js';
3
+ import '../link/link.js';
1
4
  import { css, html, LitElement } from 'lit';
2
5
  import { findFormElements, flattenMap, getFormElementData, isCustomFormElement, isNativeFormElement } from './form-helper.js';
3
6
  import { findComposedAncestor } from '../../helpers/dom.js';
4
- import { FormMixin } from './form-mixin.js';
7
+ import { getComposedActiveElement } from '../../helpers/focus.js';
8
+ import { getUniqueId } from '../../helpers/uniqueId.js';
9
+ import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
10
+ import { localizeFormElement } from './form-element-localize-helper.js';
5
11
 
6
12
  /**
7
13
  * A component that can be used to build sections containing interactive controls that are validated and submitted as a group.
@@ -9,7 +15,7 @@ import { FormMixin } from './form-mixin.js';
9
15
  * @slot - The native and custom form elements that participate in validation and submission
10
16
  * @fires d2l-form-connect - Internal event
11
17
  */
12
- class Form extends FormMixin(LitElement) {
18
+ class Form extends LocalizeCoreElement(LitElement) {
13
19
 
14
20
  static get properties() {
15
21
  return {
@@ -20,6 +26,13 @@ class Form extends FormMixin(LitElement) {
20
26
  * @type {boolean}
21
27
  */
22
28
  noNesting: { type: Boolean, attribute: 'no-nesting', reflect: true },
29
+ /**
30
+ * Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.
31
+ * @type {boolean}
32
+ */
33
+ trackChanges: { type: Boolean, attribute: 'track-changes', reflect: true },
34
+ _errors: { type: Object },
35
+ _hasErrors: { type: Boolean, attribute: '_has-errors', reflect: true },
23
36
  };
24
37
  }
25
38
 
@@ -39,21 +52,37 @@ class Form extends FormMixin(LitElement) {
39
52
 
40
53
  constructor() {
41
54
  super();
55
+ this.trackChanges = false;
56
+ this._errors = new Map();
42
57
  this._isSubForm = false;
43
58
  this._nestedForms = new Map();
59
+ this._firstUpdateResolve = null;
60
+ this._firstUpdatePromise = new Promise((resolve) => {
61
+ this._firstUpdateResolve = resolve;
62
+ });
63
+ this._tooltips = new Map();
64
+ this._validationCustoms = new Set();
65
+
66
+ this._onUnload = this._onUnload.bind(this);
67
+ this._onNativeSubmit = this._onNativeSubmit.bind(this);
44
68
 
45
69
  /** @ignore */
46
70
  this.addEventListener('d2l-form-connect', this._onFormConnect);
71
+ this.addEventListener('d2l-form-errors-change', this._onErrorsChange);
72
+ this.addEventListener('d2l-form-element-errors-change', this._onErrorsChange);
73
+ this.addEventListener('d2l-validation-custom-connected', this._validationCustomConnected);
47
74
  }
48
75
 
49
76
  connectedCallback() {
50
77
  super.connectedCallback();
78
+ window.addEventListener('beforeunload', this._onUnload);
51
79
  /** @ignore */
52
80
  this._isSubForm = !this.dispatchEvent(new CustomEvent('d2l-form-connect', { bubbles: true, composed: true, cancelable: true }));
53
81
  }
54
82
 
55
83
  disconnectedCallback() {
56
84
  super.disconnectedCallback();
85
+ window.removeEventListener('beforeunload', this._onUnload);
57
86
  /** @ignore */
58
87
  this.dispatchEvent(new CustomEvent('d2l-form-disconnect'));
59
88
  this._isSubForm = false;
@@ -62,6 +91,10 @@ class Form extends FormMixin(LitElement) {
62
91
  firstUpdated(changedProperties) {
63
92
  super.firstUpdated(changedProperties);
64
93
 
94
+ this.addEventListener('change', this._onFormElementChange);
95
+ this.addEventListener('input', this._onFormElementChange);
96
+ this.addEventListener('focusout', this._onFormElementChange);
97
+ this._firstUpdateResolve();
65
98
  this._setupDialogValidationReset();
66
99
  }
67
100
 
@@ -79,6 +112,13 @@ class Form extends FormMixin(LitElement) {
79
112
  `;
80
113
  }
81
114
 
115
+ willUpdate(changedProperties) {
116
+ super.willUpdate(changedProperties);
117
+ if (changedProperties.has('_errors')) {
118
+ this._hasErrors = this._errors.size > 0;
119
+ }
120
+ }
121
+
82
122
  async requestSubmit(submitter) {
83
123
  const errors = await this.validate();
84
124
  if (errors.size > 0) {
@@ -145,6 +185,34 @@ class Form extends FormMixin(LitElement) {
145
185
  return flattenedErrorMap;
146
186
  }
147
187
 
188
+ _displayInvalid(ele, message) {
189
+ let tooltip = this._tooltips.get(ele);
190
+ if (!tooltip) {
191
+ tooltip = document.createElement('d2l-tooltip');
192
+ tooltip.for = ele.id;
193
+ tooltip.align = 'start';
194
+ tooltip.state = 'error';
195
+ ele.parentNode.append(tooltip);
196
+ this._tooltips.set(ele, tooltip);
197
+
198
+ tooltip.appendChild(document.createTextNode(message));
199
+ } else if (tooltip.innerText.trim() !== message.trim()) {
200
+ tooltip.textContent = '';
201
+ tooltip.appendChild(document.createTextNode(message));
202
+ tooltip.updatePosition();
203
+ }
204
+ ele.setAttribute('aria-invalid', 'true');
205
+ }
206
+
207
+ _displayValid(ele) {
208
+ const tooltip = this._tooltips.get(ele);
209
+ if (tooltip) {
210
+ this._tooltips.delete(ele);
211
+ tooltip.remove();
212
+ }
213
+ ele.setAttribute('aria-invalid', 'false');
214
+ }
215
+
148
216
  _findFormElements() {
149
217
  const isFormElementPredicate = ele => this._hasSubForms(ele);
150
218
  const visitChildrenPredicate = ele => !this._hasSubForms(ele);
@@ -163,6 +231,14 @@ class Form extends FormMixin(LitElement) {
163
231
  return !this._isSubForm || this.noNesting;
164
232
  }
165
233
 
234
+ _onErrorsChange(e) {
235
+ if (e.target === this) {
236
+ return;
237
+ }
238
+ e.stopPropagation();
239
+ this._updateErrors(e.target, e.detail.errors);
240
+ }
241
+
166
242
  _onFormConnect(e) {
167
243
  if (e.target === this) {
168
244
  return;
@@ -195,6 +271,37 @@ class Form extends FormMixin(LitElement) {
195
271
 
196
272
  }
197
273
 
274
+ async _onFormElementChange(e) {
275
+ const ele = e.target;
276
+
277
+ if ((isNativeFormElement(ele) || isCustomFormElement(ele)) && e.type !== 'focusout') {
278
+ this._dirty = true;
279
+ /** Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not. */
280
+ this.dispatchEvent(new CustomEvent('d2l-form-dirty'));
281
+ }
282
+
283
+ if (!isNativeFormElement(ele)) {
284
+ return;
285
+ }
286
+ e.stopPropagation();
287
+ const errors = await this._validateFormElement(ele, e.type === 'focusout');
288
+ this._updateErrors(ele, errors);
289
+ }
290
+
291
+ _onNativeSubmit(e) {
292
+ e.preventDefault();
293
+ e.stopPropagation();
294
+ const submitter = e.submitter || getComposedActiveElement();
295
+ this.requestSubmit(submitter);
296
+ }
297
+
298
+ _onUnload(e) {
299
+ if (this.trackChanges && this._dirty) {
300
+ e.preventDefault();
301
+ e.returnValue = false;
302
+ }
303
+ }
304
+
198
305
  _setupDialogValidationReset() {
199
306
  const dialogAncestor = findComposedAncestor(
200
307
  this,
@@ -229,5 +336,59 @@ class Form extends FormMixin(LitElement) {
229
336
  this.dispatchEvent(new CustomEvent('d2l-form-submit', { detail: { formData } }));
230
337
  }
231
338
 
339
+ _updateErrors(ele, errors) {
340
+
341
+ if (!this._errors.has(ele)) {
342
+ return false;
343
+ }
344
+ if (Array.from(errors).length === 0) {
345
+ this._errors.delete(ele);
346
+ } else {
347
+ this._errors.set(ele, errors);
348
+ }
349
+ const detail = { bubbles: true, composed: true, detail: { errors: this._errors } };
350
+ /** @ignore */
351
+ this.dispatchEvent(new CustomEvent('d2l-form-errors-change', detail));
352
+ this.requestUpdate('_errors');
353
+ return true;
354
+ }
355
+
356
+ async _validateFormElement(ele, showNewErrors) {
357
+ // if validation occurs before we've rendered,
358
+ // localization may not have loaded yet
359
+ await this._firstUpdatePromise;
360
+ ele.id = ele.id || getUniqueId();
361
+ if (isCustomFormElement(ele)) {
362
+ return ele.validate(showNewErrors);
363
+ } else if (isNativeFormElement(ele)) {
364
+ const customs = [...this._validationCustoms].filter(custom => custom.forElement === ele);
365
+ const results = await Promise.all(customs.map(custom => custom.validate()));
366
+ const errors = customs.map(custom => custom.failureText).filter((_, i) => !results[i]);
367
+ if (!ele.checkValidity()) {
368
+ const validationMessage = localizeFormElement(this.localize.bind(this), ele);
369
+ errors.unshift(validationMessage);
370
+ }
371
+ if (errors.length > 0 && (showNewErrors || ele.getAttribute('aria-invalid') === 'true')) {
372
+ this._displayInvalid(ele, errors[0]);
373
+ } else {
374
+ this._displayValid(ele);
375
+ }
376
+ return errors;
377
+ }
378
+ return [];
379
+ }
380
+
381
+ _validationCustomConnected(e) {
382
+ e.stopPropagation();
383
+ const custom = e.composedPath()[0];
384
+ this._validationCustoms.add(custom);
385
+
386
+ const onDisconnect = () => {
387
+ custom.removeEventListener('d2l-validation-custom-disconnected', onDisconnect);
388
+ this._validationCustoms.delete(custom);
389
+ };
390
+ custom.addEventListener('d2l-validation-custom-disconnected', onDisconnect);
391
+ }
392
+
232
393
  }
233
394
  customElements.define('d2l-form', Form);
@@ -4,10 +4,10 @@
4
4
  The `d2l-validation-custom` component is used to add custom validation logic to native form elements like `input`, `select` and `textarea` or custom form elements created with the [`FormElementMixin`](../form/docs/form-element-mixin.md).
5
5
 
6
6
  **Native Form Elements:**
7
- - When attached to native form elements like `input`, `select` and `textarea`, both the `d2l-validation-custom` and native form element **must** be within a [`d2l-form`](../form/docs/form.md) or [`d2l-form-native`](../form/docs/form-native.md) for the validation custom to function.
7
+ - When attached to native form elements like `input`, `select` and `textarea`, both the `d2l-validation-custom` and native form element **must** be within a [`d2l-form`](../form/docs/form.md) for the validation custom to function.
8
8
 
9
9
  **Custom Form Elements:**
10
- - When attached to custom form elements created with the [`FormElementMixin`](../form/docs/form-element-mixin.md), the `d2l-validation-custom` will function even if no [`d2l-form`](../form/docs/form.md) or [`d2l-form-native`](../form/docs/form-native.md) is present.
10
+ - When attached to custom form elements created with the [`FormElementMixin`](../form/docs/form-element-mixin.md), the `d2l-validation-custom` will function even if no [`d2l-form`](../form/docs/form.md) is present.
11
11
 
12
12
  **Usage:**
13
13
  ```html
@@ -4759,10 +4759,6 @@
4759
4759
  "name": "d2l-form-dialog-demo",
4760
4760
  "path": "./components/form/demo/form-dialog-demo.js"
4761
4761
  },
4762
- {
4763
- "name": "d2l-form-native-demo",
4764
- "path": "./components/form/demo/form-native-demo.js"
4765
- },
4766
4762
  {
4767
4763
  "name": "d2l-form-panel-demo",
4768
4764
  "path": "./components/form/demo/form-panel-demo.js"
@@ -4778,100 +4774,6 @@
4778
4774
  }
4779
4775
  ]
4780
4776
  },
4781
- {
4782
- "name": "d2l-form-native",
4783
- "path": "./components/form/form-native.js",
4784
- "description": "A component that can be used to build sections containing interactive controls that are validated and submitted as a group.\nThese interactive controls are submitted using a native HTML form submission.",
4785
- "attributes": [
4786
- {
4787
- "name": "action",
4788
- "description": "The URL that processes the form submission.",
4789
- "type": "string",
4790
- "default": "\"\""
4791
- },
4792
- {
4793
- "name": "enctype",
4794
- "description": "If the value of the method attribute is post, enctype is the MIME type of the form submission.",
4795
- "type": "'application/x-www-form-urlencoded'|'multipart/form-data'|'text/plain'",
4796
- "default": "\"application/x-www-form-urlencoded\""
4797
- },
4798
- {
4799
- "name": "method",
4800
- "description": "The HTTP method to submit the form with.",
4801
- "type": "'get'|'post'",
4802
- "default": "\"get\""
4803
- },
4804
- {
4805
- "name": "target",
4806
- "description": "Indicates where to display the response after submitting the form.",
4807
- "type": "'_self '|'_blank'|'_parent'|'_top'",
4808
- "default": "\"_self\""
4809
- },
4810
- {
4811
- "name": "track-changes",
4812
- "description": "Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.",
4813
- "type": "boolean",
4814
- "default": "false"
4815
- }
4816
- ],
4817
- "properties": [
4818
- {
4819
- "name": "action",
4820
- "attribute": "action",
4821
- "description": "The URL that processes the form submission.",
4822
- "type": "string",
4823
- "default": "\"\""
4824
- },
4825
- {
4826
- "name": "enctype",
4827
- "attribute": "enctype",
4828
- "description": "If the value of the method attribute is post, enctype is the MIME type of the form submission.",
4829
- "type": "'application/x-www-form-urlencoded'|'multipart/form-data'|'text/plain'",
4830
- "default": "\"application/x-www-form-urlencoded\""
4831
- },
4832
- {
4833
- "name": "method",
4834
- "attribute": "method",
4835
- "description": "The HTTP method to submit the form with.",
4836
- "type": "'get'|'post'",
4837
- "default": "\"get\""
4838
- },
4839
- {
4840
- "name": "target",
4841
- "attribute": "target",
4842
- "description": "Indicates where to display the response after submitting the form.",
4843
- "type": "'_self '|'_blank'|'_parent'|'_top'",
4844
- "default": "\"_self\""
4845
- },
4846
- {
4847
- "name": "trackChanges",
4848
- "attribute": "track-changes",
4849
- "description": "Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.",
4850
- "type": "boolean",
4851
- "default": "false"
4852
- }
4853
- ],
4854
- "events": [
4855
- {
4856
- "name": "submit",
4857
- "description": "Dispatched when the form is submitted. Cancelling this event will prevent form submission."
4858
- },
4859
- {
4860
- "name": "formdata",
4861
- "description": "Dispatched after the entry list representing the form's data is constructed. This happens when the form is submitted just prior to submission. The form data can be obtained from the `detail`'s `formData` property."
4862
- },
4863
- {
4864
- "name": "d2l-form-dirty",
4865
- "description": "Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not."
4866
- }
4867
- ],
4868
- "slots": [
4869
- {
4870
- "name": "",
4871
- "description": "The native and custom form elements that participate in validation and submission"
4872
- }
4873
- ]
4874
- },
4875
4777
  {
4876
4778
  "name": "d2l-form",
4877
4779
  "path": "./components/form/form.js",
@@ -4913,13 +4815,13 @@
4913
4815
  "name": "d2l-form-invalid",
4914
4816
  "description": "Dispatched when the form fails validation. The error map can be obtained from the `detail`'s `errors` property."
4915
4817
  },
4916
- {
4917
- "name": "d2l-form-submit",
4918
- "description": "Dispatched when the form is submitted. The form data can be obtained from the `detail`'s `formData` property."
4919
- },
4920
4818
  {
4921
4819
  "name": "d2l-form-dirty",
4922
4820
  "description": "Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not."
4821
+ },
4822
+ {
4823
+ "name": "d2l-form-submit",
4824
+ "description": "Dispatched when the form is submitted. The form data can be obtained from the `detail`'s `formData` property."
4923
4825
  }
4924
4826
  ],
4925
4827
  "slots": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.117.0",
3
+ "version": "3.118.0",
4
4
  "description": "A collection of accessible, free, open-source web components for building Brightspace applications",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/BrightspaceUI/core.git",
@@ -1,80 +0,0 @@
1
-
2
- import '../../button/button.js';
3
- import '../../inputs/input-date.js';
4
- import '../../inputs/input-date-time-range.js';
5
- import '../../inputs/input-text.js';
6
- import '../../validation/validation-custom.js';
7
- import '../form-native.js';
8
- import { css, html, LitElement } from 'lit';
9
- import { inputStyles } from '../../inputs/input-styles.js';
10
- import { selectStyles } from '../../inputs/input-select-styles.js';
11
-
12
- class FormNativeDemo extends LitElement {
13
-
14
- static get styles() {
15
- return [inputStyles, selectStyles, css`
16
- :first-child.d2l-form-demo-container {
17
- margin-top: 18px;
18
- }
19
- .d2l-form-demo-container {
20
- margin-bottom: 10px;
21
- }
22
- `];
23
- }
24
-
25
- render() {
26
- return html`
27
- <d2l-form-native>
28
- <div class="d2l-form-demo-container">
29
- <d2l-input-text label="Name" type="text" name="name" required minlength="4" maxlength="8"></d2l-input-text>
30
- </div>
31
- <div class="d2l-form-demo-container">
32
- <d2l-input-text label="Email" name="email" type="email"></d2l-input-text>
33
- </div>
34
- <div class="d2l-form-demo-container">
35
- <d2l-validation-custom for="password" @d2l-validation-custom-validate=${this._validatePassword} failure-text="Expected hunter2 or 12345" ></d2l-validation-custom>
36
- <d2l-input-text label="Password" id="password" name="password" required type="password"></d2l-input-text>
37
- </div>
38
- <fieldset class="d2l-form-demo-container">
39
- <legend>Choose your favorite monster</legend>
40
- <input type="radio" id="kraken" name="monster" value="kraken">
41
- <label for="kraken">Kraken</label><br />
42
- <input type="radio" id="sasquatch" name="monster" value="sasquatch">
43
- <label for="sasquatch">Sasquatch</label><br />
44
- </fieldset>
45
- <div class="d2l-form-demo-container">
46
- <label for="pet-select">Favorite Pet</label><br />
47
- <select class="d2l-input-select" name="pets" id="pet-select" required>
48
- <option value="">--Please choose an option--</option>
49
- <option value="porpoise">Porpoise</option>
50
- <option value="house hippo">House Hippo</option>
51
- <option value="spiker monkey">Spider Monkey</option>
52
- <option value="capybara">Capybara</option>
53
- </select>
54
- </div>
55
- <d2l-input-date label="Date" name="my-date" required></d2l-input-date>
56
- <div class="d2l-form-demo-container">
57
- <label for="story">Tell us your story</label>
58
- <textarea class="d2l-input" minlength="20" id="story" name="story" rows="5" cols="33">It was...</textarea>
59
- </label>
60
- </div>
61
- <d2l-input-date-time-range label="Assignment Dates" required min-value="2018-08-27T12:30:00Z" max-value="2018-09-30T12:30:00Z"></d2l-input-date-time-range>
62
- <div class="d2l-form-demo-container">
63
- <label for="file">Super Secret File</label><br />
64
- <input type="file" id="file" name="super-secret-file">
65
- </div>
66
- <button name="action" value="save" type="submit" @click=${this._onClick}>Save</button>
67
- </d2l-form-native>
68
- `;
69
- }
70
-
71
- _onClick(e) {
72
- if (this.shadowRoot) this.shadowRoot.querySelector('d2l-form-native').requestSubmit(e.target);
73
- }
74
-
75
- _validatePassword(e) {
76
- e.detail.resolve(e.detail.forElement.value === 'hunter2' || e.detail.forElement.value === '12345');
77
- }
78
-
79
- }
80
- customElements.define('d2l-form-native-demo', FormNativeDemo);
@@ -1,29 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta charset="UTF-8">
7
- <link rel="stylesheet" href="../../demo/styles.css" type="text/css">
8
- <script type="module">
9
- import '../../demo/demo-page.js';
10
- import './form-native-demo.js';
11
- </script>
12
- </head>
13
-
14
- <body unresolved>
15
-
16
- <d2l-demo-page page-title="d2l-form-native">
17
-
18
- <h2>Basic</h2>
19
- <d2l-demo-snippet>
20
- <template>
21
- <d2l-form-native-demo></d2l-form-native-demo>
22
- </template>
23
- </d2l-demo-snippet>
24
-
25
- </d2l-demo-page>
26
-
27
- </body>
28
-
29
- </html>
@@ -1,202 +0,0 @@
1
- import './form-errory-summary.js';
2
- import '../tooltip/tooltip.js';
3
- import '../link/link.js';
4
- import { isCustomFormElement, isNativeFormElement } from './form-helper.js';
5
- import { getComposedActiveElement } from '../../helpers/focus.js';
6
- import { getUniqueId } from '../../helpers/uniqueId.js';
7
- import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
8
- import { localizeFormElement } from './form-element-localize-helper.js';
9
-
10
- export const FormMixin = superclass => class extends LocalizeCoreElement(superclass) {
11
-
12
- static get properties() {
13
- return {
14
- /**
15
- * Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.
16
- * @type {boolean}
17
- */
18
- trackChanges: { type: Boolean, attribute: 'track-changes', reflect: true },
19
- _errors: { type: Object },
20
- _hasErrors: { type: Boolean, attribute: '_has-errors', reflect: true },
21
- };
22
- }
23
-
24
- constructor() {
25
- super();
26
- this._onUnload = this._onUnload.bind(this);
27
- this._onNativeSubmit = this._onNativeSubmit.bind(this);
28
-
29
- this.trackChanges = false;
30
- this._errors = new Map();
31
- this._firstUpdateResolve = null;
32
- this._firstUpdatePromise = new Promise((resolve) => {
33
- this._firstUpdateResolve = resolve;
34
- });
35
- this._tooltips = new Map();
36
- this._validationCustoms = new Set();
37
-
38
- this.addEventListener('d2l-form-errors-change', this._onErrorsChange);
39
- this.addEventListener('d2l-form-element-errors-change', this._onErrorsChange);
40
- this.addEventListener('d2l-validation-custom-connected', this._validationCustomConnected);
41
- }
42
-
43
- connectedCallback() {
44
- super.connectedCallback();
45
- window.addEventListener('beforeunload', this._onUnload);
46
- }
47
-
48
- disconnectedCallback() {
49
- super.disconnectedCallback();
50
- window.removeEventListener('beforeunload', this._onUnload);
51
- }
52
-
53
- firstUpdated(changedProperties) {
54
- super.firstUpdated(changedProperties);
55
- this.addEventListener('change', this._onFormElementChange);
56
- this.addEventListener('input', this._onFormElementChange);
57
- this.addEventListener('focusout', this._onFormElementChange);
58
- this._firstUpdateResolve();
59
- }
60
-
61
- willUpdate(changedProperties) {
62
- super.willUpdate(changedProperties);
63
- if (changedProperties.has('_errors')) {
64
- this._hasErrors = this._errors.size > 0;
65
- }
66
- }
67
-
68
- // eslint-disable-next-line no-unused-vars
69
- async requestSubmit(submitter) {
70
- throw new Error('FormMixin.requestSubmit must be overridden');
71
- }
72
-
73
- async submit() {
74
- throw new Error('FormMixin.submit must be overridden');
75
- }
76
-
77
- async validate() {
78
- throw new Error('FormMixin.validate must be overridden');
79
- }
80
-
81
- _displayInvalid(ele, message) {
82
- let tooltip = this._tooltips.get(ele);
83
- if (!tooltip) {
84
- tooltip = document.createElement('d2l-tooltip');
85
- tooltip.for = ele.id;
86
- tooltip.align = 'start';
87
- tooltip.state = 'error';
88
- ele.parentNode.append(tooltip);
89
- this._tooltips.set(ele, tooltip);
90
-
91
- tooltip.appendChild(document.createTextNode(message));
92
- } else if (tooltip.innerText.trim() !== message.trim()) {
93
- tooltip.textContent = '';
94
- tooltip.appendChild(document.createTextNode(message));
95
- tooltip.updatePosition();
96
- }
97
- ele.setAttribute('aria-invalid', 'true');
98
- }
99
-
100
- _displayValid(ele) {
101
- const tooltip = this._tooltips.get(ele);
102
- if (tooltip) {
103
- this._tooltips.delete(ele);
104
- tooltip.remove();
105
- }
106
- ele.setAttribute('aria-invalid', 'false');
107
- }
108
-
109
- _onErrorsChange(e) {
110
- if (e.target === this) {
111
- return;
112
- }
113
- e.stopPropagation();
114
- this._updateErrors(e.target, e.detail.errors);
115
- }
116
-
117
- async _onFormElementChange(e) {
118
- const ele = e.target;
119
-
120
- if ((isNativeFormElement(ele) || isCustomFormElement(ele)) && e.type !== 'focusout') {
121
- this._dirty = true;
122
- /** Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not. */
123
- this.dispatchEvent(new CustomEvent('d2l-form-dirty'));
124
- }
125
-
126
- if (!isNativeFormElement(ele)) {
127
- return;
128
- }
129
- e.stopPropagation();
130
- const errors = await this._validateFormElement(ele, e.type === 'focusout');
131
- this._updateErrors(ele, errors);
132
- }
133
-
134
- _onNativeSubmit(e) {
135
- e.preventDefault();
136
- e.stopPropagation();
137
- const submitter = e.submitter || getComposedActiveElement();
138
- this.requestSubmit(submitter);
139
- }
140
-
141
- _onUnload(e) {
142
- if (this.trackChanges && this._dirty) {
143
- e.preventDefault();
144
- e.returnValue = false;
145
- }
146
- }
147
-
148
- _updateErrors(ele, errors) {
149
-
150
- if (!this._errors.has(ele)) {
151
- return false;
152
- }
153
- if (Array.from(errors).length === 0) {
154
- this._errors.delete(ele);
155
- } else {
156
- this._errors.set(ele, errors);
157
- }
158
- const detail = { bubbles: true, composed: true, detail: { errors: this._errors } };
159
- /** @ignore */
160
- this.dispatchEvent(new CustomEvent('d2l-form-errors-change', detail));
161
- this.requestUpdate('_errors');
162
- return true;
163
- }
164
-
165
- async _validateFormElement(ele, showNewErrors) {
166
- // if validation occurs before we've rendered,
167
- // localization may not have loaded yet
168
- await this._firstUpdatePromise;
169
- ele.id = ele.id || getUniqueId();
170
- if (isCustomFormElement(ele)) {
171
- return ele.validate(showNewErrors);
172
- } else if (isNativeFormElement(ele)) {
173
- const customs = [...this._validationCustoms].filter(custom => custom.forElement === ele);
174
- const results = await Promise.all(customs.map(custom => custom.validate()));
175
- const errors = customs.map(custom => custom.failureText).filter((_, i) => !results[i]);
176
- if (!ele.checkValidity()) {
177
- const validationMessage = localizeFormElement(this.localize.bind(this), ele);
178
- errors.unshift(validationMessage);
179
- }
180
- if (errors.length > 0 && (showNewErrors || ele.getAttribute('aria-invalid') === 'true')) {
181
- this._displayInvalid(ele, errors[0]);
182
- } else {
183
- this._displayValid(ele);
184
- }
185
- return errors;
186
- }
187
- return [];
188
- }
189
-
190
- _validationCustomConnected(e) {
191
- e.stopPropagation();
192
- const custom = e.composedPath()[0];
193
- this._validationCustoms.add(custom);
194
-
195
- const onDisconnect = () => {
196
- custom.removeEventListener('d2l-validation-custom-disconnected', onDisconnect);
197
- this._validationCustoms.delete(custom);
198
- };
199
- custom.addEventListener('d2l-validation-custom-disconnected', onDisconnect);
200
- }
201
-
202
- };
@@ -1,148 +0,0 @@
1
- import { css, html, LitElement } from 'lit';
2
- import { findFormElements, getFormElementData, isCustomFormElement, isNativeFormElement } from './form-helper.js';
3
- import { FormMixin } from './form-mixin.js';
4
- import { getUniqueId } from '../../helpers/uniqueId.js';
5
-
6
- /**
7
- * A component that can be used to build sections containing interactive controls that are validated and submitted as a group.
8
- * These interactive controls are submitted using a native HTML form submission.
9
- * @slot - The native and custom form elements that participate in validation and submission
10
- * @fires submit - Dispatched when the form is submitted. Cancelling this event will prevent form submission.
11
- */
12
- class FormNative extends FormMixin(LitElement) {
13
-
14
- static get properties() {
15
- return {
16
- /**
17
- * The URL that processes the form submission.
18
- * @type {string}
19
- */
20
- action: { type: String },
21
- /**
22
- * If the value of the method attribute is post, enctype is the MIME type of the form submission.
23
- * @type {'application/x-www-form-urlencoded'|'multipart/form-data'|'text/plain'}
24
- */
25
- enctype: { type: String },
26
- /**
27
- * The HTTP method to submit the form with.
28
- * @type {'get'|'post'}
29
- */
30
- method: { type: String },
31
- /**
32
- * Indicates where to display the response after submitting the form.
33
- * @type {'_self '|'_blank'|'_parent'|'_top'}
34
- */
35
- target: { type: String },
36
- };
37
- }
38
-
39
- static get styles() {
40
- return css`
41
- :host {
42
- display: block;
43
- }
44
- :host([hidden]) {
45
- display: none;
46
- }
47
- `;
48
- }
49
-
50
- constructor() {
51
- super();
52
- this.action = '';
53
- this.enctype = 'application/x-www-form-urlencoded';
54
- this.method = 'get';
55
- this.target = '_self';
56
- }
57
-
58
- render() {
59
- const errors = [...this._errors]
60
- .filter(([, eleErrors]) => eleErrors.length > 0)
61
- .map(([ele, eleErrors]) => ({ href: `#${ele.id}`, message: eleErrors[0], onClick: () => ele.focus() }));
62
- return html`
63
- <d2l-form-error-summary .errors=${errors}></d2l-form-error-summary>
64
- <slot></slot>
65
- `;
66
- }
67
-
68
- shouldUpdate(changedProperties) {
69
- if (!super.shouldUpdate(changedProperties)) {
70
- return false;
71
- }
72
- const ignoredProps = new Set(['action', 'enctype', 'method', 'target']);
73
- return [...changedProperties].filter(([prop]) => !ignoredProps.has(prop)).length > 0;
74
- }
75
-
76
- async requestSubmit(submitter) {
77
- const errors = await this.validate();
78
- if (errors.size > 0) {
79
- return;
80
- }
81
- this._dirty = false;
82
-
83
- const form = document.createElement('form');
84
- form.addEventListener('formdata', this._onFormData);
85
- form.id = getUniqueId();
86
- form.action = this.action;
87
- form.enctype = this.enctype;
88
- form.method = this.method;
89
- form.target = this.target;
90
- this.appendChild(form);
91
-
92
- let customFormData = {};
93
- const formElements = findFormElements(this);
94
- for (const ele of formElements) {
95
- const eleData = getFormElementData(ele, submitter);
96
- const isCustom = isCustomFormElement(ele);
97
- if (isCustom || ele === submitter) {
98
- customFormData = { ...customFormData, ...eleData };
99
- }
100
- if (!isCustom && isNativeFormElement(ele)) {
101
- ele.setAttribute('form', form.id);
102
- }
103
- }
104
- for (const entry of Object.entries(customFormData)) {
105
- const input = document.createElement('input');
106
- input.type = 'hidden';
107
- input.name = entry[0];
108
- input.value = entry[1];
109
- form.appendChild(input);
110
- }
111
- const submit = this.dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true }));
112
- /** Dispatched after the entry list representing the form's data is constructed. This happens when the form is submitted just prior to submission. The form data can be obtained from the `detail`'s `formData` property. */
113
- this.dispatchEvent(new CustomEvent('formdata', { detail: { formData: new FormData(form) } }));
114
- if (submit) {
115
- form.submit();
116
- }
117
- form.remove();
118
- }
119
-
120
- async submit() {
121
- return this.requestSubmit(null);
122
- }
123
-
124
- async validate() {
125
- let errors = [];
126
- const errorMap = new Map();
127
- const formElements = findFormElements(this);
128
- for (const ele of formElements) {
129
- const eleErrors = await this._validateFormElement(ele, true);
130
- if (eleErrors.length > 0) {
131
- errors = [...errors, ...eleErrors];
132
- errorMap.set(ele, eleErrors);
133
- }
134
- }
135
- this._errors = errorMap;
136
- if (this.shadowRoot && errorMap.size > 0) {
137
- const errorSummary = this.shadowRoot.querySelector('d2l-form-error-summary');
138
- this.updateComplete.then(() => errorSummary.focus());
139
- }
140
- return errorMap;
141
- }
142
-
143
- _onFormData(e) {
144
- e.stopPropagation();
145
- }
146
-
147
- }
148
- customElements.define('d2l-form-native', FormNative);