Skip to content

Commit

Permalink
feat(components): standardize ref API (#592)
Browse files Browse the repository at this point in the history
* Adds ref to Button and IconButton

* Adds additionalRefs to IconButton

* refactor: adds overloads back

* refactor: removes additional references

* feat: adds ref to Link, Anchor, Text, Button-Link

* feat: adds ref to input and textarea

* feat: adds ref to select

* feat: adds ref to checkbox

* feat: adds ref to currency input

* feat: adds ref to search input

* feat: adds ref to textarea

* feat: adds ref to radio button

* fix: remove deepRef

* fix: remove overload to fix type error with story

* fix: docs position to be picked up by ts

* feat: adds ref to selector

* feat: adds ref to toggle, closeButton, tag, switch

* feat: adds ref to badge

* fix: remove extra line

* refactor: temporarily disable static styles
  • Loading branch information
nicosommi authored Jun 4, 2020
1 parent 7769285 commit 20e61fc
Show file tree
Hide file tree
Showing 35 changed files with 686 additions and 247 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"check:licenses": "license-checker --production --summary --failOn=GPLv3",
"predeploy": "rm -rf ./public && yarn build:docs && cp ./src/CNAME ./public/CNAME",
"deploy": "gh-pages -d public",
"now-build": "yarn build:docs",
"now-build": "yarn build:storybook",
"prepublishOnly": "yarn build",
"release": "semantic-release"
},
Expand Down
36 changes: 36 additions & 0 deletions src/components/Anchor/Anchor.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,42 @@ describe('Anchor', () => {

expect(props.onClick).toHaveBeenCalledTimes(1);
});

/**
* Should accept a working ref for "button"
*/
it('should accept a working ref for a "button"', () => {
const tref = React.createRef<any>();
const { container } = render(
<Anchor ref={tref}>This is a span as button</Anchor>
);
const button = container.querySelector('span');
expect(tref.current).toBe(button);
});

/**
* Should accept a working ref for link
*/
it('should accept a working ref for a link', () => {
const tref = React.createRef<any>();
const { container } = render(
<Anchor href="http://sumup.com" ref={tref}>
Link button
</Anchor>
);
const anchor = container.querySelector('a');
expect(tref.current).toBe(anchor);
});

/**
* Should accept a working ref for span
*/
it('should accept a working ref for a span', () => {
const tref = React.createRef<any>();
const { container } = render(<Anchor ref={tref}>Text button</Anchor>);
const span = container.querySelector('span');
expect(tref.current).toBe(span);
});
});

describe('accessibility', () => {
Expand Down
26 changes: 15 additions & 11 deletions src/components/Anchor/Anchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ import { useComponents } from '../ComponentsContext';

interface BaseProps extends TextProps {
children: ReactNode;
/**
* The ref to the html dom element, it can be a button an anchor or a span, typed as any for now because of complex js manipulation with styled components
*/
ref?: React.Ref<any>;
}

type LinkElProps = Omit<HTMLProps<HTMLAnchorElement>, 'size'>;
type ButtonElProps = Omit<HTMLProps<HTMLButtonElement>, 'size'>;

Expand Down Expand Up @@ -69,23 +72,24 @@ const baseStyles = ({ theme }: StyleProps) => css`

const BaseAnchor = styled(Text)<AnchorProps>(baseStyles);

/**
* The Anchor is used to display a link or button that visually looks like
* a hyperlink. Based on the Text component, so it also supports its props.
*/
export function Anchor(props: BaseProps & LinkElProps): ReturnType;
export function Anchor(props: BaseProps & ButtonElProps): ReturnType;
export function Anchor(props: AnchorProps): ReturnType {
function AnchorComponent(props: AnchorProps, ref?: React.Ref<any>): ReturnType {
const { Link } = useComponents();
const AnchorLink = BaseAnchor.withComponent(Link);

if (!props.href && !props.onClick) {
return <Text as="span" {...props} />;
return <Text as="span" {...props} ref={ref} />;
}

if (props.href) {
return <AnchorLink {...props} />;
// typing issues with with
return <AnchorLink {...props} ref={ref as React.Ref<any>} />;
}

return <BaseAnchor as="button" {...props} />;
return <BaseAnchor as="button" {...props} ref={ref as React.Ref<any>} />;
}

/**
* The Anchor is used to display a link or button that visually looks like
* a hyperlink. Based on the Text component, so it also supports its props.
*/
export const Anchor = React.forwardRef(AnchorComponent);
18 changes: 16 additions & 2 deletions src/components/Badge/Badge.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,11 @@ const StyledBadge = styled('div')(
/**
* A badge for displaying update notifications etc.
*/
const Badge = props => <StyledBadge {...props} />;
const Badge = React.forwardRef((props, ref) => (
<StyledBadge {...props} ref={ref} />
));

Badge.displayName = 'Badge';

Badge.NEUTRAL = colorNames.NEUTRAL;
Badge.PRIMARY = colorNames.PRIMARY;
Expand All @@ -141,12 +145,22 @@ Badge.propTypes = {
Badge.SUCCESS,
Badge.WARNING,
Badge.DANGER
]),
/**
* The ref to the html button dom element
*/
ref: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({
current: PropTypes.oneOf([PropTypes.instanceOf(HTMLDivElement)])
})
])
};

Badge.defaultProps = {
circle: false,
color: Badge.NEUTRAL
color: Badge.NEUTRAL,
ref: undefined
};

/**
Expand Down
12 changes: 12 additions & 0 deletions src/components/Badge/Badge.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ describe('Badge', () => {
expect(actual).toMatchSnapshot();
});

describe('business logic', () => {
/**
* Should accept a working ref
*/
it('should accept a working ref', () => {
const tref = React.createRef();
const { container } = render(<Badge ref={tref} />);
const div = container.querySelector('div');
expect(tref.current).toBe(div);
});
});

/**
* Accessibility tests.
*/
Expand Down
26 changes: 26 additions & 0 deletions src/components/Button/Button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,32 @@ describe('Button', () => {

expect(props.onClick).toHaveBeenCalledTimes(1);
});

/**
* Should accept a working ref for button
*/
it('should accept a working ref for a button', () => {
const tref = React.createRef<HTMLButtonElement & HTMLAnchorElement>();
const { container } = render(
<Button ref={tref}>This is a button</Button>
);
const button = container.querySelector('button');
expect(tref.current).toBe(button);
});

/**
* Should accept a working ref for link
*/
it('should accept a working ref for a link', () => {
const tref = React.createRef<HTMLButtonElement & HTMLAnchorElement>();
const { container } = render(
<Button href="http://sumup.com" ref={tref}>
Link button
</Button>
);
const button = container.querySelector('a');
expect(tref.current).toBe(button);
});
});

describe('accessibility', () => {
Expand Down
35 changes: 23 additions & 12 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export interface BaseProps {
* Display an icon in addition to the text to help to identify the action.
*/
icon?: FC<SVGProps<SVGSVGElement>>;
/**
* The ref to the html dom element, it can be an anchor or a button
*/
ref?: React.Ref<HTMLButtonElement & HTMLAnchorElement>;
}

type LinkElProps = Omit<HTMLProps<HTMLAnchorElement>, 'size' | 'type'>;
Expand Down Expand Up @@ -187,24 +191,31 @@ const BaseButton = styled('button')<ButtonProps>(
stretchStyles
);

/**
* The Button component enables the user to perform an action or navigate
* to a different screen.
*/
export function Button(props: BaseProps & LinkElProps): ReturnType;
export function Button(props: BaseProps & ButtonElProps): ReturnType;
export function Button({
children,
icon: Icon,
...props
}: ButtonProps): ReturnType {
export function ButtonComponent(
props: BaseProps & LinkElProps,
ref?: React.Ref<HTMLButtonElement & HTMLAnchorElement>
): ReturnType;
export function ButtonComponent(
props: BaseProps & ButtonElProps,
ref?: React.Ref<HTMLButtonElement & HTMLAnchorElement>
): ReturnType;
export function ButtonComponent(
{ children, icon: Icon, ...props }: ButtonProps,
ref?: React.Ref<HTMLButtonElement & HTMLAnchorElement>
): ReturnType {
const { Link } = useComponents();
const LinkButton = BaseButton.withComponent(Link);
const ButtonElement = props.href ? LinkButton : BaseButton;
return (
<ButtonElement {...props}>
<ButtonElement {...props} ref={ref}>
{Icon && <Icon css={iconStyles} role="presentation" />}
{children}
</ButtonElement>
);
}

/**
* The Button component enables the user to perform an action or navigate
* to a different screen.
*/
export const Button = React.forwardRef(ButtonComponent);
46 changes: 31 additions & 15 deletions src/components/Checkbox/Checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,19 +152,19 @@ const CheckboxTooltip = styled(Tooltip)`
${tooltipBaseStyles};
`;

/**
* Checkbox component for forms.
*/
const Checkbox = ({
children,
value,
id: customId,
name,
disabled,
validationHint,
className,
...props
}) => {
const CheckboxComponent = (
{
children,
value,
id: customId,
name,
disabled,
validationHint,
className,
...props
},
ref
) => {
const id = customId || uniqueId('checkbox_');
return (
<CheckboxWrapper className={className}>
Expand All @@ -175,6 +175,7 @@ const Checkbox = ({
value={value}
type="checkbox"
disabled={disabled}
ref={ref}
/>
<CheckboxLabel {...props} htmlFor={id} disabled={disabled}>
{children}
Expand All @@ -189,6 +190,11 @@ const Checkbox = ({
);
};

/**
* Checkbox component for forms.
*/
const Checkbox = React.forwardRef(CheckboxComponent);

Checkbox.propTypes = {
/**
* Controles/Toggles the checked state.
Expand Down Expand Up @@ -231,7 +237,16 @@ Checkbox.propTypes = {
/**
* Override styles for the Checkbox component.
*/
className: PropTypes.string
className: PropTypes.string,
/**
* The ref to the html dom element
*/
ref: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({
current: PropTypes.oneOf([PropTypes.instanceOf(HTMLInputElement)])
})
])
};

Checkbox.defaultProps = {
Expand All @@ -242,7 +257,8 @@ Checkbox.defaultProps = {
invalid: false,
disabled: false,
children: null,
className: ''
className: '',
ref: undefined
};

/**
Expand Down
12 changes: 12 additions & 0 deletions src/components/Checkbox/Checkbox.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ describe('Checkbox', () => {
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
});

describe('business logic', () => {
/**
* Should accept a working ref
*/
it('should accept a working ref', () => {
const tref = React.createRef();
const { container } = render(<Checkbox ref={tref} />);
const checkbox = container.querySelector('input');
expect(tref.current).toBe(checkbox);
});
});

/**
* Accessibility tests.
*/
Expand Down
15 changes: 11 additions & 4 deletions src/components/CloseButton/CloseButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,21 @@ const buttonStyles = () => css`
/**
* A generic close button.
*/
const CloseButton = props => (
<IconButton css={buttonStyles} {...props}>
const CloseButton = React.forwardRef((props, ref) => (
<IconButton css={buttonStyles} {...props} ref={ref}>
<Cross />
</IconButton>
);
));

CloseButton.displayName = 'CloseButton';

CloseButton.propTypes = {
...IconButton.propTypes
};

CloseButton.defaultProps = {
label: 'Close'
label: 'Close',
ref: undefined
};

/**
Expand Down
12 changes: 12 additions & 0 deletions src/components/CloseButton/CloseButton.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ describe('CloseButton', () => {
expect(actual).toMatchSnapshot();
});

describe('business logic', () => {
/**
* Should accept a working ref
*/
it('should accept a working ref', () => {
const tref = React.createRef();
const { container } = render(<CloseButton ref={tref} />);
const button = container.querySelector('button');
expect(tref.current).toBe(button);
});
});

/**
* Accessibility tests.
*/
Expand Down
Loading

0 comments on commit 20e61fc

Please sign in to comment.