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 4, 2024
1 parent 8c86d1f commit 7244c63
Show file tree
Hide file tree
Showing 11 changed files with 412 additions and 68 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
36 changes: 26 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 {
instanceOfItemWithOptionalDisabled,
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';

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,14 @@ export function ItemsList<T>(props: ItemsListProps<T>) {
const itemString = instanceOfToBeCreatedItemObject(item)
? item.label
: itemToString(item);
const isItemDisabled = instanceOfItemWithOptionalDisabled(item)
? item.disabled
: false;

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

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

if (isItemDisabled) {
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
74 changes: 69 additions & 5 deletions packages/react-magma-dom/src/components/Select/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as React from 'react';
import { instanceOfDefaultItemObject, MultiSelectProps } from '.';
import {
instanceOfDefaultItemObject,
instanceOfItemWithOptionalDisabled,
MultiSelectProps,
} from '.';
import { useMultipleSelection, useSelect } from 'downshift';
import { CloseIcon } from 'react-magma-icons';
import { ItemsList } from './ItemsList';
Expand All @@ -11,6 +15,7 @@ import { ThemeContext } from '../../theme/ThemeContext';
import { I18nContext } from '../../i18n';
import { ButtonSize, ButtonVariant } from '../Button';
import { defaultComponents } from './components';
import { useForkedRef } from '../../utils';

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

function isItemDisabled(item) {
return instanceOfItemWithOptionalDisabled(item) && item.disabled;
}

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

function isDisabledItemIndex(index) {
return isItemDisabled(items[index]);
}

function getValidItemIndex(indexToCheck: number) {
if (isDisabledItemIndex(indexToCheck)) {
return -1;
} else {
return indexToCheck;
}
}

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 +115,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 +134,7 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
const {
stateReducer: passedInStateReducer,
onStateChange,
onIsOpenChange,
...selectProps
} = props;

Expand All @@ -96,6 +146,15 @@ 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;
}
Expand All @@ -110,11 +169,14 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
getItemProps,
selectItem,
openMenu,
setHighlightedIndex,
} = useSelect({
...selectProps,
items: getFilteredItems(items),
onSelectedItemChange: defaultOnSelectedItemChange,
stateReducer,
initialHighlightedIndex: getValidItemIndex(initialHighlightedIndex),
onIsOpenChange: handleOnIsOpenChange,
});

function defaultOnSelectedItemChange(changes) {
Expand All @@ -137,6 +199,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 +226,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 +248,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 +361,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 7244c63

Please sign in to comment.