{
{...mockProps}
/>
);
+ await waitForPosition();
expect(screen.getByRole('link')).toBeInTheDocument();
});
@@ -128,7 +135,6 @@ describe('Dropdown', () => {
it('should let the user select an option by clicking on the option node', async () => {
render();
await openMenu();
-
await userEvent.click(screen.getByText('Item 0'));
expect(mockProps.onChange).toHaveBeenCalledTimes(1);
expect(mockProps.onChange).toHaveBeenCalledWith({
@@ -161,15 +167,16 @@ describe('Dropdown', () => {
});
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(
);
+ await waitForPosition();
expect(screen.getByText(mockProps.items[0].label)).toBeInTheDocument();
});
- 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,
@@ -179,20 +186,23 @@ describe('Dropdown', () => {
render(
);
+ await waitForPosition();
expect(screen.getByText(mockProps.items[1])).toBeInTheDocument();
});
});
describe('Component API', () => {
- it('should accept a `ref` for the underlying button element', () => {
+ it('should accept a `ref` for the underlying button element', async () => {
const ref = React.createRef();
render();
+ await waitForPosition();
expect(ref.current).toHaveAttribute('aria-haspopup', 'listbox');
});
- it('should respect slug prop', () => {
+ it('should respect slug prop', async () => {
const { container } = render(} />);
+ await waitForPosition();
expect(container.firstChild).toHaveClass(
`${prefix}--list-box__wrapper--slug`
);
diff --git a/packages/react/src/components/Dropdown/Dropdown.stories.js b/packages/react/src/components/Dropdown/Dropdown.stories.js
index e0347e877b51..394916ab7799 100644
--- a/packages/react/src/components/Dropdown/Dropdown.stories.js
+++ b/packages/react/src/components/Dropdown/Dropdown.stories.js
@@ -69,6 +69,24 @@ const items = [
},
];
+export const ExperimentalAutoAlign = () => (
+
+
+
(item ? item.text : '')}
+ direction="top"
+ />
+
+
+);
+
export const Playground = (args) => (
*/
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;
+
/**
* Specify the direction of the dropdown. Can be either top or bottom.
*/
@@ -238,6 +250,7 @@ export type DropdownTranslationKey = ListBoxMenuIconTranslationKey;
const Dropdown = React.forwardRef(
(
{
+ autoAlign = false,
className: containerClassName,
disabled = false,
direction = 'bottom',
@@ -270,6 +283,43 @@ const Dropdown = React.forwardRef(
}: DropdownProps,
ref: ForwardedRef
) => {
+ const { refs, floatingStyles } = useFloating(
+ autoAlign
+ ? {
+ placement: direction,
+
+ // The floating element is positioned relative to its nearest
+ // containing block (usually the viewport). It will in many cases also
+ // “break” the floating element out of a clipping ancestor.
+ // https://floating-ui.com/docs/misc#clipping
+ strategy: 'fixed',
+
+ // Middleware order matters, arrow should be last
+ middleware: [
+ floatingSize({
+ apply({ rects, elements }) {
+ Object.assign(elements.floating.style, {
+ width: `${rects.reference.width}px`,
+ });
+ },
+ }),
+ flip(),
+ ],
+ whileElementsMounted: autoUpdate,
+ }
+ : {} // When autoAlign is turned off, floating-ui will not be used
+ );
+
+ useEffect(() => {
+ if (autoAlign) {
+ Object.keys(floatingStyles).forEach((style) => {
+ if (refs.floating.current) {
+ refs.floating.current.style[style] = floatingStyles[style];
+ }
+ });
+ }
+ }, [floatingStyles, autoAlign, refs.floating]);
+
const prefix = usePrefix();
const { isFluid } = useContext(FormContext);
@@ -340,6 +390,7 @@ const Dropdown = React.forwardRef(
[`${prefix}--dropdown--readonly`]: readOnly,
[`${prefix}--dropdown--${size}`]: size,
[`${prefix}--list-box--up`]: direction === 'top',
+ [`${prefix}--dropdown--autoalign`]: autoAlign,
});
const titleClasses = cx(`${prefix}--label`, {
@@ -447,6 +498,7 @@ const Dropdown = React.forwardRef(
};
const menuProps = getMenuProps();
+ const menuRef = mergeRefs(menuProps.ref, refs.setFloating);
// Slug is always size `mini`
let normalizedSlug;
@@ -475,6 +527,7 @@ const Dropdown = React.forwardRef(
warnText={warnText}
light={light}
isOpen={isOpen}
+ ref={refs.setReference}
id={id}>
{invalid && (
@@ -514,7 +567,7 @@ const Dropdown = React.forwardRef(
/>
{normalizedSlug}
-
+
{isOpen &&
items.map((item, index) => {
const isObject = item !== null && typeof item === 'object';
@@ -592,6 +645,11 @@ Dropdown.propTypes = {
'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,
+
/**
* Provide a custom className to be applied on the cds--dropdown node
*/
diff --git a/packages/react/src/components/FluidDropdown/__tests__/FluidDropdown-test.js b/packages/react/src/components/FluidDropdown/__tests__/FluidDropdown-test.js
index 94998f0c8b3d..6edb4c8f2fb5 100644
--- a/packages/react/src/components/FluidDropdown/__tests__/FluidDropdown-test.js
+++ b/packages/react/src/components/FluidDropdown/__tests__/FluidDropdown-test.js
@@ -14,6 +14,7 @@ import {
openMenu,
generateItems,
generateGenericItem,
+ waitForPosition,
} from '../../ListBox/test-helpers';
import FluidDropdown from '../FluidDropdown';
@@ -33,22 +34,25 @@ describe('FluidDropdown', () => {
};
});
- it('should render with fluid classes', () => {
+ it('should render with fluid classes', async () => {
const { container } = render();
+ await waitForPosition();
expect(container.firstChild).toHaveClass(
`${prefix}--list-box__wrapper--fluid`
);
});
- it('should render with condensed styles if isCondensed is provided', () => {
+ it('should render with condensed styles if isCondensed is provided', async () => {
const { container } = render();
+ await waitForPosition();
expect(container.firstChild).toHaveClass(
`${prefix}--list-box__wrapper--fluid--condensed`
);
});
- it('should initially render with the menu not open', () => {
+ it('should initially render with the menu not open', async () => {
render();
+ await waitForPosition();
assertMenuClosed();
});
@@ -77,7 +81,7 @@ describe('FluidDropdown', () => {
expect(itemToElement).toHaveBeenCalled();
});
- it('should render selectedItem as an element', () => {
+ it('should render selectedItem as an element', async () => {
render(
{
)}
/>
);
+ await waitForPosition();
// custom element should be rendered for the selected item
expect(
// eslint-disable-next-line testing-library/no-node-access
@@ -104,13 +109,15 @@ describe('FluidDropdown', () => {
});
describe('title', () => {
- it('renders a title', () => {
+ it('renders a title', async () => {
render();
+ await waitForPosition();
expect(screen.getByText('Email Input')).toBeInTheDocument();
});
- it('has the expected classes', () => {
+ it('has the expected classes', async () => {
render();
+ await waitForPosition();
expect(screen.getByText('Email Input')).toHaveClass(`${prefix}--label`);
});
});
@@ -138,18 +145,19 @@ describe('FluidDropdown', () => {
});
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(
);
+ await waitForPosition();
expect(screen.getByText(mockProps.items[0].label)).toBeInTheDocument();
});
- 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,
@@ -162,15 +170,17 @@ describe('FluidDropdown', () => {
initialSelectedItem={mockProps.items[1]}
/>
);
+ await waitForPosition();
expect(screen.getByText(mockProps.items[1])).toBeInTheDocument();
});
});
describe('Component API', () => {
- it('should accept a `ref` for the underlying button element', () => {
+ it('should accept a `ref` for the underlying button element', async () => {
const ref = React.createRef();
render();
+ await waitForPosition();
expect(ref.current).toHaveAttribute('aria-haspopup', 'listbox');
});
});