Skip to content
Permalink
Browse files

fix(anchor): export types and add tests (#153)

* fix(anchor): export types and add tests

* chore: prop-type dependency fixes

* chore: update anchor snapshot

* fix(anchor): refactor naming, remove defaultProps

* chore: disable defaultprop rule
  • Loading branch information...
richbachman committed Oct 30, 2019
1 parent 117c41e commit 4086ee28da829820ca8f791e4bddf2768bd8b2f9
@@ -33,16 +33,30 @@ module.exports = {
'react/destructuring-assignment': 'off',
// No jsx extension: https://github.com/facebook/create-react-app/issues/87#issuecomment-234627904
'react/jsx-filename-extension': 'off',
// Doesnt really work in our use-cases: https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/require-default-props.md
"react/require-default-props": 'off',
// Use function hoisting to improve code readability
'no-use-before-define': ['error', {functions: false, classes: true, variables: true}],
'no-use-before-define': ['error', {
functions: false,
classes: true,
variables: true
}],
// Makes no sense to allow type inferrence for expression parameters, but require typing the response
'@typescript-eslint/explicit-function-return-type': [
'error',
{allowExpressions: true, allowTypedFunctionExpressions: true},
{
allowExpressions: true,
allowTypedFunctionExpressions: true
},
],
'@typescript-eslint/no-use-before-define': [
'error',
{functions: false, classes: true, variables: true, typedefs: true},
{
functions: false,
classes: true,
variables: true,
typedefs: true
},
],
// Common abbreviations are known and readable
'unicorn/prevent-abbreviations': 'off',
@@ -83,7 +97,9 @@ module.exports = {
},
settings: {
'import/resolver': {
[path.resolve('./.eslint/resolver')]: {someConfig: ''},
[path.resolve('./.eslint/resolver')]: {
someConfig: ''
},
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.d.ts'],
},
@@ -0,0 +1,140 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Anchor it should render an anchor 1`] = `
.emotion-2 {
font-family: 'Whitney SSm A','Whitney SSm B','Helvetica Neue',Helvetica,Arial,sans-serif;
line-height: 1.15;
color: #282a2b;
font-weight: 400;
}

.emotion-2 *,
.emotion-2 *::after,
.emotion-2 *::before {
box-sizing: border-box;
}

.emotion-0 {
color: #0075c3;
-webkit-text-decoration: none;
text-decoration: none;
outline: none;
}

.emotion-0:hover {
-webkit-text-decoration: underline;
text-decoration: underline;
}

.emotion-0:focus,
.emotion-0:active {
box-shadow: 0 0 0 4px rgba(0,117,195,0.5);
-webkit-text-decoration: underline;
text-decoration: underline;
}

<div
className="emotion-2"
>
<a
className="emotion-0 emotion-1"
href="/"
>
This is an anchor
</a>
</div>
`;

exports[`Anchor it should render an external anchor 1`] = `
.emotion-2 {
font-family: 'Whitney SSm A','Whitney SSm B','Helvetica Neue',Helvetica,Arial,sans-serif;
line-height: 1.15;
color: #282a2b;
font-weight: 400;
}

.emotion-2 *,
.emotion-2 *::after,
.emotion-2 *::before {
box-sizing: border-box;
}

.emotion-0 {
color: #0075c3;
-webkit-text-decoration: none;
text-decoration: none;
outline: none;
}

.emotion-0:hover {
-webkit-text-decoration: underline;
text-decoration: underline;
}

.emotion-0:focus,
.emotion-0:active {
box-shadow: 0 0 0 4px rgba(0,117,195,0.5);
-webkit-text-decoration: underline;
text-decoration: underline;
}

<div
className="emotion-2"
>
<a
className="emotion-0 emotion-1"
href="https://twilio.com"
rel="noreferrer noopener"
target="_blank"
>
This is an anchor that links to Twilio.com with an external target and rel
</a>
</div>
`;

exports[`Anchor it should render an external anchor with overwritten target and rel 1`] = `
.emotion-2 {
font-family: 'Whitney SSm A','Whitney SSm B','Helvetica Neue',Helvetica,Arial,sans-serif;
line-height: 1.15;
color: #282a2b;
font-weight: 400;
}

.emotion-2 *,
.emotion-2 *::after,
.emotion-2 *::before {
box-sizing: border-box;
}

.emotion-0 {
color: #0075c3;
-webkit-text-decoration: none;
text-decoration: none;
outline: none;
}

.emotion-0:hover {
-webkit-text-decoration: underline;
text-decoration: underline;
}

.emotion-0:focus,
.emotion-0:active {
box-shadow: 0 0 0 4px rgba(0,117,195,0.5);
-webkit-text-decoration: underline;
text-decoration: underline;
}

<div
className="emotion-2"
>
<a
className="emotion-0 emotion-1"
href="https://twilio.com"
rel="noopener"
target="_blank"
>
This is an anchor that links to Twilio.com with the target and rel overwritten
</a>
</div>
`;
@@ -0,0 +1,41 @@
import * as React from 'react';
import renderer from 'react-test-renderer';
import {Theme} from '@twilio-paste/theme';
import {Anchor} from '../src';

describe('Anchor', () => {
it('it should render an anchor', (): void => {
const tree = renderer
.create(
<Theme.Provider>
<Anchor href="/">This is an anchor</Anchor>
</Theme.Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('it should render an external anchor', (): void => {
const tree = renderer
.create(
<Theme.Provider>
<Anchor href="https://twilio.com">
This is an anchor that links to Twilio.com with an external target and rel
</Anchor>
</Theme.Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('it should render an external anchor with overwritten target and rel', (): void => {
const tree = renderer
.create(
<Theme.Provider>
<Anchor href="https://twilio.com" target="_self" rel="noopener">
This is an anchor that links to Twilio.com with the target and rel overwritten
</Anchor>
</Theme.Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
@@ -30,6 +30,7 @@
"@emotion/core": "^10.0.10",
"@emotion/styled": "^10.0.10",
"@styled-system/theme-get": "^5.1.2",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
@@ -1,48 +1,48 @@
import * as React from 'react';
import {StyledLink} from './styles';
import {AnchorProps} from './types';

const EXTERNAL_LINK_REGEX = /^(https?:)[^\s]*$/;
import * as PropTypes from 'prop-types';
import {StyledAnchor} from './styles';

export type AnchorTargets = '_self' | '_blank' | '_parent' | '_top';
export type AnchorTabIndexes = 0 | -1;

interface Anchor {
className?: never;
children: NonNullable<React.ReactNode>;
href: string;
id?: never;
onBlur?(event: React.FocusEvent<HTMLElement>): void;
onClick?(event: React.MouseEvent<HTMLElement>): void;
onFocus?(event: React.FocusEvent<HTMLElement>): void;
rel?: string;
tabIndex?: AnchorTabIndexes;
target?: AnchorTargets;
}

const EXTERNAL_URL_REGEX = /^(https?:)[^\s]*$/;
const EXTERNAL_TARGET_DEFAULT = '_blank';
const EXTERNAL_REL_DEFAULT = 'noreferrer noopener';

const isExternalUrl = (url: string): boolean => EXTERNAL_LINK_REGEX.test(url);

const handlePropValidation = ({href, tabIndex, children}: AnchorProps): void => {
const hasHref = href != null && href !== '';
const hasTabIndex = tabIndex != null;

if (!hasHref) {
throw new Error(
`[Paste: Anchor] Missing href prop for anchor. Maybe you're looking for the [Paste: Button] component.`
);
}

if (children == null) {
throw new Error(`[Paste: Anchor] Must have non-null children.`);
}

if (hasTabIndex && !(tabIndex === 0 || tabIndex === -1)) {
throw new Error(`[Paste: Anchor] TabIndex must be 0 or -1.`);
}
};

const Anchor: React.FC<AnchorProps> = props => {
handlePropValidation(props);

return (
<StyledLink
href={props.href}
rel={isExternalUrl(props.href) && !props.rel ? EXTERNAL_REL_DEFAULT : props.rel}
onBlur={props.onBlur}
onClick={props.onClick}
onFocus={props.onFocus}
tabIndex={props.tabIndex}
target={isExternalUrl(props.href) && !props.target ? EXTERNAL_TARGET_DEFAULT : props.target}
>
{props.children}
</StyledLink>
);
const isExternalUrl = (url: string): boolean => EXTERNAL_URL_REGEX.test(url);

const Anchor: React.FC<Anchor> = props => (
<StyledAnchor
href={props.href}
rel={isExternalUrl(props.href) && !props.rel ? EXTERNAL_REL_DEFAULT : props.rel}
onBlur={props.onBlur}
onClick={props.onClick}
onFocus={props.onFocus}
tabIndex={props.tabIndex}
target={props.target || isExternalUrl(props.href) ? EXTERNAL_TARGET_DEFAULT : undefined}
>
{props.children}
</StyledAnchor>
);

Anchor.propTypes = {
children: PropTypes.node.isRequired,
tabIndex: PropTypes.oneOf([0, -1]),
target: PropTypes.oneOf(['_self', '_blank', '_parent', '_top']),
};

Anchor.displayName = 'Anchor';
export {Anchor};
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import {themeGet} from '@styled-system/theme-get';

// Link
export const StyledLink = styled.a`
export const StyledAnchor = styled.a`
color: ${themeGet('textColors.colorTextLink')};
text-decoration: none;
outline: none;

This file was deleted.

@@ -2,8 +2,7 @@ import * as React from 'react';
import {storiesOf} from '@storybook/react';
import {action} from '@storybook/addon-actions';
import {withKnobs, select, text} from '@storybook/addon-knobs';
import {Anchor} from '../src';
import {AnchorTargets, AnchorTabIndexes} from '../src/types';
import {Anchor, AnchorTargets, AnchorTabIndexes} from '../src';

const AnchorTargetOptions = ['_self', '_blank', '_parent', '_top'];
const AnchorTabIndexOptions = [0, -1];
@@ -43,6 +43,7 @@
"peerDependencies": {
"@emotion/core": "^10.0.10",
"@emotion/styled": "^10.0.10",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-uid": "^2.2.0",
@@ -50,6 +50,7 @@
"lodash": "^4.17.15",
"pretty-format": "^24.9.0",
"prism-react-renderer": "^0.1.7",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-helmet": "^5.2.1",
@@ -2,7 +2,6 @@ import * as React from 'react';
import {MDXProvider} from '@mdx-js/react';
import styled from '@emotion/styled';
import {Anchor} from '@twilio-paste/anchor';
import {AnchorProps} from '@twilio-paste/anchor/dist/types';
import {Codeblock, CodeblockProps} from '../codeblock';
import {Table, Tbody, Tr, Th, Td} from '../table';
import {Heading, AnchoredHeading, HeadingProps} from '../Heading';
@@ -74,7 +73,7 @@ export const PasteMDXProvider: React.FC<PasteMDXProviderProps> = (props: PasteMD
strong: (props: React.ComponentProps<'strong'>): React.ReactElement => <strong {...props} />,
del: (props: React.ComponentProps<'del'>): React.ReactElement => <del {...props} />,
hr: (props: React.ComponentProps<'hr'>): React.ReactElement => <StyledHr {...props} />,
a: (props: AnchorProps): React.ReactElement => <Anchor {...props} />, // eslint-disable-line jsx-a11y/anchor-has-content
a: (props: Anchor): React.ReactElement => <Anchor {...props} />, // eslint-disable-line jsx-a11y/anchor-has-content
img: (props: React.ComponentProps<'img'>): React.ReactElement => <img {...props} />, // eslint-disable-line jsx-a11y/alt-text
content: (props: React.ComponentProps<'div'>): React.ReactElement => <StyledContent {...props} />,
contentwrapper: (props: React.ComponentProps<'div'>): React.ReactElement => <StyledContentWrapper {...props} />,

1 comment on commit 4086ee2

@now

This comment has been minimized.

Please sign in to comment.
You can’t perform that action at this time.