Skip to content

Commit e67fa69

Browse files
committed
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 <crylan@nvidia.com>
1 parent e7f3717 commit e67fa69

7 files changed

Lines changed: 233 additions & 0 deletions

File tree

‎projects/core/eslint.config.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export default [
3535
'@nvidia-elements/lint/no-missing-gap-space': ['off']
3636
}
3737
},
38+
{
39+
files: ['src/button/button.test.visual.ts'],
40+
rules: {
41+
'@nvidia-elements/lint/no-excessive-primary-actions': ['off']
42+
}
43+
},
3844
{
3945
files: [
4046
'src/format-datetime/format-datetime.ts',

‎projects/lint/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export default [
7575
|`@nvidia-elements/lint/no-deprecated-popover-attributes`| Disallow use of deprecated popover attributes. | HTML |`error`|
7676
|`@nvidia-elements/lint/no-deprecated-slots`| Disallow use of deprecated slot APIs. | HTML |`error`|
7777
|`@nvidia-elements/lint/no-deprecated-tags`| Disallow use of deprecated elements in HTML. | HTML |`error`|
78+
|`@nvidia-elements/lint/no-excessive-primary-actions`| Limit primary actions to two per page. | HTML |`error`|
7879
|`@nvidia-elements/lint/no-invalid-event-listeners`| Disallow inline event handler attributes in HTML. | HTML |`error`|
7980
|`@nvidia-elements/lint/no-invalid-invoker-triggers`| Disallow use of invoker trigger attributes on non-button nve-* elements. | HTML |`error`|
8081
|`@nvidia-elements/lint/no-missing-control-label`| Require form controls to have an accessible label. | HTML |`error`|

‎projects/lint/src/eslint/configs/html.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import noDeprecatedGlobalAttributes from '../rules/no-deprecated-global-attribut
1616
importnoRestrictedAttributesfrom'../rules/no-restricted-attributes.js';
1717
importnoSlottedPopoversfrom'../rules/no-slotted-popovers.js';
1818
importnoDeprecatedSlotsfrom'../rules/no-deprecated-slots.js';
19+
importnoExcessivePrimaryActionsfrom'../rules/no-excessive-primary-actions.js';
1920
importnoMissingSlottedElementsfrom'../rules/no-missing-slotted-elements.js';
2021
importnoMissingControlLabelfrom'../rules/no-missing-control-label.js';
2122
importnoMissingIconNamefrom'../rules/no-missing-icon-name.js';
@@ -71,6 +72,7 @@ export const elementsHtmlConfig: Linter.Config = {
7172
'no-deprecated-global-attribute-value': noDeprecatedGlobalAttributeValue,
7273
'no-deprecated-global-attributes': noDeprecatedGlobalAttributes,
7374
'no-deprecated-slots': noDeprecatedSlots,
75+
'no-excessive-primary-actions': noExcessivePrimaryActions,
7476
'no-missing-slotted-elements': noMissingSlottedElements,
7577
'no-missing-control-label': noMissingControlLabel,
7678
'no-missing-icon-name': noMissingIconName,
@@ -106,6 +108,7 @@ export const elementsHtmlConfig: Linter.Config = {
106108
'@nvidia-elements/lint/no-deprecated-global-attribute-value': ['error'],
107109
'@nvidia-elements/lint/no-deprecated-global-attributes': ['error'],
108110
'@nvidia-elements/lint/no-deprecated-slots': ['error'],
111+
'@nvidia-elements/lint/no-excessive-primary-actions': ['error'],
109112
'@nvidia-elements/lint/no-missing-slotted-elements': ['error'],
110113
'@nvidia-elements/lint/no-missing-control-label': ['error'],
111114
'@nvidia-elements/lint/no-missing-icon-name': ['error'],

‎projects/lint/src/eslint/internals/index.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ describe('lintPlaygroundTemplate', () => {
1919
expect(result).toEqual([]);
2020
});
2121

22+
it('should limit emphasis buttons per template',async()=>{
23+
constcode=`
24+
<nve-button interaction="emphasis">One</nve-button>
25+
<nve-button interaction="emphasis">Two</nve-button>
26+
<nve-button interaction="emphasis">Three</nve-button>
27+
`;
28+
constresult=awaitlintTemplate(code,{strict: true});
29+
30+
expect(result.filter(message=>message.id==='excessive-primary-action')).toHaveLength(1);
31+
});
32+
2233
it('should detect restricted attributes on custom elements',async()=>{
2334
constcodeWithRestrictedAttribute='<nve-button nve-layout="pad:md">Button</nve-button>';
2435
constresult=awaitlintTemplate(codeWithRestrictedAttribute,{strict: true});
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import{beforeEach,describe,expect,it}from'vitest';
5+
import{RuleTester}from'eslint';
6+
importtype{JSRuleDefinition}from'eslint';
7+
importhtmlParserfrom'@html-eslint/parser';
8+
importnoExcessivePrimaryActionsfrom'./no-excessive-primary-actions.js';
9+
10+
construle=noExcessivePrimaryActionsasunknownasJSRuleDefinition;
11+
consterror={
12+
messageId: 'excessive-primary-action'asconst,
13+
data: {max: '2'}
14+
};
15+
16+
describe('noExcessivePrimaryActions',()=>{
17+
lettester: RuleTester;
18+
19+
beforeEach(()=>{
20+
tester=newRuleTester({
21+
languageOptions: {
22+
parser: htmlParser,
23+
parserOptions: {
24+
frontmatter: true
25+
}
26+
}
27+
});
28+
});
29+
30+
it('should define rule metadata',()=>{
31+
expect(noExcessivePrimaryActions.meta).toBeDefined();
32+
expect(noExcessivePrimaryActions.meta.type).toBe('problem');
33+
expect(noExcessivePrimaryActions.meta.docs).toBeDefined();
34+
expect(noExcessivePrimaryActions.meta.docs.description).toBe('Limit primary actions to two per page.');
35+
expect(noExcessivePrimaryActions.meta.docs.category).toBe('Best Practice');
36+
expect(noExcessivePrimaryActions.meta.docs.recommended).toBe(true);
37+
expect(noExcessivePrimaryActions.meta.docs.url).toContain('/docs/lint/');
38+
expect(noExcessivePrimaryActions.meta.schema).toEqual([]);
39+
expect(noExcessivePrimaryActions.meta.messages['excessive-primary-action']).toBe(
40+
'Limit primary actions to {{max}} per page. Reserve interaction="emphasis" for primary calls to action.'
41+
);
42+
});
43+
44+
it('should allow no more than two emphasis buttons',()=>{
45+
tester.run('valid emphasis button count',rule,{
46+
valid: [
47+
'<nve-button>Default</nve-button>',
48+
'<nve-button interaction="emphasis">Primary action</nve-button>',
49+
`<nve-button interaction="emphasis">Primary action</nve-button>
50+
<nve-button interaction="emphasis">Secondary action</nve-button>`,
51+
`<nve-button interaction="emphasis">Primary action</nve-button>
52+
<nve-button interaction="destructive">Delete</nve-button>
53+
<nve-button interaction="neutral">Cancel</nve-button>`
54+
],
55+
invalid: []
56+
});
57+
});
58+
59+
it('should ignore emphasis on elements other than nve-button',()=>{
60+
tester.run('non-button elements',rule,{
61+
valid: [
62+
`<nve-icon-button interaction="emphasis" icon-name="menu"></nve-icon-button>
63+
<custom-button interaction="emphasis">Custom action</custom-button>
64+
<button interaction="emphasis">Native action</button>`
65+
],
66+
invalid: []
67+
});
68+
});
69+
70+
it('should ignore dynamic interaction values',()=>{
71+
tester.run('dynamic interaction values',rule,{
72+
valid: [
73+
`<nve-button interaction="\${interaction}">Lit attribute</nve-button>
74+
<nve-button .interaction="\${interaction}">Lit property</nve-button>
75+
<nve-button [interaction]="interaction">Angular property</nve-button>
76+
<nve-button interaction="{{ interaction }}">Template binding</nve-button>
77+
<nve-button interaction="{interaction}">JSX expression</nve-button>`
78+
],
79+
invalid: []
80+
});
81+
});
82+
83+
it('should count separate tagged templates independently',()=>{
84+
constjavascriptTester=newRuleTester({
85+
languageOptions: {
86+
ecmaVersion: 'latest',
87+
sourceType: 'module'
88+
}
89+
});
90+
91+
javascriptTester.run('separate tagged templates',rule,{
92+
valid: [
93+
`const first = html\`
94+
<nve-button interaction="emphasis">One</nve-button>
95+
<nve-button interaction="emphasis">Two</nve-button>
96+
\`;
97+
const second = html\`
98+
<nve-button interaction="emphasis">Three</nve-button>
99+
<nve-button interaction="emphasis">Four</nve-button>
100+
\`;`
101+
],
102+
invalid: [
103+
{
104+
code: `const template = html\`
105+
<nve-button interaction="emphasis">One</nve-button>
106+
<nve-button interaction="emphasis">Two</nve-button>
107+
<nve-button interaction="emphasis">Three</nve-button>
108+
\`;`,
109+
errors: [error]
110+
}
111+
]
112+
});
113+
});
114+
115+
it('should report the third emphasis button',()=>{
116+
tester.run('third emphasis button',rule,{
117+
valid: [],
118+
invalid: [
119+
{
120+
code: `<nve-button interaction="emphasis">One</nve-button>
121+
<nve-button interaction="emphasis">Two</nve-button>
122+
<nve-button interaction="emphasis">Three</nve-button>`,
123+
errors: [error]
124+
}
125+
]
126+
});
127+
});
128+
129+
it('should report every emphasis button after the second',()=>{
130+
tester.run('multiple excessive emphasis buttons',rule,{
131+
valid: [],
132+
invalid: [
133+
{
134+
code: `<main>
135+
<nve-button interaction="emphasis">One</nve-button>
136+
<section>
137+
<nve-button interaction="emphasis">Two</nve-button>
138+
<nve-button interaction="emphasis">Three</nve-button>
139+
</section>
140+
<nve-button interaction="emphasis">Four</nve-button>
141+
</main>`,
142+
errors: [error,error]
143+
}
144+
]
145+
});
146+
});
147+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
importtype{Rule}from'eslint';
5+
import{createVisitors}from'@html-eslint/eslint-plugin/lib/rules/utils/visitors.js';
6+
import{findAttr}from'@html-eslint/eslint-plugin/lib/rules/utils/node.js';
7+
importtype{HtmlTagNode}from'../rule-types.js';
8+
9+
declareconst__ELEMENTS_PAGES_BASE_URL__: string;
10+
constMAX_EMPHASIS_BUTTONS=2;
11+
12+
construle={
13+
meta: {
14+
type: 'problem'asconst,
15+
docs: {
16+
description: 'Limit primary actions to two per page.',
17+
category: 'Best Practice',
18+
recommended: true,
19+
url: `${__ELEMENTS_PAGES_BASE_URL__}/docs/lint/`
20+
},
21+
schema: [],
22+
messages: {
23+
['excessive-primary-action']:
24+
'Limit primary actions to {{max}} per page. Reserve interaction="emphasis" for primary calls to action.'
25+
}
26+
},
27+
create(context: Rule.RuleContext){
28+
letemphasisButtonCount=0;
29+
30+
returncreateVisitors(context,{
31+
Document(){
32+
emphasisButtonCount=0;
33+
},
34+
Tag(node: HtmlTagNode){
35+
if(node.name.toLowerCase()!=='nve-button'){
36+
return;
37+
}
38+
39+
constinteraction=findAttr(node,'interaction');
40+
if(interaction?.value?.value!=='emphasis'){
41+
return;
42+
}
43+
44+
emphasisButtonCount+=1;
45+
if(emphasisButtonCount<=MAX_EMPHASIS_BUTTONS){
46+
return;
47+
}
48+
49+
context.report({
50+
node: interaction,
51+
messageId: 'excessive-primary-action',
52+
data: {max: String(MAX_EMPHASIS_BUTTONS)}
53+
});
54+
}
55+
});
56+
}
57+
}asconst;
58+
59+
exportdefaultrule;

‎projects/site/src/docs/lint/index.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ export default [
142142
<nve-grid-cell>HTML</nve-grid-cell>
143143
<nve-grid-cell><code nve-text="code">error</code></nve-grid-cell>
144144
</nve-grid-row>
145+
<nve-grid-row>
146+
<nve-grid-cell><code nve-text="code">@nvidia-elements/lint/no-excessive-primary-actions</code></nve-grid-cell>
147+
<nve-grid-cell>Limit primary actions to two per page.</nve-grid-cell>
148+
<nve-grid-cell>HTML</nve-grid-cell>
149+
<nve-grid-cell><code nve-text="code">error</code></nve-grid-cell>
150+
</nve-grid-row>
145151
<nve-grid-row>
146152
<nve-grid-cell><code nve-text="code">@nvidia-elements/lint/no-invalid-event-listeners</code></nve-grid-cell>
147153
<nve-grid-cell>Disallow inline event handler attributes in HTML.</nve-grid-cell>

0 commit comments

Comments
 (0)