diff --git a/README.md b/README.md index 5770ccff..541faaa4 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ clear to read and to maintain. - [`toBeEmpty`](#tobeempty) - [`toBeInTheDocument`](#tobeinthedocument) - [`toBeInvalid`](#tobeinvalid) + - [`toBeLabelled`](#tobelabelled) - [`toBeRequired`](#toberequired) - [`toBeValid`](#tobevalid) - [`toBeVisible`](#tobevisible) @@ -312,6 +313,78 @@ expect(getByTestId(container, 'invalid-form')).toBeInvalid()
+### `toBeLabelled` + +```typescript +toBeLabelled() +``` + +This allows you to check if a HTML element is correctly labelled. +Labelling element is important for accessibility reason, [especially for non-text content](https://www.w3.org/TR/WCAG20/#text-equiv). + +An element is labelled if it is having at least one of the following: + +- the element is an image having a [not empty `alt` attribute](https://www.w3.org/TR/WCAG20-TECHS/H37.html); +- the element is a SVG element having [a not empty `title` element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title); +- the element is having [an `aria-label` attribute](https://www.w3.org/TR/WCAG20-TECHS/ARIA6.html); +- the element is [referencing a labelling element via `aria-labelledby`](https://www.w3.org/TR/WCAG20-TECHS/ARIA10.html); +- the element is [a form input having a `title` attribute](https://www.w3.org/TR/WCAG20-TECHS/H65.html); +- the element is [an image associated to a short content](https://www.w3.org/TR/WCAG20-TECHS/H2.html) within a button or a link; +- the element is a link, a button or [an object having a text content](https://www.w3.org/TR/WCAG20-TECHS/H53.html). + +#### Examples + +```html +Test alt + + + +Test title + + +

Company Name

+

Test content

+ + +

Company Name

+

Test content

+ ` + * + * @link https://www.w3.org/TR/WCAG20-TECHS/ARIA6.html + */ +function isHavingARIALabel(element) { + return element.hasAttribute('aria-label') +} + +/* + * Checks if the element is labelled by `aria-labelledby` + * + * e.g. `` + * + * @link https://www.w3.org/TR/WCAG20-TECHS/ARIA10.html + */ +function isHavingARIALabelledBy(element) { + return element.hasAttribute('aria-labelledby') +} + +/* + * Checks if meaningful images are having an alt attribute + * + * e.g. `Text alternative` + * + * @link https://www.w3.org/TR/WCAG20-TECHS/H37.html + */ +function isHavingMeaningfulAlt(element) { + return ( + TAGS_WITH_ALT.includes(getTag(element)) && + element.hasAttribute('alt') && + element.getAttribute('alt') !== '' + ) +} + +/* + * Checks if there is a title tag as a child + * + * e.g. `Title for this non-text content` + * + * @link https://www.w3.org/TR/WCAG20-TECHS/H65.html + */ +function isHavingMeaningfulTitle(element) { + const titleTag = element.querySelector('title') + + return ( + TAGS_WITH_TITLE.includes(getTag(element)) && + titleTag !== null && + titleTag.textContent !== null + ) +} + +/* + * Checks if there is a title attribute + * + * e.g. `UN` + */ +function isHavingMeaningfulTitleAttribute(element) { + return ( + TAGS_WITH_TITLE_ATTR.includes(getTag(element)) && + element.hasAttribute('title') && + element.getAttribute('title') !== null + ) +} + +/* + * Checks if there is an adjacent image and text + * + * e.g. `` + * + * @link https://www.w3.org/TR/WCAG20-TECHS/G95.html + */ +function isHavingASiblingLabel(element) { + return ( + TAGS_IMAGES.includes(getTag(element)) && + TAGS_WITH_TEXT_CONTENT.includes(getTag(element.parentElement)) && + element.parentElement.textContent !== '' + ) +} + +/* + * Checks if the image is having a meaningful content with ARIA, an alt attribute or a title attribute + * + * @link https://www.w3.org/TR/WCAG20-TECHS/H2.html + */ +function isHavingAContentText(element) { + const isHavingLabelledImg = + element.querySelector('img') && + (isHavingARIALabel(element.querySelector('img')) || + isHavingARIALabelledBy(element.querySelector('img')) || + isHavingMeaningfulAlt(element.querySelector('img'))) + + const isHavingLabelledSVG = + element.querySelector('svg') && + (isHavingARIALabel(element.querySelector('svg')) || + isHavingARIALabelledBy(element.querySelector('svg')) || + isHavingMeaningfulTitle(element.querySelector('svg'))) + + return ( + TAGS_WITH_TEXT_CONTENT.includes(getTag(element)) && + (element.textContent !== '' || isHavingLabelledImg || isHavingLabelledSVG) + ) +} + +export function toBeLabelled(element) { + checkHtmlElement(element, toBeLabelled, this) + + const isLabelled = + isHavingARIALabel(element) || + isHavingARIALabelledBy(element) || + isHavingMeaningfulAlt(element) || + isHavingMeaningfulTitle(element) || + isHavingMeaningfulTitleAttribute(element) || + isHavingASiblingLabel(element) || + isHavingAContentText(element) + + return { + pass: isLabelled, + message: () => { + const is = isLabelled ? 'is' : 'is not' + return [ + matcherHint(`${this.isNot ? '.not' : ''}.toBeLabelled`, 'element', ''), + '', + `Received element ${is} labelled:`, + ` ${printReceived(element.cloneNode(false))}`, + ].join('\n') + }, + } +}