Skip to content

Commit

Permalink
Merge pull request #211 from hypothesis/link-pattern
Browse files Browse the repository at this point in the history
Add `hyp-link` pattern and `Link` component
  • Loading branch information
lyzadanger authored Oct 18, 2021
2 parents b510b22 + 1bfa6fa commit 31b3fe6
Show file tree
Hide file tree
Showing 14 changed files with 183 additions and 34 deletions.
33 changes: 33 additions & 0 deletions src/components/Link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import classnames from 'classnames';

/**
* @typedef {import('preact').ComponentChildren} Children
*
* @typedef LinkBaseProps
* @prop {Children} children
* @prop {string} [classes] - Additional CSS classes to apply
* @prop {import('preact').Ref<HTMLAnchorElement>} [linkRef] - Optional ref for
* the rendered anchor element
*/

/**
* @typedef {LinkBaseProps & import('preact').JSX.HTMLAttributes<HTMLAnchorElement>} LinkProps
*/

/**
* Style and add some attributes to an anchor (`<a>`) element
*
* @param {LinkProps} props
*/
export function Link({ children, classes = '', linkRef, ...restProps }) {
return (
<a
className={classnames('Hyp-Link', classes)}
ref={linkRef}
rel="noopener noreferrer"
{...restProps}
>
{children}
</a>
);
}
43 changes: 43 additions & 0 deletions src/components/test/Link-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { mount } from 'enzyme';
import { createRef } from 'preact';

import { Link } from '../Link';

describe('Link', () => {
const createComponent = (props = {}) => {
return mount(
<Link href="http://www.example.com" {...props}>
This is content inside of a Link
</Link>
);
};

it('renders children with appropriate classnames', () => {
const wrapper = createComponent();

assert.isTrue(wrapper.find('a').first().hasClass('Hyp-Link'));
});

it('applies extra classes', () => {
const wrapper = createComponent({ classes: 'foo bar' });
assert.deepEqual(
[...wrapper.find(`a.Hyp-Link.foo.bar`).getDOMNode().classList.values()],
['Hyp-Link', 'foo', 'bar']
);
});

it('passes along a `ref` to the `a` element through `linkRef`', () => {
const linkRef = createRef();
createComponent({ linkRef });

assert.instanceOf(linkRef.current, Node);
});

it('adds common `rel` attributes', () => {
const wrapper = createComponent();
assert.equal(
wrapper.find('a').first().getDOMNode().getAttribute('rel'),
'noopener noreferrer'
);
});
});
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { IconButton, LabeledButton, LinkButton } from './components/buttons';
export { LabeledCheckbox, Checkbox } from './components/Checkbox';
export { Frame, Card, Actions, Scrollbox } from './components/containers';
export { Dialog } from './components/Dialog';
export { Link } from './components/Link';
export { Modal, ConfirmModal } from './components/Modal';
export { Panel } from './components/Panel';
export { Spinner } from './components/Spinner';
Expand Down
5 changes: 3 additions & 2 deletions src/pattern-library/components/LibraryHome.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Link } from '../..';
import Library from './Library';

export default function LibraryHome() {
Expand Down Expand Up @@ -30,8 +31,8 @@ export default function LibraryHome() {

<p>
<strong>Components</strong> are{' '}
<a href="https://preactjs.com/">Preact</a> components that are built
using underlying <strong>patterns</strong>.
<Link href="https://preactjs.com/">Preact</Link> components that are
built using underlying <strong>patterns</strong>.
</p>
</Library.Page>
);
Expand Down
12 changes: 6 additions & 6 deletions src/pattern-library/components/PlaygroundApp.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import classnames from 'classnames';

import { SvgIcon } from '../../components/SvgIcon';
import { Link, SvgIcon } from '../../';

import { getRoutes } from '../routes';
import { useRoute } from '../router';
Expand Down Expand Up @@ -53,25 +53,25 @@ export default function PlaygroundApp({
<main className="PlaygroundApp">
<div className="PlaygroundApp__sidebar">
<div className="PlaygroundApp__sidebar-home">
<a href={baseURL} onClick={e => navigate(e, '/')}>
<Link href={baseURL} onClick={e => navigate(e, '/')}>
<SvgIcon name="logo" />
</a>
</Link>
</div>
{routeGroups.map(rGroup => (
<div key={rGroup.title}>
<h2>{rGroup.title}</h2>
<ul>
{rGroup.routes.map(({ route, title }) => (
<li key={title}>
<a
className={classnames('PlaygroundApp__nav-item', {
<Link
classes={classnames('hyp-link PlaygroundApp__nav-item', {
'is-active': activeRoute.route === route,
})}
href={`${route}`}
onClick={e => navigate(e, route)}
>
{title}
</a>
</Link>
</li>
))}
</ul>
Expand Down
20 changes: 20 additions & 0 deletions src/pattern-library/components/patterns/LinkComponents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Link } from '../../../';
import Library from '../Library';

export default function LinkComponents() {
return (
<Library.Page title="Links">
<Library.Pattern title="Link">
<p>
<code>Link</code> components provide some common styling and attribute
for anchor (<code>a</code>) elements.
</p>
<Library.Example>
<Library.Demo withSource>
<Link href="https://www.example.com">A link to somewhere</Link>
</Library.Demo>
</Library.Example>
</Library.Pattern>
</Library.Page>
);
}
24 changes: 24 additions & 0 deletions src/pattern-library/components/patterns/LinkPatterns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Library from '../Library';

export default function LinkPatterns() {
return (
<Library.Page title="Links">
<Library.Pattern title="Link">
<p>
The link pattern for <code>{'<a>'}</code> tags uses brand red and no
underline, as well as a rounded keyboard focus ring.
</p>
<Library.Example>
<Library.Demo withSource>
<a className="hyp-link" href="http://www.example.com">
This is a link
</a>
<a className="hyp-link" href="http://www.example.com">
This is another link
</a>
</Library.Demo>
</Library.Example>
</Library.Pattern>
</Library.Page>
);
}
14 changes: 14 additions & 0 deletions src/pattern-library/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import LayoutFoundations from './components/patterns/LayoutFoundations';

import FormPatterns from './components/patterns/FormPatterns';
import ContainerPatterns from './components/patterns/ContainerPatterns';
import LinkPatterns from './components/patterns/LinkPatterns';
import PanelPatterns from './components/patterns/PanelPatterns';
import SpinnerPatterns from './components/patterns/SpinnerPatterns';
import TablePatterns from './components/patterns/TablePatterns';
Expand All @@ -14,6 +15,7 @@ import ButtonComponents from './components/patterns/ButtonComponents';
import ContainerComponents from './components/patterns/ContainerComponents';
import DialogComponents from './components/patterns/DialogComponents';
import FormComponents from './components/patterns/FormComponents';
import LinkComponents from './components/patterns/LinkComponents';
import PanelComponents from './components/patterns/PanelComponents';
import SpinnerComponents from './components/patterns/SpinnerComponents';
import TableComponents from './components/patterns/TableComponents';
Expand Down Expand Up @@ -63,6 +65,12 @@ const routes = [
component: FormPatterns,
group: 'patterns',
},
{
route: '/patterns-links',
title: 'Links',
component: LinkPatterns,
group: 'patterns',
},
{
route: '/patterns-panels',
title: 'Panels',
Expand Down Expand Up @@ -111,6 +119,12 @@ const routes = [
component: FormComponents,
group: 'components',
},
{
route: '/components-links',
title: 'Links',
component: LinkComponents,
group: 'components',
},
{
route: '/components-panel',
title: 'Panel',
Expand Down
26 changes: 0 additions & 26 deletions styles/base/_elements.scss
Original file line number Diff line number Diff line change
@@ -1,32 +1,6 @@
@use '../mixins/focus';
@use "../variables" as var;

$-border-radius: var.$border-radius;
$-color-link: var.$color-link;
$-color-link--hover: var.$color-link--hover;

// basic standard styling for elements
// TODO: Evaluate if dependencies on variables can be reduced or eliminated
a {
@include focus.outline-on-keyboard-focus;
// Ensure the tag has width in order for :focus styling to render correctly.
display: inline-block;
color: $-color-link;
text-decoration: none;
// Give links a radius to allow :focus styling to appear similar to that of buttons
border-radius: $-border-radius;
}

a:active,
a:focus {
text-decoration: none;
}

a:hover {
text-decoration: none;
color: $-color-link--hover;
}

p {
hyphens: auto;

Expand Down
5 changes: 5 additions & 0 deletions styles/components/Link.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@use '../mixins/patterns/links';

.Hyp-Link {
@include links.link;
}
1 change: 1 addition & 0 deletions styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
@use './components/Checkbox';
@use './components/containers';
@use './components/Dialog';
@use './components/Link';
@use './components/Modal';
@use './components/Panel';
@use './components/Spinner';
Expand Down
27 changes: 27 additions & 0 deletions styles/mixins/patterns/_links.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@use '../../variables' as var;

@use '../focus';

$-border-radius: var.$border-radius;
$-color-link: var.$color-link;
$-color-link--hover: var.$color-link--hover;

@mixin link {
@include focus.outline-on-keyboard-focus;
// Ensure the tag has width in order for :focus styling to render correctly.
display: inline-block;
color: $-color-link;
text-decoration: none;
// Give links a radius to allow :focus styling to appear similar to that of buttons
border-radius: $-border-radius;

&:active,
&:focus {
text-decoration: none;
}

&:hover {
text-decoration: none;
color: $-color-link--hover;
}
}
5 changes: 5 additions & 0 deletions styles/patterns/_links.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@use '../mixins/patterns/links';

.hyp-link {
@include links.link;
}
1 change: 1 addition & 0 deletions styles/patterns/index.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@use 'containers';
@use 'forms';
@use 'links';
@use 'panels';
@use 'spinners';
@use 'tables';
Expand Down

0 comments on commit 31b3fe6

Please sign in to comment.