@brightspace-ui/core 3.24.0 → 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.
@@ -49,9 +49,6 @@ export const dropdownContentStyles = css`
49
49
  bottom: calc(100% + var(--d2l-dropdown-verticaloffset, 16px));
50
50
  top: auto;
51
51
  }
52
- :host([_fixed-positioning][opened-above]) {
53
- bottom: 0;
54
- }
55
52
 
56
53
  :host([data-mobile][opened]:not([mobile-tray])) {
57
54
  animation: var(--d2l-dropdown-animation-name) 300ms ease;
@@ -64,6 +61,11 @@ export const dropdownContentStyles = css`
64
61
  top: auto;
65
62
  }
66
63
 
64
+ :host([_fixed-positioning][opened-above]),
65
+ :host([_fixed-positioning][data-mobile][opened-above]:not([mobile-tray])) {
66
+ bottom: 0;
67
+ }
68
+
67
69
  .d2l-dropdown-content-pointer {
68
70
  clip: rect(-5px, 21px, 8px, -7px);
69
71
  display: inline-block;
@@ -301,7 +301,7 @@ The `getUTCDateTimeRange(rangeType, diff)` helper function can be used to get th
301
301
 
302
302
  This component is built to be used alongside the [d2l-filter-dimension-set](#d2l-filter-dimension-set) component. It will give you a selectable filter value which expands to allow the user to select a date range using either the `d2l-input-date-time-range` or `d2l-input-date-range` component (depending on the `type` of the component). Selection triggers the `d2l-filter-change` event, with `start-value` and `end-value` (in UTC) being included in the changes for the `selected` item.
303
303
 
304
- <!-- docs: demo code properties name:d2l-filter-dimension-set-date-time-range-value align:start autoOpen:true autoSize:false size:large -->
304
+ <!-- docs: demo code properties name:d2l-filter-dimension-set-date-time-range-value align:start autoOpen:true autoSize:false size:xlarge -->
305
305
  ```html
306
306
  <script type="module">
307
307
  import '@brightspace-ui/core/components/filter/filter.js';
@@ -311,9 +311,12 @@ This component is built to be used alongside the [d2l-filter-dimension-set](#d2l
311
311
  </script>
312
312
  <d2l-filter>
313
313
  <d2l-filter-dimension-set key="dates" text="Dates">
314
- <d2l-filter-dimension-set-date-text-value key="48hours" range="48hours"></d2l-filter-dimension-set-date-text-value>
315
- <d2l-filter-dimension-set-date-text-value key="14days" range="14days"></d2l-filter-dimension-set-date-text-value>
316
- <d2l-filter-dimension-set-date-time-range-value key="custom" selected></d2l-filter-dimension-set-date-time-range-value>
314
+ <d2l-filter-dimension-set-value key="60days" text="Last 60 days"></d2l-filter-dimension-set-value>
315
+ <d2l-filter-dimension-set-date-text-value key="lastHour" range="lastHour"></d2l-filter-dimension-set-date-text-value>
316
+ <d2l-filter-dimension-set-date-text-value key="48hours" range="48hours" disabled></d2l-filter-dimension-set-date-text-value>
317
+ <d2l-filter-dimension-set-date-text-value key="today" range="today"></d2l-filter-dimension-set-date-text-value>
318
+ <d2l-filter-dimension-set-date-text-value key="6months" range="6months"></d2l-filter-dimension-set-date-text-value>
319
+ <d2l-filter-dimension-set-date-time-range-value key="custom" type="date"></d2l-filter-dimension-set-date-time-range-value>
317
320
  </d2l-filter-dimension-set>
318
321
  </d2l-filter>
319
322
  ```
@@ -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
  }
@@ -30,6 +23,7 @@ export const PopoverMixin = superclass => class extends superclass {
30
23
  static get styles() {
31
24
  return css`
32
25
  :host {
26
+ --d2l-popover-default-animation-name: d2l-popover-animation;
33
27
  --d2l-popover-default-background-color: #ffffff;
34
28
  --d2l-popover-default-border-color: var(--d2l-color-mica);
35
29
  --d2l-popover-default-border-radius: 0.3rem;
@@ -54,7 +48,7 @@ export const PopoverMixin = superclass => class extends superclass {
54
48
  :host(:not([popover])) {
55
49
  z-index: 998; /* position on top of floating buttons */
56
50
  }
57
- :host([opened]) {
51
+ :host([_opened]) {
58
52
  display: inline-block;
59
53
  }
60
54
 
@@ -64,14 +58,24 @@ export const PopoverMixin = superclass => class extends superclass {
64
58
  border-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
65
59
  box-shadow: var(--d2l-popover-shadow, var(--d2l-popover-default-shadow));
66
60
  box-sizing: border-box;
61
+ outline: none;
62
+ }
63
+
64
+ @keyframes d2l-popover-animation {
65
+ 0% { opacity: 0; transform: translate(0, -10px); }
66
+ 100% { opacity: 1; transform: translate(0, 0); }
67
+ }
68
+ @media (prefers-reduced-motion: no-preference) {
69
+ :host([_opened]) {
70
+ animation: var(--d2l-popover-animation-name, var(--d2l-popover-default-animation-name)) 300ms ease;
71
+ }
67
72
  }
68
73
  `;
69
74
  }
70
75
 
71
76
  constructor() {
72
77
  super();
73
- this.noAutoClose = false;
74
- this.opened = false;
78
+ this.configure();
75
79
  this._useNativePopover = isSupported ? 'manual' : undefined;
76
80
  this._handleAutoCloseClick = this._handleAutoCloseClick.bind(this);
77
81
  this._handleAutoCloseFocus = this._handleAutoCloseFocus.bind(this);
@@ -79,7 +83,7 @@ export const PopoverMixin = superclass => class extends superclass {
79
83
 
80
84
  connectedCallback() {
81
85
  super.connectedCallback();
82
- if (this.opened) this._addAutoCloseHandlers();
86
+ if (this._opened) this._addAutoCloseHandlers();
83
87
  }
84
88
 
85
89
  disconnectedCallback() {
@@ -90,29 +94,57 @@ export const PopoverMixin = superclass => class extends superclass {
90
94
 
91
95
  updated(changedProperties) {
92
96
  super.updated(changedProperties);
93
- if (changedProperties.has('opened')) {
97
+ if (changedProperties.has('_opened')) {
94
98
 
95
99
  if (this._useNativePopover) {
96
- if (this.opened) this.showPopover();
100
+ if (this._opened) this.showPopover();
97
101
  else this.hidePopover();
98
102
  }
99
103
 
100
- this._previousFocusableAncestor = this.opened ? getPreviousFocusableAncestor(this, false, false) : null;
104
+ this._previousFocusableAncestor = this._opened ? getPreviousFocusableAncestor(this, false, false) : null;
105
+
106
+ if (this._opened) {
101
107
 
102
- if (this.opened) {
103
108
  this._opener = getComposedActiveElement();
104
109
  this._addAutoCloseHandlers();
105
- this._dismissibleId = setDismissible(() => this._close());
110
+ this._dismissibleId = setDismissible(() => this.close());
111
+ this._focusContent(this);
106
112
  this.dispatchEvent(new CustomEvent('d2l-popover-open', { bubbles: true, composed: true }));
107
- } else if (changedProperties.get('opened') !== undefined) {
113
+
114
+ } else if (changedProperties.get('_opened') !== undefined) {
115
+
108
116
  this._removeAutoCloseHandlers();
109
117
  this._clearDismissible();
118
+ this._focusOpener();
110
119
  this.dispatchEvent(new CustomEvent('d2l-popover-close', { bubbles: true, composed: true }));
120
+
111
121
  }
112
122
 
113
123
  }
114
124
  }
115
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
+
116
148
  _addAutoCloseHandlers() {
117
149
  this.addEventListener('blur', this._handleAutoCloseFocus, { capture: true });
118
150
  document.body.addEventListener('focus', this._handleAutoCloseFocus, { capture: true });
@@ -125,25 +157,42 @@ export const PopoverMixin = superclass => class extends superclass {
125
157
  this._dismissibleId = null;
126
158
  }
127
159
 
128
- _close() {
129
- const hide = () => {
130
- this.opened = false;
131
- };
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
+ }
132
180
 
133
- hide();
181
+ _getContentContainer() {
182
+ return this.shadowRoot.querySelector('.content');
134
183
  }
135
184
 
136
185
  _handleAutoCloseClick(e) {
137
186
 
138
- if (!this.opened || this.noAutoClose) return;
187
+ if (!this._opened || this._noAutoClose) return;
139
188
 
140
189
  const rootTarget = e.composedPath()[0];
141
- if (isComposedAncestor(this.shadowRoot.querySelector('.content'), rootTarget)
190
+ if (isComposedAncestor(this._getContentContainer(), rootTarget)
142
191
  || (this._opener !== document.body && isComposedAncestor(this._opener, rootTarget))) {
143
192
  return;
144
193
  }
145
194
 
146
- this._close();
195
+ this.close();
147
196
  }
148
197
 
149
198
  _handleAutoCloseFocus() {
@@ -151,8 +200,8 @@ export const PopoverMixin = superclass => class extends superclass {
151
200
  // todo: try to use relatedTarget instead - this logic is largely copied as-is from dropdown simply to mitigate risk of this fragile code
152
201
  setTimeout(() => {
153
202
  // we ignore focusable ancestors othrwise the popover will close when user clicks empty space inside the popover
154
- if (!this.opened
155
- || this.noAutoClose
203
+ if (!this._opened
204
+ || this._noAutoClose
156
205
  || !document.activeElement
157
206
  || document.activeElement === this._previousFocusableAncestor
158
207
  || document.activeElement === document.body) {
@@ -160,12 +209,12 @@ export const PopoverMixin = superclass => class extends superclass {
160
209
  }
161
210
 
162
211
  const activeElement = getComposedActiveElement();
163
- if (isComposedAncestor(this.shadowRoot.querySelector('.content'), activeElement)
212
+ if (isComposedAncestor(this._getContentContainer(), activeElement)
164
213
  || activeElement === this._opener) {
165
214
  return;
166
215
  }
167
216
 
168
- this._close();
217
+ this.close();
169
218
  }, 0);
170
219
 
171
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.0",
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",