Skip to content

Commit

Permalink
Added Floating UI to MultiSelect and FilterableMultiSelect (carbo…
Browse files Browse the repository at this point in the history
…n-design-system#16689)

* feat: added floating ui to multiselect and filterablemultiselect

* fix: fixed act error

* fix: added autoAlign

* fix: fixed import

* fix: updated snapshot

* test: removed wrong test
  • Loading branch information
guidari authored Jun 21, 2024
1 parent 22dea80 commit fb2dfdb
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 29 deletions.
9 changes: 9 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3418,6 +3418,9 @@ Map {
"propTypes": Object {
"aria-label": [Function],
"ariaLabel": [Function],
"autoAlign": Object {
"type": "bool",
},
"clearSelectionDescription": Object {
"type": "string",
},
Expand Down Expand Up @@ -5091,6 +5094,9 @@ Map {
"propTypes": Object {
"aria-label": [Function],
"ariaLabel": [Function],
"autoAlign": Object {
"type": "bool",
},
"clearSelectionDescription": Object {
"type": "string",
},
Expand Down Expand Up @@ -5339,6 +5345,9 @@ Map {
"render": [Function],
},
"propTypes": Object {
"autoAlign": Object {
"type": "bool",
},
"className": Object {
"type": "string",
},
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/components/ListBox/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

const prefix = 'cds';
import userEvent from '@testing-library/user-event';
import { act } from '@testing-library/react';
import { act } from 'react';

// Finding nodes in a ListBox
export const findListBoxNode = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import React, {
type FocusEvent,
type KeyboardEvent,
ReactElement,
useLayoutEffect,
} from 'react';
import { defaultFilterItems } from '../ComboBox/tools/filter';
import {
Expand All @@ -46,6 +47,12 @@ import { defaultSortItems, defaultCompareItems } from './tools/sorting';
import { usePrefix } from '../../internal/usePrefix';
import { FormContext } from '../FluidForm';
import { useSelection } from '../../internal/Selection';
import {
useFloating,
flip,
size as floatingSize,
autoUpdate,
} from '@floating-ui/react';

const {
InputBlur,
Expand Down Expand Up @@ -83,6 +90,13 @@ export interface FilterableMultiSelectProps<ItemType>
/** @deprecated */
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;

className?: string;

/**
Expand Down Expand Up @@ -274,6 +288,7 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect<
ItemType
>(
{
autoAlign = false,
className: containerClassName,
clearSelectionDescription = 'Total items selected: ',
clearSelectionText = 'To clear selection, press Delete or Backspace',
Expand Down Expand Up @@ -333,6 +348,43 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect<
selectedItems: selected,
});

const { refs, floatingStyles, middlewareData } = 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: [
flip({ crossAxis: false }),
floatingSize({
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
});
},
}),
],
whileElementsMounted: autoUpdate,
}
: {}
);

useLayoutEffect(() => {
if (autoAlign) {
Object.keys(floatingStyles).forEach((style) => {
if (refs.floating.current) {
refs.floating.current.style[style] = floatingStyles[style];
}
});
}
}, [autoAlign, floatingStyles, refs.floating, middlewareData, open]);

const textInput = useRef<HTMLInputElement>(null);
const filterableMultiSelectInstanceId = useId();

Expand Down Expand Up @@ -712,7 +764,7 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect<
warnText={warnText}
isOpen={isOpen}
size={size}>
<div className={`${prefix}--list-box__field`}>
<div className={`${prefix}--list-box__field`} ref={refs.setReference}>
{controlledSelectedItems.length > 0 && (
// @ts-expect-error: It is expecting a non-required prop called: "onClearSelection"
<ListBoxSelection
Expand Down Expand Up @@ -765,7 +817,7 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect<
</div>
{normalizedSlug}

<ListBox.Menu {...menuProps}>
<ListBox.Menu {...menuProps} ref={refs.setFloating}>
{isOpen
? sortedItems.map((item, index) => {
const isChecked =
Expand Down Expand Up @@ -846,6 +898,13 @@ FilterableMultiSelect.propTypes = {
'ariaLabel / aria-label props are no longer required for FilterableMultiSelect'
),

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

/**
* Specify the text that should be read for screen readers that describes total items selected
*/
Expand Down
83 changes: 68 additions & 15 deletions packages/react/src/components/MultiSelect/MultiSelect.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { action } from '@storybook/addon-actions';

import { WithLayer } from '../../../.storybook/templates/WithLayer';
Expand Down Expand Up @@ -115,24 +115,38 @@ const items = [
];

export const Playground = (args) => {
const ref = useRef();
useEffect(() => {
ref?.current?.scrollIntoView({ block: 'center', inline: 'center' });
});
return (
<div style={{ width: 300 }}>
<MultiSelect
label="Multiselect Label"
id="carbon-multiselect-example"
titleText="Multiselect title"
helperText="This is helper text"
items={items}
itemToString={(item) => (item ? item.text : '')}
selectionFeedback="top-after-reopen"
{...args}
/>
<div style={{ width: '5000px', height: '5000px' }}>
<div
style={{
position: 'absolute',
top: '2500px',
left: '2500px',
width: 300,
}}>
<MultiSelect
label="Multiselect Label"
id="carbon-multiselect-example"
titleText="Multiselect title"
helperText="This is helper text"
items={items}
itemToString={(item) => (item ? item.text : '')}
selectionFeedback="top-after-reopen"
ref={ref}
{...args}
/>
</div>
</div>
);
};

Playground.args = {
size: 'md',
autoAlign: false,
type: 'default',
titleText: 'This is a MultiSelect Title',
disabled: false,
Expand Down Expand Up @@ -227,7 +241,10 @@ Playground.argTypes = {

export const Default = () => {
return (
<div style={{ width: 300 }}>
<div
style={{
width: 300,
}}>
<MultiSelect
label="Multiselect Label"
id="carbon-multiselect-example"
Expand All @@ -243,7 +260,10 @@ export const Default = () => {

export const WithInitialSelectedItems = () => {
return (
<div style={{ width: 300 }}>
<div
style={{
width: 300,
}}>
<MultiSelect
label="Multiselect Label"
id="carbon-multiselect-example-2"
Expand All @@ -260,7 +280,10 @@ export const WithInitialSelectedItems = () => {

export const Filterable = (args) => {
return (
<div style={{ width: 300 }}>
<div
style={{
width: 300,
}}>
<FilterableMultiSelect
id="carbon-multiselect-example-3"
titleText="Multiselect title"
Expand Down Expand Up @@ -356,3 +379,33 @@ export const _Controlled = () => {
</div>
);
};

export const ExperimentalAutoAlign = () => {
const ref = useRef();
useEffect(() => {
ref?.current?.scrollIntoView({ block: 'center', inline: 'center' });
});
return (
<div style={{ width: '5000px', height: '5000px' }}>
<div
style={{
position: 'absolute',
top: '2500px',
left: '2500px',
width: 300,
}}>
<MultiSelect
label="Multiselect Label"
id="carbon-multiselect-example"
titleText="Multiselect title"
helperText="This is helper text"
items={items}
itemToString={(item) => (item ? item.text : '')}
selectionFeedback="top-after-reopen"
ref={ref}
autoAlign
/>
</div>
</div>
);
};
Loading

0 comments on commit fb2dfdb

Please sign in to comment.