Skip to content

Commit

Permalink
Merge branch 'main' into 16474-combobox-not-showing-correct-highlight
Browse files Browse the repository at this point in the history
  • Loading branch information
preetibansalui authored Jun 12, 2024
2 parents 00fd691 + 6ef7440 commit ab75ba7
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 84 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 { Slug } from '../Slug';
Expand Down Expand Up @@ -142,23 +143,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 @@ -168,35 +170,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 @@ -208,6 +210,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 @@ -218,7 +221,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 @@ -230,6 +233,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 @@ -238,9 +242,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 @@ -255,6 +259,7 @@ describe('ComboBox', () => {
</div>
</>
);
await waitForPosition();
const firstCombobox = screen.getByTestId('combobox-1');
const secondCombobox = screen.getByTestId('combobox-2');

Expand Down Expand Up @@ -289,6 +294,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 @@ -298,6 +304,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 @@ -641,6 +674,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 @@ -750,7 +784,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 @@ -832,6 +867,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
9 changes: 1 addition & 8 deletions packages/react/src/components/ComboButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,12 @@ import {
autoUpdate,
} from '@floating-ui/react';
import mergeRefs from '../../tools/mergeRefs';
import { MenuAlignment } from '../MenuButton';

const defaultTranslations = {
'carbon.combo-button.additional-actions': 'Additional actions',
};

export type MenuAlignment =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end';

function defaultTranslateWithId(messageId: string) {
return defaultTranslations[messageId];
}
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/components/ContainedList/ContainedList.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ render inline to the Contained List title, please see the
also remain persistent under the title, so that it remains visible on scroll,
when there are many list items passed in.


```js
export const WithPersistentSearch = () => {
const [searchTerm, setSearchTerm] = useState('');
Expand Down Expand Up @@ -139,6 +140,10 @@ export const WithPersistentSearch = () => {
};
```

`ContainedList.ContainedListItem` is deprecated, use
`import { ContainedListItem } from '@carbon/react`
import instead.

<Canvas>
<Story of={ContainedListStories.WithPersistentSearch} />
</Canvas>
Expand Down
Loading

0 comments on commit ab75ba7

Please sign in to comment.