Skip to content

Commit

Permalink
Link, Dropdown: fix A11y (opens new tab logic) (#3600)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertCarreras authored May 29, 2024
1 parent b51cfe4 commit f4d4ebd
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 88 deletions.
5 changes: 2 additions & 3 deletions packages/gestalt/src/Dropdown/OptionItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { forwardRef, Fragment, ReactNode } from 'react';
import classnames from 'classnames';
import AccessibilityOpenNewTab from '../accessibility/AccessibilityOpenNewTab';
import { useRequestAnimationFrame } from '../animation/RequestAnimationFrameContext';
import Badge from '../Badge';
import Box from '../Box';
Expand Down Expand Up @@ -155,16 +156,14 @@ const OptionItemWithForwardRef = forwardRef<HTMLElement | null | undefined, Prop
</Box>
{isExternal && (
<Box
// aria-hidden is required to prevent assistive technologies from accessing the icon as the actual link already announces that the link opens a new tab
alignItems="center"
aria-hidden
color="transparent"
display="flex"
justifyContent="center"
// marginStart is for spacing relative to Badge, should not be moved to parent Flex's gap
marginStart={2}
>
<Icon accessibilityLabel="" color={textColor} icon="visit" size={12} />
<AccessibilityOpenNewTab color={textColor} size={12} />
</Box>
)}
</Flex>
Expand Down
4 changes: 2 additions & 2 deletions packages/gestalt/src/DropdownLink.jsdom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ describe('Dropdown.Link', () => {
// eslint-disable-next-line testing-library/prefer-presence-queries -- Please fix the next time this file is touched!
expect(screen.queryByText('Beta Badge')).toBeInTheDocument();
expect(
screen.queryByText('; Opens a new tab', {
screen.queryByTitle(', Opens a new tab', {
exact: true,
}),
).toBeVisible();
).not.toBeVisible();
});
});
2 changes: 1 addition & 1 deletion packages/gestalt/src/HelpButton.jsdom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe('HelpButton', () => {

const element = screen.getByRole('link');
// @ts-expect-error - TS2339 - Property 'text' does not exist on type 'HTMLElement'.
expect(element.text).toEqual('New link text; Opens a new tab');
expect(element.text).toEqual('New link text');
});

it('renders a link spying the link trigger', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/gestalt/src/Link.jsdom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ describe('Link', () => {
).toBeVisible();

expect(
screen.getByText('; Opens a new tab', {
screen.getByTitle(', Opens a new tab', {
exact: true,
}),
).toBeVisible();
).not.toBeVisible();

render(
<Link
Expand Down
36 changes: 15 additions & 21 deletions packages/gestalt/src/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
useRef,
} from 'react';
import classnames from 'classnames';
import AccessibilityOpenNewTab from './accessibility/AccessibilityOpenNewTab';
import getAriaLabel from './accessibility/getAriaLabel';
import NewTabAccessibilityLabel from './accessibility/NewTabAccessibilityLabel';
import Box from './Box';
import { useDefaultLabelContext } from './contexts/DefaultLabelProvider';
import { useGlobalEventsHandlerContext } from './contexts/GlobalEventsHandlerProvider';
Expand Down Expand Up @@ -44,24 +44,6 @@ type ExternalLinkIcon =
size: ComponentProps<typeof Text>['size'];
};

function ExternalIcon({ externalLinkIcon }: { externalLinkIcon: ExternalLinkIcon }) {
return externalLinkIcon === 'none' ? null : (
<Box aria-hidden display="inlineBlock" marginStart={1}>
<Icon
accessibilityLabel=""
color={externalLinkIcon === 'default' ? 'default' : externalLinkIcon?.color ?? 'default'}
icon="visit"
inline
size={
externalLinkIcon === 'default'
? externalLinkIconMap['300']
: externalLinkIconMap[externalLinkIcon?.size ?? '300']
}
/>
</Box>
);
}

type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 'circle' | 'pill';

type Props = {
Expand Down Expand Up @@ -289,8 +271,20 @@ const LinkWithForwardRef = forwardRef<HTMLAnchorElement, Props>(function Link(
target={target ? `_${target}` : null}
>
{children}
<NewTabAccessibilityLabel target={target} />
<ExternalIcon externalLinkIcon={externalLinkIcon} />
{externalLinkIcon === 'none' ? null : (
<Box display="inlineBlock" marginStart={1}>
<AccessibilityOpenNewTab
color={
externalLinkIcon === 'default' ? 'default' : externalLinkIcon?.color ?? 'default'
}
size={
externalLinkIcon === 'default'
? externalLinkIconMap['300']
: externalLinkIconMap[externalLinkIcon?.size ?? '300']
}
/>
</Box>
)}
</a>
);
});
Expand Down
57 changes: 15 additions & 42 deletions packages/gestalt/src/__snapshots__/Dropdown.jsdom.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -719,34 +719,25 @@ exports[`Dropdown renders a menu of 6 items 1`] = `
/>
</div>
<div
aria-hidden="true"
class="box justifyCenter marginStart2 transparent xsDisplayFlex xsItemsCenter"
>
<svg
aria-hidden="true"
aria-label=""
class="rtlSupport default icon iconBlock"
class="rtlSupport default icon"
height="12"
role="img"
viewBox="0 0 24 24"
width="12"
>
<title>
,
Opens a new tab
</title>
<path
d="test-file-stub"
/>
</svg>
</div>
</div>
<div
class="box relative"
style="display: inline;"
>
<div
class="box xsDisplayVisuallyHidden"
>
; Opens a new tab
</div>
</div>
</a>
</div>
</div>
Expand Down Expand Up @@ -892,34 +883,25 @@ exports[`Dropdown renders a menu of 6 items 1`] = `
/>
</div>
<div
aria-hidden="true"
class="box justifyCenter marginStart2 transparent xsDisplayFlex xsItemsCenter"
>
<svg
aria-hidden="true"
aria-label=""
class="rtlSupport default icon iconBlock"
class="rtlSupport default icon"
height="12"
role="img"
viewBox="0 0 24 24"
width="12"
>
<title>
,
Opens a new tab
</title>
<path
d="test-file-stub"
/>
</svg>
</div>
</div>
<div
class="box relative"
style="display: inline;"
>
<div
class="box xsDisplayVisuallyHidden"
>
; Opens a new tab
</div>
</div>
</a>
</div>
</div>
Expand Down Expand Up @@ -969,34 +951,25 @@ exports[`Dropdown renders a menu of 6 items 1`] = `
/>
</div>
<div
aria-hidden="true"
class="box justifyCenter marginStart2 transparent xsDisplayFlex xsItemsCenter"
>
<svg
aria-hidden="true"
aria-label=""
class="rtlSupport default icon iconBlock"
class="rtlSupport default icon"
height="12"
role="img"
viewBox="0 0 24 24"
width="12"
>
<title>
,
Opens a new tab
</title>
<path
d="test-file-stub"
/>
</svg>
</div>
</div>
<div
class="box relative"
style="display: inline;"
>
<div
class="box xsDisplayVisuallyHidden"
>
; Opens a new tab
</div>
</div>
</a>
</div>
</div>
Expand Down
21 changes: 4 additions & 17 deletions packages/gestalt/src/__snapshots__/Link.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,19 @@ exports[`external link with nofollow 1`] = `
>
Link
<div
aria-hidden={true}
className="box marginStart1 xsDisplayInlineBlock"
>
<svg
aria-hidden={true}
aria-label=""
className="rtlSupport default icon"
height={16}
role="img"
viewBox="0 0 24 24"
width={16}
>
<title>
,
Opens a new tab
</title>
<path
d="test-file-stub"
/>
Expand Down Expand Up @@ -162,20 +163,6 @@ exports[`target blank 1`] = `
target="_blank"
>
Link
<div
className="box relative"
style={
Object {
"display": "inline",
}
}
>
<div
className="box xsDisplayVisuallyHidden"
>
; Opens a new tab
</div>
</div>
</a>
`;

Expand Down
23 changes: 23 additions & 0 deletions packages/gestalt/src/accessibility/AccessibilityOpenNewTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentProps } from 'react';
import classnames from 'classnames';
import { useDefaultLabelContext } from '../contexts/DefaultLabelProvider';
import Icon from '../Icon';
import styles from '../Icon.css';
import icons from '../icons/index';

type Props = {
color: ComponentProps<typeof Icon>['color'];
size: ComponentProps<typeof Icon>['size'];
};

export default function AccessibilityOpenNewTab({ size, color }: Props) {
const cs = classnames(styles.rtlSupport, styles[color ?? 'default'], styles.icon);
const { accessibilityNewTabLabel } = useDefaultLabelContext('Link');

return (
<svg className={cs} height={size} role="img" viewBox="0 0 24 24" width={size}>
<title>, {accessibilityNewTabLabel}</title>
<path d={icons.visit} />
</svg>
);
}

0 comments on commit f4d4ebd

Please sign in to comment.