@brightspace-ui/core 3.24.1 → 3.25.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.
@@ -100,10 +100,6 @@ The `d2l-collapsible-panel` element is a container that provides specific layout
100
100
  d2l-collapsible-panel {
101
101
  width: 800px;
102
102
  }
103
- /* TODO: remove this when daylight demo resizing is fixed */
104
- d2l-collapsible-panel:not([expanded]) {
105
- margin-bottom: 14rem;
106
- }
107
103
  </style>
108
104
 
109
105
  <d2l-collapsible-panel panel-title="Collapsible Panel">
@@ -238,10 +234,6 @@ class CollapsiblePanelDaylightDemo extends LitElement {
238
234
  d2l-collapsible-panel {
239
235
  width: 500px;
240
236
  }
241
- /* TODO: remove this when daylight demo resizing is fixed */
242
- d2l-collapsible-panel:not([expanded]) {
243
- margin-bottom: 12rem;
244
- }
245
237
  `];
246
238
  }
247
239
 
@@ -216,7 +216,13 @@ class CollapsiblePanel extends SkeletonMixin(FocusMixin(RtlMixin(LitElement))) {
216
216
  }
217
217
  .d2l-collapsible-panel-opener {
218
218
  align-self: self-start;
219
+ background-color: transparent;
220
+ border: none;
219
221
  margin-inline-end: var(--d2l-collapsible-panel-spacing-inline);
222
+ order: 1;
223
+ outline: none;
224
+ padding-block: 0;
225
+ padding-inline: 0;
220
226
  }
221
227
  .d2l-collapsible-panel-opener > d2l-icon-custom {
222
228
  height: 0.9rem;
@@ -307,7 +313,7 @@ class CollapsiblePanel extends SkeletonMixin(FocusMixin(RtlMixin(LitElement))) {
307
313
  }
308
314
 
309
315
  static get focusElementSelector() {
310
- return 'button.d2l-offscreen';
316
+ return '.d2l-collapsible-panel-opener';
311
317
  }
312
318
 
313
319
  disconnectedCallback() {
@@ -324,17 +330,8 @@ class CollapsiblePanel extends SkeletonMixin(FocusMixin(RtlMixin(LitElement))) {
324
330
  'scrolled': this._scrolled,
325
331
  'no-bottom-border': this._noBottomBorder,
326
332
  };
327
- const expandCollapseLabel = this.expandCollapseLabel || this.panelTitle;
328
333
 
329
334
  return html`
330
- <button
331
- aria-expanded="${this.expanded}"
332
- class="d2l-offscreen"
333
- type="button"
334
- @click="${this._toggleExpand}"
335
- @focus="${this._onFocus}"
336
- @blur="${this._onBlur}"
337
- >${expandCollapseLabel}</button>
338
335
  <div class="${classMap(classes)}" @click="${this._handlePanelClick}">
339
336
  <div class="d2l-collapsible-panel-top-sentinel"></div>
340
337
  ${this._renderHeader()}
@@ -427,22 +424,31 @@ class CollapsiblePanel extends SkeletonMixin(FocusMixin(RtlMixin(LitElement))) {
427
424
  }
428
425
 
429
426
  _renderHeader() {
427
+ const expandCollapseLabel = this.expandCollapseLabel || this.panelTitle;
430
428
  return html`
431
429
  <div class="d2l-collapsible-panel-header" @click="${this._handleHeaderClick}">
432
430
  <div class="d2l-collapsible-panel-before">
433
431
  <slot name="before" @slotchange="${this._handleBeforeSlotChange}"></slot>
434
432
  </div>
435
433
  <div class="d2l-collapsible-panel-header-primary">
436
- ${this._renderPanelTitle()}
437
- <div class="d2l-collapsible-panel-header-actions" @click="${this._handleActionsClick}">
438
- <slot name="actions"></slot>
439
- </div>
440
- <div class="d2l-collapsible-panel-opener">
434
+ <button
435
+ class="d2l-collapsible-panel-opener"
436
+ aria-expanded="${this.expanded}"
437
+ type="button"
438
+ @click="${this._handleHeaderClick}"
439
+ @focus="${this._onFocus}"
440
+ @blur="${this._onBlur}"
441
+ aria-label="${expandCollapseLabel}"
442
+ >
441
443
  <d2l-icon-custom size="tier1" class="d2l-skeletize">
442
444
  <svg xmlns="http://www.w3.org/2000/svg" width="10" height="18" fill="none" viewBox="0 0 10 18">
443
445
  <path stroke="var(--d2l-color-tungsten)" stroke-linejoin="round" stroke-width="2" d="m9 9-8 8V1l8 8Z"/>
444
446
  </svg>
445
447
  </d2l-icon-custom>
448
+ </button>
449
+ ${this._renderPanelTitle()}
450
+ <div class="d2l-collapsible-panel-header-actions" @click="${this._handleActionsClick}">
451
+ <slot name="actions"></slot>
446
452
  </div>
447
453
  </div>
448
454
  <div class="d2l-collapsible-panel-header-secondary" @click="${this._handleHeaderSecondaryClick}">
@@ -51,7 +51,8 @@ class InputDateTimeRangeTo extends SkeletonMixin(LocalizeCoreElement(LitElement)
51
51
  display: flex;
52
52
  flex-wrap: wrap;
53
53
  }
54
- :host([display-to]) div:not(.d2l-input-date-time-range-to-container-block).d2l-input-date-time-range-to-container {
54
+ :host([display-to]) div:not(.d2l-input-date-time-range-to-container-block).d2l-input-date-time-range-to-container,
55
+ :host([display-to]) div:not(.d2l-input-date-time-range-to-container-block).d2l-input-date-time-range-to-container .d2l-input-date-time-range-end-container {
55
56
  column-gap: 0.9rem;
56
57
  }
57
58
  .d2l-input-date-time-range-end-container {
@@ -114,10 +115,10 @@ class InputDateTimeRangeTo extends SkeletonMixin(LocalizeCoreElement(LitElement)
114
115
  <div class="d2l-input-date-time-range-start-container">
115
116
  <slot name="left"></slot>
116
117
  </div>
118
+ <div class="d2l-input-date-time-range-end-container">
117
119
  <div class="d2l-body-small d2l-skeletize d2l-input-date-time-range-to-to">
118
120
  ${this.localize('components.input-date-time-range-to.to')}
119
121
  </div>
120
- <div class="d2l-input-date-time-range-end-container">
121
122
  <slot name="right"></slot>
122
123
  </div>
123
124
  </div>
@@ -8,6 +8,7 @@
8
8
  <script type="module">
9
9
  import '../../button/button-subtle.js';
10
10
  import '../../demo/demo-page.js';
11
+ import '../../link/link.js';
11
12
  import '../test/popover.js';
12
13
  </script>
13
14
  </head>
@@ -32,6 +33,24 @@
32
33
  </template>
33
34
  </d2l-demo-snippet>
34
35
 
36
+ <h2>Popover (trap-focus)</h2>
37
+ <d2l-demo-snippet>
38
+ <template>
39
+ <d2l-button-subtle id="open2" text="Open"></d2l-button-subtle>
40
+ <d2l-test-popover trap-focus id="popover2" style="max-width: 400px;">
41
+ <d2l-link href="https://pirateipsum.me/" target="_blank">Pirate Ipsum</d2l-link>
42
+ <div>Sink me piracy Gold Road quarterdeck wherry long boat line pillage walk the plank Plate Fleet. Haul wind black spot strike colors deadlights lee Barbary Coast yo-ho-ho ballast gally Shiver me timbers. Sea Legs quarterdeck yard scourge of the seven seas coffer plunder lanyard holystone code of conduct belay.</div>
43
+ <d2l-button-subtle id="close2" text="Close"></d2l-button-subtle>
44
+ </d2l-test-popover>
45
+ <script>
46
+ const popover2 = document.querySelector('#popover2');
47
+ document.querySelector('#open2').addEventListener('click', () => popover2.opened = !popover2.opened);
48
+ document.querySelector('#close2').addEventListener('click', () => popover2.opened = false);
49
+ popover2.addEventListener('d2l-popover-focus-enter', e => console.log(e.type, e));
50
+ </script>
51
+ </template>
52
+ </d2l-demo-snippet>
53
+
35
54
  <script>
36
55
  document.addEventListener('d2l-popover-open', e => console.log(e.type, e));
37
56
  document.addEventListener('d2l-popover-close', e => console.log(e.type, e));
@@ -1,7 +1,8 @@
1
1
  import '../colors/colors.js';
2
+ import '../focus-trap/focus-trap.js';
2
3
  import { clearDismissible, setDismissible } from '../../helpers/dismissible.js';
3
4
  import { css, html } from 'lit';
4
- import { getComposedActiveElement, getPreviousFocusableAncestor } from '../../helpers/focus.js';
5
+ import { getComposedActiveElement, getFirstFocusableDescendant, getPreviousFocusableAncestor } from '../../helpers/focus.js';
5
6
  import { isComposedAncestor } from '../../helpers/dom.js';
6
7
 
7
8
  const isSupported = ('popover' in HTMLElement.prototype);
@@ -13,16 +14,10 @@ export const PopoverMixin = superclass => class extends superclass {
13
14
 
14
15
  static get properties() {
15
16
  return {
16
- /**
17
- * Whether the popover is open or not
18
- * @type {boolean}
19
- */
20
- opened: { type: Boolean, reflect: true },
21
- /**
22
- * Whether to disable auto-close/light-dismiss
23
- * @type {boolean}
24
- */
25
- noAutoClose: { type: Boolean, reflect: true, attribute: 'no-auto-close' },
17
+ _noAutoClose: { state: true },
18
+ _noAutoFocus: { state: true },
19
+ _opened: { type: Boolean, reflect: true, attribute: '_opened' },
20
+ _trapFocus: { state: true },
26
21
  _useNativePopover: { type: String, reflect: true, attribute: 'popover' }
27
22
  };
28
23
  }
@@ -55,7 +50,7 @@ export const PopoverMixin = superclass => class extends superclass {
55
50
  :host(:not([popover])) {
56
51
  z-index: 998; /* position on top of floating buttons */
57
52
  }
58
- :host([opened]) {
53
+ :host([_opened]) {
59
54
  display: inline-block;
60
55
  }
61
56
 
@@ -65,6 +60,7 @@ export const PopoverMixin = superclass => class extends superclass {
65
60
  border-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
66
61
  box-shadow: var(--d2l-popover-shadow, var(--d2l-popover-default-shadow));
67
62
  box-sizing: border-box;
63
+ outline: none;
68
64
  }
69
65
 
70
66
  @keyframes d2l-popover-animation {
@@ -72,8 +68,8 @@ export const PopoverMixin = superclass => class extends superclass {
72
68
  100% { opacity: 1; transform: translate(0, 0); }
73
69
  }
74
70
  @media (prefers-reduced-motion: no-preference) {
75
- :host([opened]) {
76
- animation: var(--d2l-popover-animation-name, var(--d2l-popover-default-animation-name)) 6000ms ease;
71
+ :host([_opened]) {
72
+ animation: var(--d2l-popover-animation-name, var(--d2l-popover-default-animation-name)) 300ms ease;
77
73
  }
78
74
  }
79
75
  `;
@@ -81,8 +77,7 @@ export const PopoverMixin = superclass => class extends superclass {
81
77
 
82
78
  constructor() {
83
79
  super();
84
- this.noAutoClose = false;
85
- this.opened = false;
80
+ this.configure();
86
81
  this._useNativePopover = isSupported ? 'manual' : undefined;
87
82
  this._handleAutoCloseClick = this._handleAutoCloseClick.bind(this);
88
83
  this._handleAutoCloseFocus = this._handleAutoCloseFocus.bind(this);
@@ -90,7 +85,7 @@ export const PopoverMixin = superclass => class extends superclass {
90
85
 
91
86
  connectedCallback() {
92
87
  super.connectedCallback();
93
- if (this.opened) this._addAutoCloseHandlers();
88
+ if (this._opened) this._addAutoCloseHandlers();
94
89
  }
95
90
 
96
91
  disconnectedCallback() {
@@ -99,29 +94,48 @@ export const PopoverMixin = superclass => class extends superclass {
99
94
  this._clearDismissible();
100
95
  }
101
96
 
102
- updated(changedProperties) {
103
- super.updated(changedProperties);
104
- if (changedProperties.has('opened')) {
97
+ async close() {
98
+ if (!this._opened) return;
105
99
 
106
- if (this._useNativePopover) {
107
- if (this.opened) this.showPopover();
108
- else this.hidePopover();
109
- }
100
+ this._opened = false;
110
101
 
111
- this._previousFocusableAncestor = this.opened ? getPreviousFocusableAncestor(this, false, false) : null;
112
-
113
- if (this.opened) {
114
- this._opener = getComposedActiveElement();
115
- this._addAutoCloseHandlers();
116
- this._dismissibleId = setDismissible(() => this._close());
117
- this.dispatchEvent(new CustomEvent('d2l-popover-open', { bubbles: true, composed: true }));
118
- } else if (changedProperties.get('opened') !== undefined) {
119
- this._removeAutoCloseHandlers();
120
- this._clearDismissible();
121
- this.dispatchEvent(new CustomEvent('d2l-popover-close', { bubbles: true, composed: true }));
122
- }
102
+ if (this._useNativePopover) this.hidePopover();
123
103
 
124
- }
104
+ this._previousFocusableAncestor = null;
105
+ this._removeAutoCloseHandlers();
106
+ this._clearDismissible();
107
+ await this.updateComplete; // wait before applying focus to opener
108
+ this._focusOpener();
109
+ this.dispatchEvent(new CustomEvent('d2l-popover-close', { bubbles: true, composed: true }));
110
+ }
111
+
112
+ configure(properties) {
113
+ this._noAutoClose = properties?.noAutoClose ?? false;
114
+ this._noAutoFocus = properties?.noAutoFocus ?? false;
115
+ this._trapFocus = properties?.trapFocus ?? false;
116
+ }
117
+
118
+ async open(applyFocus = true) {
119
+ if (this._opened) return;
120
+
121
+ this._applyFocus = applyFocus !== undefined ? applyFocus : true;
122
+ this._opened = true;
123
+
124
+ await this.updateComplete; // wait for popover attribute before managing top-layer
125
+ if (this._useNativePopover) this.showPopover();
126
+
127
+ this._previousFocusableAncestor = getPreviousFocusableAncestor(this, false, false);
128
+
129
+ this._opener = getComposedActiveElement();
130
+ this._addAutoCloseHandlers();
131
+ this._dismissibleId = setDismissible(() => this.close());
132
+ this._focusContent(this);
133
+ this.dispatchEvent(new CustomEvent('d2l-popover-open', { bubbles: true, composed: true }));
134
+ }
135
+
136
+ toggleOpen(applyFocus = true) {
137
+ if (this._opened) return this.close();
138
+ else return this.open(!this._noAutoFocus && applyFocus);
125
139
  }
126
140
 
127
141
  _addAutoCloseHandlers() {
@@ -136,25 +150,42 @@ export const PopoverMixin = superclass => class extends superclass {
136
150
  this._dismissibleId = null;
137
151
  }
138
152
 
139
- _close() {
140
- const hide = () => {
141
- this.opened = false;
142
- };
153
+ _focusContent(container) {
154
+ if (this._noAutoFocus || this._applyFocus === false) return;
155
+
156
+ const focusable = getFirstFocusableDescendant(container);
157
+ if (focusable) {
158
+ // Removing the rAF call can allow infinite focus looping to happen in content using a focus trap
159
+ requestAnimationFrame(() => focusable.focus());
160
+ } else {
161
+ const content = this._getContentContainer();
162
+ content.setAttribute('tabindex', '-1');
163
+ content.focus();
164
+ }
165
+ }
166
+
167
+ _focusOpener() {
168
+ if (!document.activeElement) return;
169
+ if (!isComposedAncestor(this, getComposedActiveElement())) return;
143
170
 
144
- hide();
171
+ this?._opener.focus();
172
+ }
173
+
174
+ _getContentContainer() {
175
+ return this.shadowRoot.querySelector('.content');
145
176
  }
146
177
 
147
178
  _handleAutoCloseClick(e) {
148
179
 
149
- if (!this.opened || this.noAutoClose) return;
180
+ if (!this._opened || this._noAutoClose) return;
150
181
 
151
182
  const rootTarget = e.composedPath()[0];
152
- if (isComposedAncestor(this.shadowRoot.querySelector('.content'), rootTarget)
183
+ if (isComposedAncestor(this._getContentContainer(), rootTarget)
153
184
  || (this._opener !== document.body && isComposedAncestor(this._opener, rootTarget))) {
154
185
  return;
155
186
  }
156
187
 
157
- this._close();
188
+ this.close();
158
189
  }
159
190
 
160
191
  _handleAutoCloseFocus() {
@@ -162,8 +193,8 @@ export const PopoverMixin = superclass => class extends superclass {
162
193
  // todo: try to use relatedTarget instead - this logic is largely copied as-is from dropdown simply to mitigate risk of this fragile code
163
194
  setTimeout(() => {
164
195
  // we ignore focusable ancestors othrwise the popover will close when user clicks empty space inside the popover
165
- if (!this.opened
166
- || this.noAutoClose
196
+ if (!this._opened
197
+ || this._noAutoClose
167
198
  || !document.activeElement
168
199
  || document.activeElement === this._previousFocusableAncestor
169
200
  || document.activeElement === document.body) {
@@ -171,16 +202,23 @@ export const PopoverMixin = superclass => class extends superclass {
171
202
  }
172
203
 
173
204
  const activeElement = getComposedActiveElement();
174
- if (isComposedAncestor(this.shadowRoot.querySelector('.content'), activeElement)
205
+ if (isComposedAncestor(this, activeElement)
175
206
  || activeElement === this._opener) {
176
207
  return;
177
208
  }
178
209
 
179
- this._close();
210
+ this.close();
180
211
  }, 0);
181
212
 
182
213
  }
183
214
 
215
+ _handleFocusTrapEnter() {
216
+ this._focusContent(this._getContentContainer());
217
+
218
+ /** Dispatched when user focus enters the popover (trap-focus option only) */
219
+ this.dispatchEvent(new CustomEvent('d2l-popover-focus-enter', { detail: { applyFocus: this._applyFocus } }));
220
+ }
221
+
184
222
  _removeAutoCloseHandlers() {
185
223
  this.removeEventListener('blur', this._handleAutoCloseFocus, { capture: true });
186
224
  document.body?.removeEventListener('focus', this._handleAutoCloseFocus, { capture: true }); // DE41322: document.body can be null in some scenarios
@@ -188,7 +226,13 @@ export const PopoverMixin = superclass => class extends superclass {
188
226
  }
189
227
 
190
228
  _renderPopover() {
191
- return html`<div class="content"><slot></slot></div>`;
229
+ const content = html`<div class="content"><slot></slot></div>`;
230
+
231
+ if (this._trapFocus) return html`<d2l-focus-trap @d2l-focus-trap-enter="${this._handleFocusTrapEnter}" ?trap="${this._opened}">
232
+ ${content}
233
+ </d2l-focus-trap>`;
234
+
235
+ return content;
192
236
  }
193
237
 
194
238
  };
@@ -10723,11 +10723,23 @@
10723
10723
  "type": "boolean",
10724
10724
  "default": "false"
10725
10725
  },
10726
+ {
10727
+ "name": "no-auto-focus",
10728
+ "description": "Whether to disable auto-focus on the first focusable element when opened",
10729
+ "type": "boolean",
10730
+ "default": "false"
10731
+ },
10726
10732
  {
10727
10733
  "name": "opened",
10728
10734
  "description": "Whether the popover is open or not",
10729
10735
  "type": "boolean",
10730
10736
  "default": "false"
10737
+ },
10738
+ {
10739
+ "name": "trap-focus",
10740
+ "description": "Whether to render a d2l-focus-trap around the content",
10741
+ "type": "boolean",
10742
+ "default": "false"
10731
10743
  }
10732
10744
  ],
10733
10745
  "properties": [
@@ -10738,20 +10750,38 @@
10738
10750
  "type": "boolean",
10739
10751
  "default": "false"
10740
10752
  },
10753
+ {
10754
+ "name": "noAutoFocus",
10755
+ "attribute": "no-auto-focus",
10756
+ "description": "Whether to disable auto-focus on the first focusable element when opened",
10757
+ "type": "boolean",
10758
+ "default": "false"
10759
+ },
10741
10760
  {
10742
10761
  "name": "opened",
10743
10762
  "attribute": "opened",
10744
10763
  "description": "Whether the popover is open or not",
10745
10764
  "type": "boolean",
10746
10765
  "default": "false"
10766
+ },
10767
+ {
10768
+ "name": "trapFocus",
10769
+ "attribute": "trap-focus",
10770
+ "description": "Whether to render a d2l-focus-trap around the content",
10771
+ "type": "boolean",
10772
+ "default": "false"
10747
10773
  }
10748
10774
  ],
10749
10775
  "events": [
10776
+ {
10777
+ "name": "d2l-popover-close"
10778
+ },
10750
10779
  {
10751
10780
  "name": "d2l-popover-open"
10752
10781
  },
10753
10782
  {
10754
- "name": "d2l-popover-close"
10783
+ "name": "d2l-popover-focus-enter",
10784
+ "description": "Dispatched when user focus enters the popover (trap-focus option only)"
10755
10785
  }
10756
10786
  ]
10757
10787
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.24.1",
3
+ "version": "3.25.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",