@brightspace-ui/core 3.117.0 → 3.119.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.
- package/components/form/README.md +3 -4
- package/components/form/docs/form-element-mixin.md +5 -7
- package/components/form/docs/form-element-nesting.md +1 -1
- package/components/form/docs/form.md +1 -74
- package/components/form/form.js +163 -2
- package/components/list/demo/list-nested.html +63 -1
- package/components/list/list-item-button-mixin.js +10 -1
- package/components/list/list-item-nav-button-mixin.js +124 -0
- package/components/list/list-item-nav-button.js +19 -0
- package/components/list/list.js +41 -1
- package/components/validation/README.md +2 -2
- package/custom-elements.json +289 -102
- package/package.json +1 -1
- package/components/form/demo/form-native-demo.js +0 -80
- package/components/form/demo/form-native.html +0 -29
- package/components/form/form-mixin.js +0 -202
- package/components/form/form-native.js +0 -148
@@ -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>`
|
5
|
+
## Validating and Submitting Forms Using `<d2l-form>`
|
6
6
|
|
7
|
-
|
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>`
|
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
|
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
|
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`
|
10
|
-
|
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`]
|
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
|
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.
|
package/components/form/form.js
CHANGED
@@ -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 {
|
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
|
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);
|
@@ -10,10 +10,12 @@
|
|
10
10
|
import '../../demo/demo-page.js';
|
11
11
|
import '../../dropdown/dropdown-menu.js';
|
12
12
|
import '../../dropdown/dropdown-more.js';
|
13
|
+
import '../../icons/icon.js';
|
13
14
|
import '../list-item-button.js';
|
14
15
|
import '../list-item-content.js';
|
15
16
|
import './list-item-custom.js';
|
16
17
|
import '../list-item.js';
|
18
|
+
import '../list-item-nav-button.js';
|
17
19
|
import '../list-controls.js';
|
18
20
|
import '../list.js';
|
19
21
|
import '../../menu/menu.js';
|
@@ -21,6 +23,7 @@
|
|
21
23
|
import '../../paging/pager-load-more.js';
|
22
24
|
import '../../selection/selection-action.js';
|
23
25
|
import '../../switch/switch.js';
|
26
|
+
import '../../tooltip/tooltip-help.js';
|
24
27
|
|
25
28
|
import './demo-list-nested-iterations-helper.js';
|
26
29
|
</script>
|
@@ -29,7 +32,6 @@
|
|
29
32
|
|
30
33
|
<d2l-demo-page page-title="d2l-list (nested)">
|
31
34
|
|
32
|
-
|
33
35
|
<d2l-demo-snippet>
|
34
36
|
<template>
|
35
37
|
<d2l-list grid>
|
@@ -224,6 +226,66 @@
|
|
224
226
|
</template>
|
225
227
|
</d2l-demo-snippet>
|
226
228
|
|
229
|
+
<h2>Side nav item</h2>
|
230
|
+
|
231
|
+
<d2l-demo-snippet>
|
232
|
+
<template>
|
233
|
+
<d2l-list grid style="width: 334px;">
|
234
|
+
<d2l-list-item-nav-button key="L1-1" label="Welcome!" color="#006fbf" expandable expanded draggable>
|
235
|
+
<d2l-list-item-content>
|
236
|
+
<div>Welcome!</div>
|
237
|
+
</d2l-list-item-content>
|
238
|
+
<d2l-list slot="nested" grid>
|
239
|
+
<d2l-list-item-nav-button key="L2-1" label="Syallabus Confirmation" draggable>
|
240
|
+
<d2l-list-item-content>
|
241
|
+
<div><d2l-icon style="margin-right: 0.7rem;" icon="tier2:file-document"></d2l-icon>Syallabus Confirmation</div>
|
242
|
+
<div slot="secondary"><d2l-tooltip-help text="Due: May 2, 2023 at 2 pm">Due: May 2, 2023</d2l-tooltip-help></div>
|
243
|
+
</d2l-list-item-content>
|
244
|
+
</d2l-list-item-nav-button>
|
245
|
+
</d2l-list>
|
246
|
+
</d2l-list-item-nav-button>
|
247
|
+
<d2l-list-item-nav-button key="L2-2" label="Unit 1: Poetry" color="#29a6ff" expandable expanded draggable>
|
248
|
+
<d2l-list-item-content>
|
249
|
+
<div>Unit 1: Fiction</div>
|
250
|
+
<div slot="secondary"><d2l-tooltip-help text="Starts: May 2, 2023 at 2 pm">Starts: May 2, 2023</d2l-tooltip-help></div>
|
251
|
+
</d2l-list-item-content>
|
252
|
+
<d2l-list slot="nested" grid>
|
253
|
+
<d2l-list-item-nav-button key="L3-2" label="Fiction" draggable>
|
254
|
+
<d2l-list-item-content>
|
255
|
+
<div><d2l-icon style="margin-right: 0.7rem;" icon="tier2:file-document"></d2l-icon>Fiction</div>
|
256
|
+
</d2l-list-item-content>
|
257
|
+
</d2l-list-item-nav-button>
|
258
|
+
<d2l-list-item-nav-button key="L3-2" label="Ten rules for writing fiction" draggable>
|
259
|
+
<d2l-list-item-content>
|
260
|
+
<div><d2l-icon style="margin-right: 0.7rem;" icon="tier2:file-document"></d2l-icon>Ten rules for writing fiction</div>
|
261
|
+
</d2l-list-item-content>
|
262
|
+
</d2l-list-item-nav-button>
|
263
|
+
</d2l-list>
|
264
|
+
</d2l-list-item-nav-button>
|
265
|
+
</d2l-list>
|
266
|
+
<script>
|
267
|
+
(demo => {
|
268
|
+
let currentItem = document.querySelector('d2l-list-item-nav-button[current]');
|
269
|
+
demo.addEventListener('d2l-list-item-button-click', (e) => {
|
270
|
+
console.log('d2l-list-item-nav-button: click event');
|
271
|
+
|
272
|
+
if (!e.target.expandable) {
|
273
|
+
currentItem = e.target;
|
274
|
+
return;
|
275
|
+
}
|
276
|
+
|
277
|
+
if (currentItem !== e.target) {
|
278
|
+
e.target.expanded = true;
|
279
|
+
currentItem = e.target;
|
280
|
+
} else {
|
281
|
+
e.target.expanded = !e.target.expanded;
|
282
|
+
}
|
283
|
+
});
|
284
|
+
})(document.currentScript.parentNode);
|
285
|
+
</script>
|
286
|
+
</template>
|
287
|
+
</d2l-demo-snippet>
|
288
|
+
|
227
289
|
<h2>All Iterations</h2>
|
228
290
|
|
229
291
|
<d2l-demo-snippet full-width>
|
@@ -2,6 +2,7 @@ import '../colors/colors.js';
|
|
2
2
|
import { css, html, nothing } from 'lit';
|
3
3
|
import { isInteractiveInListItemComposedPath, ListItemMixin } from './list-item-mixin.js';
|
4
4
|
import { getUniqueId } from '../../helpers/uniqueId.js';
|
5
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
5
6
|
|
6
7
|
export const ListItemButtonMixin = superclass => class extends ListItemMixin(superclass) {
|
7
8
|
static get properties() {
|
@@ -10,7 +11,8 @@ export const ListItemButtonMixin = superclass => class extends ListItemMixin(sup
|
|
10
11
|
* Disables the primary action button
|
11
12
|
* @type {boolean}
|
12
13
|
*/
|
13
|
-
buttonDisabled : { type: Boolean, attribute: 'button-disabled', reflect: true }
|
14
|
+
buttonDisabled : { type: Boolean, attribute: 'button-disabled', reflect: true },
|
15
|
+
_ariaCurrent: { type: String }
|
14
16
|
};
|
15
17
|
}
|
16
18
|
|
@@ -75,6 +77,12 @@ export const ListItemButtonMixin = superclass => class extends ListItemMixin(sup
|
|
75
77
|
this.buttonDisabled = false;
|
76
78
|
}
|
77
79
|
|
80
|
+
firstUpdated(changedProperties) {
|
81
|
+
super.firstUpdated(changedProperties);
|
82
|
+
|
83
|
+
this._button = this.shadowRoot.querySelector(`#${this._primaryActionId}`);
|
84
|
+
}
|
85
|
+
|
78
86
|
willUpdate(changedProperties) {
|
79
87
|
super.willUpdate(changedProperties);
|
80
88
|
if (changedProperties.has('buttonDisabled') && this.buttonDisabled === true) this._hoveringPrimaryAction = false;
|
@@ -115,6 +123,7 @@ export const ListItemButtonMixin = superclass => class extends ListItemMixin(sup
|
|
115
123
|
_renderPrimaryAction(labelledBy, content) {
|
116
124
|
return html`<button
|
117
125
|
id="${this._primaryActionId}"
|
126
|
+
aria-current="${ifDefined(this._ariaCurrent)}"
|
118
127
|
aria-labelledby="${labelledBy}"
|
119
128
|
@click="${this._onButtonClick}"
|
120
129
|
@focusin="${this._onButtonFocus}"
|
@@ -0,0 +1,124 @@
|
|
1
|
+
import '../colors/colors.js';
|
2
|
+
import { css } from 'lit';
|
3
|
+
import { ListItemButtonMixin } from './list-item-button-mixin.js';
|
4
|
+
|
5
|
+
export const ListItemNavButtonMixin = superclass => class extends ListItemButtonMixin(superclass) {
|
6
|
+
|
7
|
+
static get properties() {
|
8
|
+
return {
|
9
|
+
/**
|
10
|
+
* Whether the list item is the current page in a navigation context
|
11
|
+
* @type {boolean}
|
12
|
+
*/
|
13
|
+
current: { type: Boolean, reflect: true },
|
14
|
+
_childCurrent: { type: Boolean, reflect: true, attribute: '_child-current' },
|
15
|
+
};
|
16
|
+
}
|
17
|
+
|
18
|
+
static get styles() {
|
19
|
+
|
20
|
+
const styles = [ css`
|
21
|
+
:host(:not([button-disabled])) {
|
22
|
+
--d2l-list-item-content-text-color: var(--d2l-color-ferrite);
|
23
|
+
}
|
24
|
+
.d2l-list-item-content ::slotted(*) {
|
25
|
+
width: 100%;
|
26
|
+
}
|
27
|
+
:host([current]) [slot="outside-control-container"] {
|
28
|
+
border: 3px solid var(--d2l-color-celestine);
|
29
|
+
margin-block: -1px;
|
30
|
+
}
|
31
|
+
:host([_focusing-primary-action]:not([current])) [slot="outside-control-container"] {
|
32
|
+
border: 2px solid var(--d2l-color-celestine);
|
33
|
+
}
|
34
|
+
:host([current]) [slot="control-container"]::before,
|
35
|
+
:host([current]) [slot="control-container"]::after {
|
36
|
+
border-color: transparent;
|
37
|
+
}
|
38
|
+
:host([_focusing-primary-action]) .d2l-list-item-content {
|
39
|
+
--d2l-list-item-content-text-outline: none;
|
40
|
+
}
|
41
|
+
:host([_hovering-primary-action]) .d2l-list-item-content,
|
42
|
+
:host([_focusing-primary-action]) .d2l-list-item-content {
|
43
|
+
--d2l-list-item-content-text-color: var(--d2l-color-ferrite);
|
44
|
+
--d2l-list-item-content-text-decoration: none;
|
45
|
+
}
|
46
|
+
|
47
|
+
` ];
|
48
|
+
|
49
|
+
super.styles && styles.unshift(super.styles);
|
50
|
+
return styles;
|
51
|
+
}
|
52
|
+
|
53
|
+
constructor() {
|
54
|
+
super();
|
55
|
+
this.current = false;
|
56
|
+
this._childCurrent = false;
|
57
|
+
}
|
58
|
+
|
59
|
+
connectedCallback() {
|
60
|
+
super.connectedCallback();
|
61
|
+
this.addEventListener('d2l-list-item-nav-set-child-current', this.#setChildCurrent);
|
62
|
+
}
|
63
|
+
|
64
|
+
disconnectedCallback() {
|
65
|
+
super.disconnectedCallback();
|
66
|
+
this.removeEventListener('d2l-list-item-nav-set-child-current', this.#setChildCurrent);
|
67
|
+
}
|
68
|
+
|
69
|
+
firstUpdated(changedProperties) {
|
70
|
+
super.firstUpdated(changedProperties);
|
71
|
+
|
72
|
+
if (this.current) {
|
73
|
+
this.dispatchSetChildCurrentEvent(true);
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
updated(changedProperties) {
|
78
|
+
super.updated(changedProperties);
|
79
|
+
if (changedProperties.get('current') !== undefined) {
|
80
|
+
/** @ignore */
|
81
|
+
this.dispatchEvent(new CustomEvent('d2l-list-item-property-change', { bubbles: true, composed: true, detail: { name: 'current', value: this.current } }));
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
willUpdate(changedProperties) {
|
86
|
+
super.willUpdate(changedProperties);
|
87
|
+
if (changedProperties.has('current') || changedProperties.has('_childCurrent')) {
|
88
|
+
if (this.current) {
|
89
|
+
this.#setAriaCurrent('page');
|
90
|
+
} else if (this._childCurrent) {
|
91
|
+
this.#setAriaCurrent('location');
|
92
|
+
} else {
|
93
|
+
this.#setAriaCurrent(undefined);
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Internal. Do not use.
|
100
|
+
*/
|
101
|
+
dispatchSetChildCurrentEvent(val) {
|
102
|
+
/** @ignore */
|
103
|
+
this.dispatchEvent(new CustomEvent('d2l-list-item-nav-set-child-current', { bubbles: true, composed: true, detail: { value: val } }));
|
104
|
+
}
|
105
|
+
|
106
|
+
_onButtonClick(e) {
|
107
|
+
if (!this._getDescendantClicked(e)) {
|
108
|
+
this.current = true;
|
109
|
+
this._childCurrent = false;
|
110
|
+
}
|
111
|
+
super._onButtonClick(e);
|
112
|
+
}
|
113
|
+
|
114
|
+
#setAriaCurrent(val) {
|
115
|
+
this._ariaCurrent = val;
|
116
|
+
}
|
117
|
+
|
118
|
+
async #setChildCurrent(e) {
|
119
|
+
await this.updateComplete; // ensure button exists
|
120
|
+
if (e.target === this) return;
|
121
|
+
this._childCurrent = e.detail.value;
|
122
|
+
}
|
123
|
+
|
124
|
+
};
|