diff --git a/packages/components/select/package.json b/packages/components/select/package.json index b7b9986c..3a77fd0d 100644 --- a/packages/components/select/package.json +++ b/packages/components/select/package.json @@ -32,8 +32,10 @@ "react": "^18.2.0" }, "dependencies": { + "@react-ck/empty-state": "^1.1.17", + "@react-ck/input": "^1.3.15", "@react-ck/provisional": "^3.2.0", - "@react-ck/form-field": "^1.2.1", + "@react-ck/react-utils": "^1.2.12", "@react-ck/scss-utils": "^1.1.10", "@react-ck/text": "^1.5.1", "@react-ck/theme": "^1.7.5", diff --git a/packages/components/select/src/index.tsx b/packages/components/select/src/index.tsx index 9dcbe0ae..86eba03f 100644 --- a/packages/components/select/src/index.tsx +++ b/packages/components/select/src/index.tsx @@ -1,11 +1,25 @@ -import React, { useEffect, useMemo } from "react"; import styles from "./styles/index.module.scss"; -import classNames from "classnames"; -import { FormField, useFormFieldContext, type FormFieldProps } from "@react-ck/form-field"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { SelectOption } from "./SelectOption"; +import { Dropdown, Menu } from "@react-ck/provisional"; +import { Input, type InputProps } from "@react-ck/input"; +import classNames from "classnames"; +import { useNextRender, useOnClickOutside } from "@react-ck/react-utils"; +import { EmptyState } from "@react-ck/empty-state"; + +const options = ["dog", "cat", "lion", "zebra", "shark"]; -interface SelectProps extends React.SelectHTMLAttributes { - skin?: FormFieldProps["skin"]; +interface SelectProps extends Omit, "onChange" | "value"> { + skin?: InputProps["skin"]; + placeholder: InputProps["placeholder"]; + children: React.ReactNode; + search?: { + placeholder: string; + emptyStateMessage: (value: string) => React.ReactNode; + }; + onChange?: React.SelectHTMLAttributes["onChange"]; + value?: React.SelectHTMLAttributes["value"]; + multiple?: React.SelectHTMLAttributes["multiple"]; } /** @@ -15,38 +29,143 @@ interface SelectProps extends React.SelectHTMLAttributes { * @returns a React element */ -const Select = ({ skin, id, className, ...props }: Readonly): React.ReactElement => { - const formFieldContext = useFormFieldContext(); +const Select = ({ + children, + className, + onFocus, + search: searchOptions, + onChange: selectOnChange, + value: selectValue, + multiple: selectMultiple, + ...props +}: Readonly): React.ReactElement => { + const searchRef = useRef(null); + const selectRef = useRef(null); + const inputRef = useRef(null); + const dropdownRef = useRef(null); + const [open, setOpen] = useState(false); + const [selectedValues, setSelectedValues] = useState(undefined); + const [search, setSearch] = useState(""); + + const onNextRender = useNextRender(); + + const filteredOptions = useMemo( + () => options.filter((i) => i.toLowerCase().includes(search.toLowerCase())), + [search], + ); + + const handleChange = useCallback( + (value: string) => { + setSelectedValues([value]); + + setOpen(false); + + onNextRender(() => { + if (!selectRef.current) + throw new Error("Select: unable to dispatch event to native element"); - const computedSkin = useMemo( - () => formFieldContext?.skin ?? skin ?? "default", - [formFieldContext?.skin, skin], + selectRef.current.dispatchEvent( + new Event("change", { + bubbles: true, + }), + ); + }); + }, + [onNextRender], ); - const computedId = useMemo(() => formFieldContext?.id ?? id, [formFieldContext?.id, id]); + useOnClickOutside(open, [dropdownRef, inputRef], () => { + setOpen(false); + }); - // Validate usage inside form field + // Actions to do when dropdown opens useEffect(() => { - // Is not inside form field, skip - if (formFieldContext === undefined) return; + if (!open) return; - // Is inside form field - if (skin) throw new Error("When using select inside form field, define skin on the form field"); - else if (id) - throw new Error("When using select inside form field, define id on the form field"); - }, [formFieldContext, id, skin]); + onNextRender(() => { + searchRef.current?.focus(); + }); + }, [onNextRender, open]); + + // Actions to do when dropdown closes + useEffect(() => { + if (!open) return; + + setSearch(""); + }, [open]); return ( - + {selectedValues?.map((i) => ( + + ))} + + + { + setOpen(true); + onFocus?.(e); + }} + /> + + + + {searchOptions ? ( + <> + { + setSearch(e.target.value); + }} + /> + + + ) : null} + + {children ? "" : ""} + + {filteredOptions.map((i) => ( + { + handleChange(i); + }}> + {i} + + ))} + + {searchOptions && filteredOptions.length === 0 ? ( + + {searchOptions.emptyStateMessage(search)} + {/* TODO: fix empty state flex */} + + ) : null} + + + ); }; diff --git a/packages/components/select/src/styles/index.module.scss b/packages/components/select/src/styles/index.module.scss index b20130cc..0edfdc09 100644 --- a/packages/components/select/src/styles/index.module.scss +++ b/packages/components/select/src/styles/index.module.scss @@ -1,21 +1,21 @@ @use "@react-ck/theme"; -@use "@react-ck/form-field"; - -// TODO: fix height offset between standalone and non-standalone (border) .root { - @include form-field.form-field-input; + cursor: pointer; } -// When not inside form field, has own border, etc -.standalone { - @include form-field.form-field-container; +.menu { + > :first-child { + border-top-left-radius: theme.get-spacing(1); + border-top-right-radius: theme.get-spacing(1); + } - @each $key, $props in form-field.$form-field-styles { - &.skin_#{$key} { - @each $propKey, $propValue in $props { - @include theme.define-css-var(form-field, $propKey, $propValue); - } - } + > :last-child { + border-bottom-left-radius: theme.get-spacing(1); + border-bottom-right-radius: theme.get-spacing(1); } } + +.search_input { + outline: none !important; +}