diff --git a/packages/menu-bar/src/vaadin-menu-bar-buttons-mixin.d.ts b/packages/menu-bar/src/vaadin-menu-bar-buttons-mixin.d.ts index de384c82fd..62b14345d1 100644 --- a/packages/menu-bar/src/vaadin-menu-bar-buttons-mixin.d.ts +++ b/packages/menu-bar/src/vaadin-menu-bar-buttons-mixin.d.ts @@ -11,6 +11,13 @@ export declare function ButtonsMixin>( ): Constructor & Constructor & T; export declare class ButtonsMixinClass { + /** + * If true, the buttons will be collapsed into the overflow menu + * starting from the "start" end of the bar instead of the "end". + * @attr {boolean} reverse-collapse + */ + reverseCollapse: boolean | null | undefined; + protected readonly _buttons: HTMLElement[]; protected readonly _container: HTMLElement; diff --git a/packages/menu-bar/src/vaadin-menu-bar-buttons-mixin.js b/packages/menu-bar/src/vaadin-menu-bar-buttons-mixin.js index 036ad65726..cea62a917a 100644 --- a/packages/menu-bar/src/vaadin-menu-bar-buttons-mixin.js +++ b/packages/menu-bar/src/vaadin-menu-bar-buttons-mixin.js @@ -13,6 +13,15 @@ export const ButtonsMixin = (superClass) => class extends ResizeMixin(superClass) { static get properties() { return { + /** + * If true, the buttons will be collapsed into the overflow menu + * starting from the "start" end of the bar instead of the "end". + * @attr {boolean} reverse-collapse + */ + reverseCollapse: { + type: Boolean, + }, + /** * @type {boolean} * @protected @@ -25,7 +34,7 @@ export const ButtonsMixin = (superClass) => } static get observers() { - return ['_menuItemsChanged(items, items.splices)']; + return ['_menuItemsChanged(items, items.splices)', '_reverseCollapseChanged(reverseCollapse)']; } /** @@ -125,6 +134,16 @@ export const ButtonsMixin = (superClass) => this._hasOverflow = items.length > 0; } + /** + * A callback for the 'reverseCollapse' property observer. + * + * @param {boolean | null} _reverseCollapse + * @private + */ + _reverseCollapseChanged(_reverseCollapse) { + this.__detectOverflow(); + } + /** @private */ __setOverflowItems(buttons, overflow) { const container = this._container; @@ -133,27 +152,30 @@ export const ButtonsMixin = (superClass) => this._hasOverflow = true; const isRTL = this.getAttribute('dir') === 'rtl'; + const containerLeft = container.getBoundingClientRect().left; - let i; - for (i = buttons.length; i > 0; i--) { - const btn = buttons[i - 1]; - const btnStyle = getComputedStyle(btn); + const remaining = [...buttons]; + while (remaining.length) { + const lastButton = remaining[remaining.length - 1]; + const btnLeft = lastButton.getBoundingClientRect().left - containerLeft; // If this button isn't overflowing, then the rest aren't either if ( - (!isRTL && btn.offsetLeft + btn.offsetWidth < container.offsetWidth - overflow.offsetWidth) || - (isRTL && btn.offsetLeft >= overflow.offsetWidth) + (!isRTL && btnLeft + lastButton.offsetWidth < container.offsetWidth - overflow.offsetWidth) || + (isRTL && btnLeft >= overflow.offsetWidth) ) { break; } + const btn = this.reverseCollapse ? remaining.shift() : remaining.pop(); + + // Save width for buttons with component + btn.style.width = getComputedStyle(btn).width; btn.disabled = true; btn.style.visibility = 'hidden'; btn.style.position = 'absolute'; - // Save width for buttons with component - btn.style.width = btnStyle.width; } - const items = buttons.filter((_, idx) => idx >= i).map((b) => b.item); + const items = buttons.filter((b) => !remaining.includes(b)).map((b) => b.item); this.__updateOverflow(items); } } @@ -177,6 +199,14 @@ export const ButtonsMixin = (superClass) => const isSingleButton = newOverflowCount === buttons.length || (newOverflowCount === 0 && buttons.length === 1); this.toggleAttribute('has-single-button', isSingleButton); + + // Apply first/last visible attributes to the visible buttons + buttons + .filter((btn) => btn.style.visibility !== 'hidden') + .forEach((btn, index, visibleButtons) => { + btn.toggleAttribute('first-visible', index === 0); + btn.toggleAttribute('last-visible', !this._hasOverflow && index === visibleButtons.length - 1); + }); } /** @protected */ diff --git a/packages/menu-bar/test/dom/__snapshots__/menu-bar.test.snap.js b/packages/menu-bar/test/dom/__snapshots__/menu-bar.test.snap.js index dd88196b3a..69d3f6e1ac 100644 --- a/packages/menu-bar/test/dom/__snapshots__/menu-bar.test.snap.js +++ b/packages/menu-bar/test/dom/__snapshots__/menu-bar.test.snap.js @@ -10,6 +10,7 @@ snapshots["menu-bar host"] = snapshots["menu-bar shadow"] = `
{ menu.i18n = { ...menu.i18n, moreOptions: moreOptionsSv }; expect(overflow.getAttribute('aria-label')).to.equal(moreOptionsSv); }); + + describe('reverse-collapse', () => { + beforeEach(() => { + menu.reverseCollapse = true; + }); + + it('should show overflow button and hide the buttons which do not fit', () => { + assertHidden(buttons[0]); + expect(buttons[0].disabled).to.be.true; + assertHidden(buttons[1]); + expect(buttons[1].disabled).to.be.true; + assertHidden(buttons[2]); + expect(buttons[2].disabled).to.be.true; + assertVisible(buttons[3]); + expect(buttons[3].disabled).to.be.false; + assertVisible(buttons[4]); + expect(buttons[4].disabled).to.be.true; + + expect(overflow.hasAttribute('hidden')).to.be.false; + }); + + it('should set items to overflow button for buttons which do not fit', () => { + expect(overflow.item).to.be.instanceOf(Object); + expect(overflow.item.children).to.be.instanceOf(Array); + expect(overflow.item.children.length).to.equal(3); + expect(overflow.item.children[0]).to.deep.equal(menu.items[0]); + expect(overflow.item.children[1]).to.deep.equal(menu.items[1]); + expect(overflow.item.children[2]).to.deep.equal(menu.items[2]); + }); + + it('should update oveflow when reverseCollapse changes', () => { + menu.reverseCollapse = false; + assertVisible(buttons[0]); + expect(buttons[0].disabled).to.be.false; + assertVisible(buttons[1]); + expect(buttons[1].disabled).to.be.false; + assertHidden(buttons[2]); + expect(buttons[2].disabled).to.be.true; + assertHidden(buttons[3]); + expect(buttons[3].disabled).to.be.true; + assertHidden(buttons[4]); + expect(buttons[4].disabled).to.be.true; + }); + }); }); describe('has-single-button attribute', () => { diff --git a/packages/menu-bar/test/visual/lumo/menu-bar.test.js b/packages/menu-bar/test/visual/lumo/menu-bar.test.js index 7f8544f8c6..307018250d 100644 --- a/packages/menu-bar/test/visual/lumo/menu-bar.test.js +++ b/packages/menu-bar/test/visual/lumo/menu-bar.test.js @@ -44,6 +44,16 @@ describe('menu-bar', () => { await nextRender(element); await visualDiff(document.body, `${dir}-opened`); }); + + it('reverse-collapse opened', async () => { + div.style.width = '250px'; + element.reverseCollapse = true; + await nextRender(element); + element._buttons[4].click(); + const overlay = element._subMenu._overlayElement; + await oneEvent(overlay, 'vaadin-overlay-open'); + await visualDiff(document.body, `${dir}-reverse-collapse-opened`); + }); }); describe('single button', () => { diff --git a/packages/menu-bar/test/visual/lumo/screenshots/menu-bar/baseline/ltr-reverse-collapse-opened.png b/packages/menu-bar/test/visual/lumo/screenshots/menu-bar/baseline/ltr-reverse-collapse-opened.png new file mode 100644 index 0000000000..6f09ec4d94 Binary files /dev/null and b/packages/menu-bar/test/visual/lumo/screenshots/menu-bar/baseline/ltr-reverse-collapse-opened.png differ diff --git a/packages/menu-bar/test/visual/lumo/screenshots/menu-bar/baseline/rtl-reverse-collapse-opened.png b/packages/menu-bar/test/visual/lumo/screenshots/menu-bar/baseline/rtl-reverse-collapse-opened.png new file mode 100644 index 0000000000..ea5abc85a6 Binary files /dev/null and b/packages/menu-bar/test/visual/lumo/screenshots/menu-bar/baseline/rtl-reverse-collapse-opened.png differ diff --git a/packages/menu-bar/test/visual/material/menu-bar.test.js b/packages/menu-bar/test/visual/material/menu-bar.test.js index a8c5e648e4..bcbb768b6e 100644 --- a/packages/menu-bar/test/visual/material/menu-bar.test.js +++ b/packages/menu-bar/test/visual/material/menu-bar.test.js @@ -44,6 +44,17 @@ describe('menu-bar', () => { await nextRender(element); await visualDiff(document.body, `${dir}-opened`); }); + + it('reverse-collapse opened', async () => { + div.style.width = '250px'; + element.reverseCollapse = true; + element.setAttribute('theme', 'outlined'); + await nextRender(element); + element._buttons[4].click(); + const overlay = element._subMenu._overlayElement; + await oneEvent(overlay, 'vaadin-overlay-open'); + await visualDiff(document.body, `${dir}-reverse-collapse-opened`); + }); }); describe('single button', () => { diff --git a/packages/menu-bar/test/visual/material/screenshots/menu-bar/baseline/ltr-reverse-collapse-opened.png b/packages/menu-bar/test/visual/material/screenshots/menu-bar/baseline/ltr-reverse-collapse-opened.png new file mode 100644 index 0000000000..326e07e364 Binary files /dev/null and b/packages/menu-bar/test/visual/material/screenshots/menu-bar/baseline/ltr-reverse-collapse-opened.png differ diff --git a/packages/menu-bar/test/visual/material/screenshots/menu-bar/baseline/rtl-reverse-collapse-opened.png b/packages/menu-bar/test/visual/material/screenshots/menu-bar/baseline/rtl-reverse-collapse-opened.png new file mode 100644 index 0000000000..8088a20693 Binary files /dev/null and b/packages/menu-bar/test/visual/material/screenshots/menu-bar/baseline/rtl-reverse-collapse-opened.png differ diff --git a/packages/menu-bar/theme/lumo/vaadin-menu-bar-button-styles.js b/packages/menu-bar/theme/lumo/vaadin-menu-bar-button-styles.js index f401bea2f0..e6a8b22b69 100644 --- a/packages/menu-bar/theme/lumo/vaadin-menu-bar-button-styles.js +++ b/packages/menu-bar/theme/lumo/vaadin-menu-bar-button-styles.js @@ -46,14 +46,14 @@ const menuBarButton = css` padding: 0; } - :host(:first-of-type) { + :host([first-visible]) { border-radius: var(--lumo-border-radius-m) 0 0 var(--lumo-border-radius-m); /* Needed to retain the focus-ring with border-radius */ margin-left: calc(var(--lumo-space-xs) / 2); } - :host(:nth-last-of-type(2)), + :host([last-visible]), :host([part='overflow-button']) { border-radius: 0 var(--lumo-border-radius-m) var(--lumo-border-radius-m) 0; } @@ -86,12 +86,12 @@ const menuBarButton = css` border-radius: 0; } - :host([dir='rtl']:first-of-type) { + :host([dir='rtl'][first-visible]) { border-radius: 0 var(--lumo-border-radius-m) var(--lumo-border-radius-m) 0; margin-right: calc(var(--lumo-space-xs) / 2); } - :host([dir='rtl']:nth-last-of-type(2)), + :host([dir='rtl'][last-visible]), :host([dir='rtl'][part='overflow-button']) { border-radius: var(--lumo-border-radius-m) 0 0 var(--lumo-border-radius-m); } diff --git a/packages/menu-bar/theme/lumo/vaadin-menu-bar-styles.js b/packages/menu-bar/theme/lumo/vaadin-menu-bar-styles.js index 601e50b375..43f6e810c2 100644 --- a/packages/menu-bar/theme/lumo/vaadin-menu-bar-styles.js +++ b/packages/menu-bar/theme/lumo/vaadin-menu-bar-styles.js @@ -8,7 +8,7 @@ registerStyles( border-radius: var(--lumo-border-radius-m); } - :host([theme~='end-aligned']) [part$='button']:first-child, + :host([theme~='end-aligned']) [part$='button'][first-visible], :host([theme~='end-aligned'][has-single-button]) [part$='button'] { margin-inline-start: auto; } diff --git a/packages/menu-bar/theme/material/vaadin-menu-bar-button-styles.js b/packages/menu-bar/theme/material/vaadin-menu-bar-button-styles.js index ff46b1a822..8dcf96dad9 100644 --- a/packages/menu-bar/theme/material/vaadin-menu-bar-button-styles.js +++ b/packages/menu-bar/theme/material/vaadin-menu-bar-button-styles.js @@ -49,11 +49,11 @@ const menuBarButton = css` margin-right: 1px; } - :host(:first-of-type) { + :host([first-visible]) { border-radius: 4px 0 0 4px; } - :host(:nth-last-of-type(2)), + :host([last-visible]), :host([part~='overflow-button']) { border-radius: 0 4px 4px 0; } @@ -72,7 +72,7 @@ const menuBarButton = css` margin-right: -1px; } - :host([theme~='outlined']:not([dir='rtl']):nth-last-of-type(2)), + :host([theme~='outlined']:not([dir='rtl'])[last-visible]), :host([theme~='outlined']:not([dir='rtl'])[part~='overflow-button']) { margin-right: 0; } @@ -83,11 +83,11 @@ const menuBarButton = css` } /* RTL styles */ - :host([dir='rtl']:first-of-type) { + :host([dir='rtl'][first-visible]) { border-radius: 0 4px 4px 0; } - :host([dir='rtl']:nth-last-of-type(2)), + :host([dir='rtl'][last-visible]), :host([dir='rtl'][part='overflow-button']) { border-radius: 4px 0 0 4px; } @@ -100,7 +100,7 @@ const menuBarButton = css` margin-left: -1px; } - :host([theme~='outlined'][dir='rtl']:nth-last-of-type(2)), + :host([theme~='outlined'][dir='rtl'][last-visible]), :host([theme~='outlined'][dir='rtl'][part~='overflow-button']) { margin-left: 0; } diff --git a/packages/menu-bar/theme/material/vaadin-menu-bar-styles.js b/packages/menu-bar/theme/material/vaadin-menu-bar-styles.js index d30eed3014..9c6d0a0860 100644 --- a/packages/menu-bar/theme/material/vaadin-menu-bar-styles.js +++ b/packages/menu-bar/theme/material/vaadin-menu-bar-styles.js @@ -12,7 +12,7 @@ registerStyles( border-radius: 4px; } - :host([theme~='end-aligned']) [part$='button']:first-child, + :host([theme~='end-aligned']) [part$='button'][first-visible], :host([theme~='end-aligned'][has-single-button]) [part$='button'] { margin-inline-start: auto; }