diff --git a/src/assets/svg/select-arrow.svg b/src/assets/svg/select-arrow.svg new file mode 100644 index 0000000..9c2dd2d --- /dev/null +++ b/src/assets/svg/select-arrow.svg @@ -0,0 +1,3 @@ +<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M11.1315 0.6875H0.868974C0.561161 0.6875 0.389286 1.0125 0.579911 1.23438L5.71116 7.18437C5.85804 7.35469 6.14085 7.35469 6.28929 7.18437L11.4205 1.23438C11.6112 1.0125 11.4393 0.6875 11.1315 0.6875Z" fill="#262626"/> +</svg> \ No newline at end of file diff --git a/src/components/Select.stories.tsx b/src/components/Select.stories.tsx new file mode 100644 index 0000000..fe42885 --- /dev/null +++ b/src/components/Select.stories.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { ComponentMeta, ComponentStory } from "@storybook/react"; +import { useState } from "react"; + +import Select from "./Select"; + +export default { + // Title inside navigation bar + title: "Select", + // Component to test + component: Select, + // Clarifying the way how to process specific + // properties of your component and which values + // it can accept. + argTypes: {}, +} as ComponentMeta<typeof Select>; + +/** + * This is a way to define a tempalte for your component. + * + * This template should cover all the states. + * + * In most cases you should just distruct args attribute + * on a returning component. + */ + +const Template: ComponentStory<typeof Select> = (args) => { + const { options = [{ name: String }] } = args; + const [selected, setSelected] = useState(options[0]); + return ( + <div> + <Select<any> + options={options} + value={selected} + onChange={setSelected} + displayValueResolver={(options) => options.name} + /> + </div> + ); +}; + +/* -------------------------------------------------------------------------- */ +/* States of your component */ +/* -------------------------------------------------------------------------- */ + +export const Default = Template.bind({}); + +Default.args = { + options: [ + { name: "Wade Cooper" }, + { name: "Arlene Mccoy" }, + { name: "Devon Webb" }, + { name: "Tom Cook" }, + { name: "Tanya Fox" }, + { name: "Hellen Schmidt" }, + ], +}; diff --git a/src/components/Select.tsx b/src/components/Select.tsx new file mode 100644 index 0000000..752939c --- /dev/null +++ b/src/components/Select.tsx @@ -0,0 +1,121 @@ +/* -------------------------------------------------------------------------- */ +/* Imports */ +/* -------------------------------------------------------------------------- */ +import React from "react"; +import { Fragment } from "react"; +import { Listbox, Transition } from "@headlessui/react"; +import classNames from "classnames"; +import "../index.css"; +import { ReactComponent as SelectIcon } from "../assets/svg/select-arrow.svg"; + +/* -------------------------------------------------------------------------- */ +/* Component props */ +/* -------------------------------------------------------------------------- */ + +type Props<T> = { + options?: T[]; + disabled?: boolean; + className?: string; + value: T; + displayValueResolver?: (element: T) => any; + onChange: (element: T) => void; +} & Omit<React.ComponentPropsWithRef<"select">, "value" | "onChange">; + +/* -------------------------------------------------------------------------- */ +/* styles */ +/* -------------------------------------------------------------------------- */ + +const SelectButtonStyle = ` + relative w-full + cursor-default + rounded + border border-gray-50 + outline-8 + bg-white + py-2 pl-3 pr-10 text-left + hover:border-gray-300 + focus:outline-1 + focus-visible:border-gray-500 + sm:text-sm + `; + +const SelectOptionsStyle = ` + absolute z-10 mt-1 w-full max-h-56 + bg-white shadow-lg + rounded py-1 + overflow-auto + focus:outline-none + text-base + sm:text-sm + `; + +const SelectIconStyle = ` + pointer-events-none + absolute inset-y-0 right-0 + flex items-center pr-2 + `; +/* -------------------------------------------------------------------------- */ +/* Component implementation */ +/* -------------------------------------------------------------------------- */ +function Select<T>({ + className, + options = [], + value, + onChange, + displayValueResolver, + disabled, + ...props +}: Props<T>): JSX.Element { + return ( + <div className={classNames("fixed top-16 w-60", className)}> + <Listbox value={value} {...props} onChange={onChange}> + <div className="relative mt-1"> + <Listbox.Button className={`${SelectButtonStyle}`}> + {({ open }) => ( + <> + <span className="block truncate">{`${ + displayValueResolver ? displayValueResolver(value) : value + }`}</span> + <span className={`${SelectIconStyle}`}> + <SelectIcon + className={`${ + open ? "rotate-180 transform" : "font-normal" + } h-2 w-3`} + /> + </span> + </> + )} + </Listbox.Button> + + <Transition + as={Fragment} + leave="transition ease-in duration-100" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <Listbox.Options className={`${SelectOptionsStyle}`}> + {options.map((option, id) => ( + <Listbox.Option + key={id} + className={({ active, selected }) => + classNames( + active ? "text-gray-900 bg-blue-50" : "font-normal ", + "cursor-default select-none relative py-2 pl-3 pr-9", + selected ? "text-gray-900 bg-blue-100" : "font-normal " + ) + } + value={option} + > + {`${ + displayValueResolver ? displayValueResolver(option) : option + }`} + </Listbox.Option> + ))} + </Listbox.Options> + </Transition> + </div> + </Listbox> + </div> + ); +} +export default Select;