| [9af201e] | 1 | "use strict";
|
|---|
| 2 |
|
|---|
| 3 | Object.defineProperty(exports, "__esModule", {
|
|---|
| 4 | value: true
|
|---|
| 5 | });
|
|---|
| 6 | exports["default"] = void 0;
|
|---|
| 7 | var _jsxAstUtils = require("jsx-ast-utils");
|
|---|
| 8 | var _arrayPrototype = _interopRequireDefault(require("array.prototype.flatmap"));
|
|---|
| 9 | var _schemas = require("../util/schemas");
|
|---|
| 10 | var _getElementType = _interopRequireDefault(require("../util/getElementType"));
|
|---|
| 11 | var _hasAccessibleChild = _interopRequireDefault(require("../util/hasAccessibleChild"));
|
|---|
| 12 | var _isPresentationRole = _interopRequireDefault(require("../util/isPresentationRole"));
|
|---|
| 13 | function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
|
|---|
| 14 | /**
|
|---|
| 15 | * @fileoverview Enforce all elements that require alternative text have it.
|
|---|
| 16 | * @author Ethan Cohen
|
|---|
| 17 | */
|
|---|
| 18 |
|
|---|
| 19 | // ----------------------------------------------------------------------------
|
|---|
| 20 | // Rule Definition
|
|---|
| 21 | // ----------------------------------------------------------------------------
|
|---|
| 22 |
|
|---|
| 23 | var DEFAULT_ELEMENTS = ['img', 'object', 'area', 'input[type="image"]'];
|
|---|
| 24 | var schema = (0, _schemas.generateObjSchema)({
|
|---|
| 25 | elements: _schemas.arraySchema,
|
|---|
| 26 | img: _schemas.arraySchema,
|
|---|
| 27 | object: _schemas.arraySchema,
|
|---|
| 28 | area: _schemas.arraySchema,
|
|---|
| 29 | 'input[type="image"]': _schemas.arraySchema
|
|---|
| 30 | });
|
|---|
| 31 | var ariaLabelHasValue = function ariaLabelHasValue(prop) {
|
|---|
| 32 | var value = (0, _jsxAstUtils.getPropValue)(prop);
|
|---|
| 33 | if (value === undefined) {
|
|---|
| 34 | return false;
|
|---|
| 35 | }
|
|---|
| 36 | if (typeof value === 'string' && value.length === 0) {
|
|---|
| 37 | return false;
|
|---|
| 38 | }
|
|---|
| 39 | return true;
|
|---|
| 40 | };
|
|---|
| 41 | var ruleByElement = {
|
|---|
| 42 | img(context, node, nodeType) {
|
|---|
| 43 | var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt');
|
|---|
| 44 |
|
|---|
| 45 | // Missing alt prop error.
|
|---|
| 46 | if (altProp === undefined) {
|
|---|
| 47 | if ((0, _isPresentationRole["default"])(nodeType, node.attributes)) {
|
|---|
| 48 | context.report({
|
|---|
| 49 | node,
|
|---|
| 50 | message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.'
|
|---|
| 51 | });
|
|---|
| 52 | return;
|
|---|
| 53 | }
|
|---|
| 54 | // Check for `aria-label` to provide text alternative
|
|---|
| 55 | // Don't create an error if the attribute is used correctly. But if it
|
|---|
| 56 | // isn't, suggest that the developer use `alt` instead.
|
|---|
| 57 | var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
|
|---|
| 58 | if (ariaLabelProp !== undefined) {
|
|---|
| 59 | if (!ariaLabelHasValue(ariaLabelProp)) {
|
|---|
| 60 | context.report({
|
|---|
| 61 | node,
|
|---|
| 62 | message: 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.'
|
|---|
| 63 | });
|
|---|
| 64 | }
|
|---|
| 65 | return;
|
|---|
| 66 | }
|
|---|
| 67 | // Check for `aria-labelledby` to provide text alternative
|
|---|
| 68 | // Don't create an error if the attribute is used correctly. But if it
|
|---|
| 69 | // isn't, suggest that the developer use `alt` instead.
|
|---|
| 70 | var ariaLabelledbyProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
|
|---|
| 71 | if (ariaLabelledbyProp !== undefined) {
|
|---|
| 72 | if (!ariaLabelHasValue(ariaLabelledbyProp)) {
|
|---|
| 73 | context.report({
|
|---|
| 74 | node,
|
|---|
| 75 | message: 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.'
|
|---|
| 76 | });
|
|---|
| 77 | }
|
|---|
| 78 | return;
|
|---|
| 79 | }
|
|---|
| 80 | context.report({
|
|---|
| 81 | node,
|
|---|
| 82 | message: "".concat(nodeType, " elements must have an alt prop, either with meaningful text, or an empty string for decorative images.")
|
|---|
| 83 | });
|
|---|
| 84 | return;
|
|---|
| 85 | }
|
|---|
| 86 |
|
|---|
| 87 | // Check if alt prop is undefined.
|
|---|
| 88 | var altValue = (0, _jsxAstUtils.getPropValue)(altProp);
|
|---|
| 89 | var isNullValued = altProp.value === null; // <img alt />
|
|---|
| 90 |
|
|---|
| 91 | if (altValue && !isNullValued || altValue === '') {
|
|---|
| 92 | return;
|
|---|
| 93 | }
|
|---|
| 94 |
|
|---|
| 95 | // Undefined alt prop error.
|
|---|
| 96 | context.report({
|
|---|
| 97 | node,
|
|---|
| 98 | message: "Invalid alt value for ".concat(nodeType, ". Use alt=\"\" for presentational images.")
|
|---|
| 99 | });
|
|---|
| 100 | },
|
|---|
| 101 | object(context, node, unusedNodeType, elementType) {
|
|---|
| 102 | var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
|
|---|
| 103 | var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
|
|---|
| 104 | var hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
|
|---|
| 105 | var titleProp = (0, _jsxAstUtils.getLiteralPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'title'));
|
|---|
| 106 | var hasTitleAttr = !!titleProp;
|
|---|
| 107 | if (hasLabel || hasTitleAttr || (0, _hasAccessibleChild["default"])(node.parent, elementType)) {
|
|---|
| 108 | return;
|
|---|
| 109 | }
|
|---|
| 110 | context.report({
|
|---|
| 111 | node,
|
|---|
| 112 | message: 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.'
|
|---|
| 113 | });
|
|---|
| 114 | },
|
|---|
| 115 | area(context, node) {
|
|---|
| 116 | var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
|
|---|
| 117 | var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
|
|---|
| 118 | var hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
|
|---|
| 119 | if (hasLabel) {
|
|---|
| 120 | return;
|
|---|
| 121 | }
|
|---|
| 122 | var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt');
|
|---|
| 123 | if (altProp === undefined) {
|
|---|
| 124 | context.report({
|
|---|
| 125 | node,
|
|---|
| 126 | message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
|
|---|
| 127 | });
|
|---|
| 128 | return;
|
|---|
| 129 | }
|
|---|
| 130 | var altValue = (0, _jsxAstUtils.getPropValue)(altProp);
|
|---|
| 131 | var isNullValued = altProp.value === null; // <area alt />
|
|---|
| 132 |
|
|---|
| 133 | if (altValue && !isNullValued || altValue === '') {
|
|---|
| 134 | return;
|
|---|
| 135 | }
|
|---|
| 136 | context.report({
|
|---|
| 137 | node,
|
|---|
| 138 | message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
|
|---|
| 139 | });
|
|---|
| 140 | },
|
|---|
| 141 | 'input[type="image"]': function inputImage(context, node, nodeType) {
|
|---|
| 142 | // Only test input[type="image"]
|
|---|
| 143 | if (nodeType === 'input') {
|
|---|
| 144 | var typePropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'type'));
|
|---|
| 145 | if (typePropValue !== 'image') {
|
|---|
| 146 | return;
|
|---|
| 147 | }
|
|---|
| 148 | }
|
|---|
| 149 | var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
|
|---|
| 150 | var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
|
|---|
| 151 | var hasLabel = ariaLabelHasValue(ariaLabelProp) || ariaLabelHasValue(arialLabelledByProp);
|
|---|
| 152 | if (hasLabel) {
|
|---|
| 153 | return;
|
|---|
| 154 | }
|
|---|
| 155 | var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt');
|
|---|
| 156 | if (altProp === undefined) {
|
|---|
| 157 | context.report({
|
|---|
| 158 | node,
|
|---|
| 159 | message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
|
|---|
| 160 | });
|
|---|
| 161 | return;
|
|---|
| 162 | }
|
|---|
| 163 | var altValue = (0, _jsxAstUtils.getPropValue)(altProp);
|
|---|
| 164 | var isNullValued = altProp.value === null; // <area alt />
|
|---|
| 165 |
|
|---|
| 166 | if (altValue && !isNullValued || altValue === '') {
|
|---|
| 167 | return;
|
|---|
| 168 | }
|
|---|
| 169 | context.report({
|
|---|
| 170 | node,
|
|---|
| 171 | message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
|
|---|
| 172 | });
|
|---|
| 173 | }
|
|---|
| 174 | };
|
|---|
| 175 | var _default = exports["default"] = {
|
|---|
| 176 | meta: {
|
|---|
| 177 | docs: {
|
|---|
| 178 | url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/alt-text.md',
|
|---|
| 179 | description: 'Enforce all elements that require alternative text have meaningful information to relay back to end user.'
|
|---|
| 180 | },
|
|---|
| 181 | schema: [schema]
|
|---|
| 182 | },
|
|---|
| 183 | create: function create(context) {
|
|---|
| 184 | var options = context.options[0] || {};
|
|---|
| 185 | // Elements to validate for alt text.
|
|---|
| 186 | var elementOptions = options.elements || DEFAULT_ELEMENTS;
|
|---|
| 187 | // Get custom components for just the elements that will be tested.
|
|---|
| 188 | var customComponents = (0, _arrayPrototype["default"])(elementOptions, function (element) {
|
|---|
| 189 | return options[element];
|
|---|
| 190 | });
|
|---|
| 191 | var typesToValidate = new Set([].concat(customComponents, elementOptions).map(function (type) {
|
|---|
| 192 | return type === 'input[type="image"]' ? 'input' : type;
|
|---|
| 193 | }));
|
|---|
| 194 | var elementType = (0, _getElementType["default"])(context);
|
|---|
| 195 | return {
|
|---|
| 196 | JSXOpeningElement(node) {
|
|---|
| 197 | var nodeType = elementType(node);
|
|---|
| 198 | if (!typesToValidate.has(nodeType)) {
|
|---|
| 199 | return;
|
|---|
| 200 | }
|
|---|
| 201 | var DOMElement = nodeType;
|
|---|
| 202 | if (DOMElement === 'input') {
|
|---|
| 203 | DOMElement = 'input[type="image"]';
|
|---|
| 204 | }
|
|---|
| 205 |
|
|---|
| 206 | // Map nodeType to the DOM element if we are running this on a custom component.
|
|---|
| 207 | if (elementOptions.indexOf(DOMElement) === -1) {
|
|---|
| 208 | DOMElement = elementOptions.find(function (element) {
|
|---|
| 209 | var customComponentsForElement = options[element] || [];
|
|---|
| 210 | return customComponentsForElement.indexOf(nodeType) > -1;
|
|---|
| 211 | });
|
|---|
| 212 | }
|
|---|
| 213 | ruleByElement[DOMElement](context, node, nodeType, elementType);
|
|---|
| 214 | }
|
|---|
| 215 | };
|
|---|
| 216 | }
|
|---|
| 217 | };
|
|---|
| 218 | module.exports = exports.default; |
|---|