From 4a2d3d537ed473637ccd1204a3f05041b7ad440b Mon Sep 17 00:00:00 2001 From: Usman Khalid Date: Wed, 8 Jul 2026 12:25:49 -0400 Subject: [PATCH 1/3] chore: update styling for multi-line nx-menu items with description --- nx2/blocks/shared/menu/menu.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nx2/blocks/shared/menu/menu.css b/nx2/blocks/shared/menu/menu.css index 39b8f6936..9dd001801 100644 --- a/nx2/blocks/shared/menu/menu.css +++ b/nx2/blocks/shared/menu/menu.css @@ -20,7 +20,7 @@ .menu-item { display: flex; - align-items: center; + align-items: flex-start; gap: var(--s2-spacing-100); width: 100%; padding: var(--s2-spacing-75) var(--s2-spacing-100); @@ -29,7 +29,7 @@ background: none; color: var(--s2-gray-900); font-family: inherit; - font-size: var(--nx-menu-font-size, var(--s2-body-size-xs)); + font-size: var(--s2-body-size-xs); line-height: var(--s2-component-s-regular-line-height); cursor: pointer; box-sizing: border-box; From 623daa92832b5c0f912f07cb33bf90aa5c0dd364 Mon Sep 17 00:00:00 2001 From: Usman Khalid Date: Wed, 8 Jul 2026 13:27:39 -0400 Subject: [PATCH 2/3] chore: bring back nx-menu-font-size var --- nx2/blocks/shared/menu/menu.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nx2/blocks/shared/menu/menu.css b/nx2/blocks/shared/menu/menu.css index 9dd001801..598b4356a 100644 --- a/nx2/blocks/shared/menu/menu.css +++ b/nx2/blocks/shared/menu/menu.css @@ -29,7 +29,7 @@ background: none; color: var(--s2-gray-900); font-family: inherit; - font-size: var(--s2-body-size-xs); + font-size: var(--nx-menu-font-size, var(--s2-body-size-xs)); line-height: var(--s2-component-s-regular-line-height); cursor: pointer; box-sizing: border-box; From a7fa485fc3e6fdac3a829b7c69b2d0c58ecc535a Mon Sep 17 00:00:00 2001 From: Usman Khalid Date: Mon, 13 Jul 2026 09:30:00 -0400 Subject: [PATCH 3/3] feat(ew): support disabled nx-menu items --- nx2/blocks/shared/menu/menu.css | 14 +++++++ nx2/blocks/shared/menu/menu.js | 9 ++-- nx2/blocks/shared/utils/list-nav.js | 2 +- .../unit/nx/blocks/shared/menu/menu.test.js | 41 +++++++++++++++++++ 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/nx2/blocks/shared/menu/menu.css b/nx2/blocks/shared/menu/menu.css index 598b4356a..63ff7190d 100644 --- a/nx2/blocks/shared/menu/menu.css +++ b/nx2/blocks/shared/menu/menu.css @@ -41,6 +41,15 @@ background: var(--s2-gray-200); } +.menu-item:disabled { + color: light-dark(var(--s2-gray-500), var(--s2-gray-600)); + cursor: default; +} + +.menu-item:disabled:hover { + background: none; +} + .menu-item-icon { display: flex; align-items: center; @@ -63,6 +72,11 @@ } } +.menu-item:disabled .menu-item-icon path, +.menu-item:disabled .menu-item-icon text { + fill: light-dark(var(--s2-gray-500), var(--s2-gray-600)); +} + .menu-item-text { display: flex; flex-direction: column; diff --git a/nx2/blocks/shared/menu/menu.js b/nx2/blocks/shared/menu/menu.js index 5f47f44dc..e51d029c0 100644 --- a/nx2/blocks/shared/menu/menu.js +++ b/nx2/blocks/shared/menu/menu.js @@ -45,7 +45,7 @@ class NxMenu extends LitElement { if (e.newState !== 'open') return; this._trigger?.toggleAttribute('data-active', true); this._trigger?.setAttribute('aria-expanded', 'true'); - const first = this.items?.find((i) => !i.divider && !i.section); + const first = this.items?.find((i) => !i.divider && !i.section && !i.disabled); this._active = first?.id; this.updateComplete.then(() => { if (!this.ignoreFocus) this.shadowRoot.querySelector(`[data-id="${this._active}"]`)?.focus(); @@ -85,6 +85,7 @@ class NxMenu extends LitElement { } _select(item) { + if (!item || item.disabled) return; this.close(); this.dispatchEvent(new CustomEvent('select', { detail: { id: item.id }, bubbles: true, composed: true })); } @@ -119,9 +120,11 @@ class NxMenu extends LitElement { data-id=${item.id} class="menu-item ${item.id === this._active ? 'menu-item-active' : ''}" type="button" + ?disabled=${item.disabled} + aria-disabled=${item.disabled ? 'true' : nothing} @click=${() => this._select(item)} - @mouseenter=${() => { this._active = item.id; }} - @focus=${() => { this._active = item.id; }} + @mouseenter=${() => { if (!item.disabled) this._active = item.id; }} + @focus=${() => { if (!item.disabled) this._active = item.id; }} > ${item.icon ? html`` : nothing} diff --git a/nx2/blocks/shared/utils/list-nav.js b/nx2/blocks/shared/utils/list-nav.js index bf151f426..223c46247 100644 --- a/nx2/blocks/shared/utils/list-nav.js +++ b/nx2/blocks/shared/utils/list-nav.js @@ -2,7 +2,7 @@ export function listKeydown(key, { items, active, itemKey, shadowRoot, setActive, onSelect, onClose, focusActiveItem = true, }) { - const selectable = items?.filter((i) => !i.divider && !i.section) ?? []; + const selectable = items?.filter((i) => !i.divider && !i.section && !i.disabled) ?? []; if (!selectable.length) return false; const curIdx = selectable.findIndex((i) => i[itemKey] === active); diff --git a/nx2/test/unit/nx/blocks/shared/menu/menu.test.js b/nx2/test/unit/nx/blocks/shared/menu/menu.test.js index 575b00c79..c025833eb 100644 --- a/nx2/test/unit/nx/blocks/shared/menu/menu.test.js +++ b/nx2/test/unit/nx/blocks/shared/menu/menu.test.js @@ -36,3 +36,44 @@ describe('nx-menu description', () => { expect(label.textContent).to.equal('Report a bug'); }); }); + +describe('nx-menu disabled', () => { + afterEach(() => { + document.querySelectorAll('nx-menu').forEach((el) => el.remove()); + }); + + it('renders a disabled item as a disabled button', async () => { + const el = await createMenu([{ id: 'lib', label: 'Open block library', disabled: true }]); + const btn = el.shadowRoot.querySelector('[data-id="lib"]'); + expect(btn.disabled).to.be.true; + expect(btn.getAttribute('aria-disabled')).to.equal('true'); + }); + + it('does not emit select when a disabled item is clicked', async () => { + const el = await createMenu([{ id: 'lib', label: 'Open block library', disabled: true }]); + let selected = false; + el.addEventListener('select', () => { selected = true; }); + el.shadowRoot.querySelector('[data-id="lib"]').click(); + expect(selected).to.be.false; + }); + + it('skips disabled items when picking the initial active item', async () => { + const el = await createMenu([ + { id: 'lib', label: 'Open block library', disabled: true }, + { id: 'insert', label: 'Insert block' }, + ]); + el._onMenuToggle({ newState: 'open' }); + await el.updateComplete; + expect(el._active).to.equal('insert'); + }); + + it('skips disabled items during keyboard navigation', async () => { + const el = await createMenu([ + { id: 'insert', label: 'Insert block' }, + { id: 'lib', label: 'Open block library', disabled: true }, + ]); + el._active = 'insert'; + el.handleKey('ArrowDown'); + expect(el._active).to.equal('insert'); + }); +});