Skip to content

Commit

Permalink
feat: Added floating-ui hook usage for ComboBox (#16585)
Browse files Browse the repository at this point in the history
* feat: changes

* feat: added floating-ui for combobox

* feat: added test helper function to pass tests

* feat: test changes

* Update packages/react/src/components/ComboBox/ComboBox.tsx

update comments for interface

Co-authored-by: Taylor Jones <[email protected]>

* Update packages/react/src/components/ComboBox/ComboBox.tsx

update comment for prop-types

Co-authored-by: Taylor Jones <[email protected]>

* Update packages/react/src/components/ComboBox/ComboBox.tsx

removing `fallbackAxisSideDirection` and keeping it flip()

Co-authored-by: Taylor Jones <[email protected]>

* Update packages/react/src/components/ComboBox/ComboBox.tsx

adding placement as we already have direction prop coming in

Co-authored-by: Taylor Jones <[email protected]>

* feat: yarn formatted

---------

Co-authored-by: Taylor Jones <[email protected]>
Co-authored-by: Guilherme Datilio Ribeiro <[email protected]>
  • Loading branch information
3 people authored Jun 11, 2024
1 parent 534710f commit cb30e67
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 27 deletions.
3 changes: 3 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,9 @@ Map {
"type": "string",
},
"ariaLabel": [Function],
"autoAlign": Object {
"type": "bool",
},
"className": Object {
"type": "string",
},
Expand Down
33 changes: 20 additions & 13 deletions packages/react/src/components/ComboBox/ComboBox-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
assertMenuClosed,
generateItems,
generateGenericItem,
waitForPosition,
} from '../ListBox/test-helpers';
import ComboBox from '../ComboBox';
import { act } from 'react';
Expand Down Expand Up @@ -144,23 +145,24 @@ describe('ComboBox', () => {
expect(findInputNode()).toHaveDisplayValue('Apple');
});

it('should respect slug prop', () => {
it('should respect slug prop', async () => {
const { container } = render(<ComboBox {...mockProps} slug={<Slug />} />);

await waitForPosition();
expect(container.firstChild).toHaveClass(
`${prefix}--list-box__wrapper--slug`
);
});

describe('should display initially selected item found in `initialSelectedItem`', () => {
it('using an object type for the `initialSelectedItem` prop', () => {
it('using an object type for the `initialSelectedItem` prop', async () => {
render(
<ComboBox {...mockProps} initialSelectedItem={mockProps.items[0]} />
);
await waitForPosition();
expect(findInputNode()).toHaveDisplayValue(mockProps.items[0].label);
});

it('using a string type for the `initialSelectedItem` prop', () => {
it('using a string type for the `initialSelectedItem` prop', async () => {
// Replace the 'items' property in mockProps with a list of strings
mockProps = {
...mockProps,
Expand All @@ -170,35 +172,35 @@ describe('ComboBox', () => {
render(
<ComboBox {...mockProps} initialSelectedItem={mockProps.items[1]} />
);

await waitForPosition();
expect(findInputNode()).toHaveDisplayValue(mockProps.items[1]);
});
});

describe('should display selected item found in `selectedItem`', () => {
it('using an object type for the `selectedItem` prop', () => {
it('using an object type for the `selectedItem` prop', async () => {
render(<ComboBox {...mockProps} selectedItem={mockProps.items[0]} />);

await waitForPosition();
expect(findInputNode()).toHaveDisplayValue(mockProps.items[0].label);
});

it('using a string type for the `selectedItem` prop', () => {
it('using a string type for the `selectedItem` prop', async () => {
// Replace the 'items' property in mockProps with a list of strings
mockProps = {
...mockProps,
items: ['1', '2', '3'],
};

render(<ComboBox {...mockProps} selectedItem={mockProps.items[1]} />);

await waitForPosition();
expect(findInputNode()).toHaveDisplayValue(mockProps.items[1]);
});
});

describe('when disabled', () => {
it('should not let the user edit the input node', async () => {
render(<ComboBox {...mockProps} disabled={true} />);

await waitForPosition();
expect(findInputNode()).toHaveAttribute('disabled');

expect(findInputNode()).toHaveDisplayValue('');
Expand All @@ -210,6 +212,7 @@ describe('ComboBox', () => {

it('should not let the user expand the menu', async () => {
render(<ComboBox {...mockProps} disabled={true} />);
await waitForPosition();
await openMenu();
expect(findListBoxNode()).not.toHaveClass(
`${prefix}--list-box--expanded`
Expand All @@ -220,7 +223,7 @@ describe('ComboBox', () => {
describe('when readonly', () => {
it('should not let the user edit the input node', async () => {
render(<ComboBox {...mockProps} readOnly={true} />);

await waitForPosition();
expect(findInputNode()).toHaveAttribute('readonly');

expect(findInputNode()).toHaveDisplayValue('');
Expand All @@ -232,6 +235,7 @@ describe('ComboBox', () => {

it('should not let the user expand the menu', async () => {
render(<ComboBox {...mockProps} disabled={true} />);
await waitForPosition();
await openMenu();
expect(findListBoxNode()).not.toHaveClass(
`${prefix}--list-box--expanded`
Expand All @@ -240,9 +244,9 @@ describe('ComboBox', () => {
});

describe('downshift quirks', () => {
it('should set `inputValue` to an empty string if a false-y value is given', () => {
it('should set `inputValue` to an empty string if a false-y value is given', async () => {
render(<ComboBox {...mockProps} />);

await waitForPosition();
expect(findInputNode()).toHaveDisplayValue('');
});

Expand All @@ -257,6 +261,7 @@ describe('ComboBox', () => {
</div>
</>
);
await waitForPosition();
const firstCombobox = screen.getByTestId('combobox-1');
const secondCombobox = screen.getByTestId('combobox-2');

Expand Down Expand Up @@ -291,6 +296,7 @@ describe('ComboBox', () => {
});
it('should open menu without moving focus on pressing Alt+ DownArrow', async () => {
render(<ComboBox {...mockProps} />);
await waitForPosition();
act(() => {
screen.getByRole('combobox').focus();
});
Expand All @@ -300,6 +306,7 @@ describe('ComboBox', () => {

it('should close menu and return focus to combobox on pressing Alt+ UpArrow', async () => {
render(<ComboBox {...mockProps} />);
await waitForPosition();
await openMenu();
await userEvent.keyboard('{Alt>}{ArrowUp}');
assertMenuClosed(mockProps);
Expand Down
15 changes: 15 additions & 0 deletions packages/react/src/components/ComboBox/ComboBox.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ export const AllowCustomValue = () => {
</div>
);
};
export const ExperimentalAutoAlign = () => (
<div style={{ width: 400 }}>
<div style={{ height: 300 }}></div>
<ComboBox
onChange={() => {}}
id="carbon-combobox"
items={items}
itemToString={(item) => (item ? item.text : '')}
titleText="ComboBox title"
helperText="Combobox helper text"
autoAlign={true}
/>
<div style={{ height: 800 }}></div>
</div>
);

export const _WithLayer = () => (
<WithLayer>
Expand Down
43 changes: 42 additions & 1 deletion packages/react/src/components/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import mergeRefs from '../../tools/mergeRefs';
import deprecate from '../../prop-types/deprecate';
import { usePrefix } from '../../internal/usePrefix';
import { FormContext } from '../FluidForm';
import { useFloating, flip, autoUpdate } from '@floating-ui/react';

const {
InputBlur,
Expand Down Expand Up @@ -150,6 +151,13 @@ export interface ComboBoxProps<ItemType>
*/
ariaLabel?: string;

/**
* **Experimental**: Will attempt to automatically align the floating
* element to avoid collisions with the viewport and being clipped by
* ancestor elements.
*/
autoAlign?: boolean;

/**
* An optional className to add to the container node
*/
Expand Down Expand Up @@ -313,6 +321,7 @@ const ComboBox = forwardRef(
const {
['aria-label']: ariaLabel = 'Choose an item',
ariaLabel: deprecatedAriaLabel,
autoAlign = false,
className: containerClassName,
direction = 'bottom',
disabled = false,
Expand Down Expand Up @@ -342,6 +351,30 @@ const ComboBox = forwardRef(
slug,
...rest
} = props;
const { refs, floatingStyles } = useFloating(
autoAlign
? {
placement: direction,
strategy: 'fixed',
middleware: [flip()],
whileElementsMounted: autoUpdate,
}
: {}
);
const parentWidth = (refs?.reference?.current as HTMLElement)?.clientWidth;

useEffect(() => {
if (autoAlign) {
Object.keys(floatingStyles).forEach((style) => {
if (refs.floating.current) {
refs.floating.current.style[style] = floatingStyles[style];
}
});
if (parentWidth && refs.floating.current) {
refs.floating.current.style.width = parentWidth + 'px';
}
}
}, [autoAlign, floatingStyles, refs.floating, parentWidth]);
const prefix = usePrefix();
const { isFluid } = useContext(FormContext);
const textInput = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -630,6 +663,7 @@ const ComboBox = forwardRef(
light={light}
size={size}
warn={warn}
ref={refs.setReference}
warnText={warnText}
warnTextId={warnTextId}>
<div className={`${prefix}--list-box__field`}>
Expand Down Expand Up @@ -739,7 +773,8 @@ const ComboBox = forwardRef(
<ListBox.Menu
{...getMenuProps({
'aria-label': deprecatedAriaLabel || ariaLabel,
})}>
})}
ref={mergeRefs(getMenuProps().ref, refs.setFloating)}>
{isOpen
? filterItems(items, itemToString, inputValue).map(
(item, index) => {
Expand Down Expand Up @@ -821,6 +856,12 @@ ComboBox.propTypes = {
PropTypes.string,
'This prop syntax has been deprecated. Please use the new `aria-label`.'
),
/**
* **Experimental**: Will attempt to automatically align the floating
* element to avoid collisions with the viewport and being clipped by
* ancestor elements.
*/
autoAlign: PropTypes.bool,

/**
* An optional className to add to the container node
Expand Down
Loading

0 comments on commit cb30e67

Please sign in to comment.