diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82ff37ea613..29970510ce6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
## [`master`](https://github.com/elastic/eui/tree/master)
+- Added `showOnFocus` prop to `EuiScreenReaderOnly` to force display on keyboard focus ([#2976](https://github.com/elastic/eui/pull/2976))
+- Added `EuiSkipLink` component ([#2976](https://github.com/elastic/eui/pull/2976))
+
**Bug Fixes**
- Fixed `EuiDataGrid`'s sort popover to behave properly on mobile screens ([#2979](https://github.com/elastic/eui/pull/2979))
diff --git a/src-docs/src/views/accessibility/accessibility_example.js b/src-docs/src/views/accessibility/accessibility_example.js
index 140c87e7237..0986f1020a0 100644
--- a/src-docs/src/views/accessibility/accessibility_example.js
+++ b/src-docs/src/views/accessibility/accessibility_example.js
@@ -5,26 +5,56 @@ import { renderToHtml } from '../../services';
import { GuideSectionTypes } from '../../components';
import {
+ EuiCallOut,
EuiCode,
EuiLink,
EuiKeyboardAccessible,
- EuiScreenReaderOnly,
+ EuiSkipLink,
} from '../../../../src/components';
import KeyboardAccessible from './keyboard_accessible';
import ScreenReaderOnly from './screen_reader';
+import SkipLink from './skip_link';
const keyboardAccessibleSource = require('!!raw-loader!./keyboard_accessible');
const keyboardAccessibleHtml = renderToHtml(KeyboardAccessible);
+const keyboardAccessibleSnippet = `
+
+`;
const screenReaderOnlyHtml = renderToHtml(ScreenReaderOnly);
const screenReaderOnlySource = require('!!raw-loader!./screen_reader');
+const screenReaderOnlySnippet = [
+ `
+
+
+`,
+ `
+
+
+`,
+];
+
+const skipLinkHtml = renderToHtml(SkipLink);
+const skipLinkSource = require('!!raw-loader!./skip_link');
+const skipLinkSnippet = [
+ `
+ Skip to content
+
+`,
+ `
+ Skip to main content
+
+`,
+];
+
+import { ScreenReaderOnlyDocsComponent } from './props';
export const AccessibilityExample = {
title: 'Accessibility',
sections: [
{
- title: 'KeyboardAccessible',
+ title: 'Keyboard accessible',
source: [
{
type: GuideSectionTypes.JS,
@@ -37,17 +67,18 @@ export const AccessibilityExample = {
],
text: (
- You can make interactive elements keyboard-accessible with this
- component. This is necessary for non-button elements and{' '}
- a tags without
+ You can make interactive elements keyboard-accessible with the{' '}
+ EuiKeyboardAccessible component. This is necessary
+ for non-button elements and a tags without{' '}
href attributes.
),
props: { EuiKeyboardAccessible },
+ snippet: keyboardAccessibleSnippet,
demo: ,
},
{
- title: 'ScreenReaderOnly',
+ title: 'Screen reader only',
source: [
{
type: GuideSectionTypes.JS,
@@ -61,24 +92,58 @@ export const AccessibilityExample = {
text: (
- This class can be useful to add accessibility to older designs that
- are still in use, but it shouldn’t be a permanent solution.
- See{' '}
- {
-
- http://webaim.org/techniques/css/invisiblecontent/
-
- }{' '}
- for more information.
-
-
- Use a screenreader to verify that there is a second paragraph in
- this example:
+ Use the EuiScreenReaderOnly component to visually
+ hide elements while still allowing them to be read by screen
+ readers. In certain cases, you may want to use the{' '}
+ showOnFocus prop to display screen reader-only
+ content when in focus.
+
+
+ "In most cases, if content (particularly content that
+ provides functionality or interactivity) is important enough to
+ provide to screen reader users, it should probably be made
+ available to all users."{' '}
+
+ Learn more about invisible content
+
+
+
),
- props: { EuiScreenReaderOnly },
+ props: {
+ EuiScreenReaderOnly: ScreenReaderOnlyDocsComponent,
+ },
+ snippet: screenReaderOnlySnippet,
demo: ,
},
+ {
+ title: 'Skip link',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: skipLinkSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: skipLinkHtml,
+ },
+ ],
+ text: (
+
+ The EuiSkipLink component allows users to bypass
+ navigation, or ornamental elements, and quickly reach the main content
+ of the page.
+
+ ),
+ props: { EuiSkipLink },
+ snippet: skipLinkSnippet,
+ demo: ,
+ },
],
};
diff --git a/src-docs/src/views/accessibility/keyboard_accessible.js b/src-docs/src/views/accessibility/keyboard_accessible.js
index 3f68055265e..3d08c0ac598 100644
--- a/src-docs/src/views/accessibility/keyboard_accessible.js
+++ b/src-docs/src/views/accessibility/keyboard_accessible.js
@@ -1,6 +1,7 @@
import React from 'react';
import { EuiKeyboardAccessible } from '../../../../src/components';
+import { EuiText } from '../../../../src/components/text';
// For custom components, we just need to make sure they delegate props to their rendered root
// element, e.g. onClick, tabIndex, and role.
@@ -10,33 +11,42 @@ const CustomComponent = ({ children, ...rest }) => (
export default () => (
);
diff --git a/src-docs/src/views/accessibility/props.tsx b/src-docs/src/views/accessibility/props.tsx
new file mode 100644
index 00000000000..29e44b11cc8
--- /dev/null
+++ b/src-docs/src/views/accessibility/props.tsx
@@ -0,0 +1,6 @@
+import React, { FunctionComponent } from 'react';
+import { EuiScreenReaderOnlyProps } from '../../../../src/components/accessibility/screen_reader';
+
+export const ScreenReaderOnlyDocsComponent: FunctionComponent<
+ EuiScreenReaderOnlyProps
+> = () => ;
diff --git a/src-docs/src/views/accessibility/screen_reader.tsx b/src-docs/src/views/accessibility/screen_reader.tsx
index c5d74bc5b31..dc43ab1e0c1 100644
--- a/src-docs/src/views/accessibility/screen_reader.tsx
+++ b/src-docs/src/views/accessibility/screen_reader.tsx
@@ -1,16 +1,51 @@
import React from 'react';
import { EuiScreenReaderOnly } from '../../../../src/components/accessibility/screen_reader';
+import { EuiCallOut } from '../../../../src/components/call_out';
+import { EuiText } from '../../../../src/components/text';
+import { EuiTitle } from '../../../../src/components/title';
+import { EuiLink } from '../../../../src/components/link';
export default () => (
-
This is the first paragraph. It is visible to all.
-
+
+
+ Visually hide content
+
- This is the second paragraph. It is hidden for sighted users but visible
- to screen readers.
+
+ Use a screenreader to verify that there is a second paragraph in this
+ example:
+
-
-
This is the third paragraph. It is visible to all.
+
This is the first paragraph. It is visible to all.
+
+
+ This is the second paragraph. It is hidden for sighted users but
+ visible to screen readers.
+
+
+
This is the third paragraph. It is visible to all.
+
+ Show on focus
+
+
+
+ Tab through this section with your keyboard to display a ‘Skip
+ navigation’ link:
+
+
+
+ This link is visible to all on focus:{' '}
+
+ Skip navigation
+
+
+
+
);
diff --git a/src-docs/src/views/accessibility/skip_link.js b/src-docs/src/views/accessibility/skip_link.js
new file mode 100644
index 00000000000..15cf70e03c1
--- /dev/null
+++ b/src-docs/src/views/accessibility/skip_link.js
@@ -0,0 +1,61 @@
+import React, { Fragment, useState } from 'react';
+
+import { EuiSkipLink } from '../../../../src/components/accessibility/skip_link';
+import { EuiCallOut } from '../../../../src/components/call_out';
+import { EuiText } from '../../../../src/components/text';
+import { EuiSpacer } from '../../../../src/components/spacer';
+import { EuiSwitch } from '../../../../src/components/form/switch';
+
+export default () => {
+ const [isFixed, setFixed] = useState(false);
+
+ return (
+
+
+ {isFixed ? (
+
+
+ Tab through this section and a fixed{' '}
+ Skip to main content link will appear atop this
+ page.
+
+
+ ) : (
+
+
+ Tab through this section and a Skip to content{' '}
+ link will appear below.
+
+
+ )}
+
+
+ setFixed(e.target.checked)}
+ />
+
+ {isFixed ? (
+
+
+ Skip to main content
+
+
+
+ ) : (
+
+ Skip to content
+
+ )}
+
+ );
+};
diff --git a/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap b/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap
index e218d61cb91..39b7d8e177a 100644
--- a/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap
+++ b/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap
@@ -15,3 +15,12 @@ exports[`EuiScreenReaderOnly adds an accessibility class to a child element when
This paragraph is not visibile to sighted users but will be read by screenreaders.
`;
+
+exports[`EuiScreenReaderOnly will show on focus 1`] = `
+
+ Link
+
+`;
diff --git a/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap b/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap
new file mode 100644
index 00000000000..685870e4087
--- /dev/null
+++ b/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiSkipLink is rendered 1`] = `
+
+
+
+
+
+`;
+
+exports[`EuiSkipLink position absolute is rendered 1`] = `
+
+
+
+
+
+`;
+
+exports[`EuiSkipLink position fixed is rendered 1`] = `
+
+
+
+
+
+`;
+
+exports[`EuiSkipLink position static is rendered 1`] = `
+
+
+
+
+
+`;
diff --git a/src/components/accessibility/_index.scss b/src/components/accessibility/_index.scss
index 98294afdf55..b8b7ffe9a8f 100644
--- a/src/components/accessibility/_index.scss
+++ b/src/components/accessibility/_index.scss
@@ -1 +1,2 @@
@import 'screen_reader';
+@import 'skip_link';
diff --git a/src/components/accessibility/_screen_reader.scss b/src/components/accessibility/_screen_reader.scss
index 398beafda85..6cc8a8e6407 100644
--- a/src/components/accessibility/_screen_reader.scss
+++ b/src/components/accessibility/_screen_reader.scss
@@ -1,3 +1,4 @@
-.euiScreenReaderOnly {
+.euiScreenReaderOnly,
+.euiScreenReaderOnly--showOnFocus:not(:focus) {
@include euiScreenReaderOnly;
}
diff --git a/src/components/accessibility/_skip_link.scss b/src/components/accessibility/_skip_link.scss
new file mode 100644
index 00000000000..38aa7fd814d
--- /dev/null
+++ b/src/components/accessibility/_skip_link.scss
@@ -0,0 +1,20 @@
+.euiSkipLink {
+ transition: none !important; // sass-lint:disable-line no-important
+
+ &:focus {
+ animation: none !important; // sass-lint:disable-line no-important
+ }
+
+ // Set positions on focus only as to no override screenReaderOnly position
+ // When positioned absolutely, consumers still need to tell it WHERE (top,left,etc...)
+ &.euiSkipLink--absolute:focus {
+ position: absolute;
+ }
+
+ &.euiSkipLink--fixed:focus {
+ position: fixed;
+ top: $euiSizeXS;
+ left: $euiSizeXS;
+ z-index: $euiZHeader + 1;
+ }
+}
diff --git a/src/components/accessibility/index.ts b/src/components/accessibility/index.ts
index d63ef5aa800..06e7f202092 100644
--- a/src/components/accessibility/index.ts
+++ b/src/components/accessibility/index.ts
@@ -1,2 +1,3 @@
export { EuiKeyboardAccessible } from './keyboard_accessible';
export { EuiScreenReaderOnly } from './screen_reader';
+export { EuiSkipLink } from './skip_link';
diff --git a/src/components/accessibility/screen_reader.test.tsx b/src/components/accessibility/screen_reader.test.tsx
index 5b6c06e59af..db021b454ac 100644
--- a/src/components/accessibility/screen_reader.test.tsx
+++ b/src/components/accessibility/screen_reader.test.tsx
@@ -30,4 +30,14 @@ describe('EuiScreenReaderOnly', () => {
expect($paragraph).toMatchSnapshot();
});
});
+
+ test('will show on focus', () => {
+ const component = render(
+
+ Link
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
});
diff --git a/src/components/accessibility/screen_reader.tsx b/src/components/accessibility/screen_reader.tsx
index 437d8d5a5a0..55485b541e1 100644
--- a/src/components/accessibility/screen_reader.tsx
+++ b/src/components/accessibility/screen_reader.tsx
@@ -3,12 +3,23 @@ import classNames from 'classnames';
export interface EuiScreenReaderOnlyProps {
children: ReactElement;
+
+ /**
+ * For keyboard navigation, force content to display visually upon focus.
+ */
+ showOnFocus?: boolean;
}
export const EuiScreenReaderOnly: FunctionComponent<
EuiScreenReaderOnlyProps
-> = ({ children }) => {
- const classes = classNames('euiScreenReaderOnly', children.props.className);
+> = ({ children, showOnFocus }) => {
+ const classes = classNames(
+ {
+ euiScreenReaderOnly: !showOnFocus,
+ 'euiScreenReaderOnly--showOnFocus': showOnFocus,
+ },
+ children.props.className
+ );
const props = {
...children.props,
diff --git a/src/components/accessibility/skip_link.test.tsx b/src/components/accessibility/skip_link.test.tsx
new file mode 100644
index 00000000000..e7a0589d50c
--- /dev/null
+++ b/src/components/accessibility/skip_link.test.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test';
+
+import { EuiSkipLink, POSITIONS } from './skip_link';
+
+describe('EuiSkipLink', () => {
+ test('is rendered', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('position', () => {
+ POSITIONS.forEach(position => {
+ test(`${position} is rendered`, () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/src/components/accessibility/skip_link.tsx b/src/components/accessibility/skip_link.tsx
new file mode 100644
index 00000000000..a87e2880b9c
--- /dev/null
+++ b/src/components/accessibility/skip_link.tsx
@@ -0,0 +1,68 @@
+import React, { FunctionComponent, Ref } from 'react';
+import classNames from 'classnames';
+import { EuiButton, EuiButtonProps } from '../button/button';
+import { EuiScreenReaderOnly } from '../accessibility/screen_reader';
+import { PropsForAnchor, PropsForButton, ExclusiveUnion } from '../common';
+
+type Positions = 'static' | 'fixed' | 'absolute';
+export const POSITIONS = ['static', 'fixed', 'absolute'] as Positions[];
+
+export interface EuiSkipLinkProps extends EuiButtonProps {
+ /**
+ * If true, the link will be fixed to the top left of the viewport
+ */
+ position?: Positions;
+
+ /**
+ * Typically an anchor id (e.g. `a11yMainContent`), the value provided
+ * will be prepended with a hash `#` and used as the link `href`
+ */
+ destinationId: string;
+
+ tabIndex?: number;
+}
+
+type propsForAnchor = PropsForAnchor<
+ EuiSkipLinkProps,
+ {
+ buttonRef?: Ref;
+ }
+>;
+
+type propsForButton = PropsForButton<
+ EuiSkipLinkProps,
+ {
+ buttonRef?: Ref;
+ }
+>;
+
+export type Props = ExclusiveUnion;
+
+export const EuiSkipLink: FunctionComponent = ({
+ destinationId,
+ tabIndex,
+ position = 'static',
+ children,
+ className,
+ ...rest
+}) => {
+ const classes = classNames(
+ 'euiSkipLink',
+ [`euiSkipLink--${position}`],
+ className
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/src/components/index.js b/src/components/index.js
index f4cb7cf6652..764036c894d 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -4,7 +4,11 @@ export { EuiAspectRatio } from './aspect_ratio';
export { EuiAvatar } from './avatar';
-export { EuiKeyboardAccessible, EuiScreenReaderOnly } from './accessibility';
+export {
+ EuiKeyboardAccessible,
+ EuiScreenReaderOnly,
+ EuiSkipLink,
+} from './accessibility';
export { EuiBadge, EuiBetaBadge, EuiNotificationBadge } from './badge';