Skip to content

Commit

Permalink
feat(clickable): add clickable component
Browse files Browse the repository at this point in the history
This adds a clickable component which adds click specific attributes to a child element and is using
a11y best practices.

Close DCOS-22331
  • Loading branch information
Poltergeist committed May 4, 2018
1 parent b6a21ba commit a5ddbf3
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/clickable/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Clickable Name

Clickable documentation
50 changes: 50 additions & 0 deletions packages/clickable/components/clickable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from "react";

import { cx } from "emotion";
import { outline } from "../style";

export interface IClickableProps {
/**
* Children should be a HTML element.
*/
children: React.ReactElement<HTMLElement>;
/**
* Action is a event handler for the onClick and onKeypress events
*/
action: (event?: React.SyntheticEvent<HTMLElement>) => void;
/**
* The tabIndex is passed down and is the same as the native tabIndex
*/
tabIndex?: number | string;
}

class Clickable extends React.PureComponent<IClickableProps, {}> {
public static defaultProps: Partial<IClickableProps> = {
tabIndex: -1
};

constructor(props: IClickableProps) {
super(props);
this.handleKeyPress = this.handleKeyPress.bind(this);
}

public render() {
const { children, action, tabIndex } = this.props;
const { className = "" } = children.props;

return React.cloneElement(React.Children.only(children), {
onClick: action,
className: cx(className, outline),
tabIndex,
onKeyPress: this.handleKeyPress
});
}

public handleKeyPress(event: React.KeyboardEvent<HTMLElement>): void {
if (event.key === " " || event.key === "Enter") {
this.props.action(event);
}
}
}

export default Clickable;
1 change: 1 addition & 0 deletions packages/clickable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Clickable } from "./clickable";
15 changes: 15 additions & 0 deletions packages/clickable/stories/clickable.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import { withReadme } from "storybook-readme";
import Clickable from "../components/clickable";
import { action } from "@storybook/addon-actions";

const readme = require("../README.md");

storiesOf("Clickable", module)
.addDecorator(withReadme([readme]))
.addWithInfo("default", () => (
<Clickable action={action("action trigger")} tabIndex="0">
<span>Click me!</span>
</Clickable>
));
10 changes: 10 additions & 0 deletions packages/clickable/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { css } from "emotion";
import { coreColors } from "../shared/styles/color";

const { white } = coreColors();
export const outline = css`
&:focus {
box-shadow: 0 0 0 1px ${white}, 0 0 0 3px currentColor;
outline: 0;
}
`;
92 changes: 92 additions & 0 deletions packages/clickable/tests/clickable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from "react";

import { shallow } from "enzyme";
import Clickable from "../components/clickable";

describe("Clickable", () => {
it("has onClick function", () => {
const action = jest.fn();
const wrapper = shallow(
<Clickable action={action}>
<span>onClick</span>
</Clickable>
);
wrapper.simulate("click");
expect(action).toHaveBeenCalled();
});

it("has onKeyPress function and reacts on space", () => {
const action = jest.fn();
const wrapper = shallow(
<Clickable action={action}>
<span>onKeyPress</span>
</Clickable>
);
wrapper.simulate("keyPress", {
key: " ",
keyCode: 32,
which: 32
});
expect(action).toHaveBeenCalled();
});

it("has onKeyPress function and reacts on Enter", () => {
const action = jest.fn();
const wrapper = shallow(
<Clickable action={action}>
<span>onKeyPress</span>
</Clickable>
);
wrapper.simulate("keyPress", {
key: "Enter",
keyCode: 13,
which: 13
});
expect(action).toHaveBeenCalled();
});
it("does not react on e keypress", () => {
const action = jest.fn();
const wrapper = shallow(
<Clickable action={action}>
<span>onKeyPress</span>
</Clickable>
);
wrapper.simulate("keyPress", {
key: "e",
keyCode: 69,
which: 69
});
expect(action).not.toHaveBeenCalled();
});
describe("tabIndex", () => {
it("default value", () => {
const action = jest.fn();
const wrapper = shallow(
<Clickable action={action}>
<span>default tabIndex</span>
</Clickable>
);
expect(
wrapper
.find("span")
.props()
.tabIndex.toString()
).toEqual("-1");
});

it("takes 10 as a value", () => {
const action = jest.fn();
const wrapper = shallow(
<Clickable action={action} tabIndex="10">
<span>default tabIndex</span>
</Clickable>
);
expect(
wrapper
.find("span")
.props()
.tabIndex.toString()
).toEqual("10");
});
});
});

0 comments on commit a5ddbf3

Please sign in to comment.