diff --git a/projects/cli/src/mcp/ui/index.ts b/projects/cli/src/mcp/ui/index.ts index e232e7c5b0..6ad264aa79 100644 --- a/projects/cli/src/mcp/ui/index.ts +++ b/projects/cli/src/mcp/ui/index.ts @@ -20,7 +20,7 @@ export const examplesRenderResource: UIResource = { mimeType: MCP_UI_MIME_TYPE, resourceUri: 'ui://elements/example-preview', description: - 'Use this MCP app when an Elements example or template should be shown to the user. It renders validated nve-* HTML from examples_get or examples_render so users can inspect component layout, theme, and validation messages visually instead of reading markup only.', + 'Use this MCP app when an NVIDIA Elements UI example or template should be shown to the user. It renders validated nve-* HTML from examples_get or examples_render so users can inspect component layout, theme, and validation messages visually instead of reading markup only.', getHtml: () => examplesRenderHtml }; diff --git a/projects/core/.visual/tabs-group.layout-end.png b/projects/core/.visual/tabs-group.layout-end.png index 2fa1dff3b7..2b6ab09154 100644 --- a/projects/core/.visual/tabs-group.layout-end.png +++ b/projects/core/.visual/tabs-group.layout-end.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbac2cd3c2b422b4a213354b9e5a5df8d701d51d0bc7ab28f9aa7e2b3732272d -size 2252 +oid sha256:ba7e7e9fcab6751d1a05cdfcf7b668a9e99087543789076ad398c24c2ba2a205 +size 9436 diff --git a/projects/core/src/accordion/accordion.test.lighthouse.ts b/projects/core/src/accordion/accordion.test.lighthouse.ts index 367ace7306..a30823dd9e 100644 --- a/projects/core/src/accordion/accordion.test.lighthouse.ts +++ b/projects/core/src/accordion/accordion.test.lighthouse.ts @@ -23,6 +23,6 @@ describe('accordion lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(22.7); + expect(report.payload.javascript.kb).toBeLessThan(22.9); }); }); diff --git a/projects/core/src/alert/alert.test.lighthouse.ts b/projects/core/src/alert/alert.test.lighthouse.ts index bb58e18576..2a7c958c11 100644 --- a/projects/core/src/alert/alert.test.lighthouse.ts +++ b/projects/core/src/alert/alert.test.lighthouse.ts @@ -16,7 +16,7 @@ describe('alert lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(23.4); + expect(report.payload.javascript.kb).toBeLessThan(23.6); }); test('alert-group should meet lighthouse benchmarks', async () => { @@ -33,6 +33,6 @@ describe('alert lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(23.4); + expect(report.payload.javascript.kb).toBeLessThan(23.6); }); }); diff --git a/projects/core/src/button/button.test.lighthouse.ts b/projects/core/src/button/button.test.lighthouse.ts index c769c21003..cff2609786 100644 --- a/projects/core/src/button/button.test.lighthouse.ts +++ b/projects/core/src/button/button.test.lighthouse.ts @@ -18,6 +18,6 @@ describe('button lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(14.3); + expect(report.payload.javascript.kb).toBeLessThan(14.4); }); }); diff --git a/projects/core/src/chat-message/chat-message.test.lighthouse.ts b/projects/core/src/chat-message/chat-message.test.lighthouse.ts index f0513ee8af..ab849b518c 100644 --- a/projects/core/src/chat-message/chat-message.test.lighthouse.ts +++ b/projects/core/src/chat-message/chat-message.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('button lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(14.3); + expect(report.payload.javascript.kb).toBeLessThan(14.4); }); }); diff --git a/projects/core/src/color/color.test.lighthouse.ts b/projects/core/src/color/color.test.lighthouse.ts index 1ee5889845..c3a85a4bc5 100644 --- a/projects/core/src/color/color.test.lighthouse.ts +++ b/projects/core/src/color/color.test.lighthouse.ts @@ -19,6 +19,6 @@ describe('color lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(26.1); + expect(report.payload.javascript.kb).toBeLessThan(26.3); }); }); diff --git a/projects/core/src/combobox/combobox.test.lighthouse.ts b/projects/core/src/combobox/combobox.test.lighthouse.ts index 352c60e7ba..b62e30e812 100644 --- a/projects/core/src/combobox/combobox.test.lighthouse.ts +++ b/projects/core/src/combobox/combobox.test.lighthouse.ts @@ -25,7 +25,7 @@ describe('combobox lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(36.3); + expect(report.payload.javascript.kb).toBeLessThan(36.4); }); test('combobox multi select with large dataset should meet lighthouse benchmarks', async () => { diff --git a/projects/core/src/copy-button/copy-button.test.lighthouse.ts b/projects/core/src/copy-button/copy-button.test.lighthouse.ts index 6f65059427..876fe4defc 100644 --- a/projects/core/src/copy-button/copy-button.test.lighthouse.ts +++ b/projects/core/src/copy-button/copy-button.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('copy-button lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(26.2); + expect(report.payload.javascript.kb).toBeLessThan(26.4); }); }); diff --git a/projects/core/src/datetime/datetime.test.lighthouse.ts b/projects/core/src/datetime/datetime.test.lighthouse.ts index ad7be02dd1..104ebad954 100644 --- a/projects/core/src/datetime/datetime.test.lighthouse.ts +++ b/projects/core/src/datetime/datetime.test.lighthouse.ts @@ -19,6 +19,6 @@ describe('datetime lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(25.7); + expect(report.payload.javascript.kb).toBeLessThan(25.8); }); }); diff --git a/projects/core/src/dialog/dialog.test.lighthouse.ts b/projects/core/src/dialog/dialog.test.lighthouse.ts index 6523aa70d7..3751eca8c2 100644 --- a/projects/core/src/dialog/dialog.test.lighthouse.ts +++ b/projects/core/src/dialog/dialog.test.lighthouse.ts @@ -25,6 +25,6 @@ describe('dialog lighthouse report', () => { expect(report.scores.performance).toBeGreaterThan(97); // bfcache expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(25.1); + expect(report.payload.javascript.kb).toBeLessThan(25.3); }); }); diff --git a/projects/core/src/dropdown-group/dropdown-group.test.lighthouse.ts b/projects/core/src/dropdown-group/dropdown-group.test.lighthouse.ts index 7f84be3a2e..49c72fcabd 100644 --- a/projects/core/src/dropdown-group/dropdown-group.test.lighthouse.ts +++ b/projects/core/src/dropdown-group/dropdown-group.test.lighthouse.ts @@ -29,6 +29,6 @@ describe('dropdown-group lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(25); + expect(report.payload.javascript.kb).toBeLessThan(25.2); }); }); diff --git a/projects/core/src/internal/button-form-control-usage.test.ts b/projects/core/src/forms/button-form-control-usage.test.ts similarity index 82% rename from projects/core/src/internal/button-form-control-usage.test.ts rename to projects/core/src/forms/button-form-control-usage.test.ts index 91615c9867..1a270449a2 100644 --- a/projects/core/src/internal/button-form-control-usage.test.ts +++ b/projects/core/src/forms/button-form-control-usage.test.ts @@ -99,7 +99,7 @@ describe('ButtonFormControlMixin core usage', () => { expect(button.tabIndex).toBe(-1); button.disabled = false; - button.readonly = true; + button.readOnly = true; await elementIsStable(button); expect(button.readOnly).toBe(true); expect(button.hasAttribute('readonly')).toBe(true); @@ -122,6 +122,38 @@ describe('ButtonFormControlMixin core usage', () => { expect(button._internals.ariaExpanded).toBe('true'); }); + it('should expose native invoker property and attribute pairings', async () => { + const button = await createButton(usage); + const commandTarget = getElement(fixture, '#target'); + const interestTarget = document.createElement('div'); + interestTarget.id = 'interest-target'; + const popoverTarget = document.createElement('div'); + popoverTarget.id = 'popover-target'; + popoverTarget.popover = 'auto'; + fixture.append(interestTarget, popoverTarget); + + button.setAttribute('interestfor', interestTarget.id); + button.setAttribute('popovertarget', popoverTarget.id); + button.setAttribute('popovertargetaction', 'show'); + await elementIsStable(button); + + expect(button.commandForElement).toBe(commandTarget); + expect(button.interestForElement).toBe(interestTarget); + expect(button.popoverTargetElement).toBe(popoverTarget); + expect(button.popoverTargetAction).toBe('show'); + + button.commandForElement = commandTarget; + button.interestForElement = interestTarget; + button.popoverTargetElement = popoverTarget; + button.popoverTargetAction = 'hide'; + await elementIsStable(button); + + expect(button.getAttribute('commandfor')).toBe(''); + expect(button.getAttribute('interestfor')).toBe(''); + expect(button.getAttribute('popovertarget')).toBe(''); + expect(button.getAttribute('popovertargetaction')).toBe('hide'); + }); + it('should dispatch commands and suppress interaction while unavailable', async () => { const button = await createButton(usage); const target = getElement(fixture, '#target'); diff --git a/projects/core/src/icon-button/icon-button.test.lighthouse.ts b/projects/core/src/icon-button/icon-button.test.lighthouse.ts index 5820737e74..43065e8332 100644 --- a/projects/core/src/icon-button/icon-button.test.lighthouse.ts +++ b/projects/core/src/icon-button/icon-button.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('icon-button lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(19.6); + expect(report.payload.javascript.kb).toBeLessThan(19.8); }); }); diff --git a/projects/core/src/index.test.lighthouse.ts b/projects/core/src/index.test.lighthouse.ts index ec47458469..057743e91a 100644 --- a/projects/core/src/index.test.lighthouse.ts +++ b/projects/core/src/index.test.lighthouse.ts @@ -15,7 +15,7 @@ describe('lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(133.1); + expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(133.3); // if sudden drop in size, check vite bundle config and bundle demo to ensure side effects are properly preserved expect(report.payload.javascript.requests['index.js'].kb).toBeGreaterThan(120); diff --git a/projects/core/src/menu/menu.test.lighthouse.ts b/projects/core/src/menu/menu.test.lighthouse.ts index 043fe1a285..e3677aa32a 100644 --- a/projects/core/src/menu/menu.test.lighthouse.ts +++ b/projects/core/src/menu/menu.test.lighthouse.ts @@ -21,6 +21,6 @@ describe('menu lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(16.1); + expect(report.payload.javascript.kb).toBeLessThan(16.3); }); }); diff --git a/projects/core/src/month/month.test.lighthouse.ts b/projects/core/src/month/month.test.lighthouse.ts index 66ef551841..19933e4b3f 100644 --- a/projects/core/src/month/month.test.lighthouse.ts +++ b/projects/core/src/month/month.test.lighthouse.ts @@ -19,6 +19,6 @@ describe('month lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(25.7); + expect(report.payload.javascript.kb).toBeLessThan(25.8); }); }); diff --git a/projects/core/src/notification/notification.test.lighthouse.ts b/projects/core/src/notification/notification.test.lighthouse.ts index 905c4737f0..35f976b129 100644 --- a/projects/core/src/notification/notification.test.lighthouse.ts +++ b/projects/core/src/notification/notification.test.lighthouse.ts @@ -17,6 +17,6 @@ describe('notification lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(25.5); + expect(report.payload.javascript.kb).toBeLessThan(25.6); }); }); diff --git a/projects/core/src/pagination/pagination.test.lighthouse.ts b/projects/core/src/pagination/pagination.test.lighthouse.ts index 862ace6fec..11bb6c7cc4 100644 --- a/projects/core/src/pagination/pagination.test.lighthouse.ts +++ b/projects/core/src/pagination/pagination.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('pagination lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(38.6); + expect(report.payload.javascript.kb).toBeLessThan(38.7); }); }); diff --git a/projects/core/src/panel/panel.test.lighthouse.ts b/projects/core/src/panel/panel.test.lighthouse.ts index c0935d439c..de9d474f7c 100644 --- a/projects/core/src/panel/panel.test.lighthouse.ts +++ b/projects/core/src/panel/panel.test.lighthouse.ts @@ -27,6 +27,6 @@ describe('panel lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(21.6); + expect(report.payload.javascript.kb).toBeLessThan(21.8); }); }); diff --git a/projects/core/src/password/password.test.lighthouse.ts b/projects/core/src/password/password.test.lighthouse.ts index 6098e58455..bb14dc98aa 100644 --- a/projects/core/src/password/password.test.lighthouse.ts +++ b/projects/core/src/password/password.test.lighthouse.ts @@ -19,6 +19,6 @@ describe('password lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(25.8); + expect(report.payload.javascript.kb).toBeLessThan(25.9); }); }); diff --git a/projects/core/src/preferences-input/preferences-input.test.lighthouse.ts b/projects/core/src/preferences-input/preferences-input.test.lighthouse.ts index 7614f10875..8ded85b9d9 100644 --- a/projects/core/src/preferences-input/preferences-input.test.lighthouse.ts +++ b/projects/core/src/preferences-input/preferences-input.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('preferences-input lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(32.7); + expect(report.payload.javascript.kb).toBeLessThan(32.9); }); }); diff --git a/projects/core/src/search/search.test.lighthouse.ts b/projects/core/src/search/search.test.lighthouse.ts index b2f38ffdf7..4a570540a7 100644 --- a/projects/core/src/search/search.test.lighthouse.ts +++ b/projects/core/src/search/search.test.lighthouse.ts @@ -19,6 +19,6 @@ describe('search lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(25.5); + expect(report.payload.javascript.kb).toBeLessThan(25.7); }); }); diff --git a/projects/core/src/select/select.test.lighthouse.ts b/projects/core/src/select/select.test.lighthouse.ts index 1ea820c3ba..3aa33831cf 100644 --- a/projects/core/src/select/select.test.lighthouse.ts +++ b/projects/core/src/select/select.test.lighthouse.ts @@ -23,6 +23,6 @@ describe('select lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(34.8); + expect(report.payload.javascript.kb).toBeLessThan(34.9); }); }); diff --git a/projects/core/src/sort-button/sort-button.test.lighthouse.ts b/projects/core/src/sort-button/sort-button.test.lighthouse.ts index 1d25f70cac..182429e7d5 100644 --- a/projects/core/src/sort-button/sort-button.test.lighthouse.ts +++ b/projects/core/src/sort-button/sort-button.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('sort-button lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(18.9); + expect(report.payload.javascript.kb).toBeLessThan(19.1); }); }); diff --git a/projects/core/src/steps/steps.test.lighthouse.ts b/projects/core/src/steps/steps.test.lighthouse.ts index 9416f5ebdf..ff5c562a87 100644 --- a/projects/core/src/steps/steps.test.lighthouse.ts +++ b/projects/core/src/steps/steps.test.lighthouse.ts @@ -20,6 +20,6 @@ describe('steps lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(23.9); + expect(report.payload.javascript.kb).toBeLessThan(24); }); }); diff --git a/projects/core/src/tabs/tabs-group.css b/projects/core/src/tabs/tabs-group.css index 1455169a4d..de18d07e53 100644 --- a/projects/core/src/tabs/tabs-group.css +++ b/projects/core/src/tabs/tabs-group.css @@ -4,6 +4,7 @@ :host { --padding: var(--nve-ref-space-sm) 0 0 0; display: block; + width: 100%; } :host(:is([alignment='start'], [alignment='end'])) { diff --git a/projects/core/src/tag/tag.test.lighthouse.ts b/projects/core/src/tag/tag.test.lighthouse.ts index 54038378f9..adfd09e4bc 100644 --- a/projects/core/src/tag/tag.test.lighthouse.ts +++ b/projects/core/src/tag/tag.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('tag lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(18.7); + expect(report.payload.javascript.kb).toBeLessThan(18.9); }); }); diff --git a/projects/core/src/time/time.test.lighthouse.ts b/projects/core/src/time/time.test.lighthouse.ts index 0158a7e1ae..5391d1176e 100644 --- a/projects/core/src/time/time.test.lighthouse.ts +++ b/projects/core/src/time/time.test.lighthouse.ts @@ -19,6 +19,6 @@ describe('time lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(25.5); + expect(report.payload.javascript.kb).toBeLessThan(25.6); }); }); diff --git a/projects/core/src/toast/toast.test.lighthouse.ts b/projects/core/src/toast/toast.test.lighthouse.ts index 72f5dff6bf..3634332541 100644 --- a/projects/core/src/toast/toast.test.lighthouse.ts +++ b/projects/core/src/toast/toast.test.lighthouse.ts @@ -17,6 +17,6 @@ describe('toast lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(24.8); + expect(report.payload.javascript.kb).toBeLessThan(25); }); }); diff --git a/projects/core/src/tree/tree.test.lighthouse.ts b/projects/core/src/tree/tree.test.lighthouse.ts index eeee9cd050..3552d99465 100644 --- a/projects/core/src/tree/tree.test.lighthouse.ts +++ b/projects/core/src/tree/tree.test.lighthouse.ts @@ -27,6 +27,6 @@ describe('tree lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(29.9); + expect(report.payload.javascript.kb).toBeLessThan(30); }); }); diff --git a/projects/core/src/week/week.test.lighthouse.ts b/projects/core/src/week/week.test.lighthouse.ts index 82e5b89d50..9d338d38ac 100644 --- a/projects/core/src/week/week.test.lighthouse.ts +++ b/projects/core/src/week/week.test.lighthouse.ts @@ -19,6 +19,6 @@ describe('week lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(25.7); + expect(report.payload.javascript.kb).toBeLessThan(25.9); }); }); diff --git a/projects/forms/src/internal/controllers/type-interest-invoker.controller.test.ts b/projects/forms/src/internal/controllers/type-interest-invoker.controller.test.ts index bae4cae573..143cb276b4 100644 --- a/projects/forms/src/internal/controllers/type-interest-invoker.controller.test.ts +++ b/projects/forms/src/internal/controllers/type-interest-invoker.controller.test.ts @@ -159,6 +159,55 @@ describe('InterestInvokerController', () => { expect(element.interestForElement).toBe(target); }); + it('should re-resolve inferred targets when popovertarget changes', async () => { + fixture = await createFixture(html` + +
+
+ `); + const element = fixture.querySelector( + 'interest-invoker-controller-test-element' + )!; + const first = fixture.querySelector('#first')!; + const second = fixture.querySelector('#second')!; + const firstDispatch = vi.spyOn(first, 'dispatchEvent'); + const secondDispatch = vi.spyOn(second, 'dispatchEvent'); + + element.dispatchEvent(new MouseEvent('mouseenter')); + element.setAttribute('popovertarget', 'second'); + element.dispatchEvent(new MouseEvent('mouseenter')); + element.dispatchEvent(new MouseEvent('mouseleave')); + + expect(firstDispatch.mock.calls.map(([event]) => event.type)).toEqual(['interest']); + expect(secondDispatch.mock.calls.map(([event]) => event.type)).toEqual(['interest', 'loseinterest']); + expect(element.interestForElement).toBe(second); + }); + + it('should preserve explicitly assigned interest targets when popovertarget changes', async () => { + fixture = await createFixture(html` + +
+
+
+ `); + const element = fixture.querySelector( + 'interest-invoker-controller-test-element' + )!; + const first = fixture.querySelector('#first')!; + const explicit = fixture.querySelector('#explicit')!; + + element.dispatchEvent(new MouseEvent('mouseenter')); + expect(element.interestForElement).toBe(first); + + const explicitInterest = untilEvent(explicit, 'interest'); + element.interestForElement = explicit; + element.setAttribute('popovertarget', 'second'); + element.dispatchEvent(new MouseEvent('mouseenter')); + + expect((await explicitInterest).source).toBe(element); + expect(element.interestForElement).toBe(explicit); + }); + it('should no-op for missing targets and after disconnect', async () => { fixture = await createFixture( html`` diff --git a/projects/forms/src/internal/controllers/type-interest-invoker.controller.ts b/projects/forms/src/internal/controllers/type-interest-invoker.controller.ts index 2fddae086c..9dca4d12f6 100644 --- a/projects/forms/src/internal/controllers/type-interest-invoker.controller.ts +++ b/projects/forms/src/internal/controllers/type-interest-invoker.controller.ts @@ -12,6 +12,8 @@ type InterestInvokerHost = ReactiveElement & { }; export class TypeInterestInvokerController implements ReactiveController { + #inferredInterestForElement: HTMLElement | null = null; + constructor(private host: T) { this.host.addController(this); } @@ -39,36 +41,66 @@ export class TypeInterestInvokerController implem return; } - this.#updateInterestForElement(); - if (this.host.interestForElement) { - const interest = new Event('interest', { cancelable: true }) as InterestEvent; - interest.source = this.host; - this.host.interestForElement.dispatchEvent(interest); - } + this.#dispatchInterestEvent('interest'); }; - #onLoseInterest = () => { - this.#updateInterestForElement(); - if (this.host.interestForElement) { - const event = new Event('loseinterest', { cancelable: true }) as InterestEvent; - event.source = this.host; - this.host.interestForElement.dispatchEvent(event); + #onLoseInterest = () => this.#dispatchInterestEvent('loseinterest'); + + #dispatchInterestEvent(type: 'interest' | 'loseinterest') { + const target = this.#getInterestForElement(); + if (!target) { + return; } - }; - #updateInterestForElement() { + const event = new Event(type, { cancelable: true }) as InterestEvent; + event.source = this.host; + target.dispatchEvent(event); + } + + #getInterestForElement() { + const explicitTarget = this.#getExplicitInterestForElement(); + if (explicitTarget !== undefined) { + return explicitTarget; + } + + const inferredTarget = this.#getHintPopoverTarget(); + const previousTarget = this.#inferredInterestForElement; + this.#inferredInterestForElement = inferredTarget; + if (inferredTarget && this.host.interestForElement !== inferredTarget) { + this.host.interestForElement = inferredTarget; + } else if (!inferredTarget && previousTarget && this.host.interestForElement === previousTarget) { + this.host.interestForElement = null; + } + return inferredTarget; + } + + #getExplicitInterestForElement(): HTMLElement | null | undefined { const interestForIdRef = this.host.getAttribute('interestfor'); - if (interestForIdRef && !this.host.interestForElement) { - this.host.interestForElement = - getFlattenedDOMTree(this.host.getRootNode()).find(element => element.id === interestForIdRef) ?? null; + if (interestForIdRef) { + this.#inferredInterestForElement = null; + return this.#getElementById(interestForIdRef); } - const popoverTargetIdRef = this.host.getAttribute('popovertarget'); - if (popoverTargetIdRef && !interestForIdRef) { - const target = getFlattenedDOMTree(this.host.getRootNode()).find(element => element.id === popoverTargetIdRef); - if (target && target.popover === 'hint') { - this.host.interestForElement = target; - } + const interestForElement = this.host.interestForElement; + if (!interestForElement || interestForElement === this.#inferredInterestForElement) { + return undefined; } + + this.#inferredInterestForElement = null; + return interestForElement; + } + + #getHintPopoverTarget() { + const popoverTarget = this.host.popoverTargetElement; + if (popoverTarget instanceof HTMLElement && popoverTarget.popover === 'hint') { + return popoverTarget; + } + + const target = this.#getElementById(this.host.getAttribute('popovertarget')); + return target instanceof HTMLElement && target.popover === 'hint' ? target : null; + } + + #getElementById(id: string | null) { + return id ? (getFlattenedDOMTree(this.host.getRootNode()).find(element => element.id === id) ?? null) : null; } } diff --git a/projects/forms/src/internal/controllers/type-popover-trigger.controller.test.ts b/projects/forms/src/internal/controllers/type-popover-trigger.controller.test.ts index 657d306d14..fdfe9e6747 100644 --- a/projects/forms/src/internal/controllers/type-popover-trigger.controller.test.ts +++ b/projects/forms/src/internal/controllers/type-popover-trigger.controller.test.ts @@ -14,7 +14,6 @@ class PopoverTriggerControllerTestElement extends HTMLElement { interestForElement: HTMLElement | null = null; popoverTargetAction?: PopoverTargetAction; popoverTargetElement: HTMLElement | null = null; - popovertarget?: string; #controllers = new Set(); constructor() { @@ -58,7 +57,7 @@ describe('PopoverTriggerController', () => { const popover = fixture.querySelector('[popover]')!; const togglePopover = vi.spyOn(popover, 'togglePopover').mockImplementation(() => false); - element.popovertarget = 'popover'; + element.setAttribute('popovertarget', 'popover'); await emulateClick(element); expect(element.popoverTargetElement).toBe(popover); @@ -101,12 +100,12 @@ describe('PopoverTriggerController', () => { element.interestForElement = tooltip; element.popoverTargetAction = 'show'; - element.popovertarget = 'first'; + element.setAttribute('popovertarget', 'first'); await emulateClick(element); first.hidePopover(); loseInterest.mockClear(); - element.popovertarget = 'second'; + element.setAttribute('popovertarget', 'second'); await emulateClick(element); expect(element.popoverTargetElement).toBe(second); @@ -249,7 +248,7 @@ describe('PopoverTriggerController', () => { Object.defineProperty(popover, 'anchor', { configurable: true, value: 'anchor' }); element.interestForElement = tooltip; - element.popovertarget = 'popover'; + element.setAttribute('popovertarget', 'popover'); await emulateClick(element); expect(element.popoverTargetElement).toBe(popover); diff --git a/projects/forms/src/internal/controllers/type-popover-trigger.controller.ts b/projects/forms/src/internal/controllers/type-popover-trigger.controller.ts index 98fd630e15..40a300189c 100644 --- a/projects/forms/src/internal/controllers/type-popover-trigger.controller.ts +++ b/projects/forms/src/internal/controllers/type-popover-trigger.controller.ts @@ -13,7 +13,6 @@ type PopoverTriggerHost = ReactiveElement & { interestForElement: HTMLElement | null; popoverTargetAction?: PopoverTargetAction; popoverTargetElement: HTMLElement | null; - popovertarget?: string; }; export class TypePopoverTriggerController implements ReactiveController { @@ -35,7 +34,7 @@ export class TypePopoverTriggerController implemen const popoverTargetElement = this.host.popoverTargetElement; const controllerResolvedTarget = popoverTargetElement === this.#resolvedPopoverTarget; - if (!this.host.popovertarget) { + if (!this.host.getAttribute('popovertarget')) { this.#resolvedPopoverTarget = undefined; if (controllerResolvedTarget) { this.host.popoverTargetElement = null; @@ -50,7 +49,9 @@ export class TypePopoverTriggerController implemen } const resolvedPopoverTarget = - getFlattenedDOMTree(this.host.getRootNode()).find(element => element.id === this.host.popovertarget) ?? null; + getFlattenedDOMTree(this.host.getRootNode()).find( + element => element.id === this.host.getAttribute('popovertarget') + ) ?? null; if (resolvedPopoverTarget !== popoverTargetElement) { this.host.popoverTargetElement = resolvedPopoverTarget; } diff --git a/projects/forms/src/mixins/button.test.ts b/projects/forms/src/mixins/button.test.ts index 791f997922..3cb54aa7ee 100644 --- a/projects/forms/src/mixins/button.test.ts +++ b/projects/forms/src/mixins/button.test.ts @@ -552,24 +552,15 @@ describe('ButtonFormControlMixin', () => { it('should sync removable string attributes', async () => { submitButton.value = 'saved'; - submitButton.popovertarget = 'popover'; - submitButton.commandfor = 'command-target'; submitButton.command = '--test'; - submitButton.interestfor = 'interest-target'; await elementIsStable(submitButton); submitButton.removeAttribute('value'); - submitButton.removeAttribute('popovertarget'); - submitButton.removeAttribute('commandfor'); submitButton.removeAttribute('command'); - submitButton.removeAttribute('interestfor'); await elementIsStable(submitButton); expect(submitButton.value).toBeUndefined(); - expect(submitButton.popovertarget).toBeUndefined(); - expect(submitButton.commandfor).toBe(null); expect(submitButton.command).toBeUndefined(); - expect(submitButton.interestfor).toBe(null); }); }); @@ -751,6 +742,69 @@ describe('ButtonFormControlMixin', () => { expect((propertyCommand.mock.calls[0]?.[0] as CommandTestEvent).source).toBe(button); }); + it('should reflect native target properties to their corresponding attributes', async () => { + fixture = await createFixture(html` + +
+
+
+
+
+
+ `); + button = fixture.querySelector('button-form-control-mixin-test-element')!; + const attributeCommand = fixture.querySelector('#attribute-command')!; + const attributeInterest = fixture.querySelector('#attribute-interest')!; + const attributePopover = fixture.querySelector('#attribute-popover')!; + const propertyCommand = fixture.querySelector('#property-command')!; + const propertyInterest = fixture.querySelector('#property-interest')!; + const propertyPopover = fixture.querySelector('#property-popover')!; + await elementIsStable(button); + + expect(button.commandForElement).toBe(attributeCommand); + expect(button.interestForElement).toBe(attributeInterest); + expect(button.popoverTargetElement).toBe(attributePopover); + expect(button.popoverTargetAction).toBe('show'); + + button.commandForElement = propertyCommand; + button.interestForElement = propertyInterest; + button.popoverTargetElement = propertyPopover; + button.popoverTargetAction = 'hide'; + await elementIsStable(button); + + expect(button.getAttribute('commandfor')).toBe(''); + expect(button.getAttribute('interestfor')).toBe(''); + expect(button.getAttribute('popovertarget')).toBe(''); + expect(button.getAttribute('popovertargetaction')).toBe('hide'); + expect(button.commandForElement).toBe(propertyCommand); + expect(button.interestForElement).toBe(propertyInterest); + expect(button.popoverTargetElement).toBe(propertyPopover); + + button.setAttribute('commandfor', 'attribute-command'); + button.setAttribute('interestfor', 'attribute-interest'); + button.setAttribute('popovertarget', 'attribute-popover'); + button.setAttribute('popovertargetaction', 'invalid'); + await elementIsStable(button); + + expect(button.commandForElement).toBe(attributeCommand); + expect(button.interestForElement).toBe(attributeInterest); + expect(button.popoverTargetElement).toBe(attributePopover); + expect(button.popoverTargetAction).toBe('toggle'); + + button.commandForElement = null; + button.interestForElement = null; + button.popoverTargetElement = null; + + expect(button.hasAttribute('commandfor')).toBe(false); + expect(button.hasAttribute('interestfor')).toBe(false); + expect(button.hasAttribute('popovertarget')).toBe(false); + }); + it('should no-op command dispatch when clicks are default-prevented, disabled, readonly, or commandless', async () => { fixture = await createFixture(html` @@ -860,7 +914,7 @@ describe('ButtonFormControlMixin', () => { emulateClick(button); first.hidePopover(); - button.popovertarget = 'second'; + button.setAttribute('popovertarget', 'second'); await elementIsStable(button); emulateClick(button); diff --git a/projects/forms/src/mixins/button.ts b/projects/forms/src/mixins/button.ts index 5c92d37561..4ef52e9f7c 100644 --- a/projects/forms/src/mixins/button.ts +++ b/projects/forms/src/mixins/button.ts @@ -17,6 +17,7 @@ import { StateSelectedController } from '../internal/controllers/state-selected. import type { ReactiveController } from '../internal/controllers/types.js'; import { attachInternals, + getFlattenedDOMTree, getStringValue, hasAttributeValue, isFormElement, @@ -51,6 +52,14 @@ interface StringStateOptions { reflect?: boolean; } +const ELEMENT_TARGET_PROPERTIES = { + commandfor: 'commandForElement', + interestfor: 'interestForElement', + popovertarget: 'popoverTargetElement' +} as const; +type ElementTargetAttribute = keyof typeof ELEMENT_TARGET_PROPERTIES; +type ElementTargetProperty = (typeof ELEMENT_TARGET_PROPERTIES)[ElementTargetAttribute]; + type ButtonFormControlConstructor = new ( ...args: ConstructorParameters ) => InstanceType & ButtonFormControlMixinInstance; @@ -70,20 +79,19 @@ export function ButtonFormControlMixin( static formAssociated = true; #command: string | undefined; - #commandForElement: HTMLElement | null = null; - #commandfor: string | null = null; #current: CurrentState | undefined; #disabled = false; + #elementTargets: Record = { + commandForElement: null, + interestForElement: null, + popoverTargetElement: null + }; #expanded: boolean | undefined; #form: string | HTMLFormElement | null = null; #hasConnected = false; - #interestForElement: HTMLElement | null = null; - #interestfor: string | null = null; #name: string | undefined; #pendingAttributeReflections = new Map(); #popoverTargetAction: PopoverTargetAction | undefined; - #popoverTargetElement: HTMLElement | null = null; - #popovertarget: string | undefined; #pressed: boolean | undefined; #readOnly = false; #selected: boolean | undefined; @@ -220,26 +228,17 @@ export function ButtonFormControlMixin( } get popoverTargetElement(): HTMLElement | null { - return this.#popoverTargetElement; + return this.#getElementTarget('popovertarget'); } set popoverTargetElement(value: HTMLElement | null) { - const oldValue = this.#popoverTargetElement; - this.#popoverTargetElement = value; - this.#requestHostUpdate('popoverTargetElement', oldValue); - this.#queueBehaviorSync(); - } - - get popovertarget(): string { - return this.#popovertarget as string; - } - - set popovertarget(value: string | unknown) { - this.#setStringState({ property: 'popovertarget', value }); + this.#setElementTarget('popovertarget', value); } get popoverTargetAction(): PopoverTargetAction { - return this.#popoverTargetAction as PopoverTargetAction; + return this.#popoverTargetAction === 'show' || this.#popoverTargetAction === 'hide' + ? this.#popoverTargetAction + : 'toggle'; } set popoverTargetAction(value: PopoverTargetAction | unknown) { @@ -247,22 +246,11 @@ export function ButtonFormControlMixin( } get commandForElement(): HTMLElement | null { - return this.#commandForElement; + return this.#getElementTarget('commandfor'); } set commandForElement(value: HTMLElement | null) { - const oldValue = this.#commandForElement; - this.#commandForElement = value; - this.#requestHostUpdate('commandForElement', oldValue); - this.#queueBehaviorSync(); - } - - get commandfor(): string | null { - return this.#commandfor; - } - - set commandfor(value: string | null | unknown) { - this.#setStringState({ property: 'commandfor', value }); + this.#setElementTarget('commandfor', value); } get command(): string { @@ -274,22 +262,11 @@ export function ButtonFormControlMixin( } get interestForElement(): HTMLElement | null { - return this.#interestForElement; + return this.#getElementTarget('interestfor'); } set interestForElement(value: HTMLElement | null) { - const oldValue = this.#interestForElement; - this.#interestForElement = value; - this.#requestHostUpdate('interestForElement', oldValue); - this.#queueBehaviorSync(); - } - - get interestfor(): string | null { - return this.#interestfor; - } - - set interestfor(value: string | null | unknown) { - this.#setStringState({ property: 'interestfor', value }); + this.#setElementTarget('interestfor', value); } protected _controllers?: Set; @@ -345,6 +322,7 @@ export function ButtonFormControlMixin( } this.#handleBooleanAttributeChange(name, newValue); + this.#handleElementTargetAttributeChange(name, oldValue); this.#handleStringAttributeChange(name, oldValue, newValue); } @@ -376,6 +354,10 @@ export function ButtonFormControlMixin( } #handleStringAttributeChange(name: string, oldValue: string | null, newValue: string | null) { + if (name === 'commandfor' || name === 'interestfor' || name === 'popovertarget') { + return; + } + if (name === 'form') { if (!(isFormElement(this.#form) && newValue === null)) { this.#form = newValue; @@ -385,11 +367,46 @@ export function ButtonFormControlMixin( if (name === 'popovertargetaction') { this.#setStringState({ property: 'popoverTargetAction', value: newValue, reflect: false }); + return; } this.#setStringState({ property: name, value: newValue, reflect: false }); } + #handleElementTargetAttributeChange(name: string, oldValue: string | null) { + const property = ELEMENT_TARGET_PROPERTIES[name as ElementTargetAttribute]; + if (!property) { + return; + } + + const oldTarget = this.#elementTargets[property] ?? this.#getElementById(oldValue); + this.#elementTargets[property] = null; + this.#requestHostUpdate(property, oldTarget); + this.#queueBehaviorSync(); + } + + #getElementTarget(attribute: ElementTargetAttribute) { + return ( + this.#elementTargets[ELEMENT_TARGET_PROPERTIES[attribute]] ?? this.#getElementById(this.getAttribute(attribute)) + ); + } + + #getElementById(id: string | null) { + return id ? (getFlattenedDOMTree(this.getRootNode()).find(element => element.id === id) ?? null) : null; + } + + #setElementTarget(attribute: ElementTargetAttribute, value: HTMLElement | null) { + const property = ELEMENT_TARGET_PROPERTIES[attribute]; + const oldValue = this[property]; + if (oldValue === value && hasAttributeValue(this, attribute, value ? '' : null)) { + return; + } + setAttributeValue(this, attribute, value ? '' : null); + this.#elementTargets[property] = value; + this.#requestHostUpdate(property, oldValue); + this.#queueBehaviorSync(); + } + #setBooleanState({ property, value, reflect, attribute = property }: BooleanStateOptions) { const oldValue = this.#getBooleanValue(property); if (oldValue === value && (!reflect || hasAttributeValue(this, attribute, value ? '' : null))) { @@ -446,12 +463,9 @@ export function ButtonFormControlMixin( #getStringValue(property: string) { return { command: this.#command, - commandfor: this.#commandfor, current: this.#current, - interestfor: this.#interestfor, name: this.#name, popoverTargetAction: this.#popoverTargetAction, - popovertarget: this.#popovertarget, type: this.#type, value: this.#value }[property]; @@ -460,12 +474,9 @@ export function ButtonFormControlMixin( #setStringValue(property: string, value: string | null) { const setters: Record void> = { command: () => (this.#command = value ?? undefined), - commandfor: () => (this.#commandfor = value), current: () => (this.#current = value as CurrentState), - interestfor: () => (this.#interestfor = value), name: () => (this.#name = value ?? undefined), popoverTargetAction: () => (this.#popoverTargetAction = value as PopoverTargetAction), - popovertarget: () => (this.#popovertarget = value ?? undefined), type: () => (this.#type = value as ButtonType), value: () => (this.#value = value ?? undefined) }; diff --git a/projects/forms/src/mixins/button.types.ts b/projects/forms/src/mixins/button.types.ts index d560442d8f..ed1a19425b 100644 --- a/projects/forms/src/mixins/button.types.ts +++ b/projects/forms/src/mixins/button.types.ts @@ -89,16 +89,10 @@ export interface ButtonFormControlMixinInstance { /** * Establishes a relationship between a popover and its invoker button. * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/popoverTargetElement - */ - popoverTargetElement: HTMLElement | null; - - /** - * The id reference of the element that receives the popover. - * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#popovertarget * @attr popovertarget * @reflect */ - popovertarget: string; + popoverTargetElement: HTMLElement | null; /** * The popover target action to perform on the popover target element. diff --git a/projects/internals/patterns/src/dashboard.examples.ts b/projects/internals/patterns/src/dashboard.examples.ts index 3a9692f7e0..1c2e6d5e10 100644 --- a/projects/internals/patterns/src/dashboard.examples.ts +++ b/projects/internals/patterns/src/dashboard.examples.ts @@ -328,4 +328,154 @@ export const ProjectBoard = { ` -} \ No newline at end of file +}; + +/** + * @summary Multi-route dashboard with top-level navigation and URL-synchronized tab views. Use for resource management pages where nested routes preserve context and browser history while keeping overview, activity, and access tasks close together. + * @tags pattern + */ +export const AdminRoutes = { + render() { + return html` + + + OP +

Operations Admin

+ Dashboard + Teams + Billing + MK +
+
+
+

Dashboard / Services / Inference API

+

Inference API

+
+ + + Overview + Activity + Access + +
+
+ +

Requests today

+ +
+

2.4M

+

12% above the daily average

+
+
+
+ +

Success rate

+ +
+

99.98%

+

Within the 99.9% target

+
+
+
+ +

P95 latency

+ +
+

182 ms

+

18 ms below the alert threshold

+
+
+
+
+ + +
+

Service details

+

Current production configuration.

+
+
+ +
+
Region
+
us-central-1
+
Model
+
Llama 3.3 70B Instruct
+
Deployment
+
inference-api-v42
+
+
+
+
+
+ + + Event + Actor + Time + Status + + + Deployed inference-api-v42 + Maya Kim + 18 minutes ago + Completed + + + Updated autoscaling target + Jon Bell + 2 hours ago + Applied + + + Rotated service credentials + System + Yesterday + Applied + + +
+
+
+ + +
+

Platform administrators

+ Owner +
+
+ +

12 members can deploy, configure, and manage access.

+
+
+ + +
+

ML operations

+ Editor +
+
+ +

28 members can deploy and update service configuration.

+
+
+
+
+
+
+
+ ` + } +}; diff --git a/projects/internals/tools/src/api/utils.ts b/projects/internals/tools/src/api/utils.ts index 65891f9186..23697c8b50 100644 --- a/projects/internals/tools/src/api/utils.ts +++ b/projects/internals/tools/src/api/utils.ts @@ -114,25 +114,44 @@ function getAPIDescriptionMarkdown(api: PartialAPIResult) { return wrapText(description).trim(); } -function getAttributeExampleContext(attribute: Attribute) { - if (attribute.name === 'nve-layout') { - return /* html */ ` +const NVE_LAYOUT_EXAMPLE = /* html */ ` + + + +
+ +
+ +
columns 1-6
columns 7-12
+
+ + +
+
columns 1-4
+
columns 5-12
`; - } else if (attribute.name === 'nve-text') { - return /* html */ ` + +const NVE_TEXT_EXAMPLE = /* html */ ` + + +

heading

body

label

@@ -152,6 +171,12 @@ function getAttributeExampleContext(attribute: Attribute) {

start

center

end

`; + +function getAttributeExampleContext(attribute: Attribute) { + if (attribute.name === 'nve-layout') { + return NVE_LAYOUT_EXAMPLE; + } else if (attribute.name === 'nve-text') { + return NVE_TEXT_EXAMPLE; } return attribute.example ?? ''; } diff --git a/projects/lint/src/eslint/rules/no-missing-popover-trigger.test.ts b/projects/lint/src/eslint/rules/no-missing-popover-trigger.test.ts index 92001b640f..044e68e98a 100644 --- a/projects/lint/src/eslint/rules/no-missing-popover-trigger.test.ts +++ b/projects/lint/src/eslint/rules/no-missing-popover-trigger.test.ts @@ -123,7 +123,7 @@ describe('noMissingPopoverTrigger', () => { }); it('should report popover elements with template syntax content but no trigger', () => { - // Template syntax in content does not affect trigger requirement + // Content template syntax still requires a trigger tester.run('template syntax content requires trigger', rule, { valid: [], invalid: [ @@ -171,24 +171,30 @@ describe('noMissingPopoverTrigger', () => { // Template literal in popovertarget - popover should pass because trigger could match `Open Content`, - // Angular binding in popovertarget - `Open + // Angular property binding for popovertarget + `Open Content`, - // Lit binding in popovertarget - `Open + // Lit property binding for popovertarget + `Open Content`, // Template literal in commandfor `Open Content`, - // Angular binding in commandfor - `Open + // Angular property binding for commandfor + `Open Content`, - // Lit binding in commandfor - `Open + // Lit property binding for commandfor + `Open + Content`, + // Lit property binding for interestfor + `Open Content`, // JSX expression in popovertarget - `Open + `Open Content`, + // JSX property binding for an HTMLElement ref + `Open + Content`, // Vue/Handlebars in popovertarget `Open Content` diff --git a/projects/lint/src/eslint/rules/no-missing-popover-trigger.ts b/projects/lint/src/eslint/rules/no-missing-popover-trigger.ts index 0d84b87dcc..19cad67cf3 100644 --- a/projects/lint/src/eslint/rules/no-missing-popover-trigger.ts +++ b/projects/lint/src/eslint/rules/no-missing-popover-trigger.ts @@ -21,8 +21,7 @@ const DATA_BINDING_PATTERNS = [ * Check if an attribute value contains data binding syntax. */ function hasDataBinding(value: string | undefined): boolean { - if (!value) return false; - return DATA_BINDING_PATTERNS.some(pattern => pattern.test(value)); + return !!value && DATA_BINDING_PATTERNS.some(pattern => pattern.test(value)); } /** @@ -31,7 +30,8 @@ function hasDataBinding(value: string | undefined): boolean { */ function findAttrWithBinding( node: HtmlNode, - attrName: string + attrName: string, + propertyName = attrName ): { attr: ReturnType; hasBinding: boolean } { // Check for standard attribute const standard = findAttr(node, attrName); @@ -39,22 +39,16 @@ function findAttrWithBinding( return { attr: standard, hasBinding: hasDataBinding(standard.value?.value) }; } - // Check for Angular property binding: [attrName] - const angularAttr = findAttr(node, `[${attrName}]`); - if (angularAttr) { - return { attr: angularAttr, hasBinding: true }; + const jsxProperty = findAttr(node, propertyName); + if (jsxProperty && hasDataBinding(jsxProperty.value?.value)) { + return { attr: jsxProperty, hasBinding: true }; } - // Check for Lit property binding: .attrName - const litAttr = findAttr(node, `.${attrName}`); - if (litAttr) { - return { attr: litAttr, hasBinding: true }; - } - - // Check for Lit boolean-attribute binding: ?attrName - const litBoolAttr = findAttr(node, `?${attrName}`); - if (litBoolAttr) { - return { attr: litBoolAttr, hasBinding: true }; + for (const name of [`[${propertyName}]`, `.${propertyName}`, `?${attrName}`]) { + const attr = findAttr(node, name); + if (attr) { + return { attr, hasBinding: true }; + } } return { attr: undefined, hasBinding: false }; @@ -77,7 +71,11 @@ const POPOVER_ELEMENTS = [ /** * Attributes that reference a popover target. */ -const TRIGGER_ATTRIBUTES = ['popovertarget', 'commandfor', 'interestfor'] as const; +const TRIGGER_APIS = [ + ['popovertarget', 'popoverTargetElement'], + ['commandfor', 'commandForElement'], + ['interestfor', 'interestForElement'] +] as const; interface PopoverNode { node: HtmlNode; @@ -114,8 +112,8 @@ const rule = { let hasDynamicTrigger = false; function collectTriggerInfo(node: HtmlNode) { - for (const attr of TRIGGER_ATTRIBUTES) { - const { attr: found, hasBinding } = findAttrWithBinding(node, attr); + for (const [attribute, property] of TRIGGER_APIS) { + const { attr: found, hasBinding } = findAttrWithBinding(node, attribute, property); if (!found) continue; if (hasBinding) { hasDynamicTrigger = true; diff --git a/projects/site/src/docs/skills/index.md b/projects/site/src/docs/skills/index.md index 6012e1c79e..8235858310 100644 --- a/projects/site/src/docs/skills/index.md +++ b/projects/site/src/docs/skills/index.md @@ -1,7 +1,7 @@ --- { title: 'Skills', - description: 'Use NVIDIA Elements skills to give AI agents persistent project context, workflow guidance, and access to Elements UI authoring practices.', + description: 'NVIDIA skills to give AI agents persistent project context, workflow guidance, and access to NVIDIA Elements UI authoring practices.', layout: 'docs.11ty.js' } --- diff --git a/projects/styles/build/metadata.js b/projects/styles/build/metadata.js index b103beb047..8b2da61d91 100644 --- a/projects/styles/build/metadata.js +++ b/projects/styles/build/metadata.js @@ -43,9 +43,11 @@ async function generateGlobalAttributes(files, attributes) { for (const [attribute, values] of Object.entries(utilityAttributes)) { let description = ''; if (attribute === 'nve-layout') { - description = 'Layout utility for native HTML elements'; + description = + 'Layout utility for native HTML elements. Apply only to native HTML elements, not nve-* custom elements.'; } else if (attribute === 'nve-text') { - description = 'Typography style utility for native HTML elements'; + description = + 'Typography style utility for native HTML elements. Apply only to native HTML elements, not nve-* custom elements.'; } else if (attribute === 'nve-display') { description = 'Display utility for native HTML elements'; }