Skip to content
This repository has been archived by the owner on Jan 23, 2025. It is now read-only.

Commit

Permalink
ui-components: Input component with Icon
Browse files Browse the repository at this point in the history
It has to be possible to prefix Icon component at the beginning of Input element.
Current changes introduce some code refactoring where Input element is always
wrapped with outer container. And `prefix` prop is added to consume Icon component
with correct positioning and coloring.

Helpers module is supposed to contain small components like InputWrapper.
  • Loading branch information
koorosh committed May 14, 2020
1 parent a3bd24a commit f5f2785
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 48 deletions.
29 changes: 29 additions & 0 deletions packages/storybook-ui-components/stories/Input.stories.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from "react";
import { withKnobs, text, boolean, number } from "@storybook/addon-knobs";
import { NumberInput, TextInput } from "@cockroachlabs/ui-components";
import { Search } from "@cockroachlabs/icons";

export default {
title: "Input",
Expand Down Expand Up @@ -33,3 +34,31 @@ export const Text = () => {
/>
);
};

export const SearchInput = () => {
const [value, setValue] = useState();
return (
<TextInput
value={value}
disabled={boolean("disabled", false)}
onChange={setValue}
invalid={boolean("invalid", false)}
placeholder={text("Placeholder", "Search...")}
prefix={<Search />}
/>
);
};

export const NumberInputWithIcon = () => {
const [value, setValue] = useState();
return (
<NumberInput
value={value}
disabled={boolean("disabled", false)}
onChange={setValue}
invalid={boolean("invalid", false)}
placeholder={text("Placeholder", "Counter...")}
prefix={<Search />}
/>
);
};
1 change: 1 addition & 0 deletions packages/ui-components/src/Input/BaseInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface InputProps<T = string | number> {
name?: string;
disabled?: boolean;
invalid?: boolean;
prefix?: React.ReactNode;
}

const cx = classNames.bind(styles);
Expand Down
27 changes: 6 additions & 21 deletions packages/ui-components/src/Input/NumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,21 @@ import { CaretUp, CaretDown } from "@cockroachlabs/icons";
import isNumber from "../utils/isNumber";
import { BaseInput, InputProps } from "./BaseInput";
import styles from "./styles.module.scss";
import { InputPrefix, InputWrapper } from "./helpers";

const cx = classNames.bind(styles);

export type NumberInputProps = Omit<InputProps<number>, "type">;

interface InputWrapperProps {
className?: string;
}

const InputWrapper: React.FC<InputWrapperProps> = ({ children, className }) => {
return <div className={cx(className)}>{children}</div>;
};

export const NumberInput: React.FC<NumberInputProps> = ({
onChange,
value: outerValue,
initialValue,
prefix,
...props
}) => {
const { invalid, disabled, className } = props;
const [value, setValue] = useState<number>(outerValue || initialValue);
const { invalid, disabled } = props;
const [value, setValue] = useState<number>(outerValue || initialValue || 0);
const onSpinClickHandler = useCallback(
(increase: -1 | 1) => () => {
if (disabled) {
Expand All @@ -49,21 +43,12 @@ export const NumberInput: React.FC<NumberInputProps> = ({
[onChange],
);

const wrapperClassName = cx(
"container",
"number-type",
{
active: !invalid && !disabled,
disabled: disabled,
invalid: invalid,
},
className,
);
const spinButtonsGroupClassName = cx("spin-buttons-group");
const spinButton = cx("spin-button");

return (
<InputWrapper className={wrapperClassName}>
<InputWrapper disabled={disabled} invalid={invalid} className="number-type">
<InputPrefix>{prefix}</InputPrefix>
<BaseInput
{...props}
onChange={onChangeHandler}
Expand Down
15 changes: 6 additions & 9 deletions packages/ui-components/src/Input/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import React from "react";
import { BaseInput, InputProps } from "./BaseInput";
import classNames from "classnames/bind";
import styles from "./styles.module.scss";
import { InputPrefix, InputWrapper } from "./helpers";

export type TextInputProps = Omit<InputProps<string>, "type">;

const cx = classNames.bind(styles);

export const TextInput: React.FC<TextInputProps> = props => {
const { prefix, disabled, invalid } = props;
return (
<BaseInput
{...props}
type="text"
className={cx("container", props.className)}
/>
<InputWrapper disabled={disabled} invalid={invalid}>
<InputPrefix>{prefix}</InputPrefix>
<BaseInput {...props} type="text" />
</InputWrapper>
);
};
2 changes: 1 addition & 1 deletion packages/ui-components/src/Input/constants.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ $state-colors:

@function exclBorderWidth($total-width: 0, $spacing: $input-border-spacing, $border: $border-width) {
@return $total-width - ($border + $spacing) * 2;
}
}
44 changes: 44 additions & 0 deletions packages/ui-components/src/Input/helpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";
import classNames from "classnames/bind";
import styles from "./styles.module.scss";
import { InputProps } from "./BaseInput";

const cx = classNames.bind(styles);

export type InputWrapperProps = Pick<
InputProps,
"disabled" | "invalid" | "className"
>;

export interface InputPrefixProps {
className?: string;
}

export const InputPrefix: React.FC<InputPrefixProps> = ({
children,
className,
}) => {
if (!children) {
return null;
}
const classes = cx("prefix", className);
return <div className={classes}>{children}</div>;
};

export const InputWrapper: React.FC<InputWrapperProps> = ({
children,
className,
invalid,
disabled,
}) => {
const wrapperClassName = cx(
"container",
{
active: !invalid && !disabled,
disabled: disabled,
invalid: invalid,
},
className,
);
return <div className={wrapperClassName}>{children}</div>;
};
29 changes: 22 additions & 7 deletions packages/ui-components/src/Input/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
// element in case NumberInput is rendered.
.container {
border-radius: $input-border-spacing;
height: exclBorderWidth($input-height, 0);
height: exclBorderWidth($input-height);
display: inline-flex;
background-color: $crl-base-white;
min-width: exclBorderWidth($input-text-width);
width: exclBorderWidth($input-text-width);
padding: $input-border-spacing;

&.active {
border: solid $border-width $crl-neutral-4;
Expand Down Expand Up @@ -51,27 +54,39 @@
}
}

$input-side-padding: crl-gutters(1.5);
// ".input" removes most styling from element because it is responsibility of `.container` class.
.input {
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: none;
outline: none;
padding: 0 $input-side-padding;
padding: 0 crl-gutters(1.5);
position: relative;
background-color: inherit;
color: inherit;
width: 100%;

&:focus, &:hover {
border: none;
outline: none;
}
}

.container.input {
min-width: exclBorderWidth($input-text-width, $input-side-padding);
}

// Prefix component styles. Set constant width/height
// and apply colors based on container's state.
.container {
.prefix {
width: 14px; // TODO (koorosh): has to be the same as font-size
min-width: 14px;
height: 14px;
min-height: 14px;
margin: auto 0 auto 8px;
}

@each $state, $hoverColor, $focusColor in $state-colors {
&.#{$state} .prefix > svg {
fill: $hoverColor;
}
}
}
21 changes: 19 additions & 2 deletions packages/ui-components/src/Input/input.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from "react";
import { shallow } from "enzyme";
import { Search } from "@cockroachlabs/icons";
import { BaseInput, InputProps, NumberInput, TextInput } from "./index";
import exp from "constants";

describe("BaseInput", () => {
describe("Default props", () => {
Expand Down Expand Up @@ -53,13 +55,14 @@ describe("TextInput", () => {
describe("Default props", () => {
it("provides correct default values to inner <input /> element", () => {
const wrapper = shallow<InputProps>(<TextInput />);
const props = wrapper.props();
const inputWrapper = wrapper.find(BaseInput);
const props = inputWrapper.props();
expect(props.disabled).toBeFalsy();
expect(props.type).toEqual("text");
expect(props.initialValue).toBeUndefined();
expect(props.autoComplete).toBeUndefined();
expect(props.value).toBeUndefined();
expect(props.className).toEqual("container");
expect(props.className).toBeUndefined();
expect(props.name).toBeUndefined();
expect(props.style).toBeUndefined();
});
Expand Down Expand Up @@ -121,3 +124,17 @@ describe("NumberInput", () => {
});
});
});

describe("Input with prefixed Icon", () => {
it("renders Icon with TextInput component", () => {
const wrapper = shallow(<TextInput prefix={<Search />} />);
const prefixWrapper = wrapper.find(Search);
expect(prefixWrapper).toBeDefined();
});

it("renders Icon with NumberInput component", () => {
const wrapper = shallow(<NumberInput prefix={<Search />} />);
const prefixWrapper = wrapper.find(Search);
expect(prefixWrapper).toBeDefined();
});
});
9 changes: 1 addition & 8 deletions packages/ui-components/src/Input/number-input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,7 @@
&.number-type {
// Add extra space inside of container to make sure that inner element
// doesn't overlap rounded borders and outline with inner shadows
height: exclBorderWidth($input-height);
padding: $input-border-spacing;
min-width: exclBorderWidth($input-number-width);

> .input {
width: 100%;
}
width: exclBorderWidth($input-number-width);
}
}


0 comments on commit f5f2785

Please sign in to comment.