From 5f389e12da0bcc3f1e0ccd95caf27f619946d9f0 Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Mon, 27 Apr 2026 21:44:36 -0500 Subject: [PATCH 1/5] feat(lint): no deprecated global attribute value Signed-off-by: Cory Rylan --- .../internals/tools/src/context/index.test.ts | 8 -- projects/lint/README.md | 1 + projects/lint/src/eslint/configs/html.ts | 3 + .../lint/src/eslint/internals/attributes.ts | 62 ++++++++- projects/lint/src/eslint/internals/index.ts | 2 +- ...-deprecated-global-attribute-value.test.ts | 118 ++++++++++++++++++ .../no-deprecated-global-attribute-value.ts | 81 ++++++++++++ ...-unexpected-global-attribute-value.test.ts | 86 ++++++++----- .../no-unexpected-global-attribute-value.ts | 76 +++++------ projects/site/src/docs/lint/index.md | 6 + 10 files changed, 362 insertions(+), 81 deletions(-) create mode 100644 projects/lint/src/eslint/rules/no-deprecated-global-attribute-value.test.ts create mode 100644 projects/lint/src/eslint/rules/no-deprecated-global-attribute-value.ts diff --git a/projects/internals/tools/src/context/index.test.ts b/projects/internals/tools/src/context/index.test.ts index d07669eca..9527337d8 100644 --- a/projects/internals/tools/src/context/index.test.ts +++ b/projects/internals/tools/src/context/index.test.ts @@ -83,14 +83,6 @@ describe('prompts', () => { expect(result?.messages[0].content.text).toContain('api_'); }); - it('should have "playground" prompt with authoring guidelines', () => { - const playgroundPrompt = prompts.find(p => p.name === 'playground'); - expect(playgroundPrompt).toBeDefined(); - - const result = playgroundPrompt?.handler({}); - expect(result?.messages[0].content.text).toContain('playground'); - }); - it('should have "create-project" prompt for starter projects', () => { const createProjectPrompt = prompts.find(p => p.name === 'create-project'); expect(createProjectPrompt).toBeDefined(); diff --git a/projects/lint/README.md b/projects/lint/README.md index 8dd0e690e..2e2f4733f 100644 --- a/projects/lint/README.md +++ b/projects/lint/README.md @@ -64,6 +64,7 @@ export default [ | ---- | ----------- | -------- | -------- | | `@nvidia-elements/lint/no-complex-popovers` | Disallow excessive DOM complexity inside popover elements. | HTML | `error` | | `@nvidia-elements/lint/no-deprecated-attributes` | Disallow use of deprecated attributes in HTML. | HTML | `error` | +| `@nvidia-elements/lint/no-deprecated-global-attribute-value` | Disallow use of deprecated attribute values for nve-* utility attributes. | HTML | `error` | | `@nvidia-elements/lint/no-deprecated-css-imports` | Disallow use of deprecated CSS import paths. | CSS | `error` | | `@nvidia-elements/lint/no-deprecated-css-variable` | Disallow use of deprecated --mlv-* CSS theme variables. | CSS | `error` | | `@nvidia-elements/lint/no-deprecated-global-attributes` | Disallow use of deprecated global utility attributes in HTML. | HTML | `error` | diff --git a/projects/lint/src/eslint/configs/html.ts b/projects/lint/src/eslint/configs/html.ts index 241c348fe..581bd2381 100644 --- a/projects/lint/src/eslint/configs/html.ts +++ b/projects/lint/src/eslint/configs/html.ts @@ -10,6 +10,7 @@ import noDeprecatedIconNames from '../rules/no-deprecated-icon-names.js'; import noDeprecatedPopoverAttributes from '../rules/no-deprecated-popover-attributes.js'; import noUnexpectedGlobalAttributeValue from '../rules/no-unexpected-global-attribute-value.js'; import noUnexpectedStyleCustomization from '../rules/no-unexpected-style-customization.js'; +import noDeprecatedGlobalAttributeValue from '../rules/no-deprecated-global-attribute-value.js'; import noDeprecatedGlobalAttributes from '../rules/no-deprecated-global-attributes.js'; import noRestrictedAttributes from '../rules/no-restricted-attributes.js'; import noDeprecatedSlots from '../rules/no-deprecated-slots.js'; @@ -61,6 +62,7 @@ export const elementsHtmlConfig: Linter.Config = { 'no-deprecated-attributes': noDeprecatedAttributes, 'no-deprecated-icon-names': noDeprecatedIconNames, 'no-deprecated-popover-attributes': noDeprecatedPopoverAttributes, + 'no-deprecated-global-attribute-value': noDeprecatedGlobalAttributeValue, 'no-deprecated-global-attributes': noDeprecatedGlobalAttributes, 'no-deprecated-slots': noDeprecatedSlots, 'no-missing-slotted-elements': noMissingSlottedElements, @@ -91,6 +93,7 @@ export const elementsHtmlConfig: Linter.Config = { '@nvidia-elements/lint/no-deprecated-attributes': ['error'], '@nvidia-elements/lint/no-deprecated-icon-names': ['error'], '@nvidia-elements/lint/no-deprecated-popover-attributes': ['error'], + '@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-missing-slotted-elements': ['error'], diff --git a/projects/lint/src/eslint/internals/attributes.ts b/projects/lint/src/eslint/internals/attributes.ts index d0242ee11..d1f9d7c36 100644 --- a/projects/lint/src/eslint/internals/attributes.ts +++ b/projects/lint/src/eslint/internals/attributes.ts @@ -3,25 +3,54 @@ import { globalAttributes } from './metadata.js'; -export const ATTRIBUTE_EXCEPTIONS = ['debug', 'mkd', 'md']; // internal scopes - export const VALUE_BINDINGS = ['${', '{', '{{', '{%']; +const ATTRIBUTE_EXCEPTIONS = ['debug', 'mkd', 'md']; // internal scopes +const DEPRECATED_NVE_TEXT_VALUES = new Set(['eyebrow']); +const DEPRECATED_NVE_LAYOUT_VALUES = new Set(['grow']); + export const VALID_NVE_TEXT_VALUES = new Set([ ...(globalAttributes.find(attribute => attribute.name === 'nve-text')?.values?.map(value => value.name) ?? []), ...ATTRIBUTE_EXCEPTIONS ]); -export const VALID_NVE_LAYOUT_VALUES = [ +export const VALID_NVE_LAYOUT_VALUES = new Set([ ...(globalAttributes.find(attribute => attribute.name === 'nve-layout')?.values?.map(value => value.name) ?? []), ...ATTRIBUTE_EXCEPTIONS -]; +]); export const VALID_NVE_DISPLAY_VALUES = new Set([ ...(globalAttributes.find(attribute => attribute.name === 'nve-display')?.values?.map(value => value.name) ?? []), ...ATTRIBUTE_EXCEPTIONS ]); +export const DISTILLED_NVE_TEXT_VALUES = new Set( + [...VALID_NVE_TEXT_VALUES].filter(v => !isComplexAttributeValue(v) && !DEPRECATED_NVE_TEXT_VALUES.has(v)) +); + +export const DISTILLED_NVE_LAYOUT_VALUES = new Set( + [...VALID_NVE_LAYOUT_VALUES].filter(v => !isComplexAttributeValue(v) && !DEPRECATED_NVE_LAYOUT_VALUES.has(v)) +); + +export const DISTILLED_NVE_DISPLAY_VALUES = new Set( + [...VALID_NVE_DISPLAY_VALUES].filter(v => !isComplexAttributeValue(v)) +); + +// also used in @internals/metadata, these are values that often confuse agents due to complexity in playground template generation within the same context window +export function isComplexAttributeValue(value: string) { + return ( + value.includes('|') || + value.includes('@') || + value.includes('&') || + value.includes('xx') || + value.includes('-y:') || + value.includes(':none') || + value.includes('debug') || + value.includes('mkd') || + value.includes('md') + ); +} + export function recommendedNveTextValue(attributeValue: string): string | null { if (VALUE_BINDINGS.some(binding => attributeValue.includes(binding))) { return attributeValue; @@ -57,7 +86,9 @@ export function recommendedNveLayoutValue(attributeValue: string, invalidSymbols } const values = getAttributeValueSegments(attributeValue); - const validValues = new Set(VALID_NVE_LAYOUT_VALUES.filter(v => !invalidSymbols.some(symbol => v.includes(symbol)))); + const validValues = new Set( + [...VALID_NVE_LAYOUT_VALUES].filter(v => !invalidSymbols.some(symbol => v.includes(symbol))) + ); const repairs: [RegExp, string][] = [ [/^default$/, 'column'], @@ -101,6 +132,27 @@ export function recommendedNveLayoutValue(attributeValue: string, invalidSymbols } } +export function recommendedNveDisplayValue(attributeValue: string): string | null { + if (VALUE_BINDINGS.some(binding => attributeValue.includes(binding))) { + return attributeValue; + } + + const values = getAttributeValueSegments(attributeValue); + const validValues = new Set(VALID_NVE_DISPLAY_VALUES); + + const repairs: [RegExp, string][] = [ + [/^hidden$/, 'hide'], + [/^visisble$/, 'show'] + ]; + + const result: string[] = repairAttributeValueSegments(values, repairs); + if (result.some(value => !validValues.has(value))) { + return null; + } else { + return result.join(' '); + } +} + function getAttributeValueSegments(value: string) { return value .toLowerCase() diff --git a/projects/lint/src/eslint/internals/index.ts b/projects/lint/src/eslint/internals/index.ts index e79d3bcf3..c7abb97aa 100644 --- a/projects/lint/src/eslint/internals/index.ts +++ b/projects/lint/src/eslint/internals/index.ts @@ -28,7 +28,7 @@ export async function lintPlaygroundTemplate(code: string): Promise = { - '@nvidia-elements/lint/no-unexpected-global-attribute-value': ['error', { 'nve-layout': ['@', '|', '&', 'xx'] }], + '@nvidia-elements/lint/no-unexpected-global-attribute-value': ['error', { distilled: true }], '@nvidia-elements/lint/no-missing-slotted-elements': ['error', { 'nve-card': { required: ['nve-card-content'] } }], '@nvidia-elements/lint/no-missing-gap-space': ['error'] }; diff --git a/projects/lint/src/eslint/rules/no-deprecated-global-attribute-value.test.ts b/projects/lint/src/eslint/rules/no-deprecated-global-attribute-value.test.ts new file mode 100644 index 000000000..58309ac8d --- /dev/null +++ b/projects/lint/src/eslint/rules/no-deprecated-global-attribute-value.test.ts @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it, expect, beforeEach } from 'vitest'; +import { RuleTester } from 'eslint'; +import type { JSRuleDefinition } from 'eslint'; +import htmlParser from '@html-eslint/parser'; +import noDeprecatedAttributeValue, { DEPRECATED_ATTRIBUTE_VALUES } from './no-deprecated-global-attribute-value.js'; +import noDeprecatedGlobalAttributeValue from './no-deprecated-global-attribute-value.js'; + +const rule = noDeprecatedGlobalAttributeValue as unknown as JSRuleDefinition; + +const MIGRATE_PROMPT_DEPRECATIONS: { attribute: string; before: string; after: string }[] = [ + { attribute: 'nve-text', before: 'eyebrow', after: 'label sm' }, + { attribute: 'nve-layout', before: 'grow', after: 'full' } +]; + +describe('noDeprecatedGlobalAttributeValue', () => { + let tester: RuleTester; + + beforeEach(() => { + tester = new RuleTester({ + languageOptions: { + parser: htmlParser, + parserOptions: { + frontmatter: true + } + } + }); + }); + + it('should define rule metadata', () => { + expect(noDeprecatedAttributeValue.meta).toBeDefined(); + expect(noDeprecatedAttributeValue.meta.type).toBe('problem'); + expect(noDeprecatedAttributeValue.meta.fixable).toBe('code'); + expect(noDeprecatedAttributeValue.meta.hasSuggestions).toBe(true); + expect(noDeprecatedAttributeValue.meta.docs.recommended).toBe(true); + expect(noDeprecatedAttributeValue.meta.docs.url).toContain('/docs/lint/'); + }); + + it('should cover every deprecation documented in the migrate prompt', () => { + MIGRATE_PROMPT_DEPRECATIONS.forEach(({ attribute, before, after }) => { + expect(DEPRECATED_ATTRIBUTE_VALUES[attribute]?.[before]).toBe(after); + }); + }); + + it('should allow non-deprecated values', () => { + tester.run('non-deprecated values', rule, { + valid: [ + '
', + '
', + '
', + '
', + '
' + ], + invalid: [] + }); + }); + + it('should ignore template binding expressions', () => { + tester.run('template binding expressions', rule, { + valid: [ + '
', + '
', + '
' + ], + invalid: [] + }); + }); + + it('should report and auto-fix every migrate-prompt deprecation', () => { + tester.run('migrate prompt deprecations', rule, { + valid: [], + invalid: MIGRATE_PROMPT_DEPRECATIONS.map(({ attribute, before, after }) => ({ + code: `
`, + output: `
`, + errors: [ + { + messageId: 'unexpected-deprecated-global-attribute-value', + data: { attribute, value: before, alternative: after }, + suggestions: [ + { + messageId: 'suggest-replace-deprecated-global-attribute-value', + data: { value: before, alternative: after }, + output: `
` + } + ] + } + ] + })) + }); + }); + + it('should rewrite deprecated tokens within a multi-token value', () => { + tester.run('multi-token replacement', rule, { + valid: [], + invalid: [ + { + code: '
', + output: '
', + errors: [ + { + messageId: 'unexpected-deprecated-global-attribute-value', + data: { attribute: 'nve-layout', value: 'grow column', alternative: 'full column' }, + suggestions: [ + { + messageId: 'suggest-replace-deprecated-global-attribute-value', + data: { value: 'grow column', alternative: 'full column' }, + output: '
' + } + ] + } + ] + } + ] + }); + }); +}); diff --git a/projects/lint/src/eslint/rules/no-deprecated-global-attribute-value.ts b/projects/lint/src/eslint/rules/no-deprecated-global-attribute-value.ts new file mode 100644 index 000000000..90f8ac8a0 --- /dev/null +++ b/projects/lint/src/eslint/rules/no-deprecated-global-attribute-value.ts @@ -0,0 +1,81 @@ +// 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 { VALUE_BINDINGS } from '../internals/attributes.js'; +import type { HtmlTagNode } from '../rule-types.js'; + +declare const __ELEMENTS_PAGES_BASE_URL__: string; + +export const DEPRECATED_ATTRIBUTE_VALUES: Record> = { + 'nve-text': { + eyebrow: 'label sm' + }, + 'nve-layout': { + grow: 'full' + } +}; + +function rewriteValue(value: string, deprecations: Record): string | null { + const tokens = value.split(/\s+/).filter(Boolean); + const replaced = tokens.map(token => deprecations[token] ?? token); + if (tokens.every((token, i) => token === replaced[i])) return null; + return Array.from(new Set(replaced.flatMap(t => t.split(/\s+/)))).join(' '); +} + +const rule = { + meta: { + type: 'problem' as const, + fixable: 'code' as const, + hasSuggestions: true, + docs: { + description: 'Disallow use of deprecated attribute values for nve-* utility attributes.', + category: 'Best Practice', + recommended: true, + url: `${__ELEMENTS_PAGES_BASE_URL__}/docs/lint/` + }, + schema: [], + messages: { + ['unexpected-deprecated-global-attribute-value']: + 'Unexpected use of deprecated value "{{value}}" in "{{attribute}}". Use "{{alternative}}" instead.', + ['suggest-replace-deprecated-global-attribute-value']: 'Replace "{{value}}" with "{{alternative}}"' + } + }, + create(context: Rule.RuleContext) { + return createVisitors(context, { + Tag(node: HtmlTagNode) { + Object.entries(DEPRECATED_ATTRIBUTE_VALUES).forEach(([attribute, deprecations]) => { + const attr = findAttr(node, attribute); + const value = attr?.value?.value ?? ''; + if (!attr || !value) return; + if (VALUE_BINDINGS.some(binding => value.includes(binding))) return; + const alternative = rewriteValue(value, deprecations); + if (alternative === null) return; + + context.report({ + node: attr, + data: { attribute, value, alternative }, + messageId: 'unexpected-deprecated-global-attribute-value', + suggest: [ + { + messageId: 'suggest-replace-deprecated-global-attribute-value', + data: { value, alternative }, + fix: fixer => + fixer.replaceText( + attr, + `${attribute}=${attr.startWrapper.value}${alternative}${attr.endWrapper.value}` + ) + } + ], + fix: fixer => + fixer.replaceText(attr, `${attribute}=${attr.startWrapper.value}${alternative}${attr.endWrapper.value}`) + }); + }); + } + }); + } +} as const; + +export default rule; diff --git a/projects/lint/src/eslint/rules/no-unexpected-global-attribute-value.test.ts b/projects/lint/src/eslint/rules/no-unexpected-global-attribute-value.test.ts index 0daae8c18..5eaa980ce 100644 --- a/projects/lint/src/eslint/rules/no-unexpected-global-attribute-value.test.ts +++ b/projects/lint/src/eslint/rules/no-unexpected-global-attribute-value.test.ts @@ -5,17 +5,18 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { RuleTester } from 'eslint'; import type { JSRuleDefinition } from 'eslint'; import html from '@html-eslint/eslint-plugin'; -import noUnexpectedGlobalAttributeValue, { - SIMPLE_NVE_TEXT_VALUES, - SIMPLE_NVE_LAYOUT_VALUES, - SIMPLE_NVE_DISPLAY_VALUES -} from './no-unexpected-global-attribute-value.js'; +import noUnexpectedGlobalAttributeValue from './no-unexpected-global-attribute-value.js'; +import { + DISTILLED_NVE_DISPLAY_VALUES, + DISTILLED_NVE_LAYOUT_VALUES, + DISTILLED_NVE_TEXT_VALUES +} from '../internals/attributes.js'; const rule = noUnexpectedGlobalAttributeValue as unknown as JSRuleDefinition; -const textValidValues = SIMPLE_NVE_TEXT_VALUES.map(v => `"${v}"`).join(', '); -const layoutValidValues = SIMPLE_NVE_LAYOUT_VALUES.map(v => `"${v}"`).join(', '); -const displayValidValues = SIMPLE_NVE_DISPLAY_VALUES.map(v => `"${v}"`).join(', '); +const textValidValues = [...DISTILLED_NVE_TEXT_VALUES].map(v => `"${v}"`).join(', '); +const layoutValidValues = [...DISTILLED_NVE_LAYOUT_VALUES].map(v => `"${v}"`).join(', '); +const displayValidValues = [...DISTILLED_NVE_DISPLAY_VALUES].map(v => `"${v}"`).join(', '); describe('noUnexpectedAttributeValue', () => { let tester: RuleTester; @@ -328,56 +329,81 @@ describe('noUnexpectedAttributeValue', () => { }); }); - it('should not allow additional invalid symbols in nve-layout attribute values', () => { - const validValues = SIMPLE_NVE_LAYOUT_VALUES.map(v => `"${v}"`).join(', '); + it('should not allow invalid use of nve-display attribute values', () => { + tester.run('should not allow invalid use of nve-display attribute values', rule, { + valid: [], + invalid: [ + { + code: '
', + errors: [ + { + messageId: 'unexpected-attribute-value', + data: { attribute: 'nve-display', value: 'row', validValues: displayValidValues } + } + ] + } + ] + }); + }); - tester.run('should not allow additional invalid symbols in nve-layout attribute values', rule, { + it('should advertise the distilled subset only regardless if the check enforces it', () => { + // Enables advanced api options to pass validation but masks the full set from agents + tester.run('distilled option', rule, { valid: [], invalid: [ { - options: [{ 'nve-layout': ['&'] }], - code: '
', + options: [{ distilled: true }], + code: '

', errors: [ { messageId: 'unexpected-attribute-value', - data: { attribute: 'nve-layout', value: 'row &lg|row', validValues } + data: { + attribute: 'nve-text', + value: 'nope', + validValues: [...DISTILLED_NVE_TEXT_VALUES].map(v => `"${v}"`).join(', ') + } } ] }, { - options: [{ 'nve-layout': ['|'] }], - code: '
', + options: [{ distilled: false }], + code: '

', errors: [ { messageId: 'unexpected-attribute-value', - data: { attribute: 'nve-layout', value: 'row &lg|row', validValues } + data: { + attribute: 'nve-text', + value: 'nope', + validValues: [...DISTILLED_NVE_TEXT_VALUES].map(v => `"${v}"`).join(', ') + } } ] }, { - options: [{ 'nve-layout': ['|'] }], - code: '
', + options: [{ distilled: false }], + code: '
', errors: [ { messageId: 'unexpected-attribute-value', - data: { attribute: 'nve-layout', value: 'row @lg|row', validValues } + data: { + attribute: 'nve-layout', + value: 'nope', + validValues: [...DISTILLED_NVE_LAYOUT_VALUES].map(v => `"${v}"`).join(', ') + } } ] - } - ] - }); - }); - - it('should not allow invalid use of nve-display attribute values', () => { - tester.run('should not allow invalid use of nve-display attribute values', rule, { - valid: [], - invalid: [ + }, { + options: [{ distilled: false }], code: '
', errors: [ { messageId: 'unexpected-attribute-value', - data: { attribute: 'nve-display', value: 'row', validValues: displayValidValues } + data: { + attribute: 'nve-display', + value: 'row', + validValues: [...DISTILLED_NVE_DISPLAY_VALUES].map(v => `"${v}"`).join(', ') + } } ] } diff --git a/projects/lint/src/eslint/rules/no-unexpected-global-attribute-value.ts b/projects/lint/src/eslint/rules/no-unexpected-global-attribute-value.ts index a052951e0..e61774328 100644 --- a/projects/lint/src/eslint/rules/no-unexpected-global-attribute-value.ts +++ b/projects/lint/src/eslint/rules/no-unexpected-global-attribute-value.ts @@ -5,34 +5,16 @@ 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 { - VALID_NVE_DISPLAY_VALUES, - VALID_NVE_LAYOUT_VALUES, - VALID_NVE_TEXT_VALUES, - VALUE_BINDINGS, recommendedNveTextValue, - recommendedNveLayoutValue + recommendedNveLayoutValue, + DISTILLED_NVE_TEXT_VALUES, + DISTILLED_NVE_LAYOUT_VALUES, + DISTILLED_NVE_DISPLAY_VALUES, + recommendedNveDisplayValue } from '../internals/attributes.js'; import type { HtmlTagNode } from '../rule-types.js'; declare const __ELEMENTS_PAGES_BASE_URL__: string; -// also used in @internals/metadata, these are values that often confuse agents due to complexity in playground template generation within the same context window -export function isComplexAttributeValue(value: string) { - return ( - value.includes('|') || - value.includes('@') || - value.includes('&') || - value.includes('xx') || - value.includes('-y:') || - value.includes(':none') || - value.includes('debug') || - value.includes('mkd') || - value.includes('md') - ); -} - -export const SIMPLE_NVE_TEXT_VALUES = [...VALID_NVE_TEXT_VALUES].filter(v => !isComplexAttributeValue(v)); -export const SIMPLE_NVE_LAYOUT_VALUES = VALID_NVE_LAYOUT_VALUES.filter(v => !isComplexAttributeValue(v)); -export const SIMPLE_NVE_DISPLAY_VALUES = [...VALID_NVE_DISPLAY_VALUES].filter(v => !isComplexAttributeValue(v)); const rule = { meta: { @@ -44,7 +26,18 @@ const rule = { recommended: true, url: `${__ELEMENTS_PAGES_BASE_URL__}/docs/lint/` }, - schema: [{ type: 'object' }], + schema: [ + { + type: 'object', + properties: { + distilled: { + type: 'boolean', + default: true, + description: 'When true the subset of options for agents is applied.' + } + } + } + ], messages: { ['unexpected-attribute-value']: 'Unexpected value "{{value}}" in "{{attribute}}" attribute. Available values: {{validValues}}', @@ -66,7 +59,7 @@ const rule = { attribute: 'nve-text', value, alternative, - validValues: [...SIMPLE_NVE_TEXT_VALUES].map(v => `"${v}"`).join(', ') + validValues: [...DISTILLED_NVE_TEXT_VALUES].map(v => `"${v}"`).join(', ') }, messageId: alternative ? 'unexpected-attribute-value-alternative' : 'unexpected-attribute-value', suggest: alternative @@ -89,19 +82,15 @@ const rule = { const layoutAttr = findAttr(node, 'nve-layout'); if (!layoutAttr) return; const value = layoutAttr.value?.value ?? ''; - const invalidSymbols = context.options[0]?.['nve-layout'] ?? []; - const alternative = recommendedNveLayoutValue(value, invalidSymbols); + const alternative = recommendedNveLayoutValue(value); if (alternative === value) return; - const layoutValidValues = SIMPLE_NVE_LAYOUT_VALUES.filter( - v => !invalidSymbols.some((symbol: string) => v.includes(symbol)) - ); context.report({ node: layoutAttr, data: { attribute: 'nve-layout', value, alternative, - validValues: layoutValidValues.map(v => `"${v}"`).join(', ') + validValues: [...DISTILLED_NVE_LAYOUT_VALUES].map(v => `"${v}"`).join(', ') }, messageId: alternative ? 'unexpected-attribute-value-alternative' : 'unexpected-attribute-value', suggest: alternative @@ -123,18 +112,31 @@ const rule = { function checkNveDisplay(node: HtmlTagNode) { const displayAttr = findAttr(node, 'nve-display'); if (!displayAttr) return; - const values = displayAttr.value?.value?.split(' ') ?? []; - const value = values.find((v: string) => !VALID_NVE_DISPLAY_VALUES.has(v)); - const isValueBinding = VALUE_BINDINGS.some(binding => value?.includes(binding)); - if (!value || isValueBinding) return; + const value = displayAttr.value?.value ?? ''; + const alternative = recommendedNveDisplayValue(value); + if (alternative === value) return; context.report({ node, data: { attribute: 'nve-display', value, - validValues: [...SIMPLE_NVE_DISPLAY_VALUES].map(v => `"${v}"`).join(', ') + alternative, + validValues: [...DISTILLED_NVE_DISPLAY_VALUES].map(v => `"${v}"`).join(', ') }, - messageId: 'unexpected-attribute-value' + messageId: 'unexpected-attribute-value', + suggest: alternative + ? [ + { + messageId: 'suggest-replace-attribute-value', + data: { value, alternative }, + fix: fixer => + fixer.replaceText( + displayAttr, + `nve-layout=${displayAttr.startWrapper.value}${alternative}${displayAttr.endWrapper.value}` + ) + } + ] + : [] }); } diff --git a/projects/site/src/docs/lint/index.md b/projects/site/src/docs/lint/index.md index 9518c6fc6..81fb06a19 100644 --- a/projects/site/src/docs/lint/index.md +++ b/projects/site/src/docs/lint/index.md @@ -91,6 +91,12 @@ export default [ HTML error + + @nvidia-elements/lint/no-deprecated-global-attribute-value + Disallow use of deprecated attribute values for nve-* utility attributes. + HTML + error + @nvidia-elements/lint/no-deprecated-css-imports Disallow use of deprecated CSS import paths. From 791170cee08f0cd01ac5de48c81d4a04b0065981 Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Mon, 27 Apr 2026 21:45:22 -0500 Subject: [PATCH 2/5] fix(core): missing stop icon svg Signed-off-by: Cory Rylan --- projects/core/.visual/icon.dark.png | 4 ++-- projects/core/.visual/icon.png | 4 ++-- projects/core/src/icon/icons.ts | 1 + projects/core/src/icon/icons/stop.svg | 1 + projects/core/src/icon/server.ts | 1 + 5 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 projects/core/src/icon/icons/stop.svg diff --git a/projects/core/.visual/icon.dark.png b/projects/core/.visual/icon.dark.png index ee3590bb8..f4f679a25 100644 --- a/projects/core/.visual/icon.dark.png +++ b/projects/core/.visual/icon.dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c353d3c89f22dad8a6b5cfa0a1d93c1b045a9e415f8ec20f827a0c6e219e4d7 -size 119022 +oid sha256:7aeaba6ae4d60e097eb8ad5537ffffd61b46ffb2634e48768a41ee9a946d1eb8 +size 119109 diff --git a/projects/core/.visual/icon.png b/projects/core/.visual/icon.png index 6abb239f3..141b6da1e 100644 --- a/projects/core/.visual/icon.png +++ b/projects/core/.visual/icon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60a97c9dca6836c93a8480c29814d6ecd8d1d01ba658326ca6773ffd4de99dee -size 117493 +oid sha256:ba0060529915e0f3f33c82905f776d96ef0dccaf8c388e0823886e1b8edb3a73 +size 117583 diff --git a/projects/core/src/icon/icons.ts b/projects/core/src/icon/icons.ts index 726b223c0..786fe62b7 100644 --- a/projects/core/src/icon/icons.ts +++ b/projects/core/src/icon/icons.ts @@ -255,6 +255,7 @@ export const ICON_IMPORTS = { 'start': iconImport(() => import('./icons/start.svg?raw')), 'status-offline': iconImport(() => import('./icons/status-offline.svg?raw')), 'status-online': iconImport(() => import('./icons/status-online.svg?raw')), + 'stop': iconImport(() => import('./icons/stop.svg?raw')), 'stop-sign': iconImport(() => import('./icons/stop-sign.svg?raw')), 'stopwatch': iconImport(() => import('./icons/stopwatch.svg?raw')), 'strikethrough': iconImport(() => import('./icons/strikethrough.svg?raw')), diff --git a/projects/core/src/icon/icons/stop.svg b/projects/core/src/icon/icons/stop.svg new file mode 100644 index 000000000..522edb051 --- /dev/null +++ b/projects/core/src/icon/icons/stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/core/src/icon/server.ts b/projects/core/src/icon/server.ts index 23441d2e8..ecb4ce6b8 100644 --- a/projects/core/src/icon/server.ts +++ b/projects/core/src/icon/server.ts @@ -248,6 +248,7 @@ globalThis._NVE_SSR_ICON_REGISTRY = { 'start': '', 'status-offline': '', 'status-online': '', + 'stop': '', 'stop-sign': '', 'stopwatch': '', 'strikethrough': '', From 1f0519d06e91446f26525911454902ebd2e2a445 Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Mon, 27 Apr 2026 21:46:25 -0500 Subject: [PATCH 3/5] chore(docs): lint and migration cleanup Signed-off-by: Cory Rylan --- projects/core/src/grid/grid.examples.ts | 6 +- .../src/progress-bar/progress-bar.examples.ts | 2 +- .../patterns/src/heatmap.examples.ts | 2 +- .../patterns/src/subheader.examples.ts | 6 +- projects/site/src/_11ty/layouts/docs.11ty.js | 2 +- .../internal/guidelines/component-creation.md | 4 +- projects/site/src/docs/labs/index.md | 3 - projects/site/src/docs/testing/index.md | 69 ------------------- 8 files changed, 11 insertions(+), 83 deletions(-) delete mode 100644 projects/site/src/docs/testing/index.md diff --git a/projects/core/src/grid/grid.examples.ts b/projects/core/src/grid/grid.examples.ts index ca5ed93fd..c81322245 100644 --- a/projects/core/src/grid/grid.examples.ts +++ b/projects/core/src/grid/grid.examples.ts @@ -1159,7 +1159,7 @@ export const PanelDetail = { render: () => html` - + NV

Infrastructure

@@ -1186,7 +1186,7 @@ export const PanelDetail = {
-

Workflow

+

Workflow

@@ -1234,7 +1234,7 @@ export const PanelGrid = { return html` - + NV

Infrastructure

diff --git a/projects/core/src/progress-bar/progress-bar.examples.ts b/projects/core/src/progress-bar/progress-bar.examples.ts index e34c6109f..ca218b8ef 100644 --- a/projects/core/src/progress-bar/progress-bar.examples.ts +++ b/projects/core/src/progress-bar/progress-bar.examples.ts @@ -52,7 +52,7 @@ export const Max = { */ export const Labeled = { render: () => html` -
+

Upload Status

80%

diff --git a/projects/internals/patterns/src/heatmap.examples.ts b/projects/internals/patterns/src/heatmap.examples.ts index 0152405e1..50836dccc 100644 --- a/projects/internals/patterns/src/heatmap.examples.ts +++ b/projects/internals/patterns/src/heatmap.examples.ts @@ -667,7 +667,7 @@ export const OccupancyDetectionHeatmap = { /** * @summary Grid heatmap displaying thermal distribution across robotic arm joints and actuators over time. Essential for monitoring overheating risks during extended operation cycles and validating cooling system performance using viridis scale. - * @tags pattern + * @tags pattern test-case */ export const ThermalHeatmap = { render: () => html` diff --git a/projects/internals/patterns/src/subheader.examples.ts b/projects/internals/patterns/src/subheader.examples.ts index 56417c677..c30eb56d3 100644 --- a/projects/internals/patterns/src/subheader.examples.ts +++ b/projects/internals/patterns/src/subheader.examples.ts @@ -112,7 +112,7 @@ export const TabsHeaderMainPage = { /** * @summary Main page subheader with key-value metadata row stacked below the title. Ideal for displaying session details, status badges, and entity relationships. - * @tags pattern + * @tags pattern test-case */ export const StackedMetadataHeaderMainPage = { render: () => html` @@ -404,7 +404,7 @@ export const StackedKitchenSinkHeaderMainPage = { /** * @summary Detail page subheader with back arrow navigation, multi-level breadcrumb, and minimal action buttons. Use for drilling into specific records. - * @tags pattern + * @tags pattern test-case */ export const StandardHeaderDetailPage = { render: () => html` @@ -450,7 +450,7 @@ export const StandardHeaderDetailPage = { /** * @summary Detail page subheader with back navigation and tabbed content sections. Ideal for entity detail views with many data categories. - * @tags pattern + * @tags pattern test-case */ export const TabsHeaderDetailPage = { render: () => html` diff --git a/projects/site/src/_11ty/layouts/docs.11ty.js b/projects/site/src/_11ty/layouts/docs.11ty.js index 8d0b3f19b..fe2d38a00 100644 --- a/projects/site/src/_11ty/layouts/docs.11ty.js +++ b/projects/site/src/_11ty/layouts/docs.11ty.js @@ -87,7 +87,7 @@ export async function render(data) { data.tag ? `
-
+

${data.title}

${elementSummary(data.tag)} diff --git a/projects/site/src/docs/internal/guidelines/component-creation.md b/projects/site/src/docs/internal/guidelines/component-creation.md index b9626207e..6ddc6cefb 100644 --- a/projects/site/src/docs/internal/guidelines/component-creation.md +++ b/projects/site/src/docs/internal/guidelines/component-creation.md @@ -174,7 +174,7 @@ Follow the [unit testing guideline](testing-unit.md): ```typescript import { html } from 'lit'; import { describe, expect, it, beforeEach, afterEach } from 'vitest'; -import { createFixture, elementIsStable, removeFixture } from '@nvidia-elements/testing'; +import { createFixture, elementIsStable, removeFixture } from '@internals/testing'; import { ComponentName } from '@nvidia-elements/core/component-name'; import '@nvidia-elements/core/component-name/define.js'; @@ -213,7 +213,7 @@ Follow the [accessibility testing guideline](testing-accessibility.md): ```typescript import { html } from 'lit'; import { describe, expect, it, beforeEach, afterEach } from 'vitest'; -import { createFixture, elementIsStable, removeFixture, runAxe } from '@nvidia-elements/testing'; +import { createFixture, elementIsStable, removeFixture, runAxe } from '@internals/testing'; import { ComponentName } from '@nvidia-elements/core/component-name'; import '@nvidia-elements/core/component-name/define.js'; diff --git a/projects/site/src/docs/labs/index.md b/projects/site/src/docs/labs/index.md index d36c5229d..e1c01f550 100644 --- a/projects/site/src/docs/labs/index.md +++ b/projects/site/src/docs/labs/index.md @@ -8,6 +8,3 @@ # {{ title }} Labs projects are experimental packages the team is actively seeking feedback on. They may not be ready for production use and APIs may change frequently. - -Once the team considers a lab project stable, it moves from @nve-labs to the -main @nve package scope. The team may deprecate or remove labs projects at any time. diff --git a/projects/site/src/docs/testing/index.md b/projects/site/src/docs/testing/index.md deleted file mode 100644 index e280348f6..000000000 --- a/projects/site/src/docs/testing/index.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -{ - title: 'Testing', - layout: 'docs.11ty.js' -} ---- - -# Testing - -Elements has a small set of testing utilities for unit testing Lit based -Custom Elements. These utilities test the elements and are also -available for external use. Find the testing utilities at the following entrypoint: - -```shell -# add internal registry to local .npmrc file -registry={{ELEMENTS_REGISTRY_URL}} -``` - -```typescript -import { createFixture, removeFixture, elementIsStable } from '@nvidia-elements/testing'; -``` - -Below is a basic test setup for a Lit Web Component. This test uses [Vitest](https://vitest.dev/). The `createFixture` and `removeFixture` functions create a DOM element to mount the custom element, as well as remove that element once the test has completed. - -```typescript -import { html } from 'lit'; -import { describe, expect, it, beforeEach, afterEach } from 'vitest'; -import { createFixture, removeFixture, elementIsStable } from '@nvidia-elements/testing'; -import { Icon, IconVariants } from '@nvidia-elements/core/icon'; -import '@nvidia-elements/core/icon/define.js'; - -describe('nve-icon', () => { - let fixture: HTMLElement; - let element: Icon; - - beforeEach(async () => { - fixture = await createFixture(html``); - element = fixture.querySelector('nve-icon'); - await elementIsStable(element); - }); - - afterEach(() => { - removeFixture(fixture); - }); - - it('should define element', () => { - expect(customElements.get('nve-icon')).toBeDefined(); - }); -}); -``` - -When updating properties on a lit element you can use the `elementIsStable` function to wait for the element to update/render the changes to the template. Setting a property on an element often triggers many renders of that template. The `elementIsStable` function waits for pending updates to complete. - -```typescript -it('should reflect name attribute for CSS selectors', async () => { - expect(element.name).eq(undefined); - element.name = 'book'; - await elementIsStable(element); - expect(element.getAttribute('name')).toBe('book'); -}); -``` - -## Testing Environment - -If you use a testing Environment that uses a DOM mocking such as [happy-dom](https://github.com/capricorn86/happy-dom) or [js-dom](https://github.com/jsdom/jsdom) then you likely need to import the bundled polyfills to enable the elements to work in the testing environment. Currently this bundle pulls in the `ElementInternals` API which is not yet supported in these testing environments. - -```typescript -import '@nvidia-elements/core/polyfills'; -``` From 4f94a6c4dcc07058340f4fd29c9345eb87774883 Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Mon, 27 Apr 2026 21:47:05 -0500 Subject: [PATCH 4/5] fix(cli): migration url paths Signed-off-by: Cory Rylan --- projects/cli/package.json | 2 +- projects/internals/tools/src/context/index.ts | 19 +++++----- .../internals/tools/src/context/migration.md | 10 ------ .../internals/tools/src/project/starters.ts | 36 +++++++++---------- 4 files changed, 26 insertions(+), 41 deletions(-) diff --git a/projects/cli/package.json b/projects/cli/package.json index 4f685d95c..41e1933bf 100644 --- a/projects/cli/package.json +++ b/projects/cli/package.json @@ -26,7 +26,7 @@ "dist/**/*.js" ], "scripts": { - "dev": "pnpm run nve:install && pnpm dlx @modelcontextprotocol/inspector@0.21.1 node ./dist/index.js mcp", + "dev": "pnpm run nve:install && pnpm dlx @modelcontextprotocol/inspector@0.21.2 node ./dist/index.js mcp", "ci": "wireit", "build": "wireit", "lint": "wireit", diff --git a/projects/internals/tools/src/context/index.ts b/projects/internals/tools/src/context/index.ts index 6edf39fc0..be82e5eca 100644 --- a/projects/internals/tools/src/context/index.ts +++ b/projects/internals/tools/src/context/index.ts @@ -10,6 +10,8 @@ import playgroundContext from './playground.md?inline'; import integrationContext from './integration.md?inline'; import migrationContext from './migration.md?inline'; +declare const __ELEMENTS_PLAYGROUND_BASE_URL__: string; + export interface Skill { name: string; title: string; @@ -124,7 +126,7 @@ const playgroundPrompt: Prompt = { role: 'user', content: { type: 'text', - text: `${toolsContext}\n${playgroundContext}\n${authoringContext}\n---` + text: `${toolsContext}\n${playgroundContext}${authoringContext}\n---` } } ] @@ -177,17 +179,14 @@ const elementsSkill: Skill = { Elements is NVIDIA's design system for AI and Robotics applications, built for speed and scale. It provides a comprehensive library of web components (nve-*) that work across any framework. Elements covers the full spectrum of UI needs: layout primitives, typography, form controls, data grids, navigation, dialogs, theming, and accessibility. ${toolsContext} ${authoringContext} -${playgroundContext} +${__ELEMENTS_PLAYGROUND_BASE_URL__ ? playgroundContext : ''} ${integrationContext}` }; -export const prompts: Prompt[] = [ - aboutPrompt, - doctorPrompt, - searchPrompt, - playgroundPrompt, - createProjectPrompt, - migrateProjectPrompt -]; +export const prompts: Prompt[] = [aboutPrompt, doctorPrompt, searchPrompt, createProjectPrompt, migrateProjectPrompt]; + +if (__ELEMENTS_PLAYGROUND_BASE_URL__) { + prompts.push(playgroundPrompt); +} export const skills: Skill[] = [elementsSkill]; diff --git a/projects/internals/tools/src/context/migration.md b/projects/internals/tools/src/context/migration.md index 29ae3dac5..cc20d2bc0 100644 --- a/projects/internals/tools/src/context/migration.md +++ b/projects/internals/tools/src/context/migration.md @@ -188,16 +188,6 @@ Use the `api_get` tool to look up the current slot API for these components.
``` -### Testing Utilities - -```typescript -// before -import { createFixture, removeFixture, elementIsStable, emulateClick, untilEvent } from '@maglev/elements/test'; - -// after -import { createFixture, removeFixture, elementIsStable, emulateClick, untilEvent } from '@nvidia-elements/testing'; -``` - ## Step 5: Verification After applying all fixes: diff --git a/projects/internals/tools/src/project/starters.ts b/projects/internals/tools/src/project/starters.ts index 63bed91e5..faa30cbe4 100644 --- a/projects/internals/tools/src/project/starters.ts +++ b/projects/internals/tools/src/project/starters.ts @@ -17,8 +17,7 @@ import { isCommandAvailable, getNPMClient } from '../internal/node.js'; import type { Report } from '../internal/types.js'; import { writeAllAgentConfigs } from './setup-agent.js'; -declare const __ELEMENTS_PAGES_BASE_URL__: string; -declare const __ELEMENTS_REGISTRY_URL__: string; +const ELEMENTS_PAGES_BASE_URL = 'https://nvidia.github.io/elements'; export type Starter = | 'angular' @@ -40,35 +39,35 @@ export type Starter = export const startersData = { angular: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/angular.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/angular.zip`, cli: true }, bundles: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/bundles.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/bundles.zip`, cli: true }, eleventy: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/eleventy.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/eleventy.zip`, cli: true }, extensions: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/scoped-registry.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/scoped-registry.zip`, cli: false }, go: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/go.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/go.zip`, cli: true }, hugo: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/hugo.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/hugo.zip`, cli: true }, importmaps: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/importmaps.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/importmaps.zip`, cli: false }, 'lit-library': { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/lit-library.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/lit-library.zip`, cli: false }, lit: { @@ -76,11 +75,11 @@ export const startersData = { cli: false }, nextjs: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/nextjs.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/nextjs.zip`, cli: true }, nuxt: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/nuxt.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/nuxt.zip`, cli: true }, preact: { @@ -88,23 +87,23 @@ export const startersData = { cli: false }, react: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/react.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/react.zip`, cli: true }, solidjs: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/solidjs.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/solidjs.zip`, cli: true }, svelte: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/svelte.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/svelte.zip`, cli: true }, typescript: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/typescript.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/typescript.zip`, cli: true }, vue: { - zip: `${__ELEMENTS_PAGES_BASE_URL__}/starters/download/vue.zip`, + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/vue.zip`, cli: true } }; @@ -115,7 +114,6 @@ export async function archiveStarter(projectDir: string, outDir: string) { await copyProject(projectDir); writeAllAgentConfigs(dist); const packageJSON = await exportPackageFromWorkspace(projectDir); - await writeFile(`${dist}/.npmrc`, `registry=${__ELEMENTS_REGISTRY_URL__}/`); await writeFile(`${dist}/package.json`, JSON.stringify(packageJSON, undefined, 2)); await zipProject(dist); } @@ -329,8 +327,6 @@ export const claudeProjectSettings = { 'mcp__elements__api_tokens_list', 'mcp__elements__examples_list', 'mcp__elements__examples_get', - 'mcp__elements__playground_validate', - 'mcp__elements__playground_create', 'mcp__elements__project_create', 'mcp__elements__project_setup', 'mcp__elements__project_validate', From cd5f16894a7b05e9a05b935e9d2cfd526a9edd5f Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Tue, 28 Apr 2026 10:28:56 -0500 Subject: [PATCH 5/5] fix(lint): interestfor support on popover triggers Signed-off-by: Cory Rylan --- .../rules/no-missing-popover-trigger.test.ts | 16 +++++++++++++++- .../eslint/rules/no-missing-popover-trigger.ts | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) 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 cbe09a329..11be69447 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 @@ -36,7 +36,7 @@ describe('noMissingPopoverTrigger', () => { expect(noMissingPopoverTrigger.meta.schema).toBeDefined(); expect(noMissingPopoverTrigger.meta.messages).toBeDefined(); expect(noMissingPopoverTrigger.meta.messages['missing-popover-trigger']).toBe( - 'Popover element <{{tag}}> is missing a trigger element. Add a button with popovertarget="{{id}}" or commandfor="{{id}}". If programmatically controlling the popover with JavaScript, add a hidden attribute to the popover element.' + 'Popover element <{{tag}}> is missing a trigger element. Add a button with popovertarget="{{id}}", commandfor="{{id}} or interestfor="{{id}}". If programmatically controlling the popover with JavaScript, add a hidden attribute to the popover element.' ); expect(noMissingPopoverTrigger.meta.messages['missing-popover-id']).toBe( 'Popover element <{{tag}}> is missing an id attribute. Add an id to enable trigger association.' @@ -87,6 +87,20 @@ describe('noMissingPopoverTrigger', () => { }); }); + it('should allow popover elements with interestfor trigger', () => { + tester.run('valid interestfor triggers', rule, { + valid: [ + ` + `, + ` + Tooltip content`, + ` + Toggletip content` + ], + invalid: [] + }); + }); + it('should allow non-popover elements without triggers', () => { tester.run('non-popover elements', rule, { valid: ['
', 'Click me', '

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 be5dbcf3b..4a4ec028d 100644 --- a/projects/lint/src/eslint/rules/no-missing-popover-trigger.ts +++ b/projects/lint/src/eslint/rules/no-missing-popover-trigger.ts @@ -71,7 +71,7 @@ const POPOVER_ELEMENTS = [ /** * Attributes that reference a popover target. */ -const TRIGGER_ATTRIBUTES = ['popovertarget', 'commandfor'] as const; +const TRIGGER_ATTRIBUTES = ['popovertarget', 'commandfor', 'interestfor'] as const; interface PopoverNode { node: HtmlNode; @@ -93,7 +93,7 @@ const rule = { schema: [], messages: { ['missing-popover-trigger']: - 'Popover element <{{tag}}> is missing a trigger element. Add a button with popovertarget="{{id}}" or commandfor="{{id}}". If programmatically controlling the popover with JavaScript, add a hidden attribute to the popover element.', + 'Popover element <{{tag}}> is missing a trigger element. Add a button with popovertarget="{{id}}", commandfor="{{id}} or interestfor="{{id}}". If programmatically controlling the popover with JavaScript, add a hidden attribute to the popover element.', ['missing-popover-id']: 'Popover element <{{tag}}> is missing an id attribute. Add an id to enable trigger association.', ['empty-anchor-with-trigger']: