From eb616815cf03c91e58999866ad1d2d99ef118d85 Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Mon, 27 Jul 2026 12:14:59 -0500 Subject: [PATCH 1/2] chore(docs): feed title Signed-off-by: Cory Rylan --- projects/site/src/_11ty/layouts/common.js | 2 +- projects/site/src/_11ty/layouts/metadata.test.ts | 4 ++-- projects/site/src/_11ty/plugins/updates-feed.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/site/src/_11ty/layouts/common.js b/projects/site/src/_11ty/layouts/common.js index 4982860cf..6d6d0e341 100644 --- a/projects/site/src/_11ty/layouts/common.js +++ b/projects/site/src/_11ty/layouts/common.js @@ -32,7 +32,7 @@ export const renderBaseHead = data => { - ${UPDATE_FEEDS.map(({ label, outputPath, type }) => ``).join('\n ')} + ${UPDATE_FEEDS.map(({ label, outputPath, type }) => ``).join('\n ')} diff --git a/projects/site/src/_11ty/layouts/metadata.test.ts b/projects/site/src/_11ty/layouts/metadata.test.ts index 5533cdc02..5dec43d4f 100644 --- a/projects/site/src/_11ty/layouts/metadata.test.ts +++ b/projects/site/src/_11ty/layouts/metadata.test.ts @@ -397,10 +397,10 @@ describe('renderBaseHead', () => { }); expect(html).toContain( - '' + '' ); expect(html).toContain( - '' + '' ); }); }); diff --git a/projects/site/src/_11ty/plugins/updates-feed.js b/projects/site/src/_11ty/plugins/updates-feed.js index 0b78daa99..ff17cc41d 100644 --- a/projects/site/src/_11ty/plugins/updates-feed.js +++ b/projects/site/src/_11ty/plugins/updates-feed.js @@ -33,7 +33,7 @@ export const UPDATE_FEED_METADATA = { language: 'en', subtitle: 'What’s new in the NVIDIA Elements Design System, including product updates, release highlights, and announcements.', - title: 'NVIDIA Elements updates' + title: 'NVIDIA Elements Updates' }; export const RSS_FEED_TEMPLATE = ` From afa8d693a72f3eadc65214f7f37ed54cf922cb58 Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Fri, 24 Jul 2026 10:20:09 -0500 Subject: [PATCH 2/2] feat(lint): add rule to limit excessive primary actions - Introduced a new ESLint rule `@nvidia-elements/lint/no-excessive-primary-actions` to restrict the use of emphasis buttons to a maximum of two per page. - Updated ESLint configuration to include the new rule. - Added tests for the new rule to ensure correct functionality. - Updated documentation to reflect the new rule and its purpose. Signed-off-by: Cory Rylan --- projects/core/eslint.config.js | 6 + projects/lint/README.md | 1 + projects/lint/src/eslint/configs/html.ts | 3 + .../lint/src/eslint/internals/index.test.ts | 11 ++ .../no-excessive-primary-actions.test.ts | 147 ++++++++++++++++++ .../rules/no-excessive-primary-actions.ts | 59 +++++++ projects/site/src/docs/lint/index.md | 6 + 7 files changed, 233 insertions(+) create mode 100644 projects/lint/src/eslint/rules/no-excessive-primary-actions.test.ts create mode 100644 projects/lint/src/eslint/rules/no-excessive-primary-actions.ts diff --git a/projects/core/eslint.config.js b/projects/core/eslint.config.js index 3383bd1b7..529eda852 100644 --- a/projects/core/eslint.config.js +++ b/projects/core/eslint.config.js @@ -35,6 +35,12 @@ export default [ '@nvidia-elements/lint/no-missing-gap-space': ['off'] } }, + { + files: ['src/button/button.test.visual.ts'], + rules: { + '@nvidia-elements/lint/no-excessive-primary-actions': ['off'] + } + }, { files: [ 'src/format-datetime/format-datetime.ts', diff --git a/projects/lint/README.md b/projects/lint/README.md index f429da958..b7befb7bc 100644 --- a/projects/lint/README.md +++ b/projects/lint/README.md @@ -75,6 +75,7 @@ export default [ | `@nvidia-elements/lint/no-deprecated-popover-attributes` | Disallow use of deprecated popover attributes. | HTML | `error` | | `@nvidia-elements/lint/no-deprecated-slots` | Disallow use of deprecated slot APIs. | HTML | `error` | | `@nvidia-elements/lint/no-deprecated-tags` | Disallow use of deprecated elements in HTML. | HTML | `error` | +| `@nvidia-elements/lint/no-excessive-primary-actions` | Limit primary actions to two per page. | HTML | `error` | | `@nvidia-elements/lint/no-invalid-event-listeners` | Disallow inline event handler attributes in HTML. | HTML | `error` | | `@nvidia-elements/lint/no-invalid-invoker-triggers` | Disallow use of invoker trigger attributes on non-button nve-* elements. | HTML | `error` | | `@nvidia-elements/lint/no-missing-control-label` | Require form controls to have an accessible label. | HTML | `error` | diff --git a/projects/lint/src/eslint/configs/html.ts b/projects/lint/src/eslint/configs/html.ts index 663e5cb05..f47dab8c6 100644 --- a/projects/lint/src/eslint/configs/html.ts +++ b/projects/lint/src/eslint/configs/html.ts @@ -16,6 +16,7 @@ import noDeprecatedGlobalAttributes from '../rules/no-deprecated-global-attribut import noRestrictedAttributes from '../rules/no-restricted-attributes.js'; import noSlottedPopovers from '../rules/no-slotted-popovers.js'; import noDeprecatedSlots from '../rules/no-deprecated-slots.js'; +import noExcessivePrimaryActions from '../rules/no-excessive-primary-actions.js'; import noMissingSlottedElements from '../rules/no-missing-slotted-elements.js'; import noMissingControlLabel from '../rules/no-missing-control-label.js'; import noMissingIconName from '../rules/no-missing-icon-name.js'; @@ -71,6 +72,7 @@ export const elementsHtmlConfig: Linter.Config = { 'no-deprecated-global-attribute-value': noDeprecatedGlobalAttributeValue, 'no-deprecated-global-attributes': noDeprecatedGlobalAttributes, 'no-deprecated-slots': noDeprecatedSlots, + 'no-excessive-primary-actions': noExcessivePrimaryActions, 'no-missing-slotted-elements': noMissingSlottedElements, 'no-missing-control-label': noMissingControlLabel, 'no-missing-icon-name': noMissingIconName, @@ -106,6 +108,7 @@ export const elementsHtmlConfig: Linter.Config = { '@nvidia-elements/lint/no-deprecated-global-attribute-value': ['error'], '@nvidia-elements/lint/no-deprecated-global-attributes': ['error'], '@nvidia-elements/lint/no-deprecated-slots': ['error'], + '@nvidia-elements/lint/no-excessive-primary-actions': ['error'], '@nvidia-elements/lint/no-missing-slotted-elements': ['error'], '@nvidia-elements/lint/no-missing-control-label': ['error'], '@nvidia-elements/lint/no-missing-icon-name': ['error'], diff --git a/projects/lint/src/eslint/internals/index.test.ts b/projects/lint/src/eslint/internals/index.test.ts index e0e81facd..d2b50751e 100644 --- a/projects/lint/src/eslint/internals/index.test.ts +++ b/projects/lint/src/eslint/internals/index.test.ts @@ -19,6 +19,17 @@ describe('lintPlaygroundTemplate', () => { expect(result).toEqual([]); }); + it('should limit emphasis buttons per template', async () => { + const code = ` + One + Two + Three + `; + const result = await lintTemplate(code, { strict: true }); + + expect(result.filter(message => message.id === 'excessive-primary-action')).toHaveLength(1); + }); + it('should detect restricted attributes on custom elements', async () => { const codeWithRestrictedAttribute = 'Button'; const result = await lintTemplate(codeWithRestrictedAttribute, { strict: true }); diff --git a/projects/lint/src/eslint/rules/no-excessive-primary-actions.test.ts b/projects/lint/src/eslint/rules/no-excessive-primary-actions.test.ts new file mode 100644 index 000000000..a7415afa8 --- /dev/null +++ b/projects/lint/src/eslint/rules/no-excessive-primary-actions.test.ts @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it } from 'vitest'; +import { RuleTester } from 'eslint'; +import type { JSRuleDefinition } from 'eslint'; +import htmlParser from '@html-eslint/parser'; +import noExcessivePrimaryActions from './no-excessive-primary-actions.js'; + +const rule = noExcessivePrimaryActions as unknown as JSRuleDefinition; +const error = { + messageId: 'excessive-primary-action' as const, + data: { max: '2' } +}; + +describe('noExcessivePrimaryActions', () => { + let tester: RuleTester; + + beforeEach(() => { + tester = new RuleTester({ + languageOptions: { + parser: htmlParser, + parserOptions: { + frontmatter: true + } + } + }); + }); + + it('should define rule metadata', () => { + expect(noExcessivePrimaryActions.meta).toBeDefined(); + expect(noExcessivePrimaryActions.meta.type).toBe('problem'); + expect(noExcessivePrimaryActions.meta.docs).toBeDefined(); + expect(noExcessivePrimaryActions.meta.docs.description).toBe('Limit primary actions to two per page.'); + expect(noExcessivePrimaryActions.meta.docs.category).toBe('Best Practice'); + expect(noExcessivePrimaryActions.meta.docs.recommended).toBe(true); + expect(noExcessivePrimaryActions.meta.docs.url).toContain('/docs/lint/'); + expect(noExcessivePrimaryActions.meta.schema).toEqual([]); + expect(noExcessivePrimaryActions.meta.messages['excessive-primary-action']).toBe( + 'Limit primary actions to {{max}} per page. Reserve interaction="emphasis" for primary calls to action.' + ); + }); + + it('should allow no more than two emphasis buttons', () => { + tester.run('valid emphasis button count', rule, { + valid: [ + 'Default', + 'Primary action', + `Primary action + Secondary action`, + `Primary action + Delete + Cancel` + ], + invalid: [] + }); + }); + + it('should ignore emphasis on elements other than nve-button', () => { + tester.run('non-button elements', rule, { + valid: [ + ` + Custom action + ` + ], + invalid: [] + }); + }); + + it('should ignore dynamic interaction values', () => { + tester.run('dynamic interaction values', rule, { + valid: [ + `Lit attribute + Lit property + Angular property + Template binding + JSX expression` + ], + invalid: [] + }); + }); + + it('should count separate tagged templates independently', () => { + const javascriptTester = new RuleTester({ + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + } + }); + + javascriptTester.run('separate tagged templates', rule, { + valid: [ + `const first = html\` + One + Two + \`; + const second = html\` + Three + Four + \`;` + ], + invalid: [ + { + code: `const template = html\` + One + Two + Three + \`;`, + errors: [error] + } + ] + }); + }); + + it('should report the third emphasis button', () => { + tester.run('third emphasis button', rule, { + valid: [], + invalid: [ + { + code: `One + Two + Three`, + errors: [error] + } + ] + }); + }); + + it('should report every emphasis button after the second', () => { + tester.run('multiple excessive emphasis buttons', rule, { + valid: [], + invalid: [ + { + code: `
+ One +
+ Two + Three +
+ Four +
`, + errors: [error, error] + } + ] + }); + }); +}); diff --git a/projects/lint/src/eslint/rules/no-excessive-primary-actions.ts b/projects/lint/src/eslint/rules/no-excessive-primary-actions.ts new file mode 100644 index 000000000..1dd17be2c --- /dev/null +++ b/projects/lint/src/eslint/rules/no-excessive-primary-actions.ts @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { Rule } from 'eslint'; +import { createVisitors } from '@html-eslint/eslint-plugin/lib/rules/utils/visitors.js'; +import { findAttr } from '@html-eslint/eslint-plugin/lib/rules/utils/node.js'; +import type { HtmlTagNode } from '../rule-types.js'; + +declare const __ELEMENTS_PAGES_BASE_URL__: string; +const MAX_EMPHASIS_BUTTONS = 2; + +const rule = { + meta: { + type: 'problem' as const, + docs: { + description: 'Limit primary actions to two per page.', + category: 'Best Practice', + recommended: true, + url: `${__ELEMENTS_PAGES_BASE_URL__}/docs/lint/` + }, + schema: [], + messages: { + ['excessive-primary-action']: + 'Limit primary actions to {{max}} per page. Reserve interaction="emphasis" for primary calls to action.' + } + }, + create(context: Rule.RuleContext) { + let emphasisButtonCount = 0; + + return createVisitors(context, { + Document() { + emphasisButtonCount = 0; + }, + Tag(node: HtmlTagNode) { + if (node.name.toLowerCase() !== 'nve-button') { + return; + } + + const interaction = findAttr(node, 'interaction'); + if (interaction?.value?.value !== 'emphasis') { + return; + } + + emphasisButtonCount += 1; + if (emphasisButtonCount <= MAX_EMPHASIS_BUTTONS) { + return; + } + + context.report({ + node: interaction, + messageId: 'excessive-primary-action', + data: { max: String(MAX_EMPHASIS_BUTTONS) } + }); + } + }); + } +} as const; + +export default rule; diff --git a/projects/site/src/docs/lint/index.md b/projects/site/src/docs/lint/index.md index 235344883..e8aeb95b0 100644 --- a/projects/site/src/docs/lint/index.md +++ b/projects/site/src/docs/lint/index.md @@ -142,6 +142,12 @@ export default [ HTML error + + @nvidia-elements/lint/no-excessive-primary-actions + Limit primary actions to two per page. + HTML + error + @nvidia-elements/lint/no-invalid-event-listeners Disallow inline event handler attributes in HTML.