@brightspace-ui/core 3.158.2 → 3.159.1

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.
@@ -9,7 +9,9 @@ class FormErrorSummary extends LocalizeCoreElement(LitElement) {
9
9
  static get properties() {
10
10
  return {
11
11
  errors: { type: Object, attribute: false },
12
- _expanded: { type: Boolean, attribute: false }
12
+ _expanded: { type: Boolean, attribute: false },
13
+ _hasBottomMargin: { type: Boolean, attribute: '_has-bottom-margin', reflect: true },
14
+ _hasErrors: { type: Boolean, attribute: '_has-errors', reflect: true },
13
15
  };
14
16
  }
15
17
 
@@ -19,10 +21,12 @@ class FormErrorSummary extends LocalizeCoreElement(LitElement) {
19
21
  :host {
20
22
  display: block;
21
23
  }
22
-
23
24
  :host([hidden]) {
24
25
  display: none;
25
26
  }
27
+ :host([_has-bottom-margin][_has-errors]) {
28
+ margin-block-end: 1rem;
29
+ }
26
30
 
27
31
  .d2l-form-error-summary-header {
28
32
  cursor: pointer;
@@ -54,6 +58,8 @@ class FormErrorSummary extends LocalizeCoreElement(LitElement) {
54
58
  super();
55
59
  this.errors = [];
56
60
  this._expanded = true;
61
+ this._hasBottomMargin = false;
62
+ this._hasErrors = false;
57
63
  }
58
64
 
59
65
  render() {
@@ -82,6 +88,13 @@ class FormErrorSummary extends LocalizeCoreElement(LitElement) {
82
88
  return this.errors.length > 0 ? errorSummary : nothing;
83
89
  }
84
90
 
91
+ willUpdate(changedProperties) {
92
+ super.willUpdate(changedProperties);
93
+ if (changedProperties.has('errors')) {
94
+ this._hasErrors = this.errors.length > 0;
95
+ }
96
+ }
97
+
85
98
  async focus() {
86
99
  if (this.errors.length === 0) {
87
100
  super.focus();
@@ -31,7 +31,8 @@ const _findFormElementsHelper = (ele, eles, isFormElementPredicate, visitChildre
31
31
  eles.push(ele);
32
32
  }
33
33
  if (visitChildrenPredicate(ele)) {
34
- for (const child of ele.children) {
34
+ const children = ele.tagName === 'SLOT' && ['primary', 'secondary'].includes(ele.name) ? ele.assignedNodes() : ele.children;
35
+ for (const child of children) {
35
36
  _findFormElementsHelper(child, eles, isFormElementPredicate, visitChildrenPredicate);
36
37
  }
37
38
  }
@@ -1,4 +1,4 @@
1
- import './form-errory-summary.js';
1
+ import './form-error-summary.js';
2
2
  import '../tooltip/tooltip.js';
3
3
  import '../link/link.js';
4
4
  import { css, html, LitElement } from 'lit';
@@ -34,6 +34,11 @@ class Form extends LocalizeCoreElement(LitElement) {
34
34
  * @type {boolean}
35
35
  */
36
36
  trackChanges: { type: Boolean, attribute: 'track-changes', reflect: true },
37
+ /**
38
+ * Id for an alternative error summary element
39
+ * @type {string}
40
+ */
41
+ summaryId: { type: String, attribute: 'summary-id' },
37
42
  _errors: { type: Object },
38
43
  _hasErrors: { type: Boolean, attribute: '_has-errors', reflect: true },
39
44
  };
@@ -56,6 +61,7 @@ class Form extends LocalizeCoreElement(LitElement) {
56
61
  constructor() {
57
62
  super();
58
63
  this.trackChanges = false;
64
+ this.summaryId = null;
59
65
  this._errors = new Map();
60
66
  this._isSubForm = false;
61
67
  this._nestedForms = new Map();
@@ -63,6 +69,7 @@ class Form extends LocalizeCoreElement(LitElement) {
63
69
  this._firstUpdatePromise = new Promise((resolve) => {
64
70
  this._firstUpdateResolve = resolve;
65
71
  });
72
+ this._hasErrors = false;
66
73
  this._tooltips = new Map();
67
74
  this._validationCustoms = new Set();
68
75
 
@@ -76,6 +83,12 @@ class Form extends LocalizeCoreElement(LitElement) {
76
83
  this.addEventListener('d2l-validation-custom-connected', this._validationCustomConnected);
77
84
  }
78
85
 
86
+ get errorSummary() {
87
+ return [...flattenMap(this._errors)]
88
+ .filter(([, eleErrors]) => eleErrors.length > 0)
89
+ .map(([ele, eleErrors]) => ({ href: `#${ele.id}`, message: eleErrors[0], onClick: () => ele.focus() }));
90
+ }
91
+
79
92
  connectedCallback() {
80
93
  super.connectedCallback();
81
94
  window.addEventListener('beforeunload', this._onUnload);
@@ -103,11 +116,8 @@ class Form extends LocalizeCoreElement(LitElement) {
103
116
 
104
117
  render() {
105
118
  let errorSummary = null;
106
- if (this._isRootForm()) {
107
- const errors = [...flattenMap(this._errors)]
108
- .filter(([, eleErrors]) => eleErrors.length > 0)
109
- .map(([ele, eleErrors]) => ({ href: `#${ele.id}`, message: eleErrors[0], onClick: () => ele.focus() }));
110
- errorSummary = html`<d2l-form-error-summary .errors=${errors}></d2l-form-error-summary>`;
119
+ if (!this.summaryId && this._isRootForm()) {
120
+ errorSummary = html`<d2l-form-error-summary .errors=${this.errorSummary}></d2l-form-error-summary>`;
111
121
  }
112
122
  return html`
113
123
  ${errorSummary}
@@ -120,6 +130,9 @@ class Form extends LocalizeCoreElement(LitElement) {
120
130
  if (changedProperties.has('_errors')) {
121
131
  this._hasErrors = this._errors.size > 0;
122
132
  }
133
+ if ((changedProperties.has('summary-id') || changedProperties.has('_errors')) && this.summaryId) {
134
+ this.querySelector(`#${this.summaryId}`).errors = this.errorSummary;
135
+ }
123
136
  }
124
137
 
125
138
  async requestSubmit(submitter) {
@@ -1,8 +1,12 @@
1
1
  import '../icons/icon.js';
2
2
  import { css, html, LitElement } from 'lit';
3
+ import { getFlag } from '../../helpers/flags.js';
3
4
  import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
4
5
  import { MenuItemMixin } from './menu-item-mixin.js';
5
6
  import { menuItemStyles } from './menu-item-styles.js';
7
+ import { overflowEllipsisDeclarations } from '../../helpers/overflow.js';
8
+
9
+ const overflowClipEnabled = getFlag('GAUD-7887-core-components-overflow-clipping', true);
6
10
 
7
11
  class MenuItemReturn extends LocalizeCoreElement(MenuItemMixin(LitElement)) {
8
12
 
@@ -17,11 +21,13 @@ class MenuItemReturn extends LocalizeCoreElement(MenuItemMixin(LitElement)) {
17
21
  span {
18
22
  flex: auto;
19
23
  line-height: 1rem;
20
- overflow-x: hidden;
21
- overflow-y: hidden;
22
- text-decoration: none;
23
- text-overflow: ellipsis;
24
- white-space: nowrap;
24
+ ${overflowClipEnabled ? overflowEllipsisDeclarations : css`
25
+ overflow-x: hidden;
26
+ overflow-y: hidden;
27
+ text-decoration: none;
28
+ text-overflow: ellipsis;
29
+ white-space: nowrap;
30
+ `}
25
31
  }
26
32
 
27
33
  d2l-icon {
@@ -1,6 +1,10 @@
1
1
  import '../colors/colors.js';
2
2
  import { css, unsafeCSS } from 'lit';
3
+ import { getFlag } from '../../helpers/flags.js';
3
4
  import { getFocusPseudoClass } from '../../helpers/focus.js';
5
+ import { getOverflowDeclarations } from '../../helpers/overflow.js';
6
+
7
+ const overflowClipEnabled = getFlag('GAUD-7887-core-components-overflow-clipping', true);
4
8
 
5
9
  export const menuItemStyles = css`
6
10
  :host {
@@ -51,15 +55,17 @@ export const menuItemStyles = css`
51
55
  }
52
56
 
53
57
  .d2l-menu-item-text {
54
- -webkit-box-orient: vertical;
55
- display: -webkit-box;
56
58
  flex: auto;
57
- -webkit-line-clamp: var(--d2l-menu-item-lines, 2);
58
59
  line-height: 1rem;
59
- overflow-wrap: anywhere;
60
- overflow-x: hidden;
61
- overflow-y: hidden;
62
- white-space: normal;
60
+ ${overflowClipEnabled ? getOverflowDeclarations({ lines: 'var(--d2l-menu-item-lines, 2)' }) : css`
61
+ -webkit-box-orient: vertical;
62
+ display: -webkit-box;
63
+ -webkit-line-clamp: var(--d2l-menu-item-lines, 2);
64
+ overflow-wrap: anywhere;
65
+ overflow-x: hidden;
66
+ overflow-y: hidden;
67
+ white-space: normal;
68
+ `}
63
69
  }
64
70
 
65
71
  .d2l-menu-item-supporting {
@@ -4786,7 +4786,7 @@
4786
4786
  },
4787
4787
  {
4788
4788
  "name": "d2l-form-error-summary",
4789
- "path": "./components/form/form-errory-summary.js",
4789
+ "path": "./components/form/form-error-summary.js",
4790
4790
  "properties": [
4791
4791
  {
4792
4792
  "name": "errors",
@@ -4810,6 +4810,11 @@
4810
4810
  "description": "Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.",
4811
4811
  "type": "boolean",
4812
4812
  "default": "false"
4813
+ },
4814
+ {
4815
+ "name": "summary-id",
4816
+ "description": "Id for an alternative error summary element",
4817
+ "type": "string"
4813
4818
  }
4814
4819
  ],
4815
4820
  "properties": [
@@ -4819,12 +4824,22 @@
4819
4824
  "description": "Indicates that the form should opt-out of nesting.\nThis means that it will not be submitted or validated if an ancestor form is submitted or validated.\nHowever, directly submitting or validating a form with `no-nesting` will still trigger submission and validation for its descendant forms unless they also opt-out using `no-nesting`.",
4820
4825
  "type": "boolean"
4821
4826
  },
4827
+ {
4828
+ "name": "errorSummary",
4829
+ "type": "{ href: string; message: any; onClick: () => any; }[]"
4830
+ },
4822
4831
  {
4823
4832
  "name": "trackChanges",
4824
4833
  "attribute": "track-changes",
4825
4834
  "description": "Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.",
4826
4835
  "type": "boolean",
4827
4836
  "default": "false"
4837
+ },
4838
+ {
4839
+ "name": "summaryId",
4840
+ "attribute": "summary-id",
4841
+ "description": "Id for an alternative error summary element",
4842
+ "type": "string"
4828
4843
  }
4829
4844
  ],
4830
4845
  "events": [
@@ -14733,6 +14748,12 @@
14733
14748
  "description": "Whether content fills the screen or not",
14734
14749
  "type": "'fullscreen'|'normal'",
14735
14750
  "default": "\"fullscreen\""
14751
+ },
14752
+ {
14753
+ "name": "has-form",
14754
+ "description": "Whether to render an encompassing form over all panels",
14755
+ "type": " Boolean ",
14756
+ "default": "false"
14736
14757
  }
14737
14758
  ],
14738
14759
  "properties": [
@@ -14748,6 +14769,9 @@
14748
14769
  "description": "The key used to persist the divider's position to local storage. This key\nshould not be shared between pages so that users can save different divider\npositions on different pages. If no key is provided, the template will fall\nback its default size.",
14749
14770
  "type": "string"
14750
14771
  },
14772
+ {
14773
+ "name": "form"
14774
+ },
14751
14775
  {
14752
14776
  "name": "backgroundShading",
14753
14777
  "attribute": "background-shading",
@@ -14775,6 +14799,13 @@
14775
14799
  "description": "Whether content fills the screen or not",
14776
14800
  "type": "'fullscreen'|'normal'",
14777
14801
  "default": "\"fullscreen\""
14802
+ },
14803
+ {
14804
+ "name": "hasForm",
14805
+ "attribute": "has-form",
14806
+ "description": "Whether to render an encompassing form over all panels",
14807
+ "type": " Boolean ",
14808
+ "default": "false"
14778
14809
  }
14779
14810
  ],
14780
14811
  "events": [
@@ -14785,6 +14816,18 @@
14785
14816
  {
14786
14817
  "name": "d2l-template-primary-secondary-resize-end",
14787
14818
  "description": "Dispatched when a user finishes moving the divider."
14819
+ },
14820
+ {
14821
+ "name": "d2l-template-primary-secondary-form-invalid",
14822
+ "description": "Dispatched when the form fails validation. The error map can be obtained from the detail's errors property."
14823
+ },
14824
+ {
14825
+ "name": "d2l-template-primary-secondary-form-dirty",
14826
+ "description": "Dispatched whenever any form element fires an input or change event. Can be used to track whether the form is dirty or not."
14827
+ },
14828
+ {
14829
+ "name": "d2l-template-primary-secondary-form-submit",
14830
+ "description": "Dispatched when the form is submitted. The form data can be obtained from the detail's formData property."
14788
14831
  }
14789
14832
  ],
14790
14833
  "slots": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.158.2",
3
+ "version": "3.159.1",
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",
@@ -0,0 +1,55 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5
+ <meta charset="UTF-8">
6
+ <link rel="stylesheet" href="../../../components/demo/styles.css" type="text/css">
7
+ <script type="module">
8
+ import '../../../components/button/button.js';
9
+ import '../../../components/demo/demo-page.js';
10
+ import '../../../components/inputs/input-group.js';
11
+ import '../../../components/inputs/input-radio-group.js';
12
+ import '../../../components/inputs/input-radio.js';
13
+ import '../../../components/inputs/input-text.js';
14
+ import '../../../components/inputs/input-textarea.js';
15
+ import '../primary-secondary.js';
16
+ </script>
17
+ <style>
18
+ d2l-template-primary-secondary {
19
+ --d2l-template-primary-secondary-primary-padding: 20px;
20
+ }
21
+ div[slot="secondary"] {
22
+ padding: 10px;
23
+ }
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <d2l-template-primary-secondary has-form background-shading="secondary" width-type="normal">
28
+ <div slot="primary">
29
+ <h1>Title</h1>
30
+ <d2l-input-group>
31
+ <d2l-input-text name="name" label="Name" required></d2l-input-text>
32
+ <d2l-input-textarea name="description" label="Description"></d2l-input-textarea>
33
+ </d2l-input-group>
34
+ </div>
35
+ <div slot="secondary">
36
+ <d2l-input-radio-group name="band" label="Band" required>
37
+ <d2l-input-radio label="FM" value="fm"></d2l-input-radio>
38
+ <d2l-input-radio label="AM" value="am"></d2l-input-radio>
39
+ </d2l-input-radio-group>
40
+ </div>
41
+ <div slot="footer">
42
+ <d2l-button primary>Save</d2l-button>
43
+ </div>
44
+ </d2l-template-primary-secondary>
45
+ <script>
46
+ const form = document.querySelector('d2l-template-primary-secondary');
47
+ document
48
+ .querySelector('d2l-button')
49
+ .addEventListener('click', () => form.submitForm());
50
+ form.addEventListener('d2l-form-submit', (e) => {
51
+ console.log('Form submitted!', e.detail.formData);
52
+ });
53
+ </script>
54
+ </body>
55
+ </html>
@@ -1,8 +1,10 @@
1
1
  import '../../components/colors/colors.js';
2
+ import '../../components/form/form.js';
3
+ import '../../components/form/form-error-summary.js';
2
4
  import '../../components/icons/icon-custom.js';
3
5
  import '../../components/icons/icon.js';
4
6
  import '../../components/offscreen/offscreen.js';
5
- import { css, html, LitElement, unsafeCSS } from 'lit';
7
+ import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
6
8
  import { getFocusPseudoClass, getFocusRingStyles } from '../../helpers/focus.js';
7
9
  import { classMap } from 'lit/directives/class-map.js';
8
10
  import { formatPercent } from '@brightspace-ui/intl';
@@ -547,6 +549,9 @@ class MobileTouchResizer extends Resizer {
547
549
  * @slot secondary - Supplementary page content
548
550
  * @fires d2l-template-primary-secondary-resize-start - Dispatched when a user begins moving the divider.
549
551
  * @fires d2l-template-primary-secondary-resize-end - Dispatched when a user finishes moving the divider.
552
+ * @fires d2l-template-primary-secondary-form-invalid Dispatched when the form fails validation. The error map can be obtained from the detail's errors property.
553
+ * @fires d2l-template-primary-secondary-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.
554
+ * @fires d2l-template-primary-secondary-form-submit Dispatched when the form is submitted. The form data can be obtained from the detail's formData property.
550
555
  */
551
556
  class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
552
557
 
@@ -587,7 +592,12 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
587
592
  * @type {'fullscreen'|'normal'}
588
593
  */
589
594
  widthType: { type: String, attribute: 'width-type', reflect: true },
590
- _animateResize: { type: Boolean, attribute: false },
595
+ /**
596
+ * Whether to render an encompassing form over all panels
597
+ * @type { Boolean }
598
+ */
599
+ hasForm: { type: Boolean, attribute: 'has-form' },
600
+ _formErrorSummary: { type: Array },
591
601
  _hasFooter: { type: Boolean, attribute: false },
592
602
  _isCollapsed: { type: Boolean, attribute: false },
593
603
  _isExpanded: { type: Boolean, attribute: false },
@@ -599,7 +609,8 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
599
609
 
600
610
  static get styles() {
601
611
  return css`
602
- :host {
612
+ :host,
613
+ :host > d2l-form {
603
614
  bottom: 0;
604
615
  left: 0;
605
616
  overflow: hidden;
@@ -607,6 +618,7 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
607
618
  right: 0;
608
619
  top: 0;
609
620
  }
621
+
610
622
  :host([hidden]) {
611
623
  display: none;
612
624
  }
@@ -631,6 +643,7 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
631
643
  main {
632
644
  flex: 2 0 0;
633
645
  overflow-x: hidden;
646
+ padding: var(--d2l-template-primary-secondary-primary-padding, 0);
634
647
  transition: none;
635
648
  }
636
649
  :host([resizable]) main {
@@ -1003,6 +1016,12 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
1003
1016
  this._isMobile = isMobile();
1004
1017
  this._hasConnectedResizers = false;
1005
1018
  this._sizeAsPercent = 0;
1019
+
1020
+ this.hasForm = false;
1021
+ }
1022
+
1023
+ get form() {
1024
+ return this.shadowRoot.querySelector('d2l-form');
1006
1025
  }
1007
1026
 
1008
1027
  disconnectedCallback() {
@@ -1032,7 +1051,10 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
1032
1051
  const scrollClasses = {
1033
1052
  'd2l-template-scroll': isWindows
1034
1053
  };
1035
- const primarySection = html`<main class="${classMap(scrollClasses)}"><slot name="primary"></slot></main>`;
1054
+ const primarySection = html`<main class="${classMap(scrollClasses)}">
1055
+ ${this.hasForm ? html`<d2l-form-error-summary _has-bottom-margin id="form-error-summary"></d2l-form-error-summary>` : nothing}
1056
+ <slot name="primary"></slot>
1057
+ </main>`;
1036
1058
  const secondarySection = html`
1037
1059
  <div style=${styleMap(secondaryPanelStyles)} class="d2l-template-primary-secondary-secondary-container" @transitionend=${this._onTransitionEnd}>
1038
1060
  <div class="d2l-template-primary-secondary-divider-shadow"></div>
@@ -1040,7 +1062,7 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
1040
1062
  <slot name="secondary"></slot>
1041
1063
  </aside>
1042
1064
  </div>`;
1043
- return html`
1065
+ const content = html`
1044
1066
  <div class="d2l-template-primary-secondary-container">
1045
1067
  <header><slot name="header"></slot></header>
1046
1068
  <div class="d2l-template-primary-secondary-content" data-background-shading="${this.backgroundShading}" ?data-animate-resize=${this._animateResize} ?data-is-collapsed=${this._isCollapsed} ?data-is-expanded=${this._isExpanded}>
@@ -1053,6 +1075,15 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
1053
1075
  </footer>
1054
1076
  </div>
1055
1077
  `;
1078
+
1079
+ if (this.hasForm) return html`<d2l-form
1080
+ summary-id="form-error-summary"
1081
+ @d2l-form-invalid=${this.#handleInvalidForm}
1082
+ @d2l-form-submit=${this.#handleFormSubmit}
1083
+ @d2l-form-dirty=${this.#handleFormDirty}>
1084
+ ${content}
1085
+ </d2l-form>`;
1086
+ return content;
1056
1087
  }
1057
1088
 
1058
1089
  updated(changedProperties) {
@@ -1097,6 +1128,10 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
1097
1128
  }
1098
1129
  }
1099
1130
 
1131
+ submitForm() {
1132
+ this.form.submit();
1133
+ }
1134
+
1100
1135
  get _size() {
1101
1136
  return this.__size;
1102
1137
  }
@@ -1281,6 +1316,18 @@ class TemplatePrimarySecondary extends LocalizeCoreElement(LitElement) {
1281
1316
  `;
1282
1317
 
1283
1318
  }
1319
+
1320
+ #handleFormDirty(e) {
1321
+ this.dispatchEvent(new CustomEvent('d2l-template-primary-secondary-form-dirty', { detail: e.detail }));
1322
+ }
1323
+
1324
+ #handleFormSubmit(e) {
1325
+ this.dispatchEvent(new CustomEvent('d2l-template-primary-secondary-form-submit', { detail: e.detail }));
1326
+ }
1327
+
1328
+ #handleInvalidForm(e) {
1329
+ this.dispatchEvent(new CustomEvent('d2l-template-primary-secondary-form-invalid', { detail: e.detail }));
1330
+ }
1284
1331
  }
1285
1332
 
1286
1333
  customElements.define('d2l-template-primary-secondary', TemplatePrimarySecondary);