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 () => (
- -
window.alert('Div clicked')}>Click this div
-
+ + +
window.alert('Div clicked')}>Click this div
+
- - window.alert('Anchor tag clicked')}> - Click this anchor tag - - + + window.alert('Anchor tag clicked')}> + Click this anchor tag + + - - window.alert('Custom component clicked')}> - Click this custom component - - + + window.alert('Custom component clicked')}> + Click this custom component + + - -
window.alert('Outer EuiKeyboardAccessible clicked')}> - This EuiKeyboardAccessible contains another EuiKeyboardAccessible  - - window.alert('Inner EuiKeyboardAccessible clicked')}> - Clicking this inner one should call both onClick handlers - - -
-
+ +
window.alert('Outer EuiKeyboardAccessible clicked')}> + This EuiKeyboardAccessible contains another + EuiKeyboardAccessible  + + + window.alert('Inner EuiKeyboardAccessible clicked') + }> + Clicking this inner one should call both onClick handlers + + +
+
+
); 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';