@brightspace-ui/core 3.88.3 → 3.89.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.
@@ -0,0 +1,366 @@
1
+ import { css, html } from 'lit';
2
+ import { classMap } from 'lit/directives/class-map.js';
3
+ import { findComposedAncestor } from '../../helpers/dom.js';
4
+ import { getFlag } from '../../helpers/flags.js';
5
+ import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
6
+ import { PopoverMixin } from '../popover/popover-mixin.js';
7
+ import { styleMap } from 'lit/directives/style-map.js';
8
+
9
+ export const usePopoverMixin = getFlag('GAUD-7472-dropdown-popover', false);
10
+
11
+ export const DropdownPopoverMixin = superclass => class extends LocalizeCoreElement(PopoverMixin(superclass)) {
12
+
13
+ static get properties() {
14
+ return {
15
+ /**
16
+ * Optionally align dropdown to either start or end. If not set, the dropdown will attempt be centred.
17
+ * @type {'start'|'end'}
18
+ */
19
+ align: { type: String },
20
+ /**
21
+ * Override max-height. Note that the default behaviour is to be as tall as necessary within the viewport, so this property is usually not needed.
22
+ * @type {number}
23
+ */
24
+ maxHeight: { type: Number, attribute: 'max-height' },
25
+ /**
26
+ * Override default max-width (undefined). Specify a number that would be the px value.
27
+ * @type {number}
28
+ */
29
+ maxWidth: { type: Number, attribute: 'max-width' },
30
+ /**
31
+ * Override default height used for required space when `no-auto-fit` is true. Specify a number that would be the px value. Note that the default behaviour is to be as tall as necessary within the viewport, so this property is usually not needed.
32
+ * @type {number}
33
+ */
34
+ minHeight: { type: Number, attribute: 'min-height' },
35
+ /**
36
+ * Override default min-width (undefined). Specify a number that would be the px value.
37
+ * @type {number}
38
+ */
39
+ minWidth: { type: Number, attribute: 'min-width' },
40
+ /**
41
+ * Override the breakpoint at which mobile styling is used. Defaults to 616px.
42
+ * @type {number}
43
+ */
44
+ mobileBreakpointOverride: { type: Number, attribute: 'mobile-breakpoint' },
45
+ /**
46
+ * Mobile dropdown style.
47
+ * @type {'left'|'right'|'bottom'}
48
+ */
49
+ mobileTray: { type: String, attribute: 'mobile-tray' },
50
+ /**
51
+ * Opt out of automatically closing on focus or click outside of the dropdown content
52
+ * @type {boolean}
53
+ */
54
+ noAutoClose: { type: Boolean, attribute: 'no-auto-close' },
55
+ /**
56
+ * Opt out of auto-sizing
57
+ * @type {boolean}
58
+ */
59
+ noAutoFit: { type: Boolean, attribute: 'no-auto-fit' },
60
+ /**
61
+ * Opt out of focus being automatically moved to the first focusable element in the dropdown when opened
62
+ * @type {boolean}
63
+ */
64
+ noAutoFocus: { type: Boolean, attribute: 'no-auto-focus' },
65
+ /**
66
+ * Opt-out of showing a close button in the footer of tray-style mobile dropdowns.
67
+ * @type {boolean}
68
+ */
69
+ noMobileCloseButton: { type: Boolean, attribute: 'no-mobile-close-button' },
70
+ /**
71
+ * Render with no padding
72
+ * @type {boolean}
73
+ */
74
+ noPadding: { type: Boolean, attribute: 'no-padding' },
75
+ /**
76
+ * Render the footer with no padding (if it has content)
77
+ * @type {boolean}
78
+ */
79
+ noPaddingFooter: { type: Boolean, attribute: 'no-padding-footer' },
80
+ /**
81
+ * Render the header with no padding (if it has content)
82
+ * @type {boolean}
83
+ */
84
+ noPaddingHeader: { type: Boolean, attribute: 'no-padding-header' },
85
+ /**
86
+ * Render without a pointer
87
+ * @type {boolean}
88
+ */
89
+ noPointer: { type: Boolean, attribute: 'no-pointer' },
90
+ /**
91
+ * Whether the dropdown is open or not
92
+ * @type {boolean}
93
+ */
94
+ opened: { type: Boolean, reflect: true },
95
+ /**
96
+ * Optionally render a d2l-focus-trap around the dropdown content
97
+ * @type {boolean}
98
+ */
99
+ trapFocus: { type: Boolean, attribute: 'trap-focus' },
100
+ /**
101
+ * Provide custom offset, positive or negative
102
+ * @type {string}
103
+ */
104
+ verticalOffset: { type: String, attribute: 'vertical-offset' },
105
+ _blockEndScroll: { state: true },
106
+ _blockStartScroll: { state: true },
107
+ _dropdownContent: { type: Boolean, attribute: 'dropdown-content', reflect: true },
108
+ _hasFooterSlotContent: { state: true },
109
+ _hasHeaderSlotContent: { state: true }
110
+ };
111
+ }
112
+
113
+ static get styles() {
114
+ return [super.styles, css`
115
+ .dropdown-content-layout {
116
+ align-items: flex-start;
117
+ display: flex;
118
+ flex-direction: column;
119
+ }
120
+ .dropdown-content {
121
+ box-sizing: border-box;
122
+ flex: auto;
123
+ max-width: 100%;
124
+ overflow-y: auto;
125
+ padding: 1rem;
126
+ }
127
+ .dropdown-header,
128
+ .dropdown-footer {
129
+ box-sizing: border-box;
130
+ flex: none;
131
+ max-width: 100%;
132
+ min-height: 5px;
133
+ padding: 1rem;
134
+ position: relative;
135
+ width: 100%;
136
+ z-index: 2;
137
+ }
138
+ .dropdown-header {
139
+ border-block-end: 1px solid var(--d2l-popover-border-color, var(--d2l-popover-default-border-color));
140
+ border-start-end-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
141
+ border-start-start-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
142
+ }
143
+ .dropdown-footer {
144
+ border-block-start: 1px solid var(--d2l-popover-border-color, var(--d2l-popover-default-border-color));
145
+ border-end-end-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
146
+ border-end-start-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
147
+ }
148
+ .dropdown-no-header {
149
+ border-block-end: none;
150
+ padding: 0;
151
+ }
152
+ .dropdown-no-footer {
153
+ border-block-start: none;
154
+ padding: 0;
155
+ }
156
+ .dropdown-no-padding {
157
+ padding: 0;
158
+ }
159
+ .dropdown-header-scroll {
160
+ box-shadow: 0 3px 3px 0 var(--d2l-popover-shadow-color, var(--d2l-popover-default-shadow-color));
161
+ }
162
+ .dropdown-footer-scroll {
163
+ box-shadow: 0 -3px 3px 0 var(--d2l-popover-shadow-color, var(--d2l-popover-default-shadow-color));
164
+ }
165
+
166
+ :host([_mobile][_mobile-tray-location="inline-start"][opened]) .dropdown-content-layout,
167
+ :host([_mobile][_mobile-tray-location="inline-end"][opened]) .dropdown-content-layout {
168
+ height: 100vh;
169
+ }
170
+ `];
171
+ }
172
+
173
+ constructor() {
174
+ super();
175
+ this.opened = false;
176
+ this.noAutoClose = false;
177
+ this.noAutoFit = false;
178
+ this.noAutoFocus = false;
179
+ this.noPadding = false;
180
+ this.noPaddingFooter = false;
181
+ this.noPaddingHeader = false;
182
+ this.noPointer = false;
183
+ this.trapFocus = false;
184
+
185
+ this._blockEndScroll = false;
186
+ this._blockStartScroll = false;
187
+ this._dropdownContent = true;
188
+ this._hasFooterSlotContent = false;
189
+ this._hasHeaderSlotContent = false;
190
+ }
191
+
192
+ firstUpdated(changedProperties) {
193
+ super.firstUpdated(changedProperties);
194
+ this.#contentElement = this.shadowRoot?.querySelector('.dropdown-content');
195
+ this.addEventListener('d2l-popover-open', this.#handlePopoverOpen);
196
+ this.addEventListener('d2l-popover-close', this.#handlePopoverClose);
197
+ this.addEventListener('d2l-popover-position', this.#handlePopoverPosition);
198
+ }
199
+
200
+ render() {
201
+
202
+ const fillHeight = this._mobile && (this._mobileTrayLocation === 'inline-start' || this._mobileTrayLocation === 'inline-end');
203
+ const contentLayoutStyles = {
204
+ maxHeight: (!fillHeight && this._contentHeight) ? `${this._contentHeight}px` : undefined
205
+ };
206
+ const contentClasses = {
207
+ 'dropdown-content': true,
208
+ 'dropdown-no-padding': this.noPadding
209
+ };
210
+ const headerClasses = {
211
+ 'dropdown-header': true,
212
+ 'dropdown-no-header': !this._hasHeaderSlotContent,
213
+ 'dropdown-no-padding': this.noPaddingHeader,
214
+ 'dropdown-header-scroll': this._blockStartScroll
215
+ };
216
+ const footerClasses = {
217
+ 'dropdown-footer': true,
218
+ 'dropdown-no-footer': !(this._hasFooterSlotContent || (this._mobile && this._mobileTrayLocation && !this.noMobileCloseButton)),
219
+ 'dropdown-no-padding': this.noPaddingFooter,
220
+ 'dropdown-footer-scroll': this._blockEndScroll
221
+ };
222
+
223
+ const closeButtonStyles = this.#getMobileCloseButtonStyles();
224
+
225
+ const content = html`
226
+ <div class="dropdown-content-layout" style="${styleMap(contentLayoutStyles)}">
227
+ <div class="${classMap(headerClasses)}">
228
+ <slot name="header" @slotchange="${this.#handleHeaderSlotChange}"></slot>
229
+ </div>
230
+ <div class="${classMap(contentClasses)}" @scroll="${this.#toggleScrollStyles}">
231
+ <slot></slot>
232
+ </div>
233
+ <div class="${classMap(footerClasses)}">
234
+ <slot name="footer" @slotchange="${this.#handleFooterSlotChange}"></slot>
235
+ <d2l-button style=${styleMap(closeButtonStyles)} @click=${this.close}>
236
+ ${this.localize('components.dropdown.close')}
237
+ </d2l-button>
238
+ </div>
239
+ </div>
240
+ `;
241
+
242
+ return this.renderPopover(content);
243
+ }
244
+
245
+ willUpdate(changedProperties) {
246
+ if (changedProperties.has('align') || changedProperties.has('maxHeight') || changedProperties.has('maxWidth') || changedProperties.has('minHeight') || changedProperties.has('minWidth') || changedProperties.has('mobileBreakpointOverride') || changedProperties.has('mobileTray') || changedProperties.has('noAutoClose') || changedProperties.has('noAutoFit') || changedProperties.has('noAutoFocus') || changedProperties.has('noPointer') || changedProperties.has('trapFocus') || changedProperties.has('verticalOffset')) {
247
+ super.configure({
248
+ maxHeight: this.maxHeight,
249
+ maxWidth: this.maxWidth,
250
+ minHeight: this.minHeight,
251
+ minWidth: this.minWidth,
252
+ mobileBreakpoint: this.mobileBreakpointOverride,
253
+ mobileTrayLocation: this.#adaptMobileTrayLocation(this.mobileTray),
254
+ noAutoClose: this.noAutoClose,
255
+ noAutoFit: this.noAutoFit,
256
+ noAutoFocus: this.noAutoFocus,
257
+ noPointer: this.noPointer,
258
+ offset: (this.verticalOffset !== undefined ? Number.parseInt(this.verticalOffset) : undefined),
259
+ position: { location: 'block-end', span: this.#adaptPositionSpan(this.align) },
260
+ trapFocus: this.trapFocus
261
+ });
262
+ }
263
+
264
+ if (changedProperties.has('opened')) {
265
+ if (this.opened) this.open(true);
266
+ else if (changedProperties.get('opened')) this.close();
267
+ }
268
+ }
269
+
270
+ async open(applyFocus = true) {
271
+ const opener = this.#getOpener();
272
+ super.open(opener, applyFocus);
273
+ }
274
+
275
+ toggleOpen(applyFocus = true) {
276
+ const opener = this.#getOpener();
277
+ super.toggleOpen(opener, applyFocus);
278
+ }
279
+
280
+ #contentElement;
281
+
282
+ #adaptMobileTrayLocation(val) {
283
+ switch (val) {
284
+ case 'bottom': return 'block-end';
285
+ case 'left': return 'inline-start';
286
+ case 'right': return 'inline-end';
287
+ default: return undefined;
288
+ }
289
+ }
290
+
291
+ #adaptPositionSpan(val) {
292
+ switch (val) {
293
+ case 'start': return 'end';
294
+ case 'end': return 'start';
295
+ default: return 'all';
296
+ }
297
+ }
298
+
299
+ #getMobileCloseButtonStyles() {
300
+ if (!this._mobile || !this._mobileTrayLocation) {
301
+ return { display: 'none' };
302
+ }
303
+
304
+ let footerWidth;
305
+ if (this.noPaddingFooter) {
306
+ footerWidth = 'calc(100% - 24px)';
307
+ } else if (this._hasFooterSlotContent) {
308
+ footerWidth = '100%';
309
+ } else {
310
+ footerWidth = 'calc(100% + 16px)';
311
+ }
312
+
313
+ return {
314
+ display: !this.noMobileCloseButton ? 'inline-block' : 'none',
315
+ marginBlock: this._hasFooterSlotContent ? '0' : '-20px -20px',
316
+ marginInline: this._hasFooterSlotContent ? '0' : '-20px 0',
317
+ padding: this._hasFooterSlotContent && !this.noPaddingFooter ? '12px 0 0 0' : '12px',
318
+ width: footerWidth
319
+ };
320
+ }
321
+
322
+ #getOpener() {
323
+ return findComposedAncestor(this, elem => elem.dropdownOpener);
324
+ }
325
+
326
+ #handleFooterSlotChange(e) {
327
+ this._hasFooterSlotContent = e.target.assignedNodes().length !== 0;
328
+ }
329
+
330
+ #handleHeaderSlotChange(e) {
331
+ this._hasHeaderSlotContent = e.target.assignedNodes().length !== 0;
332
+ }
333
+
334
+ #handlePopoverClose() {
335
+ setTimeout(() => {
336
+ this.opened = false;
337
+
338
+ /** Dispatched when the dropdown is closed */
339
+ this.dispatchEvent(new CustomEvent('d2l-dropdown-close', { bubbles: true, composed: true }));
340
+ });
341
+ }
342
+
343
+ #handlePopoverOpen() {
344
+ this.opened = true;
345
+
346
+ if (!this.noAutoFit && this.#contentElement) {
347
+ this.#contentElement.scrollTop ??= 0;
348
+ }
349
+
350
+ /** Dispatched when the dropdown is opened */
351
+ this.dispatchEvent(new CustomEvent('d2l-dropdown-open', { bubbles: true, composed: true }));
352
+ }
353
+
354
+ #handlePopoverPosition() {
355
+ this.#toggleScrollStyles();
356
+
357
+ /** Dispatched when the dropdown position finishes adjusting */
358
+ this.dispatchEvent(new CustomEvent('d2l-dropdown-position', { bubbles: true, composed: true }));
359
+ }
360
+
361
+ #toggleScrollStyles() {
362
+ this._blockEndScroll = this.#contentElement.scrollHeight - (this.#contentElement.scrollTop + this.#contentElement.clientHeight) >= 5;
363
+ this._blockStartScroll = this.#contentElement.scrollTop !== 0;
364
+ }
365
+
366
+ };
@@ -1,63 +1,127 @@
1
1
  import { css, LitElement } from 'lit';
2
+ import { DropdownPopoverMixin, usePopoverMixin } from './dropdown-popover-mixin.js';
2
3
  import { DropdownContentMixin } from './dropdown-content-mixin.js';
3
4
  import { dropdownContentStyles } from './dropdown-content-styles.js';
4
5
 
5
- /**
6
- * A container for a "d2l-tabs" component. It provides additional support on top of "d2l-dropdown-content" for automatic resizing when the tab changes.
7
- * @slot - Anything inside of "d2l-dropdown-content" that isn't in the "header" or "footer" slots appears as regular content
8
- * @slot header - Sticky container at the top of the dropdown
9
- * @slot footer - Sticky container at the bottom of the dropdown
10
- * @fires d2l-dropdown-open - Dispatched when the dropdown is opened
11
- */
12
- class DropdownTabs extends DropdownContentMixin(LitElement) {
13
-
14
- static get styles() {
15
- return [ dropdownContentStyles, css`
16
- ::slotted(d2l-tabs) {
17
- margin-bottom: 0;
18
- }
19
- `];
20
- }
6
+ if (usePopoverMixin) {
21
7
 
22
- firstUpdated(changedProperties) {
23
- super.firstUpdated(changedProperties);
8
+ /**
9
+ * A container for a "d2l-tabs" component. It provides additional support on top of "d2l-dropdown-content" for automatic resizing when the tab changes.
10
+ * @slot - Anything inside of "d2l-dropdown-content" that isn't in the "header" or "footer" slots appears as regular content
11
+ * @slot header - Sticky container at the top of the dropdown
12
+ * @slot footer - Sticky container at the bottom of the dropdown
13
+ * @fires d2l-dropdown-open - Dispatched when the dropdown is opened
14
+ */
15
+ class DropdownTabs extends DropdownPopoverMixin(LitElement) {
24
16
 
25
- this.addEventListener('d2l-dropdown-open', this._onOpen);
26
- this.addEventListener('d2l-menu-resize', this._onMenuResize);
27
- this.addEventListener('d2l-tab-panel-selected', this._onTabSelected);
28
- }
17
+ static get styles() {
18
+ return [super.styles, css`
19
+ ::slotted(d2l-tabs) {
20
+ margin-bottom: 0;
21
+ }
22
+ `];
23
+ }
29
24
 
30
- render() {
31
- return this._renderContent();
32
- }
25
+ firstUpdated(changedProperties) {
26
+ super.firstUpdated(changedProperties);
33
27
 
34
- _getTabsElement() {
35
- if (!this.shadowRoot) return undefined;
36
- return this.shadowRoot.querySelector('.d2l-dropdown-content-container > slot')
37
- .assignedNodes()
38
- .filter(node => node.hasAttribute && node.tagName === 'D2L-TABS')[0];
39
- }
28
+ this.addEventListener('d2l-dropdown-open', this.#handleOpen);
29
+ this.addEventListener('d2l-menu-resize', this.#handleMenuResize);
30
+ this.addEventListener('d2l-tab-panel-selected', this.#handleTabSelected);
31
+ }
40
32
 
41
- _onMenuResize(e) {
42
- const tabs = this._getTabsElement();
43
- const tabListRect = tabs.getTabListRect();
44
- // need to include height of tablist, dropdown padding, tab margins
45
- const rect = {
46
- height: e.detail.height + tabListRect.height + 52,
47
- width: e.detail.width
48
- };
49
- this.__position(rect, { updateAboveBelow: this._initializingHeight });
50
- this._initializingHeight = false;
51
- }
33
+ #initializingHeight;
34
+
35
+ #handleMenuResize(e) {
36
+
37
+ const tabs = this.shadowRoot?.querySelector('.dropdown-content > slot')
38
+ .assignedNodes()
39
+ .filter(node => node.hasAttribute && node.tagName === 'D2L-TABS')[0];
40
+
41
+ if (!tabs) return;
42
+ const tabListRect = tabs.getTabListRect();
43
+
44
+ // need to include height of tablist, dropdown padding, tab margins
45
+ const rect = {
46
+ height: e.detail.height + tabListRect.height + 52,
47
+ width: e.detail.width
48
+ };
49
+ this.position(rect, { updateLocation: this.#initializingHeight });
50
+ this.#initializingHeight = false;
51
+ }
52
+
53
+ #handleOpen(e) {
54
+ if (e.target !== this) return;
55
+ this.#initializingHeight = true;
56
+ }
57
+
58
+ #handleTabSelected() {
59
+ this.position();
60
+ }
52
61
 
53
- _onOpen(e) {
54
- if (e.target !== this) return;
55
- this._initializingHeight = true;
56
62
  }
63
+ customElements.define('d2l-dropdown-tabs', DropdownTabs);
64
+
65
+ } else {
66
+
67
+ /**
68
+ * A container for a "d2l-tabs" component. It provides additional support on top of "d2l-dropdown-content" for automatic resizing when the tab changes.
69
+ * @slot - Anything inside of "d2l-dropdown-content" that isn't in the "header" or "footer" slots appears as regular content
70
+ * @slot header - Sticky container at the top of the dropdown
71
+ * @slot footer - Sticky container at the bottom of the dropdown
72
+ * @fires d2l-dropdown-open - Dispatched when the dropdown is opened
73
+ */
74
+ class DropdownTabs extends DropdownContentMixin(LitElement) {
75
+
76
+ static get styles() {
77
+ return [ dropdownContentStyles, css`
78
+ ::slotted(d2l-tabs) {
79
+ margin-bottom: 0;
80
+ }
81
+ `];
82
+ }
83
+
84
+ firstUpdated(changedProperties) {
85
+ super.firstUpdated(changedProperties);
86
+
87
+ this.addEventListener('d2l-dropdown-open', this._onOpen);
88
+ this.addEventListener('d2l-menu-resize', this._onMenuResize);
89
+ this.addEventListener('d2l-tab-panel-selected', this._onTabSelected);
90
+ }
91
+
92
+ render() {
93
+ return this._renderContent();
94
+ }
95
+
96
+ _getTabsElement() {
97
+ if (!this.shadowRoot) return undefined;
98
+ return this.shadowRoot.querySelector('.d2l-dropdown-content-container > slot')
99
+ .assignedNodes()
100
+ .filter(node => node.hasAttribute && node.tagName === 'D2L-TABS')[0];
101
+ }
102
+
103
+ _onMenuResize(e) {
104
+ const tabs = this._getTabsElement();
105
+ const tabListRect = tabs.getTabListRect();
106
+ // need to include height of tablist, dropdown padding, tab margins
107
+ const rect = {
108
+ height: e.detail.height + tabListRect.height + 52,
109
+ width: e.detail.width
110
+ };
111
+ this.__position(rect, { updateAboveBelow: this._initializingHeight });
112
+ this._initializingHeight = false;
113
+ }
114
+
115
+ _onOpen(e) {
116
+ if (e.target !== this) return;
117
+ this._initializingHeight = true;
118
+ }
119
+
120
+ _onTabSelected() {
121
+ this.__position();
122
+ }
57
123
 
58
- _onTabSelected() {
59
- this.__position();
60
124
  }
125
+ customElements.define('d2l-dropdown-tabs', DropdownTabs);
61
126
 
62
127
  }
63
- customElements.define('d2l-dropdown-tabs', DropdownTabs);
@@ -16,9 +16,9 @@
16
16
  window.wireUpPopover = demo => {
17
17
  const popover = demo.querySelector('d2l-test-popover');
18
18
  const openButton = demo.querySelector('d2l-button-subtle[text="Open"]');
19
- openButton.addEventListener('click', () => popover.opened = !popover.opened);
19
+ openButton.addEventListener('click', () => popover.open(openButton));
20
20
  const closeButton = demo.querySelector('d2l-button-subtle[text="Close"]');
21
- if (closeButton) closeButton.addEventListener('click', () => popover.opened = false);
21
+ if (closeButton) closeButton.addEventListener('click', () => popover.close());
22
22
  };
23
23
  </script>
24
24
  </head>