Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚸 Autocomplete: implement native popover #3416

Merged
merged 2 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const mockResizeObserver = jest.fn(() => ({

beforeAll(() => {
window.ResizeObserver = mockResizeObserver
HTMLDivElement.prototype.showPopover = jest.fn()
HTMLDivElement.prototype.hidePopover = jest.fn()

//https://github.com/TanStack/virtual/issues/641
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down Expand Up @@ -52,7 +54,7 @@ describe('Autocomplete', () => {
expect(optionsList).toMatchSnapshot()
})
it('Has provided label', async () => {
render(<Autocomplete disablePortal label={labelText} options={items} />)
render(<Autocomplete label={labelText} options={items} />)

// The same label is used for both the input field and the list of options
const labeledNodes = await screen.findAllByLabelText(labelText)
Expand All @@ -69,13 +71,7 @@ describe('Autocomplete', () => {
})

it('Has provided ReactNode label', async () => {
render(
<Autocomplete
disablePortal
label={<div>{labelText}</div>}
options={items}
/>,
)
render(<Autocomplete label={<div>{labelText}</div>} options={items} />)

// The same label is used for both the input field and the list of options
const labeledNodes = await screen.findAllByLabelText(labelText)
Expand All @@ -95,7 +91,6 @@ describe('Autocomplete', () => {
const labler = (text: string) => `${text}+1`
render(
<Autocomplete
disablePortal
options={itemObjects}
label={labelText}
optionLabel={(item) => labler(item.label)}
Expand Down Expand Up @@ -128,7 +123,6 @@ describe('Autocomplete', () => {
}
render(
<Autocomplete
disablePortal
options={itemObjects}
label={labelText}
optionLabel={(item) => item.label}
Expand All @@ -153,9 +147,7 @@ describe('Autocomplete', () => {
})

it('Can be disabled', async () => {
render(
<Autocomplete disablePortal label={labelText} options={items} disabled />,
)
render(<Autocomplete label={labelText} options={items} disabled />)
const labeledNodes = await screen.findAllByLabelText(labelText)
const input = labeledNodes[0]

Expand Down Expand Up @@ -189,7 +181,6 @@ describe('Autocomplete', () => {
options={items}
data-testid="styled-autocomplete"
multiple={true}
disablePortal={true}
allowSelectAll={true}
onOptionsChange={onChange}
/>,
Expand Down Expand Up @@ -219,7 +210,7 @@ describe('Autocomplete', () => {
})

it('Can open the options on button click', async () => {
render(<Autocomplete disablePortal options={items} label={labelText} />)
render(<Autocomplete options={items} label={labelText} />)

const labeledNodes = await screen.findAllByLabelText(labelText)
const optionsList = labeledNodes[1]
Expand All @@ -243,7 +234,6 @@ describe('Autocomplete', () => {
return (
<Autocomplete
multiple
disablePortal
options={items}
label={labelText}
selectedOptions={selected}
Expand Down Expand Up @@ -277,7 +267,7 @@ describe('Autocomplete', () => {
})

it('Can filter results by input value', async () => {
render(<Autocomplete disablePortal options={items} label={labelText} />)
render(<Autocomplete options={items} label={labelText} />)
const labeledNodes = await screen.findAllByLabelText(labelText)
const input = labeledNodes[0]
const optionsList = labeledNodes[1]
Expand All @@ -301,7 +291,6 @@ describe('Autocomplete', () => {
it('Second option is first when first option is disabled', async () => {
render(
<Autocomplete
disablePortal
options={items}
label={labelText}
optionDisabled={(item) => item === items[0]}
Expand Down Expand Up @@ -330,7 +319,7 @@ describe('Autocomplete', () => {
})

it('Clears the input text on blur when no option is selected', async () => {
render(<Autocomplete disablePortal options={items} label={labelText} />)
render(<Autocomplete options={items} label={labelText} />)
const labeledNodes = await screen.findAllByLabelText(labelText)
const input = labeledNodes[0]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,15 @@ import {
useToken,
bordersTemplate,
useIsomorphicLayoutEffect,
useIsInDialog,
} from '@equinor/eds-utils'
import { AutocompleteOption } from './Option'
import {
offset,
flip,
shift,
size,
autoUpdate,
useFloating,
useInteractions,
FloatingPortal,
MiddlewareState,
} from '@floating-ui/react'
import { Variants } from '../types'
Expand All @@ -76,6 +73,16 @@ const StyledList = styled(List)(
`,
)

const StyledPopover = styled('div').withConfig({
shouldForwardProp: () => true, //workaround to avoid warning until popover gets added to react types
})<{ popover: string }>`
inset: unset;
border: 0;
padding: 0;
margin: 0;
overflow: visible;
`

const HelperText = styled(_HelperText)`
margin-top: 8px;
margin-left: 8px;
Expand Down Expand Up @@ -275,7 +282,9 @@ export type AutocompleteProps<T> = {
allowSelectAll?: boolean
/** Custom option template */
optionComponent?: (option: T, isSelected: boolean) => ReactNode
/** Disable use of react portal for dropdown */
/** Disable use of react portal for dropdown
* @deprecated Autocomplete now uses the native popover api to render the dropdown. This prop will be removed in a future version
*/
disablePortal?: boolean
/** Custom filter function for options */
optionsFilter?: (option: T, inputValue: string) => boolean
Expand Down Expand Up @@ -335,6 +344,12 @@ function AutocompleteInner<T>(
...other
} = props

if (disablePortal) {
console.warn(
'Autocomplete "disablePortal" prop has been deprecated. Autocomplete now uses the native popover api',
)
}

Comment on lines +347 to +352
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice developer experience 🎖️

const isControlled = Boolean(selectedOptions)
const [inputOptions, setInputOptions] = useState(options)
const [_availableItems, setAvailableItems] = useState(inputOptions)
Expand Down Expand Up @@ -721,8 +736,9 @@ function AutocompleteInner<T>(
placement: 'bottom-start',
middleware: [
offset(4),
flip(),
shift({ padding: 8 }),
flip({
boundary: document?.body,
}),
size({
apply({ rects, elements }: MiddlewareState) {
const anchorWidth = `${rects.reference.width}px`
Expand All @@ -743,6 +759,14 @@ function AutocompleteInner<T>(
}
}, [refs.reference, refs.floating, update, isOpen])

useEffect(() => {
if (isOpen) {
refs.floating.current.showPopover()
} else {
refs.floating.current.hidePopover()
}
}, [isOpen, refs.floating])

const clear = () => {
resetCombobox()
resetSelection()
Expand All @@ -759,18 +783,15 @@ function AutocompleteInner<T>(
[selectedItems, getLabel],
)

//temporary fix when inside dialog. Should be replaced by popover api when it is ready
const inDialog = useIsInDialog(refs.domReference.current)

const optionsList = (
<div
<StyledPopover
popover="manual"
{...getFloatingProps({
ref: refs.setFloating,
style: {
position: strategy,
top: y || 0,
left: x || 0,
zIndex: 1500,
},
})}
>
Expand Down Expand Up @@ -881,7 +902,7 @@ function AutocompleteInner<T>(
)
})}
</StyledList>
</div>
</StyledPopover>
)

const inputProps = getInputProps(
Expand Down Expand Up @@ -951,13 +972,7 @@ function AutocompleteInner<T>(
icon={helperIcon}
/>
)}
{disablePortal || inDialog ? (
optionsList
) : (
<FloatingPortal id="eds-autocomplete-container">
{optionsList}
</FloatingPortal>
)}
{optionsList}
</Container>
</ThemeProvider>
)
Expand Down
Loading