@brightspace-ui/core 3.24.1 → 3.25.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.
@@ -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>
@@ -1,7 +1,7 @@
1
1
  import '../colors/colors.js';
2
2
  import { clearDismissible, setDismissible } from '../../helpers/dismissible.js';
3
3
  import { css, html } from 'lit';
4
- import { getComposedActiveElement, getPreviousFocusableAncestor } from '../../helpers/focus.js';
4
+ import { getComposedActiveElement, getFirstFocusableDescendant, getPreviousFocusableAncestor } from '../../helpers/focus.js';
5
5
  import { isComposedAncestor } from '../../helpers/dom.js';
6
6
 
7
7
  const isSupported = ('popover' in HTMLElement.prototype);
@@ -13,16 +13,9 @@ export const PopoverMixin = superclass => class extends superclass {
13
13
 
14
14
  static get properties() {
15
15
  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' },
16
+ _noAutoClose: { state: true },
17
+ _noAutoFocus: { state: true },
18
+ _opened: { type: Boolean, reflect: true, attribute: '_opened' },
26
19
  _useNativePopover: { type: String, reflect: true, attribute: 'popover' }
27
20
  };
28
21
  }
@@ -55,7 +48,7 @@ export const PopoverMixin = superclass => class extends superclass {
55
48
  :host(:not([popover])) {
56
49
  z-index: 998; /* position on top of floating buttons */
57
50
  }
58
- :host([opened]) {
51
+ :host([_opened]) {
59
52
  display: inline-block;
60
53
  }
61
54
 
@@ -65,6 +58,7 @@ export const PopoverMixin = superclass => class extends superclass {
65
58
  border-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
66
59
  box-shadow: var(--d2l-popover-shadow, var(--d2l-popover-default-shadow));
67
60
  box-sizing: border-box;
61
+ outline: none;
68
62
  }
69
63
 
70
64
  @keyframes d2l-popover-animation {
@@ -72,8 +66,8 @@ export const PopoverMixin = superclass => class extends superclass {
72
66
  100% { opacity: 1; transform: translate(0, 0); }
73
67
  }
74
68
  @media (prefers-reduced-motion: no-preference) {
75
- :host([opened]) {
76
- animation: var(--d2l-popover-animation-name, var(--d2l-popover-default-animation-name)) 6000ms ease;
69
+ :host([_opened]) {
70
+ animation: var(--d2l-popover-animation-name, var(--d2l-popover-default-animation-name)) 300ms ease;
77
71
  }
78
72
  }
79
73
  `;
@@ -81,8 +75,7 @@ export const PopoverMixin = superclass => class extends superclass {
81
75
 
82
76
  constructor() {
83
77
  super();
84
- this.noAutoClose = false;
85
- this.opened = false;
78
+ this.configure();
86
79
  this._useNativePopover = isSupported ? 'manual' : undefined;
87
80
  this._handleAutoCloseClick = this._handleAutoCloseClick.bind(this);
88
81
  this._handleAutoCloseFocus = this._handleAutoCloseFocus.bind(this);
@@ -90,7 +83,7 @@ export const PopoverMixin = superclass => class extends superclass {
90
83
 
91
84
  connectedCallback() {
92
85
  super.connectedCallback();
93
- if (this.opened) this._addAutoCloseHandlers();
86
+ if (this._opened) this._addAutoCloseHandlers();
94
87
  }
95
88
 
96
89
  disconnectedCallback() {
@@ -101,29 +94,57 @@ export const PopoverMixin = superclass => class extends superclass {
101
94
 
102
95
  updated(changedProperties) {
103
96
  super.updated(changedProperties);
104
- if (changedProperties.has('opened')) {
97
+ if (changedProperties.has('_opened')) {
105
98
 
106
99
  if (this._useNativePopover) {
107
- if (this.opened) this.showPopover();
100
+ if (this._opened) this.showPopover();
108
101
  else this.hidePopover();
109
102
  }
110
103
 
111
- this._previousFocusableAncestor = this.opened ? getPreviousFocusableAncestor(this, false, false) : null;
104
+ this._previousFocusableAncestor = this._opened ? getPreviousFocusableAncestor(this, false, false) : null;
105
+
106
+ if (this._opened) {
112
107
 
113
- if (this.opened) {
114
108
  this._opener = getComposedActiveElement();
115
109
  this._addAutoCloseHandlers();
116
- this._dismissibleId = setDismissible(() => this._close());
110
+ this._dismissibleId = setDismissible(() => this.close());
111
+ this._focusContent(this);
117
112
  this.dispatchEvent(new CustomEvent('d2l-popover-open', { bubbles: true, composed: true }));
118
- } else if (changedProperties.get('opened') !== undefined) {
113
+
114
+ } else if (changedProperties.get('_opened') !== undefined) {
115
+
119
116
  this._removeAutoCloseHandlers();
120
117
  this._clearDismissible();
118
+ this._focusOpener();
121
119
  this.dispatchEvent(new CustomEvent('d2l-popover-close', { bubbles: true, composed: true }));
120
+
122
121
  }
123
122
 
124
123
  }
125
124
  }
126
125
 
126
+ close() {
127
+ this._opened = false;
128
+ return this.updateComplete;
129
+ }
130
+
131
+ configure(properties) {
132
+ this._noAutoClose = properties?.noAutoClose ?? false;
133
+ this._noAutoFocus = properties?.noAutoFocus ?? false;
134
+ this._opened = properties?.opened ?? false;
135
+ }
136
+
137
+ open(applyFocus = true) {
138
+ this._applyFocus = applyFocus !== undefined ? applyFocus : true;
139
+ this._opened = true;
140
+ return this.updateComplete;
141
+ }
142
+
143
+ toggleOpen(applyFocus = true) {
144
+ if (this._opened) return this.close();
145
+ else return this.open(!this._noAutoFocus && applyFocus);
146
+ }
147
+
127
148
  _addAutoCloseHandlers() {
128
149
  this.addEventListener('blur', this._handleAutoCloseFocus, { capture: true });
129
150
  document.body.addEventListener('focus', this._handleAutoCloseFocus, { capture: true });
@@ -136,25 +157,42 @@ export const PopoverMixin = superclass => class extends superclass {
136
157
  this._dismissibleId = null;
137
158
  }
138
159
 
139
- _close() {
140
- const hide = () => {
141
- this.opened = false;
142
- };
160
+ _focusContent(container) {
161
+ if (this._noAutoFocus || this._applyFocus === false) return;
162
+
163
+ const focusable = getFirstFocusableDescendant(container);
164
+ if (focusable) {
165
+ // Removing the rAF call can allow infinite focus looping to happen in content using a focus trap
166
+ requestAnimationFrame(() => focusable.focus());
167
+ } else {
168
+ const content = this._getContentContainer();
169
+ content.setAttribute('tabindex', '-1');
170
+ content.focus();
171
+ }
172
+ }
173
+
174
+ _focusOpener() {
175
+ if (!document.activeElement) return;
176
+ if (!isComposedAncestor(this, getComposedActiveElement())) return;
177
+
178
+ this?._opener.focus();
179
+ }
143
180
 
144
- hide();
181
+ _getContentContainer() {
182
+ return this.shadowRoot.querySelector('.content');
145
183
  }
146
184
 
147
185
  _handleAutoCloseClick(e) {
148
186
 
149
- if (!this.opened || this.noAutoClose) return;
187
+ if (!this._opened || this._noAutoClose) return;
150
188
 
151
189
  const rootTarget = e.composedPath()[0];
152
- if (isComposedAncestor(this.shadowRoot.querySelector('.content'), rootTarget)
190
+ if (isComposedAncestor(this._getContentContainer(), rootTarget)
153
191
  || (this._opener !== document.body && isComposedAncestor(this._opener, rootTarget))) {
154
192
  return;
155
193
  }
156
194
 
157
- this._close();
195
+ this.close();
158
196
  }
159
197
 
160
198
  _handleAutoCloseFocus() {
@@ -162,8 +200,8 @@ export const PopoverMixin = superclass => class extends superclass {
162
200
  // todo: try to use relatedTarget instead - this logic is largely copied as-is from dropdown simply to mitigate risk of this fragile code
163
201
  setTimeout(() => {
164
202
  // we ignore focusable ancestors othrwise the popover will close when user clicks empty space inside the popover
165
- if (!this.opened
166
- || this.noAutoClose
203
+ if (!this._opened
204
+ || this._noAutoClose
167
205
  || !document.activeElement
168
206
  || document.activeElement === this._previousFocusableAncestor
169
207
  || document.activeElement === document.body) {
@@ -171,12 +209,12 @@ export const PopoverMixin = superclass => class extends superclass {
171
209
  }
172
210
 
173
211
  const activeElement = getComposedActiveElement();
174
- if (isComposedAncestor(this.shadowRoot.querySelector('.content'), activeElement)
212
+ if (isComposedAncestor(this._getContentContainer(), activeElement)
175
213
  || activeElement === this._opener) {
176
214
  return;
177
215
  }
178
216
 
179
- this._close();
217
+ this.close();
180
218
  }, 0);
181
219
 
182
220
  }
@@ -10723,6 +10723,12 @@
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",
@@ -10738,6 +10744,13 @@
10738
10744
  "type": "boolean",
10739
10745
  "default": "false"
10740
10746
  },
10747
+ {
10748
+ "name": "noAutoFocus",
10749
+ "attribute": "no-auto-focus",
10750
+ "description": "Whether to disable auto-focus on the first focusable element when opened",
10751
+ "type": "boolean",
10752
+ "default": "false"
10753
+ },
10741
10754
  {
10742
10755
  "name": "opened",
10743
10756
  "attribute": "opened",
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.0",
4
4
  "description": "A collection of accessible, free, open-source web components for building Brightspace applications",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/BrightspaceUI/core.git",