Merge pull request 'Creating a select component using HeadlessUI' (#68) from custom-select into develop

Reviewed-on: http://85.143.176.51:3000/free-land/front-end/pulls/68
This commit is contained in:
Denis Gorbunov 2022-08-12 14:13:44 +00:00
commit 2ee7705d5c
3 changed files with 181 additions and 0 deletions

View File

@ -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>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -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" },
],
};

121
src/components/Select.tsx Normal file
View File

@ -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;