This repository has been archived by the owner on Apr 25, 2023. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: rot1024 <aayhrot@gmail.com> Co-authored-by: KaWaite <34051327+KaWaite@users.noreply.github.com>
- Loading branch information
1 parent
dec1dd1
commit 4d0a401
Showing
45 changed files
with
2,727 additions
and
409 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Meta, Story } from "@storybook/react"; | ||
import React from "react"; | ||
|
||
import AutoComplete, { Props } from "."; | ||
|
||
export default { | ||
title: "atoms/AutoComplete", | ||
component: AutoComplete, | ||
} as Meta; | ||
|
||
const sampleItems: { value: string; label: string }[] = [ | ||
{ | ||
value: "hoge", | ||
label: "hoge", | ||
}, | ||
{ | ||
value: "fuga", | ||
label: "fuga", | ||
}, | ||
]; | ||
|
||
const addItem = (value: string) => { | ||
sampleItems.push({ value, label: value }); | ||
}; | ||
|
||
const handleSelect = (value: string) => { | ||
console.log("select ", value); | ||
}; | ||
|
||
const handleCreate = (value: string) => { | ||
console.log("create ", value); | ||
addItem(value); | ||
}; | ||
|
||
export const Default: Story<Props<string>> = () => { | ||
return <AutoComplete items={sampleItems} onCreate={handleCreate} onSelect={handleSelect} />; | ||
}; | ||
|
||
export const Creatable: Story<Props<string>> = () => { | ||
return ( | ||
<AutoComplete items={sampleItems} onCreate={handleCreate} onSelect={handleSelect} creatable /> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/* eslint-disable testing-library/no-wait-for-multiple-assertions */ | ||
/* eslint-disable testing-library/no-unnecessary-act */ | ||
import React from "react"; | ||
|
||
import { act, fireEvent, render, screen, waitFor } from "@reearth/test/utils"; | ||
|
||
import AutoComplete from "./index"; | ||
|
||
const sampleItems: { value: string; label: string }[] = [ | ||
{ | ||
value: "hoge", | ||
label: "hoge", | ||
}, | ||
{ | ||
value: "fuga", | ||
label: "fuga", | ||
}, | ||
]; | ||
|
||
test("component should be renered", async () => { | ||
await act(async () => { | ||
render(<AutoComplete />); | ||
}); | ||
}); | ||
|
||
test("component should render items", async () => { | ||
await act(async () => { | ||
render(<AutoComplete items={sampleItems} />); | ||
}); | ||
expect(screen.getByText(/hoge/)).toBeInTheDocument(); | ||
expect(screen.getByText(/fuga/)).toBeInTheDocument(); | ||
}); | ||
|
||
test("component should be inputtable", async () => { | ||
await act(async () => { | ||
render(<AutoComplete items={sampleItems} />); | ||
}); | ||
|
||
const input = screen.getByRole("textbox"); | ||
fireEvent.change(input, { target: { value: "hoge" } }); | ||
expect(screen.getByText("hoge")).toBeInTheDocument(); | ||
}); | ||
|
||
describe("Ccomponent should be searchable", () => { | ||
test("component should leave selects hit", async () => { | ||
await act(async () => { | ||
render(<AutoComplete items={sampleItems} />); | ||
const input = screen.getByRole("textbox"); | ||
fireEvent.change(input, { target: { value: "hoge" } }); | ||
expect(screen.getByText("hoge")).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
test("component shouldn't leave selects which don't hit inputted text", async () => { | ||
await act(async () => { | ||
render(<AutoComplete items={sampleItems} />); | ||
const input = screen.getByRole("textbox"); | ||
fireEvent.change(input, { target: { value: "hoge" } }); | ||
await waitFor(() => { | ||
expect(screen.queryByText("fuga")).not.toBeInTheDocument(); | ||
}); | ||
}); | ||
}); | ||
|
||
test("component should trigger onSelect function with click event", async () => { | ||
await act(async () => { | ||
const handleSelect = jest.fn((value: string) => { | ||
console.log(value); | ||
}); | ||
render(<AutoComplete items={sampleItems} onSelect={handleSelect} />); | ||
const input = screen.getByRole("textbox"); | ||
await act(async () => { | ||
fireEvent.change(input, { target: { value: "hoge" } }); | ||
const option = screen.getByText(/hoge/); | ||
fireEvent.click(option); | ||
}); | ||
await waitFor(() => { | ||
expect(handleSelect).toBeCalled(); | ||
expect(handleSelect.mock.calls[0][0]).toBe("hoge"); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import React, { useCallback, useEffect, useRef, useState } from "react"; | ||
|
||
import { metricsSizes, styled } from "@reearth/theme"; | ||
|
||
import Icon from "../Icon"; | ||
import SelectCore from "../Select/core"; | ||
import { Option, OptionIcon } from "../SelectOption"; | ||
|
||
export type Item<Value extends string | number = string> = { | ||
value: Value; | ||
label: string; | ||
icon?: string; | ||
}; | ||
|
||
export type Props<Value extends string | number> = { | ||
className?: string; | ||
placeholder?: string; | ||
items?: Item<Value>[]; | ||
fullWidth?: boolean; | ||
creatable?: boolean; | ||
onCreate?: (value: Value) => void; | ||
onSelect?: (value: Value) => void; | ||
}; | ||
|
||
function AutoComplete<Value extends string | number>({ | ||
className, | ||
placeholder, | ||
items, | ||
fullWidth = false, | ||
creatable, | ||
onCreate, | ||
onSelect, | ||
}: Props<Value>): JSX.Element | null { | ||
const [filterText, setFilterText] = useState(""); | ||
const [itemState, setItems] = useState<Item<Value>[]>(items ?? []); | ||
useEffect(() => { | ||
setItems(items?.filter(i => i.label.includes(filterText)) ?? []); | ||
}, [filterText, items]); | ||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
const handleInputChange = useCallback( | ||
(e: React.ChangeEvent<HTMLInputElement>) => { | ||
setFilterText?.(e.currentTarget.value); | ||
}, | ||
[setFilterText], | ||
); | ||
|
||
const isValueType = useCallback((value: any): value is Value => { | ||
return !!value; | ||
}, []); | ||
|
||
const handleSelect = useCallback( | ||
(value: Value) => { | ||
itemState.length ? onSelect?.(value) : creatable && onCreate?.(value); | ||
setFilterText(""); | ||
}, | ||
[creatable, itemState.length, onCreate, onSelect], | ||
); | ||
const handleKeyDown = useCallback( | ||
(e: React.KeyboardEvent<HTMLInputElement>) => { | ||
if (e.key === "Enter") { | ||
e.preventDefault(); | ||
if (!isValueType(filterText)) return; | ||
handleSelect(filterText); | ||
} | ||
}, | ||
[filterText, handleSelect, isValueType], | ||
); | ||
|
||
return ( | ||
<SelectCore | ||
className={className} | ||
fullWidth={fullWidth} | ||
selectComponent={ | ||
<StyledTextBox | ||
value={filterText} | ||
onChange={handleInputChange} | ||
onKeyDown={handleKeyDown} | ||
placeholder={placeholder} | ||
/> | ||
} | ||
onChange={handleSelect} | ||
ref={ref} | ||
options={itemState?.map(i => { | ||
return ( | ||
<Option key={i.value} value={i.value} label={i.label}> | ||
<OptionIcon size="xs">{i.icon && <Icon icon={i.icon} />}</OptionIcon> | ||
{i.label} | ||
</Option> | ||
); | ||
})}></SelectCore> | ||
); | ||
} | ||
|
||
const StyledTextBox = styled.input` | ||
outline: none; | ||
width: 100%; | ||
border: none; | ||
background-color: ${({ theme }) => theme.properties.bg}; | ||
padding-left: ${metricsSizes.xs}px; | ||
padding-right: ${metricsSizes.xs}px; | ||
caret-color: ${({ theme }) => theme.main.text}; | ||
color: ${({ theme }) => theme.main.text}; | ||
`; | ||
|
||
export default AutoComplete; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.