diff --git a/cypress/component/Avatar.spec.tsx b/cypress/component/Avatar.spec.tsx index 5e7ad071a7..8c9153dc6d 100644 --- a/cypress/component/Avatar.spec.tsx +++ b/cypress/component/Avatar.spec.tsx @@ -1,6 +1,22 @@ +import {DecorativeInitials} from '../../modules/react/avatar/stories/examples/DecorativeInitials'; import {Image} from '../../modules/react/avatar/stories/examples/Image'; describe('Avatar', () => { + context('Decorative initials', () => { + beforeEach(() => { + cy.mount(); + }); + + it('should not have any axe errors', () => { + cy.checkA11y(); + }); + + it('should keep the initials visible but hidden from assistive technology beside the name', () => { + cy.findByText('NS').should('be.visible').and('have.attr', 'aria-hidden', 'true'); + cy.findByText('Nicholas Smith').should('be.visible').and('not.have.attr', 'aria-hidden'); + }); + }); + context(`Image Example`, () => { beforeEach(() => { // Stub the remote image so this spec does not depend on picsum.photos (blocked/slow in CI). diff --git a/modules/react/avatar/lib/Avatar.tsx b/modules/react/avatar/lib/Avatar.tsx index 68fefe264a..13523f0fa8 100644 --- a/modules/react/avatar/lib/Avatar.tsx +++ b/modules/react/avatar/lib/Avatar.tsx @@ -19,7 +19,9 @@ export interface AvatarProps extends BaseAvatarProps, AvatarNameProps { */ objectFit?: Property.ObjectFit; /** - * If true, the Avatar won't forward the `name` prop to the `alt` attribute of the image. This is useful when the Avatar is purely decorative and is rendered next to a name or text. + * If true, hides the image and fallback initials from assistive technology. This is useful when + * the Avatar is purely decorative and is rendered next to a name or text. + * @default false */ isDecorative?: boolean; } @@ -147,6 +149,7 @@ export const Avatar = createComponent('div')({ )} diff --git a/modules/react/avatar/spec/Avatar.spec.tsx b/modules/react/avatar/spec/Avatar.spec.tsx index f167b8374c..0f849c7123 100644 --- a/modules/react/avatar/spec/Avatar.spec.tsx +++ b/modules/react/avatar/spec/Avatar.spec.tsx @@ -1,9 +1,46 @@ -import {render, waitFor} from '@testing-library/react'; +import {fireEvent, isInaccessible, render, waitFor} from '@testing-library/react'; import * as React from 'react'; import {Avatar} from '../lib/Avatar'; describe('Avatar', () => { + verifyComponent(Avatar, {props: {name: 'John Doe'}}); + + describe('decorative initials', () => { + it.each([undefined, false, true])( + 'should expose initials only when isDecorative is not true (%s)', + isDecorative => { + const screen = render(); + const initials = screen.getByText('JD'); + + expect(initials).toBeVisible(); + expect(isInaccessible(initials)).toBe(isDecorative === true); + } + ); + + it('should hide custom decorative initials from assistive technology', () => { + const screen = render(); + + expect(screen.getByText('J')).toBeVisible(); + expect(isInaccessible(screen.getByText('J'))).toBe(true); + }); + + it('should keep decorative fallback initials hidden while loading and after an image error', () => { + const screen = render(); + const initials = screen.getByText('JD'); + const image = screen.container.querySelector('img')!; + + expect(isInaccessible(initials)).toBe(true); + fireEvent.error(image); + expect(initials).toBeVisible(); + expect(isInaccessible(initials)).toBe(true); + + fireEvent.load(image); + expect(screen.queryByText('JD')).not.toBeInTheDocument(); + expect(isInaccessible(image)).toBe(true); + }); + }); + it('shoudld show the initials JD given the name is John Doe', () => { const screen = render(); diff --git a/modules/react/avatar/stories/Avatar.mdx b/modules/react/avatar/stories/Avatar.mdx index 697ad7a248..905b13a2d7 100644 --- a/modules/react/avatar/stories/Avatar.mdx +++ b/modules/react/avatar/stories/Avatar.mdx @@ -1,16 +1,13 @@ import {Meta} from '@storybook/blocks'; -import { - ExampleCodeBlock, - Specifications, - SymbolDoc, -} from '@workday/canvas-kit-docs'; +import {ExampleCodeBlock, Specifications, SymbolDoc} from '@workday/canvas-kit-docs'; import {Avatar} from '@workday/canvas-kit-react/avatar'; import {Basic} from './examples/Basic'; import {Custom} from './examples/Custom'; import {Decorative} from './examples/Decorative'; +import {DecorativeInitials} from './examples/DecorativeInitials'; import {Image} from './examples/Image'; import {Size} from './examples/Size'; import {Variant} from './examples/Variant'; @@ -82,11 +79,15 @@ For complete control over styling and behavior, use the `BaseAvatar` component: ### Accessibility -If the Avatar is purely decorative, you can set the `isDecorative` prop to `true` to prevent the -`name` prop from being forwarded to the `alt` attribute of the image. +If the Avatar is rendered next to the user's name, set `isDecorative` to `true` to hide the image +and fallback initials from assistive technology. This avoids announcing the same name twice. +The same applies when no image is provided: + + + ## Component API diff --git a/modules/react/avatar/stories/Avatar.stories.ts b/modules/react/avatar/stories/Avatar.stories.ts index ca7a545717..0a0ba33811 100644 --- a/modules/react/avatar/stories/Avatar.stories.ts +++ b/modules/react/avatar/stories/Avatar.stories.ts @@ -6,6 +6,7 @@ import mdxDoc from './Avatar.mdx'; import {Basic as BasicExample} from './examples/Basic'; import {Custom as CustomExample} from './examples/Custom'; import {Decorative as DecorativeExample} from './examples/Decorative'; +import {DecorativeInitials as DecorativeInitialsExample} from './examples/DecorativeInitials'; import {Image as ImageExample} from './examples/Image'; import {Size as SizeExample} from './examples/Size'; import {Variant as VariantExample} from './examples/Variant'; @@ -46,3 +47,7 @@ export const Custom: Story = { export const Decorative: Story = { render: DecorativeExample, }; + +export const DecorativeInitials: Story = { + render: DecorativeInitialsExample, +}; diff --git a/modules/react/avatar/stories/examples/DecorativeInitials.tsx b/modules/react/avatar/stories/examples/DecorativeInitials.tsx new file mode 100644 index 0000000000..7b65a9e805 --- /dev/null +++ b/modules/react/avatar/stories/examples/DecorativeInitials.tsx @@ -0,0 +1,18 @@ +import {Avatar} from '@workday/canvas-kit-react/avatar'; +import {Text} from '@workday/canvas-kit-react/text'; +import {createStyles} from '@workday/canvas-kit-styling'; +import {system} from '@workday/canvas-tokens-web'; + +const containerStyles = createStyles({ + display: 'inline-flex', + gap: system.gap.sm, + alignItems: 'center', +}); +export const DecorativeInitials = () => { + return ( +
+ + Nicholas Smith +
+ ); +};