From ef1f1a26834a7da5e96c2c036ea14e89577c35f4 Mon Sep 17 00:00:00 2001 From: Andrii Vorobiov Date: Thu, 7 May 2020 18:26:36 +0300 Subject: [PATCH] ui-components: Input Base implementation of Input component - Added Storybook as a playground to validate styles. - Supports only `text` type yet. - Added tests to validate default state and user interaction with component --- .../stories/Input.stories.js | 22 +++++ .../ui-components/src/Input/Input.module.scss | 41 +++++++++ packages/ui-components/src/Input/Input.tsx | 85 +++++++++++++++++++ packages/ui-components/src/Input/index.ts | 1 + .../ui-components/src/Input/input.test.tsx | 50 +++++++++++ packages/ui-components/src/index.js | 1 + 6 files changed, 200 insertions(+) create mode 100644 packages/storybook-ui-components/stories/Input.stories.js create mode 100644 packages/ui-components/src/Input/Input.module.scss create mode 100644 packages/ui-components/src/Input/Input.tsx create mode 100644 packages/ui-components/src/Input/index.ts create mode 100644 packages/ui-components/src/Input/input.test.tsx diff --git a/packages/storybook-ui-components/stories/Input.stories.js b/packages/storybook-ui-components/stories/Input.stories.js new file mode 100644 index 000000000..d1a408e47 --- /dev/null +++ b/packages/storybook-ui-components/stories/Input.stories.js @@ -0,0 +1,22 @@ +import React, { useState } from "react"; +import { withKnobs, text, boolean } from "@storybook/addon-knobs"; +import { Input } from "@cockroachlabs/ui-components"; + +export default { + title: "Input", + components: Input, + decorators: [withKnobs], +}; + +export const Demo = () => { + const [value, setValue] = useState(); + return ( + + ); +}; diff --git a/packages/ui-components/src/Input/Input.module.scss b/packages/ui-components/src/Input/Input.module.scss new file mode 100644 index 000000000..cede0a4e3 --- /dev/null +++ b/packages/ui-components/src/Input/Input.module.scss @@ -0,0 +1,41 @@ +@import "../styles/tokens.scss"; + +.input { + height: 40px; + border-radius: 3px; + font-family: Source Sans Pro, sans-serif; // TODO (koorosh): replace with token value + font-size: 14px; // TODO (koorosh): replace with token value (default font size) + line-height: 22px; + border: solid 1px $crl-neutral-4; + background-color: $crl-base-white; + padding: 0 crl-gutters(1.5); + + &:focus { + border: solid 1px $crl-base-blue; + } + + &:hover { + border: solid 1px $crl-neutral-5; + } +} + +.invalid { + border: solid 1px $crl-base-red; + background-color: $crl-red-1; + color: $crl-base-red; + + &:focus, &:hover { + border: solid 1px $crl-base-red; + } +} + +.disabled { + background: $crl-neutral-1; + color: $crl-base-text--light; + border: solid 1px $crl-neutral-3; + cursor: default; + + &:focus, &:hover { + border: solid 1px $crl-neutral-3; + } +} diff --git a/packages/ui-components/src/Input/Input.tsx b/packages/ui-components/src/Input/Input.tsx new file mode 100644 index 000000000..523f165ad --- /dev/null +++ b/packages/ui-components/src/Input/Input.tsx @@ -0,0 +1,85 @@ +import React, { + ChangeEvent, + CSSProperties, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import classNames from "classnames/bind"; +import styles from "./input.module.scss"; + +export interface InputProps { + type?: string; + className?: string; + style?: CSSProperties; + initialValue?: string; + value?: string; + autoComplete?: string; + placeholder?: string; + onChange?: (value: string) => void; + name?: string; + disabled?: boolean; + invalid?: boolean; +} + +const cx = classNames.bind(styles); + +const Input: React.FC = ({ + name, + type = "text", + autoComplete = "off", + className, + style, + value: outerValue, + initialValue = "", + placeholder, + onChange, + disabled = false, + invalid = false, +}) => { + const [value, setValue] = useState(outerValue || initialValue); + + useEffect(() => { + setValue(outerValue); + }, [outerValue]); + + const onChangeHandler = useCallback( + (event: ChangeEvent) => { + setValue(event.target.value); + if (onChange && !disabled) { + onChange(event.target.value); + } + }, + [onChange, disabled], + ); + + const classnames = useMemo( + () => + cx( + "input", + { + disabled, + invalid, + }, + className, + ), + [className, invalid, disabled], + ); + + return ( + + ); +}; + +export default Input; diff --git a/packages/ui-components/src/Input/index.ts b/packages/ui-components/src/Input/index.ts new file mode 100644 index 000000000..a5f313c2e --- /dev/null +++ b/packages/ui-components/src/Input/index.ts @@ -0,0 +1 @@ +export { default as Input } from "./Input"; diff --git a/packages/ui-components/src/Input/input.test.tsx b/packages/ui-components/src/Input/input.test.tsx new file mode 100644 index 000000000..b7bf416ed --- /dev/null +++ b/packages/ui-components/src/Input/input.test.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { shallow } from "enzyme"; +import Input from "./Input"; + +describe("Input", () => { + describe("Default props", () => { + it("provides correct default values to inner element", () => { + const wrapper = shallow(); + const props = wrapper.props(); + expect(props.disabled).toBeFalsy(); + expect(props.value).toEqual(""); + expect(props.className).toEqual("input"); + expect(props.name).toBeUndefined(); + expect(props.style).toBeUndefined(); + expect(props.type).toEqual("text"); + }); + }); + + describe("Style classes", () => { + it("sets .disabled class when Input is disabled", () => { + const wrapper = shallow(); + expect(wrapper.hasClass("disabled")).toBeTruthy(); + }); + + it("sets .invalid class when Input is disabled", () => { + const wrapper = shallow(); + expect(wrapper.hasClass("invalid")).toBeTruthy(); + }); + }); + + describe("onChange handler", () => { + it("calls callback function with typed text", () => { + const onChangeSpyFn = jasmine.createSpy(); + const text = "some text"; + const wrapper = shallow(); + wrapper.simulate("change", { target: { value: text } }); + expect(onChangeSpyFn).toHaveBeenCalledWith(text); + }); + + it("does not call callback when Input is disabled", () => { + const onChangeSpyFn = jasmine.createSpy(); + const text = "some text"; + const wrapper = shallow( + , + ); + wrapper.simulate("change", { target: { value: text } }); + expect(onChangeSpyFn).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/ui-components/src/index.js b/packages/ui-components/src/index.js index 5c7042709..d60ef4102 100644 --- a/packages/ui-components/src/index.js +++ b/packages/ui-components/src/index.js @@ -1 +1,2 @@ export { Badge } from "./Badge"; +export { Input } from "./Input";