Skip to content

Commit

Permalink
feat(inputsearch): add new input search component (#134)
Browse files Browse the repository at this point in the history
* feat(inputsearch): add new input search component

* feat(inputsearch): add index and stories

* style(inputsearch): add intent states and user states

* feat(inputsearch): add stories

* test(inputsearch): add tests, onsubmit fuction todo

* test(inputsearch): add tests, make input controllable, pass submit event on click and enter

* chore(inputsearch): lint errors

* refactor(menuselect): change input to sds InputSearch to standardize styling

* fix(tabs): add delay to chromatic snapshot capture to prevent micro changes in chromatic

* fix(tooltip): add delay to tooltip placement story to prevent baseline bugs
  • Loading branch information
mdjmoore authored Mar 8, 2022
1 parent 7495aa1 commit 4f75054
Show file tree
Hide file tree
Showing 11 changed files with 503 additions and 39 deletions.
10 changes: 10 additions & 0 deletions src/core/InputSearch/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<InputSearch /> Test story renders snapshot 1`] = `
<label
class="css-1m92ss4"
for="test-round"
>
Round Search
</label>
`;
160 changes: 160 additions & 0 deletions src/core/InputSearch/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { action } from "@storybook/addon-actions";
import { Args, Story } from "@storybook/react";
import React from "react";
import InputSearch from "./index";

const Demo = (props: Args): JSX.Element => {
const { id, placeholder, label, disabled, sdsStyle, sdsStage, intent } =
props;
const handleSubmit = (value: string) => {
console.log(value);
};
return (
<InputSearch
id={id}
placeholder={placeholder}
label={label}
disabled={disabled}
sdsStyle={sdsStyle}
sdsStage={sdsStage}
intent={intent}
handleSubmit={handleSubmit}
/>
);
};

export default {
argTypes: {
disabled: {
control: { type: "boolean" },
},
id: {
control: { type: "text" },
required: true,
},
intent: {
control: { type: "radio" },
options: ["default", "error", "warning"],
},
label: {
control: { type: "text" },
required: true,
},
placeholder: {
control: { type: "text" },
},
sdsStage: {
control: { type: "radio" },
options: ["default", "userInput"],
},
sdsStyle: {
control: { type: "radio" },
options: ["rounded", "square"],
},
},
component: Demo,
title: "InputSearch",
};

const Template: Story = (args) => <Demo {...args} />;

export const Default = Template.bind({});

Default.args = {
disabled: false,
id: "Test",
label: "Search",
placeholder: "Search",
};

Default.parameters = {
snapshot: {
skip: true,
},
};

const RoundLivePreviewDemo = (props: Args): JSX.Element => {
return (
<InputSearch
{...props}
id="squareSearchPreview"
label="Search"
sdsStyle="rounded"
placeholder="Search"
handleSubmit={action("onSubmit")}
/>
);
};

const RoundLivePreviewTemplate: Story = (args) => (
<RoundLivePreviewDemo {...args} />
);

export const RoundLivePreview = RoundLivePreviewTemplate.bind({});

RoundLivePreview.parameters = {
snapshot: {
skip: true,
},
};

const SquareLivePreviewDemo = (props: Args): JSX.Element => {
return (
<InputSearch
{...props}
id="squareSearchPreview"
label="Search"
sdsStyle="square"
placeholder="Search"
handleSubmit={action("onSubmit")}
/>
);
};

const SquareLivePreviewTemplate: Story = (args) => (
<SquareLivePreviewDemo {...args} />
);

export const SquareLivePreview = SquareLivePreviewTemplate.bind({});

SquareLivePreview.parameters = {
snapshot: {
skip: true,
},
};

const TestDemo = (props: Args): JSX.Element => {
return (
<>
<InputSearch
id="test-round"
sdsStyle="rounded"
label="Round Search"
placeholder="Search"
data-testid="inputSearchRound"
handleSubmit={action("onSubmit")}
{...props}
/>
<InputSearch
id="test-square"
sdsStyle="square"
label="Square Search"
placeholder="Search"
data-testid="inputSearchSquare"
handleSubmit={action("onSubmit")}
{...props}
/>
{/* @ts-expect-error testing fail state */}
<InputSearch
sdsStyle="square"
placeholder="Search"
data-testid="inputSearchFail"
handleSubmit={action("onSubmit")}
/>
</>
);
};

const TestTemplate: Story = (args) => <TestDemo {...args} />;

export const Test = TestTemplate.bind({});
40 changes: 40 additions & 0 deletions src/core/InputSearch/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { generateSnapshots } from "@chanzuckerberg/story-utils";
import { composeStory } from "@storybook/testing-react";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import * as snapshotTestStoryFile from "./index.stories";
import Meta, { Test as TestStory } from "./index.stories";

// Returns a component that already contain all decorators from story level, meta level and global level.
const Test = composeStory(TestStory, Meta);

describe("<InputSearch />", () => {
generateSnapshots(snapshotTestStoryFile);

it("renders inputSearch component", () => {
render(<Test {...Test.args} />);
const inputSearchRoundElement = screen.getByTestId("inputSearchRound");
expect(inputSearchRoundElement).not.toBeNull();
const inputSearchSquareElement = screen.getByTestId("inputSearchSquare");
expect(inputSearchSquareElement).not.toBeNull();
});

it("component fails if missing id or label prop", () => {
render(<Test />);
const inputTextElement = screen.queryByTestId("inputSearchFail");
expect(inputTextElement).toBeNull();
});

it("no label is rendered, but component is still accessible", () => {
render(<Test />);
const inputSearchElement = screen.getByLabelText("Round Search");
expect(inputSearchElement).not.toBeNull();
});

it("input value updates on change", () => {
render(<Test />);
const input = screen.getByLabelText("Round Search") as HTMLInputElement;
fireEvent.change(input, { target: { value: "apple" } });
expect(input.value).toBe("apple");
});
});
108 changes: 108 additions & 0 deletions src/core/InputSearch/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
InputAdornment,
TextFieldProps as RawTextFieldSearchProps,
} from "@material-ui/core";
import React, { forwardRef, useState } from "react";
import Icon from "../Icon";
import IconButton from "../IconButton";
import { ExtraProps, StyledLabel, StyledSearchBase } from "./style";

export interface AccessibleInputSearchProps {
label: string;
placeholder?: string;
id: string;
handleSubmit?: (value: string) => void;
}

export type InputSearchProps = RawTextFieldSearchProps &
AccessibleInputSearchProps &
ExtraProps;

const InputSearch = forwardRef<HTMLInputElement, InputSearchProps>(
function InputSearch(props, ref): JSX.Element {
const {
label,
id,
placeholder,
sdsStyle = "square",
intent = "default",
handleSubmit,
...rest
} = props;

const [hasValue, setHasValue] = useState<boolean>(false);
const [value, setValue] = useState<string>("");

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.value) {
setHasValue(true);
} else {
setHasValue(false);
}
setValue(event.target.value);
};

const localHandleSubmit = () => {
if (handleSubmit) handleSubmit(value);
};

const handleKeyPress = (event: React.KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
if (handleSubmit) handleSubmit(value);
}
};

if (!id || !label) {
// eslint-disable-next-line no-console
console.error(
`Error: czifui component InputText requires id and label props for accessibility.`
);
return <></>;
}

const inputProps = {
"aria-label": `${label}`,
role: "search",
};

return (
<>
<StyledLabel htmlFor={id}>{label}</StyledLabel>
<StyledSearchBase
ref={ref}
// passed to mui Input
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={localHandleSubmit}
data-testId="searchButton"
sdsType="secondary"
>
<Icon sdsIcon="search" sdsSize="s" sdsType="interactive" />
</IconButton>
</InputAdornment>
),
// passed to html input
inputProps: inputProps,
}}
type="text"
id={id}
variant="outlined"
size="small"
placeholder={placeholder}
value={value}
sdsStyle={sdsStyle}
sdsStage={hasValue ? "userInput" : "default"}
onChange={handleChange}
onKeyPress={handleKeyPress}
intent={intent}
{...rest}
/>
</>
);
}
);

export default InputSearch;
Loading

0 comments on commit 4f75054

Please sign in to comment.