Skip to content

Commit

Permalink
add Autocomplete component
Browse files Browse the repository at this point in the history
  • Loading branch information
lerte committed Nov 11, 2024
1 parent edf26ad commit cb657dd
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 12 deletions.
8 changes: 8 additions & 0 deletions apps/docs/content/components/autocomplete.md
Original file line number Diff line number Diff line change
@@ -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

<usage></usage>
3 changes: 3 additions & 0 deletions apps/docs/src/components/Aside.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ const components = [
{
name: 'Ripple'
},
{
name: 'Autocomplete'
},
{
name: 'Select'
},
Expand Down
25 changes: 25 additions & 0 deletions apps/docs/src/usages/autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="max-sm:flex-wrap flex gap-4">
<Autocomplete label="project" onSelectionChange={(e) => console.log(e)}>
{options.map(({ id, name }) => (
<Item key={id}>{name}</Item>
))}
</Autocomplete>
<Autocomplete variant="outlined" label="project">
{options.map(({ id, name }) => (
<Item key={id}>{name}</Item>
))}
</Autocomplete>
</div>
)
}
79 changes: 79 additions & 0 deletions packages/actify/src/components/Autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -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<T>
extends Omit<AriaComboBoxProps<T>, 'children'>,
ComboBoxStateOptions<T> {
variant?: 'filled' | 'outlined'
}

const Autocomplete = <T extends object>(props: AutocompleteProps<T>) => {
// 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<HTMLInputElement>(null)
const listBoxRef = React.useRef(null)
const popoverRef = React.useRef<HTMLDivElement>(null)

const { inputProps, listBoxProps } = useComboBox(
{
...props,
inputRef,
listBoxRef,
popoverRef
},
state
)

return (
<div style={{ display: 'inline-flex', flexDirection: 'column' }}>
<TextField
label={props.label}
variant={props.variant}
inputRef={inputRef}
inputProps={inputProps}
trailingIcon={
<Icon
style={{ cursor: 'pointer' }}
onClick={() => {
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
</Icon>
}
/>

{state.isOpen && (
<Popover
state={state}
triggerRef={inputRef}
popoverRef={popoverRef}
placement="bottom start"
>
<ListBox
state={state}
listBoxRef={listBoxRef}
listBoxProps={listBoxProps}
/>
</Popover>
)}
</div>
)
}

Autocomplete.displayName = 'Actify.Autocomplete'
export { Autocomplete }
2 changes: 2 additions & 0 deletions packages/actify/src/components/Autocomplete/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Autocomplete } from './Autocomplete'
export { Item } from 'react-stately'
4 changes: 3 additions & 1 deletion packages/actify/src/components/ListBox/ListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import styles from './listbox.module.css'

interface ListBoxProps<T> extends AriaListBoxOptions<T> {
state: ListState<T>
listBoxRef?: React.RefObject<HTMLElement>
listBoxProps?: AriaListBoxOptions<T>
listBoxRef?: React.RefObject<HTMLElement | null>
}

const ListBox = <T extends object>(props: ListBoxProps<T>) => {
const ref = React.useRef(null)
const { listBoxRef = ref, state } = props
Expand Down
1 change: 1 addition & 0 deletions packages/actify/src/components/ListBox/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { ListBox } from './ListBox'
export { Option } from './Option'
9 changes: 6 additions & 3 deletions packages/actify/src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> {
referenceWidth?: number
children: React.ReactNode
state: OverlayTriggerState
popoverRef?: React.RefObject<Element | null>
}

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<HTMLDivElement>) || React.useRef(null)
const { popoverProps, underlayProps, arrowProps, placement } = usePopover(
{
...props,
Expand Down
37 changes: 29 additions & 8 deletions packages/actify/src/components/TextFields/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import React from 'react'
import styles from './text-field.module.css'

interface TextFieldProps extends AriaTextFieldProps {
ref?: React.RefObject<HTMLElement | null>
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
inputRef?: React.RefObject<Element | null>
variant?: 'filled' | 'outlined'
suffixText?: string
prefixText?: string
Expand All @@ -23,17 +26,23 @@ interface TextFieldProps extends AriaTextFieldProps {
| 'textarea'
}
const TextField = (props: TextFieldProps) => {
const ref = React.useRef(null)
const {
label,
suffixText,
prefixText,
leadingIcon,
trailingIcon,
type = 'text',
variant = 'filled'
variant = 'filled',
inputRef: propInputRef,
inputProps: propInputProps
} = props

const inputRef =
(propInputRef as
| React.RefObject<HTMLInputElement>
| React.RefObject<HTMLTextAreaElement>) || React.useRef(null)

const {
inputProps,
descriptionProps,
Expand All @@ -42,7 +51,7 @@ const TextField = (props: TextFieldProps) => {
validationErrors
} = useTextField(
{ ...props, inputElementType: type == 'textarea' ? 'textarea' : 'input' },
ref
inputRef
)

const { focusProps, isFocused } = useFocusRing()
Expand All @@ -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 && <span className={styles['prefix']}>{prefixText}</span>}
{type == 'textarea' ? (
<textarea
ref={ref}
{...mergeProps(focusProps, inputProps as TextFieldAria)}
{...mergeProps(
focusProps,
inputProps as TextFieldAria,
propInputProps
)}
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
/>
) : (
<input
ref={ref}
style={{
overflowX: 'hidden',
textAlign: 'inherit',
caretColor: 'var(--_caret-color)'
}}
{...mergeProps(focusProps, inputProps as TextFieldAria)}
{...mergeProps(
focusProps,
inputProps as TextFieldAria,
propInputProps
)}
ref={inputRef as React.RefObject<HTMLInputElement>}
/>
)}
{suffixText && <span className={styles['suffix']}>{suffixText}</span>}
Expand Down
1 change: 1 addition & 0 deletions packages/actify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export * from './components/SideSheets'
export * from './components/Ripple'
export * from './components/FocusRing'
export * from './components/Select'
export * from './components/Autocomplete'
export * from './components/Sliders'
export * from './components/Switch'
export * from './components/Tabs'
Expand Down

0 comments on commit cb657dd

Please sign in to comment.