Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(multiselect): new All option for multiselect #16236

Merged
merged 62 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
1689661
feat: Add selectAll option in Multiselect
preetibansalui Apr 18, 2024
5d94668
disabled items and length corrected
Gururajj77 Apr 18, 2024
9e2dfaf
changes for select all
Gururajj77 Apr 19, 2024
a7ba8b4
changes
Gururajj77 Apr 22, 2024
9616225
feat(multiselect): unifinshed functionality on the multiselect
Gururajj77 Apr 22, 2024
3ba9e19
fix: select all option when all items are selected
preetibansalui Apr 23, 2024
225c716
Merge branch 'main' into multiselect-feat
Gururajj77 Apr 23, 2024
ca58748
feat(multiselect): using filteredItems from prev changes
Gururajj77 Apr 23, 2024
09b4c0e
fix: added intermediate checkbox icon
preetibansalui Apr 23, 2024
beedba5
fix: fixed All option should not be in active state as per design
preetibansalui Apr 23, 2024
f625231
feat(multiselect): tests and avt fixed
Gururajj77 Apr 24, 2024
d04135c
fix: added prop for selectall option and changed css for all option
preetibansalui Apr 24, 2024
028028a
feat(multiselect): modified avt for default
Gururajj77 Apr 24, 2024
ea5681f
feat: test fixed
Gururajj77 Apr 24, 2024
04ca9e4
feat: added test case for select all
Gururajj77 Apr 24, 2024
698f11a
fix: format issue
preetibansalui Apr 25, 2024
256e72e
feat(multiselect): added missed proptypes and typescript types
Gururajj77 May 3, 2024
dc25278
feat: updated tests
Gururajj77 May 3, 2024
b8bf158
feat: fixing the build
Gururajj77 May 3, 2024
ebe3ca1
feat: updates for test success
Gururajj77 May 3, 2024
404fced
Merge branch 'main' into multiselect-feat
Gururajj77 May 7, 2024
d93fb79
feat: changes according to review comments(except tests)
Gururajj77 May 14, 2024
6efeca5
feat: fixed hasSelectAll and added warning
Gururajj77 May 15, 2024
d0b1576
feat: made hasselectall prop experimental
Gururajj77 Jun 3, 2024
c80b250
Merge branch 'main' into multiselect-feat
preetibansalui Jun 6, 2024
9f1269b
fix: removed duplicate OnChangeData
preetibansalui Jun 6, 2024
aadeef8
feat: ran yarn test
Gururajj77 Jun 6, 2024
33da11e
Merge branch 'main' into multiselect-feat
preetibansalui Jun 10, 2024
a082b01
fix: should deselect all on click to indeterminate icon
preetibansalui Jun 10, 2024
c3a9bee
chore: added a new story for selectAll and a prop for selectAll label
preetibansalui Jun 10, 2024
b3f48fc
fix: active state of selectAll option
preetibansalui Jun 10, 2024
1519919
fix: changed items to match with documentation
preetibansalui Jun 10, 2024
23363fc
fix: update snapshot
preetibansalui Jun 10, 2024
ca69bc5
fix: alignment issue
preetibansalui Jun 14, 2024
5b9107f
fix: firefox issue
preetibansalui Jun 26, 2024
2853da2
Merge branch 'main' into multiselect-feat
Gururajj77 Jul 23, 2024
5508689
feat: format run
Gururajj77 Jul 23, 2024
675af07
feat: fixed storybook
Gururajj77 Jul 23, 2024
5720baa
Merge branch 'main' into multiselect-feat
Gururajj77 Jul 24, 2024
d5e1535
feat: formatting ci fail fix
Gururajj77 Jul 24, 2024
bac4ada
feat: select all is exempt from sorting
Gururajj77 Jul 29, 2024
08119aa
Merge branch 'main' into multiselect-feat
tay1orjones Jul 29, 2024
13ba415
chore: remove isSelectAll from ListBoxMenuItem
tay1orjones Jul 29, 2024
2f236f8
chore: update prop names
tay1orjones Jul 29, 2024
624fc1e
chore: rename variables
tay1orjones Jul 29, 2024
7c691f7
feat: changed from prop to object property
Gururajj77 Aug 5, 2024
c2e0649
fix: focus issue
preetibansalui Aug 6, 2024
584cfa8
fix: remove console
preetibansalui Aug 6, 2024
2f6e1a4
Merge branch 'main' into multiselect-feat
Gururajj77 Aug 7, 2024
a162c66
feat: fixed tests
Gururajj77 Aug 7, 2024
0b5fb7c
feat: fixed typing test
Gururajj77 Aug 7, 2024
fc8641c
fix: focus issue
preetibansalui Aug 7, 2024
af158c5
Update packages/styles/scss/components/multiselect/_multiselect.scss
Gururajj77 Aug 7, 2024
5a34364
Merge branch 'main' into multiselect-feat
Gururajj77 Aug 9, 2024
0816deb
fix: sort issue resolved
preetibansalui Aug 23, 2024
5f082b6
fix: test case
preetibansalui Aug 23, 2024
85177dc
Merge branch 'main' into multiselect-feat
preetibansalui Aug 23, 2024
0a3ae2e
Merge branch 'main' into multiselect-feat
Gururajj77 Aug 26, 2024
d76cec9
Merge branch 'main' into multiselect-feat
Gururajj77 Aug 27, 2024
03d6457
fix: alignment issue
preetibansalui Aug 27, 2024
38e2798
test(multiselect): refactor assertions off data attributes
tay1orjones Aug 30, 2024
c9536a7
Merge branch 'main' into multiselect-feat
tay1orjones Aug 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/react/src/components/ListBox/ListBoxMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
*/

import cx from 'classnames';
import React, { ForwardedRef, useEffect, useRef, useState } from 'react';
import React, {
ForwardedRef,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import PropTypes from 'prop-types';
import { usePrefix } from '../../internal/usePrefix';
import { ForwardRefReturn, ReactAttr } from '../../types/common';
Expand All @@ -24,6 +30,11 @@ function useIsTruncated(ref) {
}

export interface ListBoxMenuItemProps extends ReactAttr<HTMLLIElement> {
/**
* Specify any children nodes that should be rendered inside of the ListBox
* Menu Item
*/
children?: ReactNode;
/**
* Specify whether the current menu item is "active".
*/
Expand All @@ -38,6 +49,11 @@ export interface ListBoxMenuItemProps extends ReactAttr<HTMLLIElement> {
* Specify whether the item should be disabled
*/
disabled?: boolean;

/**
* Provide an optional tooltip for the ListBoxMenuItem
*/
title?: string;
}

export type ListBoxMenuItemForwardedRef =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ Playground.args = {
clearSelectionDescription: 'Total items selected: ',
useTitleInItem: false,
clearSelectionText: 'To clear selection, press Delete or Backspace,',
selectAll: false,
selectAllItemText: 'All options',
};

Playground.argTypes = {
Expand Down Expand Up @@ -383,6 +385,59 @@ export const _Controlled = () => {
);
};

export const SelectAll = () => {
const [label, setLabel] = useState('Choose options');

const onChange = (value) => {
if (value.selectedItems.length == 1) {
setLabel('Option selected');
} else if (value.selectedItems.length > 1) {
setLabel('Options selected');
} else {
setLabel('Choose options');
}
};

return (
<div style={{ width: 300 }}>
<MultiSelect
label={label}
id="carbon-multiselect-example"
titleText="Multiselect title"
helperText="This is helper text"
items={[
{
id: 'downshift-1-item-0',
text: 'Editor',
},
{
id: 'downshift-1-item-1',
text: 'Owner',
},
{
id: 'downshift-1-item-2',
text: 'Uploader',
},
{
id: 'downshift-1-item-3',
text: 'Reader - a disabled item',
disabled: true,
},
{
id: 'select-all',
text: 'All roles',
isSelectAll: true,
},
]}
itemToString={(item) => (item ? item.text : '')}
selectionFeedback="top-after-reopen"
onChange={onChange}
selectAllItemText="All roles"
/>
</div>
);
};

const aiLabel = (
<AILabel className="slug-container">
<AILabelContent>
Expand Down
124 changes: 73 additions & 51 deletions packages/react/src/components/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import PropTypes from 'prop-types';
import React, {
ForwardedRef,
useContext,
useRef,
useState,
useMemo,
ReactNode,
Expand All @@ -43,6 +42,8 @@ import { keys, match } from '../../internal/keyboard';
import { usePrefix } from '../../internal/usePrefix';
import { FormContext } from '../FluidForm';
import { ListBoxProps } from '../ListBox/ListBox';
import Checkbox from '../Checkbox';
import type { InternationalProps } from '../../types/common';
import type { TranslateWithId } from '../../types/common';
import { noopFn } from '../../internal/noopFn';
import {
Expand Down Expand Up @@ -330,6 +331,36 @@ const MultiSelect = React.forwardRef(
}: MultiSelectProps<ItemType>,
ref: ForwardedRef<HTMLButtonElement>
) => {
const sortOptions = {
selectedItems: selected,
itemToString,
compareItems,
locale,
};

const filteredItems = useMemo(() => {
return sortItems!(
items.filter((item) => {
if (typeof item === 'object' && item !== null) {
for (const key in item) {
if (Object.hasOwn(item, key) && item[key] === undefined) {
return false; // Return false if any property has an undefined value
}
}
}
return true; // Return true if item is not an object with undefined values
}),
sortOptions as SortItemsOptions<ItemType>
);
}, [items]);

let selectAll = filteredItems.some((item) => (item as any).isSelectAll);
if ((selected ?? []).length > 0 && selectAll) {
console.warn(
'Warning: `selectAll` should not be used when `selectedItems` is provided. Please pass either `selectAll` or `selectedItems`, not both.'
);
selectAll = false;
}
const prefix = usePrefix();
const { isFluid } = useContext(FormContext);
const multiSelectInstanceId = useId();
Expand All @@ -339,16 +370,6 @@ const MultiSelect = React.forwardRef(
const [prevOpenProp, setPrevOpenProp] = useState(open);
const [topItems, setTopItems] = useState([]);
const [itemsCleared, setItemsCleared] = useState(false);
const {
selectedItems: controlledSelectedItems,
onItemChange,
clearSelection,
} = useSelection({
disabled,
initialSelectedItems,
onChange,
selectedItems: selected,
});

const { refs, floatingStyles, middlewareData } = useFloating(
autoAlign
Expand Down Expand Up @@ -387,19 +408,18 @@ const MultiSelect = React.forwardRef(
}
}, [autoAlign, floatingStyles, refs.floating, middlewareData, open]);

// Filter out items with an object having undefined values
const filteredItems = useMemo(() => {
return items.filter((item) => {
if (typeof item === 'object' && item !== null) {
for (const key in item) {
if (Object.hasOwn(item, key) && item[key] === undefined) {
return false; // Return false if any property has an undefined value
}
}
}
return true; // Return true if item is not an object with undefined values
});
}, [items]);
const {
selectedItems: controlledSelectedItems,
onItemChange,
clearSelection,
} = useSelection({
disabled,
initialSelectedItems,
onChange,
selectedItems: selected,
selectAll,
filteredItems,
});

const selectProps: UseSelectProps<ItemType> = {
stateReducer,
Expand All @@ -416,7 +436,7 @@ const MultiSelect = React.forwardRef(
);
},
selectedItem: controlledSelectedItems,
items: filteredItems,
items: filteredItems as ItemType[],
isItemDisabled(item, _index) {
return (item as any).disabled;
},
Expand Down Expand Up @@ -526,19 +546,13 @@ const MultiSelect = React.forwardRef(
selectedItems && selectedItems.length > 0,
[`${prefix}--list-box--up`]: direction === 'top',
[`${prefix}--multi-select--readonly`]: readOnly,
[`${prefix}--multi-select--selectall`]: selectAll,
});

// needs to be capitalized for react to render it correctly
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ItemToElement = itemToElement!;

const sortOptions = {
selectedItems: controlledSelectedItems,
itemToString,
compareItems,
locale,
};

if (selectionFeedback === 'fixed') {
sortOptions.selectedItems = [];
} else if (selectionFeedback === 'top-after-reopen') {
Expand Down Expand Up @@ -590,7 +604,7 @@ const MultiSelect = React.forwardRef(
} else {
return {
...changes,
highlightedIndex: props.items.indexOf(highlightedIndex),
highlightedIndex: filteredItems.indexOf(highlightedIndex),
};
}
case ToggleButtonKeyDownArrowDown:
Expand Down Expand Up @@ -661,13 +675,17 @@ const MultiSelect = React.forwardRef(
selectedItems.length > 0 &&
selectedItems.map((item) => (item as selectedItemType)?.text);

const selectedItemsWithoutSelectAll = selectedItems.filter(
(item: any) => !item.isSelectAll
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be guarded behind selectAll? If so, the usages will need to be updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we do not need a guard for this as it's a simple filtering which will remove items with isSelectAll key-value, which will pass anyway if user would not pass an object with isSelectAll


// Memoize the value of getMenuProps to avoid an infinite loop
const menuProps = useMemo(
() =>
getMenuProps({
ref: autoAlign ? refs.setFloating : null,
}),
[autoAlign]
[autoAlign, getMenuProps, refs.setFloating]
);

return (
Expand Down Expand Up @@ -712,7 +730,7 @@ const MultiSelect = React.forwardRef(
clearSelection={
!disabled && !readOnly ? clearSelection : noopFn
}
selectionCount={selectedItems.length}
selectionCount={selectedItemsWithoutSelectAll.length}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
translateWithId={translateWithId!}
disabled={disabled}
Expand Down Expand Up @@ -743,15 +761,16 @@ const MultiSelect = React.forwardRef(
</div>
<ListBox.Menu {...menuProps}>
{isOpen &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sortItems!(
filteredItems,
sortOptions as SortItemsOptions<ItemType>
).map((item, index) => {
filteredItems.map((item, index) => {
const isChecked =
selectedItems.filter((selected) => isEqual(selected, item))
.length > 0;

const isIndeterminate =
selectedItems.length !== 0 &&
item['isSelectAll'] &&
!isChecked;

const itemProps = getItemProps({
item,
// we don't want Downshift to set aria-selected for us
Expand All @@ -763,24 +782,27 @@ const MultiSelect = React.forwardRef(
return (
<ListBox.MenuItem
key={itemProps.id}
isActive={isChecked}
isActive={isChecked && !item['isSelectAll']}
aria-label={itemText}
isHighlighted={highlightedIndex === index}
title={itemText}
disabled={itemProps['aria-disabled']}
{...itemProps}>
<div className={`${prefix}--checkbox-wrapper`}>
<span
<Checkbox
id={`${itemProps.id}__checkbox`}
labelText={
itemToElement ? (
<ItemToElement key={itemProps.id} {...item} />
) : (
itemText
)
}
checked={isChecked}
title={useTitleInItem ? itemText : undefined}
className={`${prefix}--checkbox-label`}
data-contained-checkbox-state={isChecked}
id={`${itemProps.id}__checkbox`}>
{itemToElement ? (
<ItemToElement key={itemProps.id} {...item} />
) : (
itemText
)}
</span>
indeterminate={isIndeterminate}
disabled={disabled}
/>
</div>
</ListBox.MenuItem>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import FilterableMultiSelect from '../FilterableMultiSelect';
import {
Expand All @@ -15,12 +15,11 @@ import {
findMenuIconNode,
generateItems,
generateGenericItem,
waitForPosition,
} from '../../ListBox/test-helpers';
import { AILabel } from '../../AILabel';

const prefix = 'cds';

const waitForPosition = () => act(async () => {});
const openMenu = async () => {
await userEvent.click(screen.getByRole('combobox'));
};
Expand Down
Loading
Loading