diff --git a/README.md b/README.md index 22c9d86..94aa29e 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po | [emptyswatch-needs-labelling](docs/rules/emptyswatch-needs-labelling.md) | Accessibility: EmptySwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | ✅ | | | | [field-needs-labelling](docs/rules/field-needs-labelling.md) | Accessibility: Field must have label | ✅ | | | | [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 | ✅ | | | +| [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="". | ✅ | | | | [imageswatch-needs-labelling](docs/rules/imageswatch-needs-labelling.md) | Accessibility: ImageSwatch must have an accessible name via aria-label, Tooltip, aria-labelledby, etc.. | ✅ | | | | [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 | ✅ | | | | [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. | ✅ | | 🔧 | diff --git a/docs/rules/image-needs-alt.md b/docs/rules/image-needs-alt.md new file mode 100644 index 0000000..452769c --- /dev/null +++ b/docs/rules/image-needs-alt.md @@ -0,0 +1,34 @@ +# 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`) + +💼 This rule is enabled in the ✅ `recommended` config. + + + +## Rule details + +This rule requires all `` 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=""`). + + +Examples of **incorrect** code for this rule: + +```jsx + +``` + +```jsx +{null} +``` + +Examples of **correct** code for this rule: + +```jsx +A dog playing in a park. +``` + +```jsx + +``` + +## Further Reading + +- [`` Accessibility](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#accessibility) diff --git a/lib/index.ts b/lib/index.ts index 1a49a29..8fd98c1 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -33,6 +33,7 @@ module.exports = { "@microsoft/fluentui-jsx-a11y/emptyswatch-needs-labelling": "error", "@microsoft/fluentui-jsx-a11y/field-needs-labelling": "error", "@microsoft/fluentui-jsx-a11y/image-button-missing-aria": "error", + "@microsoft/fluentui-jsx-a11y/image-needs-alt": "error", "@microsoft/fluentui-jsx-a11y/imageswatch-needs-labelling": "error", "@microsoft/fluentui-jsx-a11y/input-components-require-accessible-name": "error", "@microsoft/fluentui-jsx-a11y/link-missing-labelling": "error", @@ -74,6 +75,7 @@ module.exports = { "emptyswatch-needs-labelling": rules.emptySwatchNeedsLabelling, "field-needs-labelling": rules.fieldNeedsLabelling, "image-button-missing-aria": rules.imageButtonMissingAria, + "image-needs-alt": rules.imageNeedsAlt, "imageswatch-needs-labelling": rules.imageSwatchNeedsLabelling, "input-components-require-accessible-name": rules.inputComponentsRequireAccessibleName, "link-missing-labelling": rules.linkMissingLabelling, diff --git a/lib/rules/image-needs-alt.ts b/lib/rules/image-needs-alt.ts new file mode 100644 index 0000000..8d8f2fb --- /dev/null +++ b/lib/rules/image-needs-alt.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ESLintUtils } from "@typescript-eslint/utils"; +import { makeLabeledControlRule } from "../util/ruleFactory"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const rule = ESLintUtils.RuleCreator.withoutDocs( + makeLabeledControlRule({ + component: "Image", + messageId: "imageNeedsAlt", + description: + 'Accessibility: Image must have alt attribute with a meaningful description of the image. If the image is decorative, use alt="".', + requiredProps: ["alt"], + allowFieldParent: false, + allowHtmlFor: false, + allowLabelledBy: false, + allowWrappingLabel: false, + allowTooltipParent: false, + allowDescribedBy: false, + allowLabeledChild: false + }) +); + +export default rule; diff --git a/lib/rules/index.ts b/lib/rules/index.ts index d246932..d0db540 100644 --- a/lib/rules/index.ts +++ b/lib/rules/index.ts @@ -16,6 +16,7 @@ export { default as dialogsurfaceNeedsAria } from "./dialogsurface-needs-aria"; export { default as dropdownNeedsLabelling } from "./dropdown-needs-labelling"; export { default as fieldNeedsLabelling } from "./field-needs-labelling"; export { default as imageButtonMissingAria } from "./buttons/image-button-missing-aria"; +export { default as imageNeedsAlt } from "./image-needs-alt"; export { default as inputComponentsRequireAccessibleName } from "./input-components-require-accessible-name"; export { default as linkMissingLabelling } from "./link-missing-labelling"; export { default as menuItemNeedsLabelling } from "./menu-item-needs-labelling"; diff --git a/lib/util/hasDefinedProp.ts b/lib/util/hasDefinedProp.ts new file mode 100644 index 0000000..43fd81e --- /dev/null +++ b/lib/util/hasDefinedProp.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { TSESTree } from "@typescript-eslint/utils"; +import { JSXOpeningElement } from "estree-jsx"; +import { hasProp, getPropValue, getProp } from "jsx-ast-utils"; + +/** + * Determines if the property exists and has a non-nullish value. + * @param attributes The attributes on the visited node + * @param name The name of the prop to check + * @returns Whether the specified prop exists and is not null or undefined + * @example + * // + * hasDefinedProp(attributes, 'src') // true + * // + * hasDefinedProp(attributes, 'src') // true + * // + * hasDefinedProp(attributes, 'src') // false + * // + * hasDefinedProp(attributes, 'src') // false + * // + * hasDefinedProp(attributes, 'src') // false + * // + * hasDefinedProp(attributes, 'src') // false + * // + * hasDefinedProp(attributes, 'src') // false + */ +const hasDefinedProp = (attributes: TSESTree.JSXOpeningElement["attributes"], name: string): boolean => { + if (!hasProp(attributes as JSXOpeningElement["attributes"], name)) { + return false; + } + + const prop = getProp(attributes as JSXOpeningElement["attributes"], name); + + // Safely get the value of the prop, handling potential undefined or null values + const propValue = prop ? getPropValue(prop) : undefined; + + // Return true if the prop value is not null or undefined + return propValue !== null && propValue !== undefined; +}; + +export { hasDefinedProp }; diff --git a/lib/util/ruleFactory.ts b/lib/util/ruleFactory.ts index 17352ce..7e285f7 100644 --- a/lib/util/ruleFactory.ts +++ b/lib/util/ruleFactory.ts @@ -14,26 +14,43 @@ import { elementType } from "jsx-ast-utils"; import { JSXOpeningElement } from "estree-jsx"; import { hasToolTipParent } from "./hasTooltipParent"; import { hasLabeledChild } from "./hasLabeledChild"; +import { hasDefinedProp } from "./hasDefinedProp"; import { hasTextContentChild } from "./hasTextContentChild"; +/** + * Configuration options for a rule created via the `ruleFactory` + */ export type LabeledControlConfig = { + /** The name of the component that the rule applies to. @example 'Image', /Image|Icon/ */ component: string | RegExp; + /** The unique id of the problem message. @example 'itemNeedsLabel' */ messageId: string; + /** A short description of the rule. */ description: string; - labelProps: string[]; // e.g. ["aria-label", "title", "label"] - allowFieldParent: boolean; // Accept a parent wrapper as providing the label. - allowHtmlFor: boolean; // Accept