@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.
@@ -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);
@@ -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
+ };