Skip to content

Commit

Permalink
feat(Select): Support disabling individual items
Browse files Browse the repository at this point in the history
  • Loading branch information
moathabuhamad-cengage committed Nov 5, 2024
1 parent 8c86d1f commit bcc3d6d
Show file tree
Hide file tree
Showing 12 changed files with 418 additions and 69 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
34 changes: 24 additions & 10 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 @@ -98,7 +103,7 @@ export function ItemsList<T>(props: ItemsListProps<T>) {
}

return (
<div ref={setFloating} style={{...floatingElementStyles, zIndex: '2'}}>
<div ref={setFloating} style={{ ...floatingElementStyles, zIndex: '2' }}>
<StyledCard
hasDropShadow
isInverse={isInverse}
Expand All @@ -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
20 changes: 20 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,26 @@ 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('Red')).toHaveStyleRule('cursor', 'not-allowed');
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: 71 additions & 6 deletions packages/react-magma-dom/src/components/Select/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as React from 'react';
import { instanceOfDefaultItemObject, MultiSelectProps } from '.';
import {
instanceOfDefaultItemObject,
MultiSelectProps,
} from '.';
import { useMultipleSelection, useSelect } from 'downshift';
import { CloseIcon } from 'react-magma-icons';
import { ItemsList } from './ItemsList';
Expand All @@ -11,6 +14,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,14 +50,48 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
setFloating,
setReference,
isClearable,
initialHighlightedIndex,
} = props;

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

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 {
getSelectedItemProps,
getDropdownProps,
Expand All @@ -71,6 +110,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 +129,7 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
const {
stateReducer: passedInStateReducer,
onStateChange,
onIsOpenChange,
...selectProps
} = props;

Expand All @@ -96,11 +141,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 +170,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 +200,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 +227,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 +249,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 +362,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

0 comments on commit bcc3d6d

Please sign in to comment.