Skip to content

Commit

Permalink
[EuiSkipLink] Add overrideLinkBehavior prop for SPA/hash routers (#…
Browse files Browse the repository at this point in the history
…5957)

* Add support / `overrideAnchorBehavior` prop for hash routers

* Update EUI docs site with skip link

* Fix FF-only cross browser scrolling bug that occurs when CSS changes between position absolute and position fixed

* changelog

* [PR feedback] rename overrideAnchorBehavior to overrideLinkBehavior

* [PR feedback] Fix scrollTo -> scrollIntoView
  • Loading branch information
Constance authored Jun 13, 2022
1 parent 1d00c47 commit a39ba3e
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 4 deletions.
10 changes: 9 additions & 1 deletion src-docs/src/views/app_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
EuiErrorBoundary,
EuiPage,
EuiPageBody,
EuiSkipLink,
} from '../../../src/components';

import { keys } from '../../../src/services';
Expand Down Expand Up @@ -68,6 +69,13 @@ export const AppView = ({ children, currentRoute }) => {

return (
<LinkWrapper>
<EuiSkipLink
destinationId="start-of-content"
position="fixed"
overrideLinkBehavior
>
Skip to content
</EuiSkipLink>
<GuidePageHeader onToggleLocale={toggleLocale} selectedLocale={locale} />
<EuiPage paddingSize="none">
<EuiErrorBoundary>
Expand All @@ -79,7 +87,7 @@ export const AppView = ({ children, currentRoute }) => {
/>
</EuiErrorBoundary>

<EuiPageBody paddingSize="none" panelled>
<EuiPageBody paddingSize="none" panelled id="start-of-content">
{children({ theme })}
</EuiPageBody>
</EuiPage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ exports[`EuiSkipLink props position absolute is rendered 1`] = `

exports[`EuiSkipLink props position fixed is rendered 1`] = `
<a
class="euiButton euiButton--primary euiButton--small euiButton--fill euiScreenReaderOnly--showOnFocus euiSkipLink css-o3tocm-euiSkipLink-fixed"
class="euiButton euiButton--primary euiButton--small euiButton--fill euiScreenReaderOnly--showOnFocus euiSkipLink css-1c2hhvc-euiSkipLink-fixed"
href="#somewhere"
rel="noreferrer"
tabindex="0"
Expand Down
3 changes: 2 additions & 1 deletion src/components/accessibility/skip_link/skip_link.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ export const euiSkipLinkStyles = ({ euiTheme }: UseEuiTheme) => {
}
`,
fixed: css`
position: fixed !important; // Needs to override euiScreenReaderOnly - prevents scroll jumping in Firefox
&:focus {
position: fixed;
inset-block-start: ${euiTheme.size.xs};
inset-inline-start: ${euiTheme.size.xs};
z-index: ${Number(euiTheme.levels.header) + 1};
Expand Down
22 changes: 21 additions & 1 deletion src/components/accessibility/skip_link/skip_link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import React from 'react';
import { render } from 'enzyme';
import { render, mount } from 'enzyme';
import { requiredProps } from '../../../test';

import { EuiSkipLink, POSITIONS } from './skip_link';
Expand All @@ -24,6 +24,26 @@ describe('EuiSkipLink', () => {
});

describe('props', () => {
test('overrideLinkBehavior prevents default link behavior and manually scrolls and focuses the destination', () => {
const scrollSpy = jest.fn();
const focusSpy = jest.fn();
jest.spyOn(document, 'getElementById').mockReturnValue({
scrollIntoView: scrollSpy,
focus: focusSpy,
} as any);

const component = mount(
<EuiSkipLink destinationId="somewhere" overrideLinkBehavior />
);

const preventDefault = jest.fn();
component.find('EuiButton').simulate('click', { preventDefault });

expect(preventDefault).toHaveBeenCalled();
expect(scrollSpy).toHaveBeenCalled();
expect(focusSpy).toHaveBeenCalled();
});

test('tabIndex is rendered', () => {
const component = render(
<EuiSkipLink destinationId="somewhere" tabIndex={-1} />
Expand Down
22 changes: 22 additions & 0 deletions src/components/accessibility/skip_link/skip_link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ interface EuiSkipLinkInterface extends EuiButtonProps {
* will be prepended with a hash `#` and used as the link `href`
*/
destinationId: string;
/**
* If default HTML anchor link behavior is not desired (e.g. for SPAs with hash routing),
* setting this flag to true will manually scroll to and focus the destination element
* without changing the browser URL's hash
*/
overrideLinkBehavior?: boolean;
/**
* When position is fixed, this is forced to `0`
*/
Expand All @@ -52,6 +58,7 @@ export type EuiSkipLinkProps = ExclusiveUnion<propsForAnchor, propsForButton>;

export const EuiSkipLink: FunctionComponent<EuiSkipLinkProps> = ({
destinationId,
overrideLinkBehavior,
tabIndex,
position = 'static',
children,
Expand All @@ -75,6 +82,21 @@ export const EuiSkipLink: FunctionComponent<EuiSkipLinkProps> = ({
href: `#${destinationId}`,
};
}
if (overrideLinkBehavior) {
optionalProps = {
...optionalProps,
onClick: (e: React.MouseEvent) => {
e.preventDefault();

const destinationEl = document.getElementById(destinationId);
if (!destinationEl) return;

destinationEl.scrollIntoView();
destinationEl.tabIndex = -1; // Ensure the destination content is focusable
destinationEl.focus({ preventScroll: true }); // Scrolling is already handled above, and focus's autoscroll behaves oddly around fixed headers
},
};
}

return (
<EuiScreenReaderOnly showOnFocus>
Expand Down
1 change: 1 addition & 0 deletions upcoming_changelogs/5957.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added the `overrideLinkBehavior` prop to `EuiSkipLink` for applications that use hash routers

0 comments on commit a39ba3e

Please sign in to comment.