mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
lint rule with autofix to disallow the disabled input on button elements
This commit is contained in:
@@ -293,7 +293,8 @@
|
|||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
// Custom DSpace Angular 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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -10,10 +10,13 @@ import {
|
|||||||
bundle,
|
bundle,
|
||||||
RuleExports,
|
RuleExports,
|
||||||
} from '../../util/structure';
|
} from '../../util/structure';
|
||||||
|
import * as noDisabledAttr from './no-disabled-attr';
|
||||||
import * as themedComponentUsages from './themed-component-usages';
|
import * as themedComponentUsages from './themed-component-usages';
|
||||||
|
|
||||||
const index = [
|
const index = [
|
||||||
themedComponentUsages,
|
themedComponentUsages,
|
||||||
|
noDisabledAttr,
|
||||||
|
|
||||||
] as unknown as RuleExports[];
|
] as unknown as RuleExports[];
|
||||||
|
|
||||||
export = {
|
export = {
|
||||||
|
144
lint/src/rules/html/no-disabled-attr.ts
Normal file
144
lint/src/rules/html/no-disabled-attr.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import {
|
||||||
|
TmplAstBoundAttribute,
|
||||||
|
TmplAstTextAttribute
|
||||||
|
} 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<Message, unknown[]>) {
|
||||||
|
const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some dynamic angular inputs will have disabled as name because of how Angular handles this internally (e.g [class.disabled]="isDisabled")
|
||||||
|
* But these aren't actually the disabled attribute we're looking for, we can determine this by checking the details of the keySpan
|
||||||
|
*/
|
||||||
|
function isOtherAttributeDisabled(node: TmplAstBoundAttribute | TmplAstTextAttribute): boolean {
|
||||||
|
// if the details are not null, and the details are not 'disabled', then it's not the disabled attribute we're looking for
|
||||||
|
return node.keySpan?.details !== null && node.keySpan?.details !== 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the disabled text with [dsBtnDisabled] in the template
|
||||||
|
*/
|
||||||
|
function replaceDisabledText(text: string ): string {
|
||||||
|
const hasBrackets = text.includes('[') && text.includes(']');
|
||||||
|
const newDisabledText = hasBrackets ? 'dsBtnDisabled' : '[dsBtnDisabled]';
|
||||||
|
return text.replace('disabled', newDisabledText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputIsChildOfButton(node: any): boolean {
|
||||||
|
return (node.parent?.tagName === 'button' || node.parent?.name === 'button');
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportAndFix(node: TmplAstBoundAttribute | TmplAstTextAttribute) {
|
||||||
|
if (!inputIsChildOfButton(node) || isOtherAttributeDisabled(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceSpan = node.sourceSpan;
|
||||||
|
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 = replaceDisabledText(disabledText);
|
||||||
|
return fixer.replaceTextRange([sourceSpan.start.offset, sourceSpan.end.offset], newText);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'BoundAttribute[name="disabled"]'(node: TmplAstBoundAttribute) {
|
||||||
|
reportAndFix(node);
|
||||||
|
},
|
||||||
|
'TextAttribute[name="disabled"]'(node: TmplAstTextAttribute) {
|
||||||
|
reportAndFix(node);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tests = {
|
||||||
|
plugin: info.name,
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
name: 'should use [dsBtnDisabled] in HTML templates',
|
||||||
|
code: `
|
||||||
|
<button [dsBtnDisabled]="true">Submit</button>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'disabled attribute is still valid on non-button elements',
|
||||||
|
code: `
|
||||||
|
<input disabled="true">
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '[disabled] attribute is still valid on non-button elements',
|
||||||
|
code: `
|
||||||
|
<input [disabled]="true">
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'angular dynamic attributes that use disabled are still valid',
|
||||||
|
code: `
|
||||||
|
<button [class.disabled]="isDisabled">Submit</button>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
name: 'should not use disabled attribute in HTML templates',
|
||||||
|
code: `
|
||||||
|
<button disabled="true">Submit</button>
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
|
||||||
|
output: `
|
||||||
|
<button [dsBtnDisabled]="true">Submit</button>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should not use [disabled] attribute in HTML templates',
|
||||||
|
code: `
|
||||||
|
<button [disabled]="true">Submit</button>
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
|
||||||
|
output: `
|
||||||
|
<button [dsBtnDisabled]="true">Submit</button>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as NamedTests;
|
||||||
|
|
||||||
|
export default rule;
|
Reference in New Issue
Block a user