diff --git a/.eslintrc.json b/.eslintrc.json index 5fb4c121717..a9e44c59374 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -293,7 +293,8 @@ ], "rules": { // Custom DSpace Angular rules - "dspace-angular-html/themed-component-usages": "error" + "dspace-angular-html/themed-component-usages": "error", + "dspace-angular-html/no-disabled-attr": "error" } }, { diff --git a/lint/src/rules/html/index.ts b/lint/src/rules/html/index.ts index 7c1370ae2d4..120751d65aa 100644 --- a/lint/src/rules/html/index.ts +++ b/lint/src/rules/html/index.ts @@ -10,10 +10,13 @@ import { bundle, RuleExports, } from '../../util/structure'; +import * as noDisabledAttr from './no-disabled-attr'; import * as themedComponentUsages from './themed-component-usages'; const index = [ themedComponentUsages, + noDisabledAttr, + ] as unknown as RuleExports[]; export = { diff --git a/lint/src/rules/html/no-disabled-attr.ts b/lint/src/rules/html/no-disabled-attr.ts new file mode 100644 index 00000000000..0ed9dbb457c --- /dev/null +++ b/lint/src/rules/html/no-disabled-attr.ts @@ -0,0 +1,87 @@ +import {TmplAstBoundAttribute, TmplAstElement} from '@angular-eslint/bundled-angular-compiler'; +import { TemplateParserServices } from '@angular-eslint/utils'; +import { + ESLintUtils, + TSESLint, +} from '@typescript-eslint/utils'; + +import { + DSpaceESLintRuleInfo, + NamedTests, +} from '../../util/structure'; +import { getSourceCode } from '../../util/typescript'; + +export enum Message { + USE_DSBTN_DISABLED = 'mustUseDsBtnDisabled', +} + +export const info = { + name: 'no-disabled-attr', + meta: { + docs: { + description: `Buttons should use the \`dsBtnDisabled\` directive instead of the HTML \`disabled\` attribute for accessibility reasons.`, + }, + type: 'problem', + fixable: 'code', + schema: [], + messages: { + [Message.USE_DSBTN_DISABLED]: 'Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.', + }, + }, + defaultOptions: [], +} as DSpaceESLintRuleInfo; + +export const rule = ESLintUtils.RuleCreator.withoutDocs({ + ...info, + create(context: TSESLint.RuleContext) { + const parserServices = getSourceCode(context).parserServices as TemplateParserServices; + + return { + 'BoundAttribute[name="disabled"]'(node: TmplAstBoundAttribute) { + // Only check for disabled attributes on button elements and ensure it's not a class binding + if ((node as any).parent.name !== 'button' || node.name.includes('class.')) { + return; + } + + const sourceSpan = node.sourceSpan; + console.log('name', node.name); + context.report({ + messageId: Message.USE_DSBTN_DISABLED, + loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan), + fix(fixer) { + const templateText = sourceSpan.start.file.content; + const disabledText = templateText.slice(sourceSpan.start.offset, sourceSpan.end.offset); + const newText = disabledText.replace('disabled', '[dsBtnDisabled]'); + return fixer.replaceTextRange([sourceSpan.start.offset, sourceSpan.end.offset], newText); + }, + }); + }, + }; + }, +}); + +export const tests = { + plugin: info.name, + valid: [ + { + name: 'should use [dsBtnDisabled] in HTML templates', + code: ` + + `, + }, + ], + invalid: [ + { + name: 'should not use disabled attribute in HTML templates', + code: ` + + `, + errors: [{ messageId: Message.USE_DSBTN_DISABLED }], + output: ` + + `, + }, + ], +} as NamedTests; + +export default rule;