Skip to content

Commit

Permalink
Merge pull request #19 from tuzkituan/dev/select
Browse files Browse the repository at this point in the history
Dev/select
  • Loading branch information
tuzkituan authored Nov 1, 2023
2 parents fe4a5a7 + 9f924f5 commit c7f1a31
Show file tree
Hide file tree
Showing 5 changed files with 457 additions and 78 deletions.
114 changes: 59 additions & 55 deletions lib/components/input/input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from "react";
import { forwardRef, useMemo } from "react";
import { twMerge } from "tailwind-merge";
import { useComponentStyle } from "../../customization/styles/theme.context";
import { Box } from "../box/box";
Expand All @@ -9,62 +9,66 @@ const defaultProps: Partial<IInput> = {
variant: "filled",
};

export const Input = (props: IInput) => {
const theme = useComponentStyle("Input");
const {
className = "",
size,
variant,
leftElement,
rightElement,
...restProps
} = { ...defaultProps, ...props };
export const Input = forwardRef<HTMLInputElement, IInput>(
(props: IInput, ref) => {
const theme = useComponentStyle("Input");
const {
className = "",
size,
variant,
leftElement,
rightElement,
...restProps
} = { ...defaultProps, ...props };

const containerClasses = useMemo(() => {
return twMerge(
theme.container({
size,
variant,
addonLeft: !!leftElement,
addonRight: !!rightElement,
}),
theme.group(),
className
);
}, [theme, size, variant, leftElement, rightElement, className]);
const containerClasses = useMemo(() => {
return twMerge(
theme.container({
size,
variant,
addonLeft: !!leftElement,
addonRight: !!rightElement,
}),
theme.group(),
className
);
}, [theme, size, variant, leftElement, rightElement, className]);

const inputClasses = useMemo(() => {
return twMerge(
theme.input({
size,
variant,
isInGroup: !!leftElement || !!rightElement,
}),
className
);
}, [theme, size, variant, leftElement, rightElement, className]);
const inputClasses = useMemo(() => {
return twMerge(
theme.input({
size,
variant,
isInGroup: !!leftElement || !!rightElement,
}),
className
);
}, [theme, size, variant, leftElement, rightElement, className]);

const leftElementClasses = useMemo(() => {
return twMerge(theme.leftElement());
}, [theme]);
const leftElementClasses = useMemo(() => {
return twMerge(theme.leftElement());
}, [theme]);

const rightElementClasses = useMemo(() => {
return twMerge(theme.rightElement());
}, [theme]);
const rightElementClasses = useMemo(() => {
return twMerge(theme.rightElement());
}, [theme]);

return (
<div className={containerClasses}>
{leftElement ? (
<Box className={leftElementClasses}>{leftElement}</Box>
) : null}
<input
placeholder="Placeholder"
className={inputClasses}
{...restProps}
/>
{rightElement ? (
<Box className={rightElementClasses}>{rightElement}</Box>
) : null}
</div>
);
};
return (
<div className={containerClasses}>
{leftElement ? (
<Box className={leftElementClasses}>{leftElement}</Box>
) : null}
<input
placeholder="Placeholder"
className={inputClasses}
ref={ref}
{...restProps}
/>
{rightElement ? (
<Box className={rightElementClasses}>{rightElement}</Box>
) : null}
</div>
);
}
);
Input.displayName = "Input";
28 changes: 26 additions & 2 deletions lib/components/select/select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react";

import { Select } from "./select";
import { MapPinLine } from "@phosphor-icons/react";

const meta = {
title: "LAYOUT/Select",
title: "FORMS/Select",
component: Select,
tags: ["autodocs"],
argTypes: {},
Expand All @@ -13,5 +14,28 @@ export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {},
args: {
variant: "outline",
size: "md",
isClearable: true,
// leftElement: <MapPinLine />,
options: [
{
label: "This is a really long option label, you see?",
value: 1,
},
{
label: "Option 2",
value: 2,
},
{
label: "Option 3",
value: 3,
},
{
label: "Option 4",
value: 4,
},
],
},
};
184 changes: 167 additions & 17 deletions lib/components/select/select.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,177 @@
import { AnimatePresence, motion } from "framer-motion";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useLayer } from "react-laag";
import { twMerge } from "tailwind-merge";
import { useComponentStyle } from "../../customization/styles/theme.context";
import { ISelect } from "./select.types";
import { useMemo } from "react";
import { ISelect, ISelectOption } from "./select.types";
import { Box } from "../box/box";
import { CaretDown, CaretUp, X } from "@phosphor-icons/react";

const defaultProps: Partial<ISelect> = {
children: undefined,
};
export const Select = ({
options = [],
variant = "outline",
size = "md",
value,
defaultValue,
onChange,
leftElement,
rightElement: rightElementProp,
isClearable = false,
}: ISelect) => {
const inputRef = useRef<HTMLDivElement | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(null);

// STATE
const [isOpen, setOpen] = useState(false);
const [valueState, setValueState] = useState<ISelectOption | undefined>(
value || defaultValue
);

export const Select = (props: ISelect) => {
// VARS
const theme = useComponentStyle("Select");
const {
children,
className = "",
...restProps
} = { ...defaultProps, ...props };
const rightElement = useMemo(() => {
return rightElementProp || isOpen ? <CaretUp /> : <CaretDown />;
}, [isOpen, rightElementProp]);

const { triggerProps, layerProps, renderLayer } = useLayer({
isOpen: isOpen,
auto: true,
triggerOffset: 4,
placement: "bottom-start",
onOutsideClick: () => {
setOpen(false);
},
});

// CLASSES
const containerClasses = useMemo(() => {
return twMerge(
theme.container({
variant,
size,
}),
theme.group()
);
}, [size, theme, variant]);

const secContainerClasses = useMemo(() => {
return twMerge(
theme.secContainer({
addonLeft: !!leftElement,
addonRight: !!rightElement,
})
);
}, [leftElement, rightElement, theme]);

const inputClasses = useMemo(() => {
return twMerge(
theme.input({
variant,
size,
isInGroup: !!leftElement || !!rightElement,
})
);
}, [leftElement, rightElement, size, theme, variant]);

const dropdownClasses = useMemo(() => {
return twMerge(theme.dropdown());
}, [theme]);

const leftElementClasses = useMemo(() => {
return twMerge(theme.leftElement());
}, [theme]);

const rightElementClasses = useMemo(() => {
return twMerge(theme.rightElement());
}, [theme]);

const clearElementClasses = useMemo(() => {
return twMerge(theme.clearElement());
}, [theme]);

// FUNCTIONS
const onClear = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation()
setOpen(false);
setValueState(undefined);
};
useEffect(() => {
if (inputRef.current && dropdownRef.current) {
const inputWidth = inputRef.current.offsetWidth;
dropdownRef.current.style.width = `${inputWidth}px`;
}
}, [isOpen, inputRef.current?.offsetWidth]);

useEffect(() => {
setValueState(value);
}, [value]);

const classes = useMemo(() => {
return twMerge(theme.base(), className)
}, [className, theme]);
// UI
const renderOptions = () => {
return options.map((x: ISelectOption) => (
<li
key={x.value}
value={x.value}
className={twMerge(
theme.option({
isSelected: x.value === valueState?.value,
})
)}
onClick={() => {
onChange?.(x);
setValueState(x);
setOpen(false);
}}
>
{x.label}
</li>
));
};

return (
<div className={classes} {...restProps}>
{children}
</div>
<>
<div
{...triggerProps}
onClick={() => {
const _isOpen = !isOpen;
setOpen(_isOpen);
}}
className={containerClasses}
>
<div ref={inputRef} className={secContainerClasses}>
{!!leftElement && (
<Box className={leftElementClasses}>{leftElement}</Box>
)}
<div className={inputClasses}>
{valueState?.label || (
<span className={theme.placeholder()}>Placeholder</span>
)}
</div>
{isClearable && !!valueState?.value && (
<Box className={clearElementClasses} onClick={onClear}>
<X />
</Box>
)}
{!!rightElement && (
<Box className={rightElementClasses}>{rightElement}</Box>
)}
</div>
</div>
{isOpen &&
renderLayer(
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
{...layerProps}
>
<div className={dropdownClasses} ref={dropdownRef}>
<ul>{renderOptions()}</ul>
</div>
</motion.div>
</AnimatePresence>
)}
</>
);
};
25 changes: 23 additions & 2 deletions lib/components/select/select.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
export interface ISelect {
children?: React.ReactNode;
import { ReactNode, SelectHTMLAttributes } from "react";

export interface ISelect
extends Omit<
SelectHTMLAttributes<HTMLSelectElement>,
"size" | "value" | "defaultValue" | "onChange"
> {
options?: ISelectOption[];
size?: "xs" | "sm" | "md" | "lg";
variant?: "outline" | "filled" | "flushed" | "unstyled";
value?: ISelectOption;
defaultValue?: ISelectOption;
onChange?: (value: ISelectOption) => void;
leftElement?: React.ReactNode;
rightElement?: React.ReactNode;
isClearable?: boolean;
}

export type ISelectValue = string | number;

export interface ISelectOption {
label?: ReactNode;
value?: ISelectValue;
}
Loading

0 comments on commit c7f1a31

Please sign in to comment.