Skip to content

Commit

Permalink
feat(select, filterable-select, multi-select): add support for overri…
Browse files Browse the repository at this point in the history
…ding the list width

Adds support for overriding the list width via `listWidth` prop. Adds support for placing
list at `top-start`, `top-end`, `bottom-start` and `botton-end`. When `top` or `bottom`
is passed to `listPlacement` it will internally append `-end` to it.

fix #6861
  • Loading branch information
edleeks87 committed Sep 27, 2024
1 parent 28670f6 commit e609527
Show file tree
Hide file tree
Showing 17 changed files with 410 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, {
useRef,
useMemo,
} from "react";
import { flip, offset, size, Side } from "@floating-ui/dom";
import { flip, offset, size } from "@floating-ui/dom";
import {
useVirtualizer,
defaultRangeExtractor,
Expand Down Expand Up @@ -38,6 +38,14 @@ import Loader from "../../../loader";
import Option, { OptionProps } from "../../option";
import SelectListContext from "./select-list.context";

export type ListPlacement =
| "top"
| "bottom"
| "top-start"
| "bottom-start"
| "top-end"
| "bottom-end";

export interface SelectListProps {
/** The ID for the parent <div> */
id?: string;
Expand Down Expand Up @@ -78,7 +86,7 @@ export interface SelectListProps {
/** When true component will work in multi column mode, children should consist of OptionRow components in this mode */
multiColumn?: boolean;
/** Placement of the select list relative to the input element */
listPlacement?: "top" | "bottom";
listPlacement?: ListPlacement;
/** Use the opposite list placement if the set placement does not fit */
flipEnabled?: boolean;
/** @private @ignore
Expand All @@ -96,6 +104,8 @@ export interface SelectListProps {
virtualScrollOverscan?: number;
/** @private @ignore A callback for when a mouseDown event occurs on the component */
onMouseDown?: () => void;
/** Override the default width of the list element. Number passed is converted into pixel value */
listWidth?: number;
}

const TABLE_HEADER_HEIGHT = 48;
Expand Down Expand Up @@ -125,6 +135,7 @@ const SelectList = React.forwardRef(
multiselectValues,
enableVirtualScroll,
virtualScrollOverscan = 5,
listWidth,
...listProps
}: SelectListProps,
listContainerRef: React.ForwardedRef<HTMLDivElement>
Expand Down Expand Up @@ -614,7 +625,7 @@ const SelectList = React.forwardRef(
size({
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
width: `${listWidth ?? rects.reference.width}px`,
});
},
}),
Expand All @@ -626,7 +637,7 @@ const SelectList = React.forwardRef(
]
: []),
],
[flipEnabled]
[listWidth, flipEnabled]
);

const loader = isLoading ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import withFilter from "../__internal__/utils/with-filter.hoc";
import StyledSelect from "../select.style";
import SelectList, {
SelectListProps,
ListPlacement,
} from "../__internal__/select-list/select-list.component";
import isExpectedOption from "../__internal__/utils/is-expected-option";
import isNavigationKey from "../__internal__/utils/is-navigation-key";
Expand Down Expand Up @@ -72,7 +73,7 @@ export interface FilterableSelectProps
/** Maximum list height - defaults to 180 */
listMaxHeight?: number;
/** Placement of the select list in relation to the input element */
listPlacement?: "top" | "bottom";
listPlacement?: ListPlacement;
/** Use the opposite list placement if the set placement does not fit */
flipEnabled?: boolean;
/** Set this prop to enable a virtualised list of options. If it is not used then all options will be in the
Expand All @@ -89,6 +90,8 @@ export interface FilterableSelectProps
isOptional?: boolean;
/** Flag to configure component as mandatory */
required?: boolean;
/** Override the default width of the list element. Number passed is converted into pixel value */
listWidth?: number;
}

export const FilterableSelect = React.forwardRef<
Expand Down Expand Up @@ -134,6 +137,7 @@ export const FilterableSelect = React.forwardRef<
disableDefaultFiltering = false,
isOptional,
required,
listWidth,
...textboxProps
},
ref
Expand Down Expand Up @@ -638,6 +642,19 @@ export const FilterableSelect = React.forwardRef<
};
}

let placement: ListPlacement;

switch (listPlacement) {
case "top":
placement = "top-end";
break;
case "bottom":
placement = "bottom-end";
break;
default:
placement = listPlacement;
}

const selectListProps = {
ref: listboxRef,
id: selectListId.current,
Expand All @@ -656,11 +673,12 @@ export const FilterableSelect = React.forwardRef<
onListScrollBottom,
tableHeader,
multiColumn,
listPlacement,
listPlacement: listWidth !== undefined ? placement : listPlacement,
flipEnabled,
isOpen,
enableVirtualScroll,
virtualScrollOverscan,
listWidth,
};

const selectList = disableDefaultFiltering ? (
Expand Down
10 changes: 10 additions & 0 deletions src/components/select/filterable-select/filterable-select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ You can use `listMaxHeight` prop to override default max height value of select

<Canvas of={FilterableSelectStories.ListHeight} />

### List width

You can use `listWidth` prop to override the width of the select list. By default the list
will have the same width as the input.

<Canvas
name="list width"
of={FilterableSelectStories.ListWidth}
/>

### Controlled Usage

<Canvas of={FilterableSelectStories.Controlled} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ describe("FilterableSelect", () => {
).toBeVisible();
});

it.each(["top", "bottom"])(
it.each(["top", "bottom", "top-end", "bottom-end"])(
"the listPlacement prop should be passed",
(listPlacement) => {
const wrapper = renderSelect({ listPlacement });
Expand All @@ -500,6 +500,18 @@ describe("FilterableSelect", () => {
}
);

it.each(["top", "bottom"])(
"the listPlacement prop should be overridden when listWidth is set",
(listPlacement) => {
const wrapper = renderSelect({ listPlacement, listWidth: 100 });

simulateDropdownEvent(wrapper, "click");
expect(wrapper.find(SelectList).prop("listPlacement")).toBe(
`${listPlacement}-end`
);
}
);

it("the flipEnabled prop should be passed", () => {
const wrapper = renderSelect({ flipEnabled: false });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Typography from "../../typography";
import {
CustomSelectChangeEvent,
FilterableSelect,
FilterableSelectProps,
Option,
OptionRow,
} from "..";
Expand Down Expand Up @@ -850,3 +851,46 @@ CustomFilterAndOptionStyle.storyName = "Custom Filter and Option Style";
CustomFilterAndOptionStyle.parameters = {
chromatic: { disableSnapshot: true },
};

export const ListWidth: Story = () => {
const [listPlacement, setListPlacement] = useState<
FilterableSelectProps["listPlacement"]
>("bottom-end");
const [value, setValue] = useState<string>("");
const handleChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
setValue(ev.target.value);
};
return (
<>
<Button mr={1} onClick={() => setListPlacement("top-end")}>
Top end
</Button>
<Button mr={1} onClick={() => setListPlacement("bottom-end")}>
Bottom end
</Button>
<Button mr={1} onClick={() => setListPlacement("top-start")}>
Top start
</Button>
<Button onClick={() => setListPlacement("bottom-start")}>
Bottom start
</Button>
<Box mt="200px" ml="200px" width="200px">
<FilterableSelect
name="listWidth"
id="listWidth"
label="color"
labelInline
listWidth={350}
listPlacement={listPlacement}
value={value}
onChange={handleChange}
>
<Option text="Amber" value="1" />
<Option text="Black" value="2" />
<Option text="Blue" value="3" />
</FilterableSelect>
</Box>
</>
);
};
ListWidth.storyName = "List width";
26 changes: 22 additions & 4 deletions src/components/select/multi-select/multi-select.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import React, {
useMemo,
} from "react";
import invariant from "invariant";
import { Side } from "@floating-ui/dom";

import { filterOutStyledSystemSpacingProps } from "../../../style/utils";
import SelectTextbox, {
FormInputPropTypes,
} from "../__internal__/select-textbox";
import guid from "../../../__internal__/utils/helpers/guid";
import withFilter from "../__internal__/utils/with-filter.hoc";
import SelectList from "../__internal__/select-list/select-list.component";
import SelectList, {
ListPlacement,
} from "../__internal__/select-list/select-list.component";
import {
StyledSelectPillContainer,
StyledSelectMultiSelect,
Expand Down Expand Up @@ -77,7 +78,7 @@ export interface MultiSelectProps
/** Maximum list height - defaults to 180 */
listMaxHeight?: number;
/** Placement of the select list in relation to the input element */
listPlacement?: "top" | "bottom";
listPlacement?: ListPlacement;
/** Use the opposite list placement if the set placement does not fit */
flipEnabled?: boolean;
/** Wraps the pill text when it would overflow the input width */
Expand All @@ -91,6 +92,8 @@ export interface MultiSelectProps
virtualScrollOverscan?: number;
/** Flag to configure component as optional. */
isOptional?: boolean;
/** Override the default width of the list element. Number passed is converted into pixel value */
listWidth?: number;
}

export const MultiSelect = React.forwardRef<HTMLInputElement, MultiSelectProps>(
Expand Down Expand Up @@ -132,6 +135,7 @@ export const MultiSelect = React.forwardRef<HTMLInputElement, MultiSelectProps>(
virtualScrollOverscan,
isOptional,
required,
listWidth,
...textboxProps
},
ref
Expand Down Expand Up @@ -649,6 +653,19 @@ export const MultiSelect = React.forwardRef<HTMLInputElement, MultiSelectProps>(
};
}

let placement: ListPlacement;

switch (listPlacement) {
case "top":
placement = "top-end";
break;
case "bottom":
placement = "bottom-end";
break;
default:
placement = listPlacement;
}

const selectList = (
<FilterableSelectList
ref={listboxRef}
Expand All @@ -664,13 +681,14 @@ export const MultiSelect = React.forwardRef<HTMLInputElement, MultiSelectProps>(
isLoading={isLoading}
tableHeader={tableHeader}
multiColumn={multiColumn}
listPlacement={listPlacement}
listPlacement={listWidth !== undefined ? placement : listPlacement}
listMaxHeight={listMaxHeight}
flipEnabled={flipEnabled}
multiselectValues={actualValue}
isOpen={isOpen}
enableVirtualScroll={enableVirtualScroll}
virtualScrollOverscan={virtualScrollOverscan}
listWidth={listWidth}
>
{children}
</FilterableSelectList>
Expand Down
10 changes: 10 additions & 0 deletions src/components/select/multi-select/multi-select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ You can use `listMaxHeight` prop to override default max height value of select

<Canvas of={MultiSelectStories.ListHeight} />

### List width

You can use `listWidth` prop to override the width of the select list. By default the list
will have the same width as the input.

<Canvas
name="list width"
of={MultiSelectStories.ListWidth}
/>

### Controlled Usage

<Canvas of={MultiSelectStories.Controlled} />
Expand Down
14 changes: 13 additions & 1 deletion src/components/select/multi-select/multi-select.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ describe("MultiSelect", () => {
});
});

it.each(["top", "bottom"])(
it.each(["top", "bottom", "top-end", "bottom-end"])(
"the listPlacement prop should be passed",
(listPlacement) => {
const wrapper = renderSelect({ listPlacement });
Expand All @@ -311,6 +311,18 @@ describe("MultiSelect", () => {
}
);

it.each(["top", "bottom"])(
"the listPlacement prop should be overridden when listWidth is set",
(listPlacement) => {
const wrapper = renderSelect({ listPlacement, listWidth: 100 });

simulateDropdownEvent(wrapper, "click");
expect(wrapper.find(SelectList).prop("listPlacement")).toBe(
`${listPlacement}-end`
);
}
);

it("the flipEnabled prop should be passed", () => {
const wrapper = renderSelect({ flipEnabled: false });

Expand Down
Loading

0 comments on commit e609527

Please sign in to comment.