@brightspace-ui/core 2.38.3 → 2.39.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.
@@ -1,5 +1,5 @@
1
1
  # Overflow Groups
2
- The `d2l-overflow-group` element can be used to add responsiveness to a set of buttons, links or menus.
2
+ The `d2l-overflow-group` element can be used to add responsiveness to a set of buttons, links or menus. The `OverflowGroupMixin` allows for using the chomping logic without having to use those specific element types.
3
3
 
4
4
  <!-- docs: demo autoSize:false display:block size:medium -->
5
5
  ```html
@@ -70,3 +70,41 @@ Items added to this container element will no longer wrap onto a second line whe
70
70
 
71
71
  Looking for an enhancement not listed here? Create a GitHub issue!
72
72
  <!-- docs: end hidden content -->
73
+
74
+ ## OverflowGroupMixin
75
+ This mixin allows for creation of an overflow group that handles chomping when using elements that are not buttons, links, or menus, or when wanting an overflow container that is not a `d2l-dropdown`.
76
+
77
+ ### How to Use
78
+
79
+ **Import:**
80
+ ```javascript
81
+ import { OverflowGroupMixin } from '@brightspace-ui/core/components/overflow-group/overflow-group-mixin.js';
82
+
83
+ class OtherOverflowGroup extends OverflowGroupMixin(LitElement) {
84
+ ...
85
+ ```
86
+
87
+ **Styles:**
88
+
89
+ ```javascript
90
+ static get styles() {
91
+ return [ super.styles ];
92
+ }
93
+ ```
94
+
95
+ **Functionality:**
96
+
97
+ The functions `getOverflowContainer` and `convertToOverflowItem` need to be implemented by consumers of the mixin.
98
+
99
+ ```javascript
100
+ convertToOverflowItem(node) {
101
+ // return html of overflow item. For example:
102
+ return html`<d2l-menu-item text="${node.text}"></d2l-menu-item>`;
103
+ }
104
+
105
+ getOverflowContainer(overflowItems, mini) {
106
+ // return html of overflow menu. "mini" specifies if smaller menu option should be used, where applicable. For example:
107
+ if (mini) html`<d2l-dropdown-context-menu text="Overflow Menu"><d2l-dropdown-menu>${overflowItems}</d2l-dropdown-menu></d2l-dropdown-context-menu>`;
108
+ else return html`<d2l-dropdown-button text="Overflow Menu"><d2l-dropdown-menu>${overflowItems}</d2l-dropdown-menu></d2l-dropdown-button>`;
109
+ }
110
+ ```
@@ -0,0 +1,344 @@
1
+ import { css, html, nothing } from 'lit';
2
+ import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
3
+ import { offscreenStyles } from '../offscreen/offscreen.js';
4
+ import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js';
5
+
6
+ export const OVERFLOW_CLASS = 'd2l-overflow-container';
7
+ export const OVERFLOW_MINI_CLASS = 'd2l-overflow-container-mini';
8
+
9
+ const AUTO_SHOW_CLASS = 'd2l-button-group-show';
10
+ const AUTO_NO_SHOW_CLASS = 'd2l-button-group-no-show';
11
+
12
+ const OPENER_TYPE = {
13
+ DEFAULT: 'default',
14
+ ICON: 'icon'
15
+ };
16
+
17
+ async function filterAsync(arr, callback) {
18
+ const fail = Symbol();
19
+ const results = await Promise.all(arr.map(async item => {
20
+ const callbackResult = await callback(item);
21
+ return callbackResult ? item : fail;
22
+ }));
23
+ return results.filter(i => i !== fail);
24
+ }
25
+
26
+ export const OverflowGroupMixin = superclass => class extends LocalizeCoreElement(superclass) {
27
+
28
+ static get properties() {
29
+ return {
30
+ /**
31
+ * Use predefined classes on slot elements to set min and max slotted items to show
32
+ * @type {boolean}
33
+ */
34
+ autoShow: {
35
+ type: Boolean,
36
+ attribute: 'auto-show',
37
+ },
38
+ /**
39
+ * minimum amount of slotted items to show
40
+ * @type {number}
41
+ */
42
+ minToShow: {
43
+ type: Number,
44
+ reflect: true,
45
+ attribute: 'min-to-show',
46
+ },
47
+ /**
48
+ * maximum amount of slotted items to show
49
+ * @type {number}
50
+ */
51
+ maxToShow: {
52
+ type: Number,
53
+ reflect: true,
54
+ attribute: 'max-to-show',
55
+ },
56
+ /**
57
+ * @ignore
58
+ */
59
+ openerType: {
60
+ type: String,
61
+ attribute: 'opener-type'
62
+ },
63
+ _chompIndex: {
64
+ state: true
65
+ },
66
+ _mini: {
67
+ state: true
68
+ }
69
+ };
70
+ }
71
+
72
+ static get styles() {
73
+ return [offscreenStyles, css`
74
+ :host {
75
+ display: block;
76
+ }
77
+ :host([hidden]) {
78
+ display: none;
79
+ }
80
+ .d2l-overflow-group-container {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ justify-content: var(--d2l-overflow-group-justify-content, normal);
84
+ }
85
+ .d2l-overflow-group-container ::slotted([data-is-chomped]) {
86
+ display: none !important;
87
+ }
88
+ `];
89
+ }
90
+
91
+ constructor() {
92
+ super();
93
+
94
+ this._handleItemMutation = this._handleItemMutation.bind(this);
95
+ this._handleResize = this._handleResize.bind(this);
96
+ this._resizeObserver = new ResizeObserver((entries) => requestAnimationFrame(() => this._handleResize(entries)));
97
+
98
+ this._isObserving = false;
99
+ this._mini = this.openerType === OPENER_TYPE.ICON;
100
+ this._overflowContainerHidden = false;
101
+ this._slotItems = [];
102
+
103
+ this.autoShow = false;
104
+ this.maxToShow = -1;
105
+ this.minToShow = 1;
106
+ this.openerType = OPENER_TYPE.DEFAULT;
107
+ }
108
+
109
+ disconnectedCallback() {
110
+ super.disconnectedCallback();
111
+
112
+ if (this._isObserving) {
113
+ this._isObserving = false;
114
+ this._resizeObserver.disconnect();
115
+ }
116
+ }
117
+
118
+ render() {
119
+ const chompedOverflowItems = this._overflowItems ? this._overflowItems.slice(this._chompIndex) : [];
120
+ const overflowContainer = (!this._overflowContainerHidden && this._overflowItems)
121
+ ? this.getOverflowContainer(chompedOverflowItems, this._mini)
122
+ : nothing;
123
+
124
+ this._slotItems.forEach((element, index) => {
125
+ if (!this._overflowContainerHidden && index >= this._chompIndex) {
126
+ element.setAttribute('data-is-chomped', '');
127
+ } else {
128
+ element.removeAttribute('data-is-chomped');
129
+ }
130
+ });
131
+
132
+ return html`
133
+ <div class="d2l-overflow-group-container">
134
+ <slot @slotchange="${this._handleSlotChange}"></slot>
135
+ ${overflowContainer}
136
+ </div>
137
+ `;
138
+ }
139
+
140
+ update(changedProperties) {
141
+ super.update(changedProperties);
142
+
143
+ if (!this._isObserving) {
144
+ this._isObserving = true;
145
+ this._resizeObserver.observe(this.shadowRoot.querySelector('.d2l-overflow-group-container'));
146
+ }
147
+
148
+ if (changedProperties.has('autoShow') && this.autoShow) {
149
+ this._autoDetectBoundaries(this._slotItems);
150
+ }
151
+
152
+ if (changedProperties.has('minToShow')
153
+ || changedProperties.has('maxToShow')) {
154
+ this._chomp();
155
+ }
156
+
157
+ // Slight hack to get the overflow container width the first time it renders
158
+ if (!this._overflowContainerWidth) {
159
+ // this action needs to be deferred until first render of our overflow container
160
+ requestAnimationFrame(() => {
161
+ this._chomp();
162
+ });
163
+ }
164
+ }
165
+
166
+ _autoDetectBoundaries(items) {
167
+ if (!items) return;
168
+
169
+ let minToShow, maxToShow;
170
+ for (let i = 0; i < items.length; i++) {
171
+ if (!items[i].classList) continue;
172
+
173
+ if (items[i].classList.contains(AUTO_SHOW_CLASS)) {
174
+ minToShow = i + 1;
175
+ }
176
+ if (maxToShow === undefined && items[i].classList.contains(AUTO_NO_SHOW_CLASS)) {
177
+ maxToShow = i;
178
+ }
179
+ }
180
+
181
+ if (minToShow !== undefined) {
182
+ this.minToShow = minToShow;
183
+ }
184
+ if (maxToShow !== undefined) {
185
+ this.maxToShow = maxToShow;
186
+ }
187
+ }
188
+
189
+ _chomp() {
190
+ if (!this.shadowRoot || !this._itemLayouts) return;
191
+
192
+ this._overflowContainer = this.shadowRoot.querySelector(`.${OVERFLOW_CLASS}`);
193
+ this._overflowContainerMini = this.shadowRoot.querySelector(`.${OVERFLOW_MINI_CLASS}`);
194
+ if (this.openerType === OPENER_TYPE.ICON && this._overflowContainerMini) {
195
+ this._overflowContainerWidth = this._overflowContainerMini.offsetWidth;
196
+ } else if (this._overflowContainer) {
197
+ this._overflowContainerWidth = this._overflowContainer.offsetWidth;
198
+ }
199
+
200
+ const showing = {
201
+ count: 0,
202
+ width: 0
203
+ };
204
+
205
+ let isSoftOverflowing, isForcedOverflowing;
206
+ for (let i = 0; i < this._itemLayouts.length; i++) {
207
+ const itemLayout = this._itemLayouts[i];
208
+
209
+ // handle minimum items to show
210
+ if (showing.count < this.minToShow) {
211
+ showing.width += itemLayout.width;
212
+ showing.count += 1;
213
+ itemLayout.trigger = 'force-show';
214
+ itemLayout.isChomped = false;
215
+ continue;
216
+ }
217
+
218
+ // handle maximum items to show
219
+ if (this.maxToShow >= 0 && showing.count >= this.maxToShow) {
220
+ isForcedOverflowing = true;
221
+ itemLayout.isChomped = true;
222
+ itemLayout.trigger = 'force-hide';
223
+ continue;
224
+ }
225
+
226
+ // chomp or unchomp based on space available, and we've already handled min/max above
227
+ if (!isSoftOverflowing && showing.width + itemLayout.width < this._availableWidth) {
228
+ showing.width += itemLayout.width;
229
+ showing.count += 1;
230
+ itemLayout.isChomped = false;
231
+ itemLayout.trigger = 'soft-show';
232
+
233
+ } else {
234
+ isSoftOverflowing = true;
235
+ itemLayout.isChomped = true;
236
+ itemLayout.trigger = 'soft-hide';
237
+
238
+ }
239
+
240
+ }
241
+ // if there is at least one showing and no more to be hidden, enable collapsing overflow container to mini overflow container
242
+ this._overflowContainerHidden = this._itemLayouts.length === showing.count;
243
+ if (!this._overflowContainerHidden && (isSoftOverflowing || isForcedOverflowing)) {
244
+ for (let j = this._itemLayouts.length; j--;) {
245
+ if (showing.width + this._overflowContainerWidth < this._availableWidth) {
246
+ break;
247
+ }
248
+ const itemLayoutOverflowing = this._itemLayouts[j];
249
+ if (itemLayoutOverflowing.trigger !== 'soft-show') {
250
+ continue;
251
+ }
252
+ showing.width -= itemLayoutOverflowing.width;
253
+ showing.count -= 1;
254
+ isSoftOverflowing = true;
255
+ itemLayoutOverflowing.trigger = 'soft-hide';
256
+ itemLayoutOverflowing.isChomped = true;
257
+ }
258
+ }
259
+ const overflowOverflowing = (showing.width + this._overflowContainerWidth >= this._availableWidth);
260
+ const swapToMini = overflowOverflowing && !this._overflowContainerHidden;
261
+
262
+ this._mini = this.openerType === OPENER_TYPE.ICON || swapToMini;
263
+ this._chompIndex = this._overflowContainerHidden ? null : showing.count;
264
+
265
+ /** Dispatched when there is an update performed to the overflow group */
266
+ this.dispatchEvent(new CustomEvent('d2l-overflow-group-updated', { composed: false, bubbles: true }));
267
+ }
268
+
269
+ _getItemLayouts(filteredNodes) {
270
+ const items = filteredNodes.map((node) => {
271
+ const computedStyles = window.getComputedStyle(node);
272
+
273
+ return {
274
+ type: node.tagName.toLowerCase(),
275
+ isChomped: false,
276
+ isHidden: computedStyles.display === 'none',
277
+ width: Math.ceil(parseFloat(computedStyles.width) || 0)
278
+ + parseInt(computedStyles.marginRight) || 0
279
+ + parseInt(computedStyles.marginLeft) || 0,
280
+ node: node
281
+ };
282
+ });
283
+
284
+ return items.filter(({ isHidden }) => !isHidden);
285
+ }
286
+
287
+ async _getItems() {
288
+ // get the items from the slot
289
+ this._slotItems = await this._getSlotItems();
290
+ // convert them to layout items (calculate widths)
291
+ this._itemLayouts = this._getItemLayouts(this._slotItems);
292
+ // convert to overflow items (for overflow container)
293
+ this._overflowItems = this._slotItems.map((node) => this.convertToOverflowItem(node));
294
+ }
295
+
296
+ async _getSlotItems() {
297
+ const filteredNodes = await filterAsync(this.shadowRoot.querySelector('slot').assignedNodes({ flatten: true }), async(node) => {
298
+ if (node.nodeType !== Node.ELEMENT_NODE) return false;
299
+ if (node.updateComplete) await node.updateComplete;
300
+ return node.tagName.toLowerCase() !== 'template';
301
+ });
302
+
303
+ return filteredNodes;
304
+ }
305
+
306
+ _handleItemMutation(mutations) {
307
+ if (!mutations || mutations.length === 0) return;
308
+ if (this._updateOverflowItemsRequested) return;
309
+
310
+ this._updateOverflowItemsRequested = true;
311
+ setTimeout(() => {
312
+ this._overflowItems = this._slotItems.map(node => this.convertToOverflowItem(node));
313
+ this._updateOverflowItemsRequested = false;
314
+ this.requestUpdate();
315
+ }, 0);
316
+ }
317
+
318
+ _handleResize(entries) {
319
+ this._availableWidth = Math.ceil(entries[0].contentRect.width);
320
+ this._chomp();
321
+ }
322
+
323
+ _handleSlotChange() {
324
+ requestAnimationFrame(async() => {
325
+ await this._getItems();
326
+
327
+ this._slotItems.forEach(item => {
328
+ const observer = new MutationObserver(this._handleItemMutation);
329
+ observer.observe(item, {
330
+ attributes: true, /* required for legacy-Edge, otherwise attributeFilter throws a syntax error */
331
+ attributeFilter: ['disabled', 'text', 'selected'],
332
+ childList: false,
333
+ subtree: true
334
+ });
335
+ });
336
+
337
+ if (this.autoShow) {
338
+ this._autoDetectBoundaries(this._slotItems);
339
+ }
340
+
341
+ this._chomp();
342
+ });
343
+ }
344
+ };
@@ -11,20 +11,10 @@ import '../menu/menu-item.js';
11
11
  import '../menu/menu-item-separator.js';
12
12
  import '../menu/menu-item-link.js';
13
13
  import { css, html, LitElement } from 'lit';
14
+ import { OVERFLOW_CLASS, OVERFLOW_MINI_CLASS, OverflowGroupMixin } from './overflow-group-mixin.js';
14
15
  import { ifDefined } from 'lit/directives/if-defined.js';
15
- import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
16
- import { offscreenStyles } from '../offscreen/offscreen.js';
17
- import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js';
18
16
  import { RtlMixin } from '../../mixins/rtl-mixin.js';
19
17
 
20
- const AUTO_SHOW_CLASS = 'd2l-button-group-show';
21
- const AUTO_NO_SHOW_CLASS = 'd2l-button-group-no-show';
22
-
23
- const OPENER_TYPE = {
24
- DEFAULT: 'default',
25
- ICON: 'icon'
26
- };
27
-
28
18
  const OPENER_STYLE = {
29
19
  DEFAULT: 'default',
30
20
  SUBTLE: 'subtle',
@@ -61,140 +51,36 @@ function createMenuItemSeparator() {
61
51
  return html`<d2l-menu-item-separator></d2l-menu-item-separator>`;
62
52
  }
63
53
 
64
- function createMenuItemMenu(node) {
65
- const menuOpener =
66
- node.querySelector('d2l-dropdown-button')
67
- || node.querySelector('d2l-dropdown-button-subtle');
68
-
69
- const openerText = node.text || menuOpener.text;
70
- const disabled = !!node.disabled;
71
- const subMenu = node.querySelector('d2l-menu');
72
-
73
- const subItems = Array.from(subMenu.children).map((node) => convertToDropdownItem(node));
74
-
75
- return html`<d2l-menu-item
76
- ?disabled=${disabled}
77
- text="${openerText}">
78
- <d2l-menu>
79
- ${subItems}
80
- </d2l-menu>
81
- </d2l-menu-item>`;
82
- }
83
-
84
- function convertToDropdownItem(node) {
85
- const tagName = node.tagName.toLowerCase();
86
- switch (tagName) {
87
- case 'd2l-button':
88
- case 'd2l-button-subtle':
89
- case 'button':
90
- case 'd2l-button-icon':
91
- case 'd2l-selection-action':
92
- return createMenuItem(node);
93
- case 'a':
94
- case 'd2l-link':
95
- return createMenuItemLink(node);
96
- case 'd2l-menu':
97
- case 'd2l-dropdown':
98
- case 'd2l-dropdown-button':
99
- case 'd2l-dropdown-button-subtle':
100
- case 'd2l-dropdown-context-menu':
101
- case 'd2l-dropdown-more':
102
- case 'd2l-selection-action-dropdown':
103
- return createMenuItemMenu(node);
104
- case 'd2l-menu-item':
105
- case 'd2l-selection-action-menu-item':
106
- // if the menu item has children treat it as a menu item menu
107
- if (node.children.length > 0) {
108
- return createMenuItemMenu(node);
109
- } else {
110
- return createMenuItem(node);
111
- }
112
- }
113
- if (node.getAttribute('role') === 'separator') {
114
- return createMenuItemSeparator();
115
- }
116
- }
117
-
118
54
  /**
119
55
  *
120
56
  * A component that can be used to display a set of buttons, links or menus that will be put into a dropdown menu when they no longer fit on the first line of their container
121
57
  * @slot - Buttons, dropdown buttons, links or other items to be added to the container
122
- * @fires d2l-overflow-group-updated - Dispatched when there is an update performed to the overflow group
58
+ * @attr {'default'|'icon'} [opener-type="default"] - Set the opener type to 'icon' for a `...` menu icon instead of `More actions` text
123
59
  */
124
- class OverflowGroup extends RtlMixin(LocalizeCoreElement(LitElement)) {
60
+ class OverflowGroup extends OverflowGroupMixin(RtlMixin(LitElement)) {
125
61
 
126
62
  static get properties() {
127
63
  return {
128
- /**
129
- * Use predefined classes on slot elements to set min and max buttons to show
130
- * @type {boolean}
131
- */
132
- autoShow: {
133
- type: Boolean,
134
- attribute: 'auto-show',
135
- },
136
- /**
137
- * minimum amount of buttons to show
138
- * @type {number}
139
- */
140
- minToShow: {
141
- type: Number,
142
- reflect: true,
143
- attribute: 'min-to-show',
144
- },
145
- /**
146
- * maximum amount of buttons to show
147
- * @type {number}
148
- */
149
- maxToShow: {
150
- type: Number,
151
- reflect: true,
152
- attribute: 'max-to-show',
153
- },
154
- /**
155
- * Set the opener type to 'icon' for a `...` menu icon instead of `More actions` text
156
- * @type {'default'|'icon'}
157
- */
158
- openerType: {
159
- type: String,
160
- attribute: 'opener-type'
161
- },
162
64
  /**
163
65
  * Setting this property will change the style of the overflow menu opener
164
66
  * @type {'default'|'subtle'}
67
+ * @default "default"
165
68
  */
166
69
  openerStyle: {
167
70
  type: String,
168
71
  reflect: true,
169
72
  attribute: 'opener-style',
170
- },
171
- _mini: {
172
- type: Boolean,
173
- reflect: true
174
- },
175
- _chompIndex: {
176
- type: Number,
177
73
  }
178
74
  };
179
75
  }
180
76
 
181
77
  static get styles() {
182
- return [offscreenStyles, css`
183
- :host {
184
- display: block;
185
- }
186
- :host([hidden]) {
187
- display: none;
188
- }
78
+ return [super.styles, css`
189
79
  :host([opener-style="subtle"]) {
190
80
  --d2l-button-icon-fill-color: var(--d2l-color-celestine);
191
81
  --d2l-button-icon-fill-color-hover: var(--d2l-color-celestine-minus-1);
192
82
  }
193
- .d2l-overflow-group-container {
194
- display: flex;
195
- flex-wrap: wrap;
196
- justify-content: var(--d2l-overflow-group-justify-content, normal);
197
- }
83
+
198
84
  ::slotted(d2l-button),
199
85
  ::slotted(d2l-link),
200
86
  ::slotted(span),
@@ -229,291 +115,93 @@ class OverflowGroup extends RtlMixin(LocalizeCoreElement(LitElement)) {
229
115
  margin-left: 0.2rem;
230
116
  margin-right: 0;
231
117
  }
232
- .d2l-overflow-group-container ::slotted([data-is-chomped]) {
233
- display: none !important;
234
- }
235
118
  `];
236
119
  }
237
120
 
238
121
  constructor() {
239
122
  super();
240
- this._handleItemMutation = this._handleItemMutation.bind(this);
241
- this._handleResize = this._handleResize.bind(this);
242
-
243
- this._throttledResize = (entries) => requestAnimationFrame(() => this._handleResize(entries));
244
-
245
- this._overflowHidden = false;
246
- this.autoShow = false;
247
- this.maxToShow = -1;
248
- this.minToShow = 1;
249
123
  this.openerStyle = OPENER_STYLE.DEFAULT;
250
- this.openerType = OPENER_TYPE.DEFAULT;
251
- this._mini = this.openerType === OPENER_TYPE.ICON;
252
- this._resizeObserver = null;
253
- this._slotItems = [];
254
- }
255
-
256
- disconnectedCallback() {
257
- super.disconnectedCallback();
258
- if (this._resizeObserver) this._resizeObserver.disconnect();
259
124
  }
260
125
 
261
- async firstUpdated() {
262
- super.firstUpdated();
263
-
264
- // selected elements
265
- this._buttonSlot = this.shadowRoot.querySelector('slot');
266
-
267
- this._container = this.shadowRoot.querySelector('.d2l-overflow-group-container');
268
-
269
- this._resizeObserver = new ResizeObserver(this._throttledResize);
270
- this._resizeObserver.observe(this._container);
271
- }
272
-
273
- render() {
274
- const overflowMenu = this._getOverflowMenu();
275
-
276
- this._slotItems.forEach((element, index) => {
277
- if (!this._overflowMenuHidden && index >= this._chompIndex) {
278
- element.setAttribute('data-is-chomped', '');
279
- } else {
280
- element.removeAttribute('data-is-chomped');
281
- }
282
- });
283
-
284
- return html`
285
- <div class="d2l-overflow-group-container">
286
- <slot @slotchange="${this._handleSlotChange}"></slot>
287
- ${overflowMenu}
288
- </div>
289
- `;
290
- }
291
-
292
- update(changedProperties) {
293
- super.update(changedProperties);
294
-
295
- if (changedProperties.get('autoShow')) {
296
- this._getItemLayouts(this._slotItems);
297
- this._autoDetectBoundaries(this._itemLayouts);
298
- }
299
-
300
- if (changedProperties.get('minToShow')
301
- || changedProperties.get('maxToShow')) {
302
- this._chomp();
303
- }
304
-
305
- // Slight hack to get the overflow menu width the first time it renders
306
- if (!this._overflowMenuWidth) {
307
- // this action needs to be deferred until first render of our overflow button
308
- requestAnimationFrame(() => {
309
- this._chomp();
310
- });
311
- }
312
- }
313
-
314
- _autoDetectBoundaries(items) {
315
-
316
- let minToShow, maxToShow;
317
- for (let i = 0; i < items.length; i++) {
318
- if (items[i].classList.contains(AUTO_SHOW_CLASS)) {
319
- minToShow = i + 1;
320
- }
321
- if (maxToShow === undefined && items[i].classList.contains(AUTO_NO_SHOW_CLASS)) {
322
- maxToShow = i;
323
- }
324
- }
325
-
326
- if (minToShow !== undefined) {
327
- this.minToShow = minToShow;
328
- }
329
- if (maxToShow !== undefined) {
330
- this.maxToShow = maxToShow;
331
- }
332
- }
333
-
334
- _chomp() {
335
- if (!this.shadowRoot || !this._itemLayouts) return;
336
-
337
- this._overflowMenu = this.shadowRoot.querySelector('.d2l-overflow-dropdown');
338
- this._overflowMenuMini = this.shadowRoot.querySelector('.d2l-overflow-dropdown-mini');
339
- if (this.openerType === OPENER_TYPE.ICON && this._overflowMenuMini) {
340
- this._overflowMenuWidth = this._overflowMenuMini.offsetWidth;
341
- } else if (this._overflowMenu) {
342
- this._overflowMenuWidth = this._overflowMenu.offsetWidth;
343
- }
344
-
345
- const showing = {
346
- count: 0,
347
- width: 0
348
- };
349
-
350
- let isSoftOverflowing, isForcedOverflowing;
351
- for (let i = 0; i < this._itemLayouts.length; i++) {
352
- const itemLayout = this._itemLayouts[i];
353
-
354
- // handle minimum items to show
355
- if (showing.count < this.minToShow) {
356
- showing.width += itemLayout.width;
357
- showing.count += 1;
358
- itemLayout.trigger = 'force-show';
359
- itemLayout.isChomped = false;
360
- continue;
361
- }
362
-
363
- // handle maximum items to show
364
- if (this.maxToShow >= 0 && showing.count >= this.maxToShow) {
365
- isForcedOverflowing = true;
366
- itemLayout.isChomped = true;
367
- itemLayout.trigger = 'force-hide';
368
- continue;
369
- }
370
-
371
- // chomp or unchomp based on space available, and we've already handled min/max above
372
- if (!isSoftOverflowing && showing.width + itemLayout.width < this._availableWidth) {
373
- showing.width += itemLayout.width;
374
- showing.count += 1;
375
- itemLayout.isChomped = false;
376
- itemLayout.trigger = 'soft-show';
377
-
378
- } else {
379
- isSoftOverflowing = true;
380
- itemLayout.isChomped = true;
381
- itemLayout.trigger = 'soft-hide';
382
-
383
- }
384
-
385
- }
386
- // if there is at least one showing and no more to be hidden, enable collapsing more button to [...]
387
- this._overflowMenuHidden = this._itemLayouts.length === showing.count;
388
- if (!this._overflowMenuHidden && (isSoftOverflowing || isForcedOverflowing)) {
389
- for (let j = this._itemLayouts.length; j--;) {
390
- if (showing.width + this._overflowMenuWidth < this._availableWidth) {
391
- break;
392
- }
393
- const itemLayoutOverflowing = this._itemLayouts[j];
394
- if (itemLayoutOverflowing.trigger !== 'soft-show') {
395
- continue;
126
+ convertToOverflowItem(node) {
127
+ const tagName = node.tagName.toLowerCase();
128
+ switch (tagName) {
129
+ case 'd2l-button':
130
+ case 'd2l-button-subtle':
131
+ case 'button':
132
+ case 'd2l-button-icon':
133
+ case 'd2l-selection-action':
134
+ return createMenuItem(node);
135
+ case 'a':
136
+ case 'd2l-link':
137
+ return createMenuItemLink(node);
138
+ case 'd2l-menu':
139
+ case 'd2l-dropdown':
140
+ case 'd2l-dropdown-button':
141
+ case 'd2l-dropdown-button-subtle':
142
+ case 'd2l-dropdown-context-menu':
143
+ case 'd2l-dropdown-more':
144
+ case 'd2l-selection-action-dropdown':
145
+ return this._createMenuItemMenu(node);
146
+ case 'd2l-menu-item':
147
+ case 'd2l-selection-action-menu-item':
148
+ // if the menu item has children treat it as a menu item menu
149
+ if (node.children.length > 0) {
150
+ return this._createMenuItemMenu(node);
151
+ } else {
152
+ return createMenuItem(node);
396
153
  }
397
- showing.width -= itemLayoutOverflowing.width;
398
- showing.count -= 1;
399
- isSoftOverflowing = true;
400
- itemLayoutOverflowing.trigger = 'soft-hide';
401
- itemLayoutOverflowing.isChomped = true;
402
- }
403
154
  }
404
- const overflowDropdownOverflowing = (showing.width + this._overflowMenuWidth >= this._availableWidth);
405
- const swapToMiniButton = overflowDropdownOverflowing && !this._overflowMenuHidden;
406
-
407
- this._mini = this.openerType === OPENER_TYPE.ICON || swapToMiniButton;
408
- this._chompIndex = this._overflowMenuHidden ? null : showing.count;
409
-
410
- this.dispatchEvent(new CustomEvent('d2l-overflow-group-updated', { composed: false, bubbles: true }));
411
- }
412
-
413
- _getItemLayouts(filteredNodes) {
414
- const items = filteredNodes.map((node) => {
415
- const computedStyles = window.getComputedStyle(node);
416
-
417
- return {
418
- type: node.tagName.toLowerCase(),
419
- isChomped: false,
420
- isHidden: computedStyles.display === 'none',
421
- width: Math.ceil(parseFloat(computedStyles.width) || 0)
422
- + parseInt(computedStyles.marginRight) || 0
423
- + parseInt(computedStyles.marginLeft) || 0,
424
- node: node
425
- };
426
- });
427
-
428
- return items.filter(({ isHidden }) => !isHidden);
429
- }
430
-
431
- _getItems() {
432
- // get the items from the button slot
433
- this._slotItems = this._getSlotItems();
434
- // convert them to layout items (calculate widths)
435
- this._itemLayouts = this._getItemLayouts(this._slotItems);
436
- // convert to dropdown items (for overflow menu)
437
- this._dropdownItems = this._slotItems.map((node) => convertToDropdownItem(node));
155
+ if (node.getAttribute('role') === 'separator') {
156
+ return createMenuItemSeparator();
157
+ }
438
158
  }
439
159
 
440
- _getOverflowMenu() {
441
- if (this._overflowMenuHidden) {
442
- return;
443
- }
160
+ getOverflowContainer(overflowItems, mini) {
444
161
  const moreActionsText = this.localize('components.overflow-group.moreActions');
445
- const overflowItems = this._dropdownItems ? this._dropdownItems.slice(this._chompIndex) : [];
446
162
  const menu = html`<d2l-dropdown-menu>
447
163
  <d2l-menu label="${moreActionsText}">
448
164
  ${overflowItems}
449
165
  </d2l-menu>
450
166
  </d2l-dropdown-menu>`;
451
167
 
452
- if (this._mini) {
453
- return html`<d2l-dropdown-more class="d2l-overflow-dropdown-mini" text="${moreActionsText}">
168
+ if (mini) {
169
+ return html`<d2l-dropdown-more class="${OVERFLOW_MINI_CLASS} d2l-overflow-dropdown-mini" text="${moreActionsText}">
454
170
  ${menu}
455
171
  </d2l-dropdown-more>`;
456
172
  }
457
173
 
458
174
  if (this.openerStyle === OPENER_STYLE.SUBTLE) {
459
- return html`<d2l-dropdown-button-subtle class="d2l-overflow-dropdown" text="${moreActionsText}">
175
+ return html`<d2l-dropdown-button-subtle class="${OVERFLOW_CLASS} d2l-overflow-dropdown" text="${moreActionsText}">
460
176
  ${menu}
461
177
  </d2l-dropdown-button-subtle>`;
462
178
  }
463
179
 
464
- return html`<d2l-dropdown-button class="d2l-overflow-dropdown" text="${moreActionsText}">
180
+ return html`<d2l-dropdown-button class="${OVERFLOW_CLASS} d2l-overflow-dropdown" text="${moreActionsText}">
465
181
  ${menu}
466
182
  </d2l-dropdown-button>`;
467
183
  }
468
184
 
469
- _getSlotItems() {
470
- const nodes = this._buttonSlot.assignedNodes({ flatten: true });
471
- const filteredNodes = nodes.filter((node) => {
472
- const isNode = node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() !== 'template';
473
- return isNode;
474
- });
185
+ _createMenuItemMenu(node) {
186
+ const menuOpener =
187
+ node.querySelector('d2l-dropdown-button')
188
+ || node.querySelector('d2l-dropdown-button-subtle');
475
189
 
476
- return filteredNodes;
477
- }
478
-
479
- _handleItemMutation(mutations) {
480
- if (!mutations || mutations.length === 0) return;
481
- if (this._updateDropdownItemsRequested) return;
190
+ const openerText = node.text || menuOpener.text;
191
+ const disabled = !!node.disabled;
192
+ const subMenu = node.querySelector('d2l-menu');
482
193
 
483
- this._updateDropdownItemsRequested = true;
484
- setTimeout(() => {
485
- this._dropdownItems = this._slotItems.map(node => convertToDropdownItem(node));
486
- this._updateDropdownItemsRequested = false;
487
- this.requestUpdate();
488
- }, 0);
489
- }
194
+ const subItems = Array.from(subMenu.children).map((node) => this.convertToOverflowItem(node));
490
195
 
491
- _handleResize(entries) {
492
- this._availableWidth = Math.ceil(entries[0].contentRect.width);
493
- this._chomp();
196
+ return html`<d2l-menu-item
197
+ ?disabled=${disabled}
198
+ text="${openerText}">
199
+ <d2l-menu>
200
+ ${subItems}
201
+ </d2l-menu>
202
+ </d2l-menu-item>`;
494
203
  }
495
204
 
496
- _handleSlotChange() {
497
- requestAnimationFrame(() => {
498
- this._getItems();
499
-
500
- this._slotItems.forEach(item => {
501
- const observer = new MutationObserver(this._handleItemMutation);
502
- observer.observe(item, {
503
- attributes: true, /* required for legacy-Edge, otherwise attributeFilter throws a syntax error */
504
- attributeFilter: ['disabled', 'text'],
505
- childList: false,
506
- subtree: false
507
- });
508
- });
509
-
510
- if (this.autoShow) {
511
- this._autoDetectBoundaries(this._slotItems);
512
- }
513
-
514
- this._chomp();
515
- });
516
- }
517
205
  }
518
206
 
519
207
  customElements.define('d2l-overflow-group', OverflowGroup);
@@ -8760,71 +8760,69 @@
8760
8760
  "path": "./components/overflow-group/overflow-group.js",
8761
8761
  "description": "\nA component that can be used to display a set of buttons, links or menus that will be put into a dropdown menu when they no longer fit on the first line of their container",
8762
8762
  "attributes": [
8763
+ {
8764
+ "name": "opener-style",
8765
+ "description": "Setting this property will change the style of the overflow menu opener",
8766
+ "type": "'default'|'subtle'",
8767
+ "default": "\"\\\"default\\\"\""
8768
+ },
8763
8769
  {
8764
8770
  "name": "auto-show",
8765
- "description": "Use predefined classes on slot elements to set min and max buttons to show",
8771
+ "description": "Use predefined classes on slot elements to set min and max slotted items to show",
8766
8772
  "type": "boolean",
8767
8773
  "default": "false"
8768
8774
  },
8769
8775
  {
8770
8776
  "name": "max-to-show",
8771
- "description": "maximum amount of buttons to show",
8777
+ "description": "maximum amount of slotted items to show",
8772
8778
  "type": "number",
8773
8779
  "default": "-1"
8774
8780
  },
8775
8781
  {
8776
8782
  "name": "min-to-show",
8777
- "description": "minimum amount of buttons to show",
8783
+ "description": "minimum amount of slotted items to show",
8778
8784
  "type": "number",
8779
8785
  "default": "1"
8780
8786
  },
8781
- {
8782
- "name": "opener-style",
8783
- "description": "Setting this property will change the style of the overflow menu opener",
8784
- "type": "'default'|'subtle'",
8785
- "default": "\"DEFAULT\""
8786
- },
8787
8787
  {
8788
8788
  "name": "opener-type",
8789
8789
  "description": "Set the opener type to 'icon' for a `...` menu icon instead of `More actions` text",
8790
8790
  "type": "'default'|'icon'",
8791
- "default": "\"DEFAULT\""
8791
+ "default": "\"default\""
8792
8792
  }
8793
8793
  ],
8794
8794
  "properties": [
8795
+ {
8796
+ "name": "openerStyle",
8797
+ "attribute": "opener-style",
8798
+ "description": "Setting this property will change the style of the overflow menu opener",
8799
+ "type": "'default'|'subtle'",
8800
+ "default": "\"\\\"default\\\"\""
8801
+ },
8795
8802
  {
8796
8803
  "name": "autoShow",
8797
8804
  "attribute": "auto-show",
8798
- "description": "Use predefined classes on slot elements to set min and max buttons to show",
8805
+ "description": "Use predefined classes on slot elements to set min and max slotted items to show",
8799
8806
  "type": "boolean",
8800
8807
  "default": "false"
8801
8808
  },
8802
8809
  {
8803
8810
  "name": "maxToShow",
8804
8811
  "attribute": "max-to-show",
8805
- "description": "maximum amount of buttons to show",
8812
+ "description": "maximum amount of slotted items to show",
8806
8813
  "type": "number",
8807
8814
  "default": "-1"
8808
8815
  },
8809
8816
  {
8810
8817
  "name": "minToShow",
8811
8818
  "attribute": "min-to-show",
8812
- "description": "minimum amount of buttons to show",
8819
+ "description": "minimum amount of slotted items to show",
8813
8820
  "type": "number",
8814
8821
  "default": "1"
8815
8822
  },
8816
- {
8817
- "name": "openerStyle",
8818
- "attribute": "opener-style",
8819
- "description": "Setting this property will change the style of the overflow menu opener",
8820
- "type": "'default'|'subtle'",
8821
- "default": "\"DEFAULT\""
8822
- },
8823
8823
  {
8824
8824
  "name": "openerType",
8825
- "attribute": "opener-type",
8826
- "description": "Set the opener type to 'icon' for a `...` menu icon instead of `More actions` text",
8827
- "type": "'default'|'icon'",
8825
+ "type": "string",
8828
8826
  "default": "\"DEFAULT\""
8829
8827
  }
8830
8828
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "2.38.3",
3
+ "version": "2.39.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",