Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Link] Better support of component="button" #15863

Merged
merged 5 commits into from May 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/material-ui/src/Link/Link.d.ts
Expand Up @@ -17,7 +17,8 @@ export type LinkClassKey =
| 'underlineNone'
| 'underlineHover'
| 'underlineAlways'
| 'button';
| 'button'
| 'focusVisible';

export type LinkBaseProps = React.AnchorHTMLAttributes<HTMLAnchorElement> &
Omit<TypographyProps, 'component'>;
Expand Down
45 changes: 43 additions & 2 deletions packages/material-ui/src/Link/Link.js
Expand Up @@ -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 = {
Expand Down Expand Up @@ -44,35 +46,66 @@ 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 (
<Typography
className={clsx(
classes.root,
{
[classes.button]: component === 'button',
[classes.focusVisible]: focusVisible,
},
classes[`underline${capitalize(underline)}`],
className,
)}
classes={TypographyClasses}
color={color}
component={component}
ref={ref}
onBlur={handleBlur}
onFocus={handleFocus}
ref={handlerRef}
variant={variant}
{...other}
/>
Expand Down Expand Up @@ -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.
*/
Expand Down
47 changes: 45 additions & 2 deletions 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('<Link />', () => {
let mount;
let shallow;
Expand All @@ -29,17 +36,53 @@ describe('<Link />', () => {
}));

it('should render children', () => {
const wrapper = shallow(<Link href="/">Home</Link>);
const wrapper = mount(<Link href="/">Home</Link>);
assert.strictEqual(wrapper.contains('Home'), true);
});

it('should pass props to the <Typography> component', () => {
const wrapper = shallow(
const wrapper = mount(
<Link href="/" color="primary">
Test
</Link>,
);
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(
<Link href="/" {...handlers}>
Home
</Link>,
);

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(<Link href="/">Home</Link>);
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);
});
});
});
1 change: 1 addition & 0 deletions pages/api/link.md
Expand Up @@ -43,6 +43,7 @@ This property accepts the following keys:
| <span class="prop-name">underlineHover</span> | Styles applied to the root element if `underline="hover"`.
| <span class="prop-name">underlineAlways</span> | Styles applied to the root element if `underline="always"`.
| <span class="prop-name">button</span> | Styles applied to the root element if `component="button"`.
| <span class="prop-name">focusVisible</span> | 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)
Expand Down