UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

87 lines 4.15 kB
import { isNodeOfType } from 'eslint-codemod-utils'; import { createLintRule } from '../utils/create-lint-rule'; const elementsAccessibleNameProps = ['label', 'titleId']; const POPUP_PACKAGE = '@atlaskit/popup'; const POPUP_IMPORT_SOURCE = '@atlaskit/popup/popup'; const ENTRY_POINT_POPUP_PACKAGE = '@atlassian/entry-points/popup-trigger'; const rule = createLintRule({ meta: { name: 'use-popup-label', type: 'suggestion', docs: { description: 'Encourages to provide accessible name for Atlassian Design System Popup component.', recommended: true, severity: 'warn' }, messages: { missingLabelProp: 'Missing accessible name. If there is no visible content to associate use `label` prop, otherwise pass id of element to `titleId` prop to be associated as label.', labelPropShouldHaveContents: 'Define string that labels the interactive element.', titleIdShouldHaveValue: '`titleId` should reference the id of element that define accessible name.', noBothPropsUsage: 'Do not include both `titleId` and `label` properties. Use `titleId` if the label text is available in the DOM to reference it, otherwise use `label` to provide accessible name explicitly.' }, hasSuggestions: true }, create(context) { const contextLocalIdentifier = []; return { ImportDeclaration(node) { if (node.source.value === POPUP_PACKAGE || node.source.value === POPUP_IMPORT_SOURCE || node.source.value === ENTRY_POINT_POPUP_PACKAGE) { if (node.specifiers.length) { node.specifiers.forEach(spec => { if (spec.type === 'ImportDefaultSpecifier' && node.source.value !== ENTRY_POINT_POPUP_PACKAGE) { contextLocalIdentifier.push(spec.local.name); } if (spec.type === 'ImportSpecifier' && 'name' in spec.imported) { if (node.source.value === POPUP_IMPORT_SOURCE && spec.imported.name === 'Popup') { contextLocalIdentifier.push(spec.local.name); } if (node.source.value === ENTRY_POINT_POPUP_PACKAGE && spec.imported.name === 'PopupTrigger') { contextLocalIdentifier.push(spec.local.name); } } }); } } }, JSXElement(node) { if (!isNodeOfType(node, 'JSXElement')) { return; } if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { return; } const name = node.openingElement.name.name; if (contextLocalIdentifier.includes(name)) { const componentRoleDialogProp = node.openingElement.attributes.find(attr => isNodeOfType(attr, 'JSXAttribute') && isNodeOfType(attr.name, 'JSXIdentifier') && attr.value && isNodeOfType(attr.value, 'Literal') && attr.name.name === 'role' && attr.value.value === 'dialog'); const componentLabelProps = node.openingElement.attributes.filter(attr => isNodeOfType(attr, 'JSXAttribute') && isNodeOfType(attr.name, 'JSXIdentifier') && elementsAccessibleNameProps.includes(attr.name.name)); if (componentLabelProps.length === 1) { const prop = componentLabelProps[0]; if ('value' in prop && prop.value) { if (isNodeOfType(prop.value, 'Literal') && !prop.value.value || isNodeOfType(prop.value, 'JSXExpressionContainer') && !prop.value.expression) { context.report({ node: prop, messageId: prop.name.name === 'label' ? 'labelPropShouldHaveContents' : 'titleIdShouldHaveValue' }); } } } else if (componentLabelProps.length > 1) { context.report({ node: node.openingElement, messageId: 'noBothPropsUsage' }); } else { if (componentRoleDialogProp) { context.report({ node: node.openingElement, messageId: 'missingLabelProp' }); } else { return; } } } } }; } }); export default rule;