Creating a select component using HeadlessUI
This commit is contained in:
parent
213f5bbd3d
commit
245bd8e2ba
3
src/assets/svg/select-arrow.svg
Normal file
3
src/assets/svg/select-arrow.svg
Normal 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 |
57
src/components/Select.stories.tsx
Normal file
57
src/components/Select.stories.tsx
Normal 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" },
|
||||||
|
],
|
||||||
|
};
|
122
src/components/Select.tsx
Normal file
122
src/components/Select.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* 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
|
||||||
|
shadow-md
|
||||||
|
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;
|
Loading…
x
Reference in New Issue
Block a user