Skip to content

Commit

Permalink
feat(Select): Support disabling individual items (#1493)
Browse files Browse the repository at this point in the history
  • Loading branch information
moathabuhamad-cengage authored Dec 16, 2024
1 parent 649f7de commit 79435c9
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 70 deletions.
5 changes: 5 additions & 0 deletions .changeset/feat-select-disabled-items.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-magma-dom': minor
---

feat(Select): Support disabling individual items in Select and Multi Select components
32 changes: 23 additions & 9 deletions packages/react-magma-dom/src/components/Select/ItemsList.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React from 'react';
import { ThemeContext } from '../../theme/ThemeContext';
import { I18nContext } from '../../i18n';
import { StyledCard, StyledItem, StyledList } from './shared';
import styled from '@emotion/styled';
import { ReferenceType } from '@floating-ui/react-dom';
import {
UseSelectGetItemPropsOptions,
UseSelectGetMenuPropsOptions,
} from 'downshift';
import { instanceOfToBeCreatedItemObject } from '.';
import React from 'react';
import {
instanceOfToBeCreatedItemObject,
} from '.';
import { I18nContext } from '../../i18n';
import { ThemeContext } from '../../theme/ThemeContext';
import { convertStyleValueToString } from '../../utils';
import { Spinner } from '../Spinner';
import {
defaultComponents,
ItemRenderOptions,
SelectComponents,
} from './components';
import { convertStyleValueToString } from '../../utils';
import { Spinner } from '../Spinner';
import styled from '@emotion/styled';
import { ReferenceType } from '@floating-ui/react-dom';
import { StyledCard, StyledItem, StyledList } from './shared';
import { isItemDisabled } from './utils';

interface ItemsListProps<T> {
customComponents?: SelectComponents<T>;
Expand All @@ -31,6 +34,7 @@ interface ItemsListProps<T> {
maxHeight?: number | string;
menuStyle?: React.CSSProperties;
setFloating?: (node: ReferenceType) => void;
setHighlightedIndex?: (index: number) => void;
}

const NoItemsMessage = styled.span<{
Expand Down Expand Up @@ -67,6 +71,7 @@ export function ItemsList<T>(props: ItemsListProps<T>) {
maxHeight,
menuStyle,
setFloating,
setHighlightedIndex,
} = props;

const theme = React.useContext(ThemeContext);
Expand Down Expand Up @@ -117,10 +122,12 @@ export function ItemsList<T>(props: ItemsListProps<T>) {
const itemString = instanceOfToBeCreatedItemObject(item)
? item.label
: itemToString(item);
const isDisabled = isItemDisabled(item)

const { ref, ...otherDownshiftItemProps } = getItemProps({
item,
index,
disabled: isDisabled,
});

const key = `${itemString}${index}`;
Expand All @@ -133,9 +140,16 @@ export function ItemsList<T>(props: ItemsListProps<T>) {
itemString,
key,
theme,
isDisabled: isDisabled,
...otherDownshiftItemProps,
};

if (isDisabled) {
itemProps.onMouseEnter = () => {
setHighlightedIndex && setHighlightedIndex(-1);
};
}

return <Item<T> {...itemProps} key={key} />;
})
) : (
Expand Down
19 changes: 19 additions & 0 deletions packages/react-magma-dom/src/components/Select/MultiSelect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,25 @@ describe('Select', () => {
expect(getByText(helperMessage)).toBeInTheDocument();
});

it('should handle disabled items', () => {
const items = [
{ label: 'Red', value: 'red', disabled: true },
{ label: 'Blue', value: 'blue', disabled: false },
{ label: 'Green', value: 'green' },
];

const { getByLabelText, getByText } = render(
<MultiSelect labelText={labelText} items={items} />
);

const renderedSelect = getByLabelText(labelText, { selector: 'div' });
fireEvent.click(renderedSelect);

expect(getByText('Red')).toHaveAttribute('aria-disabled', 'true');
expect(getByText('Blue')).toHaveAttribute('aria-disabled', 'false');
expect(getByText('Green')).toHaveAttribute('aria-disabled', 'false');
});

describe('events', () => {
it('onBlur', () => {
const onBlur = jest.fn();
Expand Down
77 changes: 70 additions & 7 deletions packages/react-magma-dom/src/components/Select/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { ThemeContext } from '../../theme/ThemeContext';
import { I18nContext } from '../../i18n';
import { ButtonSize, ButtonVariant } from '../Button';
import { defaultComponents } from './components';
import { useForkedRef } from '../../utils';
import { isItemDisabled } from './utils';

export function MultiSelect<T>(props: MultiSelectProps<T>) {
const {
Expand Down Expand Up @@ -45,12 +47,47 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
setFloating,
setReference,
isClearable,
initialHighlightedIndex,
} = props;

function checkSelectedItemValidity(itemToCheck: T) {
return (
items.findIndex(i => itemToString(i) === itemToString(itemToCheck)) !== -1
const itemIndex = items.findIndex(
i => itemToString(i) === itemToString(itemToCheck)
);

return !isItemDisabled(itemToCheck) && itemIndex !== -1 && !isItemDisabled(items[itemIndex]);
}

function getFilteredItemIndex(item: T, filteredItems: T[]) {
const index = filteredItems.findIndex(
filteredItem => itemToString(filteredItem) === itemToString(item)
);

if (isItemDisabled(filteredItems[index])) {
return -1;
}
return index;
}

function handleOnIsOpenChange(changes) {
const { isOpen: changedIsOpen, selectedItem: changedSelectedItem } =
changes;

if (changedIsOpen && changedSelectedItem) {
if (isItemDisabled(changedSelectedItem)) {
setHighlightedIndex(-1);
} else {
setHighlightedIndex(
items.findIndex(
i => itemToString(i) === itemToString(changedSelectedItem)
)
);
}
}

onIsOpenChange &&
typeof onIsOpenChange === 'function' &&
onIsOpenChange(changes);
}

const {
Expand All @@ -71,6 +108,11 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
...(props.selectedItems && {
selectedItems: props.selectedItems.filter(checkSelectedItemValidity),
}),
...(props.defaultSelectedItems && {
defaultSelectedItems: props.defaultSelectedItems.filter(
checkSelectedItemValidity
),
}),
});

function getFilteredItems(unfilteredItems) {
Expand All @@ -85,6 +127,7 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
const {
stateReducer: passedInStateReducer,
onStateChange,
onIsOpenChange,
...selectProps
} = props;

Expand All @@ -96,11 +139,26 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
...changes,
selectedItem: state.selectedItem,
};
case useSelect.stateChangeTypes.ItemClick:
case useSelect.stateChangeTypes.MenuKeyDownEnter:
if (isItemDisabled(changes.selectedItem)) {
return {
...changes,
selectedItem: state.selectedItem,
};
}
return changes;
default:
return changes;
}
}

const filteredItems = getFilteredItems(items);
const initialIndex = getFilteredItemIndex(
items[initialHighlightedIndex],
filteredItems
);

const {
isOpen,
getToggleButtonProps,
Expand All @@ -110,11 +168,14 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
getItemProps,
selectItem,
openMenu,
setHighlightedIndex,
} = useSelect({
...selectProps,
items: getFilteredItems(items),
items: filteredItems,
onSelectedItemChange: defaultOnSelectedItemChange,
stateReducer,
initialHighlightedIndex: initialIndex,
onIsOpenChange: handleOnIsOpenChange,
});

function defaultOnSelectedItemChange(changes) {
Expand All @@ -137,6 +198,9 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
const theme = React.useContext(ThemeContext);
const i18n = React.useContext(I18nContext);

const toggleButtonRef = React.useRef<HTMLButtonElement>();
const forkedtoggleButtonRef = useForkedRef(innerRef || null, toggleButtonRef);

const toggleButtonProps = getToggleButtonProps({
...getDropdownProps({
onBlur,
Expand All @@ -161,7 +225,7 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
onKeyUp: (event: any) => onKeyUp?.(event),
onFocus,
preventKeyAction: isOpen,
...(innerRef && { ref: innerRef }),
...(forkedtoggleButtonRef && { ref: forkedtoggleButtonRef }),
}),
disabled: disabled,
});
Expand All @@ -183,8 +247,6 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
return allItems.join(', ');
}

const toggleButtonRef = React.useRef<HTMLButtonElement>();

const clearIndicatori18n =
selectedItems.length > 1
? i18n.select.multi.clearIndicatorAriaLabel
Expand Down Expand Up @@ -298,9 +360,10 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
isInverse={isInverse}
items={getFilteredItems(items)}
itemToString={itemToString}
maxHeight={itemListMaxHeight || theme.select.menu.maxHeight}
maxHeight={itemListMaxHeight ?? theme.select.menu.maxHeight}
menuStyle={menuStyle}
setFloating={setFloating}
setHighlightedIndex={setHighlightedIndex}
/>
</SelectContainer>
);
Expand Down
Loading

2 comments on commit 79435c9

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

Please sign in to comment.