Skip to content

Commit eccfd93

Browse files
author
Iryna Vasylenko
committed
Fix merge conflicts
2 parents 0d2c950 + 80724a6 commit eccfd93

File tree

16 files changed

+385
-36
lines changed

16 files changed

+385
-36
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po
126126
| [emptyswatch-needs-labelling](docs/rules/emptyswatch-needs-labelling.md) | Accessibility: EmptySwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | ✅ | | |
127127
| [field-needs-labelling](docs/rules/field-needs-labelling.md) | Accessibility: Field must have label | ✅ | | |
128128
| [image-button-missing-aria](docs/rules/image-button-missing-aria.md) | Accessibility: Image buttons must have accessible labelling: title, aria-label, aria-labelledby, aria-describedby | ✅ | | |
129+
| [image-needs-alt](docs/rules/image-needs-alt.md) | Accessibility: Image must have alt attribute with a meaningful description of the image. If the image is decorative, use alt="". | ✅ | | |
129130
| [imageswatch-needs-labelling](docs/rules/imageswatch-needs-labelling.md) | Accessibility: ImageSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | ✅ | | |
130131
| [input-components-require-accessible-name](docs/rules/input-components-require-accessible-name.md) | Accessibility: Input fields must have accessible labelling: aria-label, aria-labelledby or an associated label | ✅ | | |
131132
| [link-missing-labelling](docs/rules/link-missing-labelling.md) | Accessibility: Image links must have an accessible name. Add either text content, labelling to the image or labelling to the link itself. | ✅ | | 🔧 |
@@ -148,4 +149,4 @@ Any use of third-party trademarks or logos are subject to those third-party's po
148149
| [tooltip-not-recommended](docs/rules/tooltip-not-recommended.md) | Accessibility: Prefer text content or aria over a tooltip for these components MenuItem, SpinButton | ✅ | | |
149150
| [visual-label-better-than-aria-suggestion](docs/rules/visual-label-better-than-aria-suggestion.md) | Visual label is better than an aria-label because sighted users can't read the aria-label text. | | ✅ | |
150151

151-
<!-- end auto-generated rules list -->
152+
<!-- end auto-generated rules list -->

docs/rules/image-needs-alt.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Accessibility: Image must have alt attribute with a meaningful description of the image. If the image is decorative, use alt="" (`@microsoft/fluentui-jsx-a11y/image-needs-alt`)
2+
3+
💼 This rule is enabled in the ✅ `recommended` config.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
## Rule details
8+
9+
This rule requires all `<Image>` components have non-empty alternative text. The `alt` attribute should provide a clear and concise text replacement for the image's content. It should *not* describe the presence of the image itself or the file name of the image. Purely decorative images should have empty `alt` text (`alt=""`).
10+
11+
12+
Examples of **incorrect** code for this rule:
13+
14+
```jsx
15+
<Image src="image.png" />
16+
```
17+
18+
```jsx
19+
<Image src="image.png" alt={null} />
20+
```
21+
22+
Examples of **correct** code for this rule:
23+
24+
```jsx
25+
<Image src="image.png" alt="A dog playing in a park." />
26+
```
27+
28+
```jsx
29+
<Image src="decorative-image.png" alt="" />
30+
```
31+
32+
## Further Reading
33+
34+
- [`<img>` Accessibility](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#accessibility)

lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ module.exports = {
3333
"@microsoft/fluentui-jsx-a11y/emptyswatch-needs-labelling": "error",
3434
"@microsoft/fluentui-jsx-a11y/field-needs-labelling": "error",
3535
"@microsoft/fluentui-jsx-a11y/image-button-missing-aria": "error",
36+
"@microsoft/fluentui-jsx-a11y/image-needs-alt": "error",
3637
"@microsoft/fluentui-jsx-a11y/imageswatch-needs-labelling": "error",
3738
"@microsoft/fluentui-jsx-a11y/input-components-require-accessible-name": "error",
3839
"@microsoft/fluentui-jsx-a11y/link-missing-labelling": "error",
@@ -75,6 +76,7 @@ module.exports = {
7576
"emptyswatch-needs-labelling": rules.emptySwatchNeedsLabelling,
7677
"field-needs-labelling": rules.fieldNeedsLabelling,
7778
"image-button-missing-aria": rules.imageButtonMissingAria,
79+
"image-needs-alt": rules.imageNeedsAlt,
7880
"imageswatch-needs-labelling": rules.imageSwatchNeedsLabelling,
7981
"input-components-require-accessible-name": rules.inputComponentsRequireAccessibleName,
8082
"link-missing-labelling": rules.linkMissingLabelling,

lib/rules/image-needs-alt.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { ESLintUtils } from "@typescript-eslint/utils";
5+
import { makeLabeledControlRule } from "../util/ruleFactory";
6+
7+
//------------------------------------------------------------------------------
8+
// Rule Definition
9+
//------------------------------------------------------------------------------
10+
11+
const rule = ESLintUtils.RuleCreator.withoutDocs(
12+
makeLabeledControlRule({
13+
component: "Image",
14+
messageId: "imageNeedsAlt",
15+
description:
16+
'Accessibility: Image must have alt attribute with a meaningful description of the image. If the image is decorative, use alt="".',
17+
requiredProps: ["alt"],
18+
allowFieldParent: false,
19+
allowHtmlFor: false,
20+
allowLabelledBy: false,
21+
allowWrappingLabel: false,
22+
allowTooltipParent: false,
23+
allowDescribedBy: false,
24+
allowLabeledChild: false
25+
})
26+
);
27+
28+
export default rule;

lib/rules/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export { default as dialogsurfaceNeedsAria } from "./dialogsurface-needs-aria";
1616
export { default as dropdownNeedsLabelling } from "./dropdown-needs-labelling";
1717
export { default as fieldNeedsLabelling } from "./field-needs-labelling";
1818
export { default as imageButtonMissingAria } from "./buttons/image-button-missing-aria";
19+
export { default as imageNeedsAlt } from "./image-needs-alt";
1920
export { default as inputComponentsRequireAccessibleName } from "./input-components-require-accessible-name";
2021
export { default as linkMissingLabelling } from "./link-missing-labelling";
2122
export { default as menuItemNeedsLabelling } from "./menu-item-needs-labelling";

lib/util/hasDefinedProp.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { TSESTree } from "@typescript-eslint/utils";
5+
import { JSXOpeningElement } from "estree-jsx";
6+
import { hasProp, getPropValue, getProp } from "jsx-ast-utils";
7+
8+
/**
9+
* Determines if the property exists and has a non-nullish value.
10+
* @param attributes The attributes on the visited node
11+
* @param name The name of the prop to check
12+
* @returns Whether the specified prop exists and is not null or undefined
13+
* @example
14+
* // <img src="image.png" />
15+
* hasDefinedProp(attributes, 'src') // true
16+
* // <img src="" />
17+
* hasDefinedProp(attributes, 'src') // true
18+
* // <img src={null} />
19+
* hasDefinedProp(attributes, 'src') // false
20+
* // <img src={undefined} />
21+
* hasDefinedProp(attributes, 'src') // false
22+
* // <img src={1} />
23+
* hasDefinedProp(attributes, 'src') // false
24+
* // <img src={true} />
25+
* hasDefinedProp(attributes, 'src') // false
26+
* // <img />
27+
* hasDefinedProp(attributes, 'src') // false
28+
*/
29+
const hasDefinedProp = (attributes: TSESTree.JSXOpeningElement["attributes"], name: string): boolean => {
30+
if (!hasProp(attributes as JSXOpeningElement["attributes"], name)) {
31+
return false;
32+
}
33+
34+
const prop = getProp(attributes as JSXOpeningElement["attributes"], name);
35+
36+
// Safely get the value of the prop, handling potential undefined or null values
37+
const propValue = prop ? getPropValue(prop) : undefined;
38+
39+
// Return true if the prop value is not null or undefined
40+
return propValue !== null && propValue !== undefined;
41+
};
42+
43+
export { hasDefinedProp };

lib/util/ruleFactory.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,34 @@ import { elementType } from "jsx-ast-utils";
1414
import { JSXOpeningElement } from "estree-jsx";
1515
import { hasToolTipParent } from "./hasTooltipParent";
1616
import { hasLabeledChild } from "./hasLabeledChild";
17+
import { hasDefinedProp } from "./hasDefinedProp";
1718
import { hasTextContentChild } from "./hasTextContentChild";
1819
import { hasTriggerProp } from "./hasTriggerProp";
1920

21+
/**
22+
* Configuration options for a rule created via the `ruleFactory`
23+
*/
2024
export type LabeledControlConfig = {
25+
/** The name of the component that the rule applies to. @example 'Image', /Image|Icon/ */
2126
component: string | RegExp;
27+
/** The unique id of the problem message. @example 'itemNeedsLabel' */
2228
messageId: string;
29+
/** A short description of the rule. */
2330
description: string;
24-
labelProps: string[]; // e.g. ["aria-label", "title", "label"]
25-
allowFieldParent: boolean; // Accept a parent <Field label="..."> wrapper as providing the label.
26-
allowHtmlFor: boolean; // Accept <label htmlFor="..."> association.
27-
allowLabelledBy: boolean; // Accept aria-labelledby association.
28-
allowWrappingLabel: boolean; // Accept being wrapped in a <label> element.
29-
allowTooltipParent: boolean; // Accept a parent <Tooltip content="..."> wrapper as providing the label.
31+
/** Properties that are required to have a non-`null` and non-`undefined` value. @example ["alt"] */
32+
requiredProps?: string[];
33+
/** Labeling properties that are required to have at least one non-empty value. @example ["aria-label", "title", "label"] */
34+
labelProps?: string[];
35+
/** Accept a parent `<Field label="...">` wrapper as providing the label. */
36+
allowFieldParent: boolean;
37+
/** Accept `<label htmlFor="...">` association. */
38+
allowHtmlFor: boolean;
39+
/** Accept aria-labelledby association. */
40+
allowLabelledBy: boolean;
41+
/** Accept being wrapped in a `<label>` element. */
42+
allowWrappingLabel: boolean;
43+
/** Accept a parent `<Tooltip content="...">` wrapper as providing the label. */
44+
allowTooltipParent: boolean;
3045
/**
3146
* Accept aria-describedby as a labeling strategy.
3247
* NOTE: This is discouraged for *primary* labeling; prefer text/aria-label/labelledby.
@@ -63,16 +78,22 @@ export function hasAccessibleLabel(
6378
context: TSESLint.RuleContext<string, []>,
6479
config: LabeledControlConfig
6580
): boolean {
66-
const allowFieldParent = !!config.allowFieldParent;
67-
const allowWrappingLabel = !!config.allowWrappingLabel;
68-
const allowHtmlFor = !!config.allowHtmlFor;
69-
const allowLabelledBy = !!config.allowLabelledBy;
70-
const allowTooltipParent = !!config.allowTooltipParent;
71-
const allowDescribedBy = !!config.allowDescribedBy;
72-
const allowLabeledChild = !!config.allowLabeledChild;
81+
const {
82+
allowFieldParent,
83+
allowWrappingLabel,
84+
allowHtmlFor,
85+
allowLabelledBy,
86+
allowTooltipParent,
87+
allowDescribedBy,
88+
allowLabeledChild,
89+
requiredProps,
90+
labelProps
91+
} = config;
7392
const allowTextContentChild = !!config.allowTextContentChild;
93+
7494
if (allowFieldParent && hasFieldParent(context)) return true;
75-
if (config.labelProps?.some(p => hasNonEmptyProp(opening.attributes, p))) return true;
95+
if (requiredProps?.every(p => hasDefinedProp(opening.attributes, p))) return true;
96+
if (labelProps?.some(p => hasNonEmptyProp(opening.attributes, p))) return true;
7697
if (allowWrappingLabel && isInsideLabelTag(context)) return true;
7798
if (allowHtmlFor && hasAssociatedLabelViaHtmlFor(opening, context)) return true;
7899
if (allowLabelledBy && hasAssociatedLabelViaAriaLabelledBy(opening, context)) return true;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
"lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
4343
"lint:js": "eslint .",
4444
"test": "jest",
45+
"test:branch": "npm run test -- -o",
46+
"test:watch": "npm run test -- --watch",
4547
"lint:docs": "markdownlint **/*.md",
4648
"update:eslint-docs": "eslint-doc-generator",
4749
"fix:md": "npm run lint:docs -- --fix",

scripts/boilerplate/doc.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
const docBoilerplateGenerator = (name, description) => `# ${description} (@microsoft/fluentui-jsx-a11y/${name})
4+
const { withCRLF } = require("./util");
5+
6+
const docBoilerplateGenerator = (name, description) =>
7+
withCRLF(`# ${description} (@microsoft/fluentui-jsx-a11y/${name})
58
69
Write a useful explanation here!
710
@@ -18,5 +21,5 @@ Write more details here!
1821
\`\`\`
1922
2023
## Further Reading
21-
`;
24+
`);
2225
module.exports = docBoilerplateGenerator;

scripts/boilerplate/rule.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
const ruleBoilerplate = (name, description) => `// Copyright (c) Microsoft Corporation.
4+
const { withCRLF } = require("./util");
5+
6+
const ruleBoilerplate = (name, description) =>
7+
withCRLF(`// Copyright (c) Microsoft Corporation.
58
// Licensed under the MIT License.
69
710
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
@@ -41,5 +44,5 @@ const rule = createRule({
4144
});
4245
4346
export default rule;
44-
`;
47+
`);
4548
module.exports = ruleBoilerplate;

0 commit comments

Comments
 (0)