From 44ef8fa6308d2ed43ed190b371d8ab313f7cea75 Mon Sep 17 00:00:00 2001 From: Kyzyl-ool Kezhik Date: Tue, 7 Nov 2023 17:13:31 +0100 Subject: [PATCH] feat: accessible descriptions and labels for various components (#674) * feat: added descriptions for content buttons, links, basic cards, caption --- src/blocks/Header/Header.tsx | 8 ++++++- src/blocks/Tabs/Tabs.tsx | 14 +++++++++++- src/components/BackLink/BackLink.tsx | 7 ++++-- src/components/Button/Button.tsx | 1 - src/components/FileLink/FileLink.tsx | 2 ++ .../FullscreenImage/FullscreenImage.tsx | 7 +++--- src/components/Image/Image.tsx | 2 ++ src/components/Link/Link.tsx | 4 ++++ src/components/Title/Title.tsx | 4 +++- src/models/constructor-items/blocks.ts | 2 ++ src/models/constructor-items/common.ts | 7 ++++-- src/sub-blocks/BasicCard/BasicCard.tsx | 12 +++++++++- src/sub-blocks/Content/Content.tsx | 22 ++++++++++++++++++- 13 files changed, 79 insertions(+), 13 deletions(-) diff --git a/src/blocks/Header/Header.tsx b/src/blocks/Header/Header.tsx index 4d9037854..3ebe6e29a 100644 --- a/src/blocks/Header/Header.tsx +++ b/src/blocks/Header/Header.tsx @@ -1,5 +1,7 @@ import React, {useContext} from 'react'; +import {useUniqId} from '@gravity-ui/uikit'; + import {Button, HTML, Media, RouterLink} from '../../components'; import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs/HeaderBreadcrumbs'; import {getMediaImage} from '../../components/Media/Image/utils'; @@ -96,6 +98,7 @@ export const HeaderBlock = (props: WithChildren) => { const imageThemed = image && getThemedValue(image, theme); const videoThemed = video && getThemedValue(video, theme); const fullWidth = backgroundThemed?.fullWidth || backgroundThemed?.fullWidthMedia; + const titleId = useUniqId(); return (
) => { {overtitle} )} -

+

{status} {renderTitle ? renderTitle(title) : {title}}

@@ -157,6 +160,9 @@ export const HeaderBlock = (props: WithChildren) => { key={index} className={b('button')} size="xl" + extraProps={{ + 'aria-describedby': titleId, + }} {...button} /> diff --git a/src/blocks/Tabs/Tabs.tsx b/src/blocks/Tabs/Tabs.tsx index 8ac8db90f..84a677338 100644 --- a/src/blocks/Tabs/Tabs.tsx +++ b/src/blocks/Tabs/Tabs.tsx @@ -1,5 +1,7 @@ import React, {Fragment, useRef, useState} from 'react'; +import {useUniqId} from '@gravity-ui/uikit'; + import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; import ButtonTabs, {ButtonTabsItemProps} from '../../components/ButtonTabs/ButtonTabs'; import FullscreenImage from '../../components/FullscreenImage/FullscreenImage'; @@ -36,6 +38,7 @@ export const TabsBlock = ({ const ref = useRef(null); const mediaWidth = ref?.current?.offsetWidth; const mediaHeight = mediaWidth && getHeight(mediaWidth); + const captionId = useUniqId(); let imageProps; @@ -43,6 +46,11 @@ export const TabsBlock = ({ const themedImage = getThemedValue(activeTabData.image, theme); imageProps = themedImage && getMediaImage(themedImage); + if (activeTabData.caption && imageProps) { + Object.assign(imageProps, { + 'aria-describedby': captionId, + }); + } } const showMedia = Boolean(activeTabData?.media || imageProps); @@ -99,7 +107,11 @@ export const TabsBlock = ({ )} - {activeTabData?.caption &&

{activeTabData.caption}

} + {activeTabData?.caption && ( +

+ {activeTabData.caption} +

+ )} ); diff --git a/src/components/BackLink/BackLink.tsx b/src/components/BackLink/BackLink.tsx index 38311e481..7d2973abb 100644 --- a/src/components/BackLink/BackLink.tsx +++ b/src/components/BackLink/BackLink.tsx @@ -1,4 +1,4 @@ -import React, {ReactNode, useCallback, useContext} from 'react'; +import React, {HTMLProps, ReactNode, useCallback, useContext} from 'react'; import {Button, ButtonSize, Icon} from '@gravity-ui/uikit'; @@ -9,7 +9,7 @@ import {DefaultEventNames, Tabbable} from '../../models'; export type Theme = 'default' | 'special'; -export interface BackLinkProps extends Tabbable { +export interface BackLinkProps extends Tabbable { url: string; title: ReactNode; theme?: Theme; @@ -17,6 +17,7 @@ export interface BackLinkProps extends Tabbable { className?: string; shouldHandleBackAction?: boolean; onClick?: () => void; + extraProps?: HTMLProps; } export default function BackLink(props: BackLinkProps) { @@ -30,6 +31,7 @@ export default function BackLink(props: BackLinkProps) { shouldHandleBackAction = false, onClick, tabIndex, + extraProps, } = props; const handleAnalytics = useAnalytics(DefaultEventNames.ShareButton, url); @@ -59,6 +61,7 @@ export default function BackLink(props: BackLinkProps) { href={shouldHandleBackAction ? undefined : url} onClick={shouldHandleBackAction ? backActionHandler : undefined} tabIndex={tabIndex} + extraProps={extraProps} > {title} diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 3150076af..a2915403f 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -18,7 +18,6 @@ import './Button.scss'; export interface ButtonProps extends Omit, QAProps { className?: string; url?: string; - urlTitle?: string; onClick?: React.MouseEventHandler; } diff --git a/src/components/FileLink/FileLink.tsx b/src/components/FileLink/FileLink.tsx index 546b461bd..970f00448 100644 --- a/src/components/FileLink/FileLink.tsx +++ b/src/components/FileLink/FileLink.tsx @@ -57,6 +57,7 @@ const FileLink = (props: WithChildren) => { onClick, tabIndex, urlTitle, + extraProps, } = props; const fileExt = getFileExt(href) as FileExtension; const labelTheme = (FileExtensionThemes[fileExt] || 'unknown') as LabelProps['theme']; @@ -74,6 +75,7 @@ const FileLink = (props: WithChildren) => { tabIndex={tabIndex} title={urlTitle} {...getLinkProps(href, hostname)} + {...extraProps} > {text} diff --git a/src/components/FullscreenImage/FullscreenImage.tsx b/src/components/FullscreenImage/FullscreenImage.tsx index c5605d512..33e73023c 100644 --- a/src/components/FullscreenImage/FullscreenImage.tsx +++ b/src/components/FullscreenImage/FullscreenImage.tsx @@ -1,4 +1,4 @@ -import React, {CSSProperties, useState} from 'react'; +import React, {CSSProperties, HTMLProps, useState} from 'react'; import {Icon, Modal} from '@gravity-ui/uikit'; @@ -14,6 +14,7 @@ export interface FullscreenImageProps extends ImageProps { imageClassName?: string; modalImageClass?: string; imageStyle?: CSSProperties; + extraProps?: HTMLProps; } const b = block('fullscreen-image'); @@ -21,14 +22,14 @@ const FULL_SCREEN_ICON_SIZE = 18; const CLOSE_ICON_SIZE = 30; const FullscreenImage = (props: FullscreenImageProps) => { - const {imageClassName, modalImageClass, imageStyle, alt = i18n('img-alt')} = props; + const {imageClassName, modalImageClass, imageStyle, alt = i18n('img-alt'), extraProps} = props; const [isOpened, setIsOpened] = useState(false); const openModal = () => setIsOpened(true); const closeModal = () => setIsOpened(false); return ( -
+
{ onClick, containerClassName, qa, + ...rest } = props; const [imgLoadingError, setImgLoadingError] = useState(false); @@ -113,6 +114,7 @@ const Image = (props: ImageProps) => { style={style} onClick={onClick} onError={() => setImgLoadingError(true)} + {...rest} /> ); diff --git a/src/components/Link/Link.tsx b/src/components/Link/Link.tsx index bcde8c025..9d772549d 100644 --- a/src/components/Link/Link.tsx +++ b/src/components/Link/Link.tsx @@ -58,6 +58,7 @@ const LinkBlock = (props: WithChildren) => { tabIndex, qa, urlTitle, + extraProps, } = props; const qaAttributes = getQaAttrubutes(qa, ['normal']); @@ -82,6 +83,7 @@ const LinkBlock = (props: WithChildren) => { url={href} onClick={onClick} tabIndex={tabIndex} + extraProps={extraProps} /> ); case 'file-link': @@ -94,6 +96,7 @@ const LinkBlock = (props: WithChildren) => { textSize={textSize} onClick={onClick} tabIndex={tabIndex} + extraProps={extraProps} /> ); case 'normal': { @@ -109,6 +112,7 @@ const LinkBlock = (props: WithChildren) => { title={urlTitle} {...linkProps} data-qa={qaAttributes.normal} + {...extraProps} > {arrow ? ( diff --git a/src/components/Title/Title.tsx b/src/components/Title/Title.tsx index be9efbb8a..4daa0f398 100644 --- a/src/components/Title/Title.tsx +++ b/src/components/Title/Title.tsx @@ -13,6 +13,7 @@ const b = block('title'); export interface TitleProps extends TitleParams { colSizes?: GridColumnSizesType; + id?: string; } const Title = ({ @@ -20,6 +21,7 @@ const Title = ({ subtitle, className, colSizes = {all: 12, sm: 8}, + id, }: TitleProps & ClassNameProps) => { if (!title && !subtitle) { return null; @@ -29,7 +31,7 @@ const Title = ({ !title || typeof title === 'string' ? ({text: title} as TitleItemProps) : title; return ( -
+
{text && ( diff --git a/src/models/constructor-items/blocks.ts b/src/models/constructor-items/blocks.ts index 1558424b3..7e04338eb 100644 --- a/src/models/constructor-items/blocks.ts +++ b/src/models/constructor-items/blocks.ts @@ -352,7 +352,9 @@ export interface ContentItemProps { export interface ContentBlockProps { title?: TitleItemBaseProps | string; + titleId?: string; text?: string; + textId?: string; additionalInfo?: string; links?: LinkProps[]; buttons?: ButtonProps[]; diff --git a/src/models/constructor-items/common.ts b/src/models/constructor-items/common.ts index b615819cf..b35814786 100644 --- a/src/models/constructor-items/common.ts +++ b/src/models/constructor-items/common.ts @@ -1,4 +1,4 @@ -import React, {CSSProperties, ReactNode} from 'react'; +import React, {CSSProperties, HTMLProps, ReactNode} from 'react'; import {ButtonView, ButtonProps as UikitButtonProps} from '@gravity-ui/uikit'; @@ -124,7 +124,7 @@ interface LoopProps { // images -export interface ImageInfoProps { +export interface ImageInfoProps extends Pick, 'aria-describedby'> { alt?: string; disableCompress?: boolean; } @@ -179,6 +179,7 @@ export interface LinkProps extends AnalyticsEventsBase, Stylable, Tabbable { target?: string; metrikaGoals?: MetrikaGoal; pixelEvents?: ButtonPixel; + extraProps?: HTMLProps; } export interface FileLinkProps extends ClassNameProps, Tabbable { @@ -189,6 +190,7 @@ export interface FileLinkProps extends ClassNameProps, Tabbable { theme?: ContentTheme; urlTitle?: string; onClick?: () => void; + extraProps?: HTMLProps; } // buttons @@ -205,6 +207,7 @@ export interface ButtonProps Pick { text: string; url: string; + urlTitle?: string; primary?: boolean; theme?: ButtonTheme; img?: ButtonImageProps | string; diff --git a/src/sub-blocks/BasicCard/BasicCard.tsx b/src/sub-blocks/BasicCard/BasicCard.tsx index f091a816b..c6552e229 100644 --- a/src/sub-blocks/BasicCard/BasicCard.tsx +++ b/src/sub-blocks/BasicCard/BasicCard.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import {useUniqId} from '@gravity-ui/uikit'; + import {Content} from '../'; import CardBase from '../../components/CardBase/CardBase'; import Image from '../../components/Image/Image'; @@ -24,9 +26,15 @@ const BasicCard = (props: BasicCardProps) => { ...cardParams } = props; const iconProps = icon && getMediaImage(icon); + const titleId = useUniqId(); + const descriptionId = useUniqId(); return ( - +
{iconProps && ( @@ -37,7 +45,9 @@ const BasicCard = (props: BasicCardProps) => { )} { const { title, + titleId: titleIdFromProps, text, + textId, additionalInfo, size = 'l', links, @@ -69,6 +73,8 @@ const Content = (props: ContentProps) => { : title; const hasTitle = Boolean(title); + const defaultTitleId = useUniqId(); + const titleId = titleIdFromProps || defaultTitleId; return ( { sizes={colSizes} qa={qaAttributes.container} > - {title && } + {title && ( + <Title + className={b('title')} + title={titleProps} + colSizes={{all: 12}} + id={titleId} + /> + )} {text && ( <div className={b('text', {['without-title']: !hasTitle})}> <YFMWrapper content={text} modifiers={{constructor: true, [`constructor-size-${size}`]: true}} + id={textId} /> </div> )} @@ -108,6 +122,9 @@ const Content = (props: ContentProps) => { textSize={getLinkSize(size)} key={link.url} qa={qaAttributes.link} + extraProps={{ + 'aria-describedby': link.urlTitle ? undefined : titleId, + }} /> ))} </div> @@ -121,6 +138,9 @@ const Content = (props: ContentProps) => { key={item.url} size={getButtonSize(size)} qa={qaAttributes.button} + extraProps={{ + 'aria-describedby': item.urlTitle ? undefined : titleId, + }} /> ))} </div>