From cb657dd2b18a2d993c23f7df72c99e5aa38945a3 Mon Sep 17 00:00:00 2001 From: lerte smith Date: Tue, 12 Nov 2024 01:00:04 +0800 Subject: [PATCH] add Autocomplete component --- apps/docs/content/components/autocomplete.md | 8 ++ apps/docs/src/components/Aside.tsx | 3 + apps/docs/src/usages/autocomplete.tsx | 25 ++++++ .../components/Autocomplete/Autocomplete.tsx | 79 +++++++++++++++++++ .../src/components/Autocomplete/index.ts | 2 + .../actify/src/components/ListBox/ListBox.tsx | 4 +- .../actify/src/components/ListBox/index.ts | 1 + .../actify/src/components/Popover/Popover.tsx | 9 ++- .../src/components/TextFields/TextField.tsx | 37 +++++++-- packages/actify/src/index.ts | 1 + 10 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 apps/docs/content/components/autocomplete.md create mode 100644 apps/docs/src/usages/autocomplete.tsx create mode 100644 packages/actify/src/components/Autocomplete/Autocomplete.tsx create mode 100644 packages/actify/src/components/Autocomplete/index.ts diff --git a/apps/docs/content/components/autocomplete.md b/apps/docs/content/components/autocomplete.md new file mode 100644 index 0000000..a9538d9 --- /dev/null +++ b/apps/docs/content/components/autocomplete.md @@ -0,0 +1,8 @@ +--- +title: Autocomplete +description: Autocomplete component offers simple and flexible type-ahead functionality. This is useful when searching large sets of data or even dynamically requesting information from an API. +--- + +## Usage + + diff --git a/apps/docs/src/components/Aside.tsx b/apps/docs/src/components/Aside.tsx index 80b527e..569d03b 100644 --- a/apps/docs/src/components/Aside.tsx +++ b/apps/docs/src/components/Aside.tsx @@ -111,6 +111,9 @@ const components = [ { name: 'Ripple' }, + { + name: 'Autocomplete' + }, { name: 'Select' }, diff --git a/apps/docs/src/usages/autocomplete.tsx b/apps/docs/src/usages/autocomplete.tsx new file mode 100644 index 0000000..c084fba --- /dev/null +++ b/apps/docs/src/usages/autocomplete.tsx @@ -0,0 +1,25 @@ +import { Autocomplete, Item } from 'actify' + +export default () => { + const options = [ + { id: 1, name: 'Actify' }, + { id: 2, name: 'Ngroker' }, + { id: 3, name: 'Taildoor' }, + { id: 4, name: 'Hugola' } + ] + + return ( +
+ console.log(e)}> + {options.map(({ id, name }) => ( + {name} + ))} + + + {options.map(({ id, name }) => ( + {name} + ))} + +
+ ) +} diff --git a/packages/actify/src/components/Autocomplete/Autocomplete.tsx b/packages/actify/src/components/Autocomplete/Autocomplete.tsx new file mode 100644 index 0000000..cc47544 --- /dev/null +++ b/packages/actify/src/components/Autocomplete/Autocomplete.tsx @@ -0,0 +1,79 @@ +import { AriaComboBoxProps, useComboBox, useFilter } from 'react-aria' +import { ComboBoxStateOptions, useComboBoxState } from 'react-stately' + +import { Icon } from '../Icon' +import { ListBox } from '../ListBox' +import { Popover } from '../Popover' +import React from 'react' +import { TextField } from '../TextFields' + +interface AutocompleteProps + extends Omit, 'children'>, + ComboBoxStateOptions { + variant?: 'filled' | 'outlined' +} + +const Autocomplete = (props: AutocompleteProps) => { + // Setup filter function and state + const { contains } = useFilter({ sensitivity: 'base' }) + const state = useComboBoxState({ ...props, defaultFilter: contains }) + + // Setup refs and get props for child elements. + const inputRef = React.useRef(null) + const listBoxRef = React.useRef(null) + const popoverRef = React.useRef(null) + + const { inputProps, listBoxProps } = useComboBox( + { + ...props, + inputRef, + listBoxRef, + popoverRef + }, + state + ) + + return ( +
+ { + state.setOpen(!state.isOpen) + // If the input is focused, move the cursor to the end + inputRef.current?.setSelectionRange( + inputRef.current.value.length, + inputRef.current.value.length + ) + }} + > + Arrow_Drop_Down + + } + /> + + {state.isOpen && ( + + + + )} +
+ ) +} + +Autocomplete.displayName = 'Actify.Autocomplete' +export { Autocomplete } diff --git a/packages/actify/src/components/Autocomplete/index.ts b/packages/actify/src/components/Autocomplete/index.ts new file mode 100644 index 0000000..3cfe0fe --- /dev/null +++ b/packages/actify/src/components/Autocomplete/index.ts @@ -0,0 +1,2 @@ +export { Autocomplete } from './Autocomplete' +export { Item } from 'react-stately' diff --git a/packages/actify/src/components/ListBox/ListBox.tsx b/packages/actify/src/components/ListBox/ListBox.tsx index bded735..826244d 100644 --- a/packages/actify/src/components/ListBox/ListBox.tsx +++ b/packages/actify/src/components/ListBox/ListBox.tsx @@ -7,8 +7,10 @@ import styles from './listbox.module.css' interface ListBoxProps extends AriaListBoxOptions { state: ListState - listBoxRef?: React.RefObject + listBoxProps?: AriaListBoxOptions + listBoxRef?: React.RefObject } + const ListBox = (props: ListBoxProps) => { const ref = React.useRef(null) const { listBoxRef = ref, state } = props diff --git a/packages/actify/src/components/ListBox/index.ts b/packages/actify/src/components/ListBox/index.ts index 2c903ee..861f5e6 100644 --- a/packages/actify/src/components/ListBox/index.ts +++ b/packages/actify/src/components/ListBox/index.ts @@ -1 +1,2 @@ export { ListBox } from './ListBox' +export { Option } from './Option' diff --git a/packages/actify/src/components/Popover/Popover.tsx b/packages/actify/src/components/Popover/Popover.tsx index 4127405..3615d47 100644 --- a/packages/actify/src/components/Popover/Popover.tsx +++ b/packages/actify/src/components/Popover/Popover.tsx @@ -14,16 +14,19 @@ interface PopoverProps extends Omit { referenceWidth?: number children: React.ReactNode state: OverlayTriggerState + popoverRef?: React.RefObject } const Popover = ({ - children, state, - offset = 8, + children, + offset = 16, referenceWidth, + popoverRef: propRef, ...props }: PopoverProps) => { - const popoverRef = React.useRef(null) + const popoverRef = + (propRef as React.RefObject) || React.useRef(null) const { popoverProps, underlayProps, arrowProps, placement } = usePopover( { ...props, diff --git a/packages/actify/src/components/TextFields/TextField.tsx b/packages/actify/src/components/TextFields/TextField.tsx index c7afcd6..ee2ba69 100644 --- a/packages/actify/src/components/TextFields/TextField.tsx +++ b/packages/actify/src/components/TextFields/TextField.tsx @@ -6,6 +6,9 @@ import React from 'react' import styles from './text-field.module.css' interface TextFieldProps extends AriaTextFieldProps { + ref?: React.RefObject + inputProps?: React.InputHTMLAttributes + inputRef?: React.RefObject variant?: 'filled' | 'outlined' suffixText?: string prefixText?: string @@ -23,7 +26,6 @@ interface TextFieldProps extends AriaTextFieldProps { | 'textarea' } const TextField = (props: TextFieldProps) => { - const ref = React.useRef(null) const { label, suffixText, @@ -31,9 +33,16 @@ const TextField = (props: TextFieldProps) => { leadingIcon, trailingIcon, type = 'text', - variant = 'filled' + variant = 'filled', + inputRef: propInputRef, + inputProps: propInputProps } = props + const inputRef = + (propInputRef as + | React.RefObject + | React.RefObject) || React.useRef(null) + const { inputProps, descriptionProps, @@ -42,7 +51,7 @@ const TextField = (props: TextFieldProps) => { validationErrors } = useTextField( { ...props, inputElementType: type == 'textarea' ? 'textarea' : 'input' }, - ref + inputRef ) const { focusProps, isFocused } = useFocusRing() @@ -64,24 +73,36 @@ const TextField = (props: TextFieldProps) => { trailingIcon, focused: isFocused, count: inputProps.value?.toString().length, - populated: inputProps.value ? true : false + populated: propInputProps + ? !!propInputProps.value + : inputProps.value + ? true + : false }} > {prefixText && {prefixText}} {type == 'textarea' ? (