diff --git a/packages/material-ui/src/Link/Link.d.ts b/packages/material-ui/src/Link/Link.d.ts index 1bdcd9d419fd4e..93c5c93265d62e 100644 --- a/packages/material-ui/src/Link/Link.d.ts +++ b/packages/material-ui/src/Link/Link.d.ts @@ -17,7 +17,8 @@ export type LinkClassKey = | 'underlineNone' | 'underlineHover' | 'underlineAlways' - | 'button'; + | 'button' + | 'focusVisible'; export type LinkBaseProps = React.AnchorHTMLAttributes & Omit; diff --git a/packages/material-ui/src/Link/Link.js b/packages/material-ui/src/Link/Link.js index 8d2cce4f4c7de1..3b01ed1b7505b5 100644 --- a/packages/material-ui/src/Link/Link.js +++ b/packages/material-ui/src/Link/Link.js @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import { capitalize } from '../utils/helpers'; import withStyles from '../styles/withStyles'; +import { useIsFocusVisible } from '../utils/focusVisible'; +import { useForkRef } from '../utils/reactHelpers'; import Typography from '../Typography'; export const styles = { @@ -44,27 +46,56 @@ export const styles = { '&::-moz-focus-inner': { borderStyle: 'none', // Remove Firefox dotted outline. }, + '&$focusVisible': { + outline: 'auto', + }, }, + /* Styles applied to the root element if the link is keyboard focused. */ + focusVisible: {}, }; const Link = React.forwardRef(function Link(props, ref) { const { classes, className, - component = 'a', color = 'primary', + component = 'a', + onBlur, + onFocus, TypographyClasses, underline = 'hover', variant = 'inherit', ...other } = props; + const { isFocusVisible, onBlurVisible, ref: focusVisibleRef } = useIsFocusVisible(); + const [focusVisible, setFocusVisible] = React.useState(false); + const handlerRef = useForkRef(ref, focusVisibleRef); + const handleBlur = event => { + if (focusVisible) { + onBlurVisible(); + setFocusVisible(false); + } + if (onBlur) { + onBlur(event); + } + }; + const handleFocus = event => { + if (isFocusVisible(event)) { + setFocusVisible(true); + } + if (onFocus) { + onFocus(event); + } + }; + return ( @@ -110,6 +143,14 @@ Link.propTypes = { * Either a string to use a DOM element or a component. */ component: PropTypes.elementType, + /** + * @ignore + */ + onBlur: PropTypes.func, + /** + * @ignore + */ + onFocus: PropTypes.func, /** * `classes` property applied to the [`Typography`](/api/typography/) element. */ diff --git a/packages/material-ui/src/Link/Link.test.js b/packages/material-ui/src/Link/Link.test.js index 6292747bb88604..95eaf93d0991e0 100644 --- a/packages/material-ui/src/Link/Link.test.js +++ b/packages/material-ui/src/Link/Link.test.js @@ -1,10 +1,17 @@ import React from 'react'; import { assert } from 'chai'; +import { spy } from 'sinon'; import { createMount, createShallow, getClasses } from '@material-ui/core/test-utils'; import describeConformance from '../test-utils/describeConformance'; import Link from './Link'; import Typography from '../Typography'; +function focusVisible(element) { + element.blur(); + document.dispatchEvent(new window.Event('keydown')); + element.focus(); +} + describe('', () => { let mount; let shallow; @@ -29,12 +36,12 @@ describe('', () => { })); it('should render children', () => { - const wrapper = shallow(Home); + const wrapper = mount(Home); assert.strictEqual(wrapper.contains('Home'), true); }); it('should pass props to the component', () => { - const wrapper = shallow( + const wrapper = mount( Test , @@ -42,4 +49,40 @@ describe('', () => { const typography = wrapper.find(Typography); assert.strictEqual(typography.props().color, 'primary'); }); + + describe('event callbacks', () => { + it('should fire event callbacks', () => { + const events = ['onBlur', 'onFocus']; + + const handlers = events.reduce((result, n) => { + result[n] = spy(); + return result; + }, {}); + + const wrapper = shallow( + + Home + , + ); + + events.forEach(n => { + const event = n.charAt(2).toLowerCase() + n.slice(3); + wrapper.simulate(event, { target: { tagName: 'a' } }); + assert.strictEqual(handlers[n].callCount, 1, `should have called the ${n} handler`); + }); + }); + }); + + describe('keyboard focus', () => { + it('should add the focusVisible class when focused', () => { + const wrapper = mount(Home); + const anchor = wrapper.find('a').instance(); + + assert.strictEqual(anchor.classList.contains(classes.focusVisible), false); + focusVisible(anchor); + assert.strictEqual(anchor.classList.contains(classes.focusVisible), true); + anchor.blur(); + assert.strictEqual(anchor.classList.contains(classes.focusVisible), false); + }); + }); }); diff --git a/pages/api/link.md b/pages/api/link.md index 0bcad9758a5043..dfac15d14e5f17 100644 --- a/pages/api/link.md +++ b/pages/api/link.md @@ -43,6 +43,7 @@ This property accepts the following keys: | underlineHover | Styles applied to the root element if `underline="hover"`. | underlineAlways | Styles applied to the root element if `underline="always"`. | button | Styles applied to the root element if `component="button"`. +| focusVisible | Styles applied to the root element if the link is keyboard focused. Have a look at the [overriding styles with classes](/customization/components/#overriding-styles-with-classes) section and the [implementation of the component](https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Link/Link.js)