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 Oct 28, 2024
1 parent 8c86d1f commit 48b4fa8
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 65 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
55 changes: 53 additions & 2 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 Down Expand Up @@ -45,14 +49,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 +114,9 @@ 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 +131,7 @@ export function MultiSelect<T>(props: MultiSelectProps<T>) {
const {
stateReducer: passedInStateReducer,
onStateChange,
onIsOpenChange,
...selectProps
} = props;

Expand All @@ -110,11 +157,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 Down Expand Up @@ -298,9 +348,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
60 changes: 52 additions & 8 deletions packages/react-magma-dom/src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Meta, Story } from '@storybook/react/types-6-0';
import React from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import { Select, SelectOptions, SelectProps, MultiSelectProps } from './';
import { LabelPosition } from '../Label';
import { HelpIcon } from 'react-magma-icons';
import { ButtonSize, ButtonType, ButtonVariant } from '../Button';
import { Card } from '../Card';
import { CardBody } from '../Card/CardBody';
import { Tooltip } from '../Tooltip';
import { IconButton } from '../IconButton';
import { HelpIcon } from 'react-magma-icons';
import { ButtonSize, ButtonType, ButtonVariant } from '../Button';
import { LabelPosition } from '../Label';
import { Tooltip } from '../Tooltip';
import { MultiSelectProps, Select, SelectOptions, SelectProps } from './';

const Template: Story<SelectProps<SelectOptions>> = args => (
<Select {...args} />
Expand Down Expand Up @@ -40,8 +40,8 @@ export const Default = Template.bind({});
Default.args = {
labelText: 'Example',
items: [
{ label: 'Red', value: 'red' },
{ label: 'Blue', value: 'blue' },
{ label: 'Red', value: 'red' , disabled: false},
{ label: 'Blue', value: 'blue' , disabled: true},
{ label: 'Green', value: 'green' },
{ label: 'Purple mountain majesty', value: 'purple' },
],
Expand Down Expand Up @@ -153,3 +153,47 @@ LeftAlignedLabelWithContainer.decorators = [
</Card>
),
];

export const WithDisabledItems = Template.bind({});
WithDisabledItems.args = {
...Default.args,
items: [
{ label: 'First-Item-Disabled', value: 'First-Item-Disabled', disabled: true },
{ label: 'Red', value: 'red' },
{ label: 'Blue-Disabled', value: 'blue', disabled: true },
{ label: 'Green', value: 'green' },
{ label: 'Purple mountain majesty', value: 'purple' },
{ label: 'Orange-Disabled', value: 'Orange-Disabled', disabled: true },
{ label: 'Yellow-Disabled', value: 'Yellow-Disabled', disabled: true },
],
isClearable: true,
defaultSelectedItem: { label: 'Blue-Disabled', value: 'blue', disabled: true },
initialSelectedItem: { label: 'Orange-Disabled', value: 'disabled', disabled: true },
};

export const MultiWithDisabledItems = (props: MultiSelectProps<SelectOptions>) => (
<Select
isMulti={true}
{...props}
items={[
{ label: 'Red', value: 'red' },
{ label: 'Green', value: 'green' },
{ label: 'Blue-Disabled', value: 'blue', disabled: true },
{ label: 'Purple mountain majesty', value: 'purple' },
{ label: 'Orange', value: 'orange', disabled: false },
{ label: 'Yellow-Disabled', value: 'Yellow-Disabled', disabled: true },
]}
isClearable
initialHighlightedIndex= {2}
defaultSelectedItems={[
{ label: 'Red', value: 'red' },
{ label: 'Orange', value: 'orange', disabled: false },
{ label: 'Yellow-Disabled', value: 'Yellow-Disabled', disabled: true },
]}
initialSelectedItems={[
{ label: 'Red', value: 'red' },
{ label: 'Orange', value: 'orange', disabled: false },
{ label: 'Yellow-Disabled', value: 'Yellow-Disabled', disabled: true },
]}
/>
);
29 changes: 24 additions & 5 deletions packages/react-magma-dom/src/components/Select/Select.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import { act, fireEvent, render } from '@testing-library/react';
import React from 'react';
import { HelpIcon } from 'react-magma-icons';
import { Select } from '.';
import { defaultI18n } from '../../i18n/default';
import { magma } from '../../theme/magma';
import { Modal } from '../Modal';
import { Tooltip } from '../Tooltip';
import { IconButton } from '../IconButton';
import { HelpIcon } from 'react-magma-icons';
import { ButtonSize, ButtonType, ButtonVariant } from '../Button';
import { IconButton } from '../IconButton';
import { LabelPosition } from '../Label';
import { Modal } from '../Modal';
import { Tooltip } from '../Tooltip';

describe('Select', () => {
const labelText = 'Label';
Expand Down Expand Up @@ -625,6 +625,25 @@ describe('Select', () => {
'flex'
);
});

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(
<Select 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', () => {
Expand Down
Loading

0 comments on commit 48b4fa8

Please sign in to comment.