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 21, 2024
1 parent 8c86d1f commit 42a91f7
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 49 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
28 changes: 18 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 Down Expand Up @@ -98,7 +101,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 +120,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,6 +140,7 @@ export function ItemsList<T>(props: ItemsListProps<T>) {
itemString,
key,
theme,
isDisabled:isItemDisabled,
...otherDownshiftItemProps,
};

Expand Down
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
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
13 changes: 10 additions & 3 deletions packages/react-magma-dom/src/components/Select/components.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react';
import { ArrowDropDownIcon, IconProps } from 'react-magma-icons';
import { ThemeInterface } from '../../theme/magma';
import { IconButton, IconButtonProps } from '../IconButton';
import { Spinner, SpinnerProps } from '../Spinner';
import { IconProps, ArrowDropDownIcon } from 'react-magma-icons';
import { StyledItem } from './shared';
import { ThemeInterface } from '../../theme/magma';

export type ItemRenderOptions<T> = {
key: string;
Expand Down Expand Up @@ -46,10 +46,17 @@ export function DefaultItem<T>({
itemRef,
itemString,
isInverse,
isDisabled,
...props
}: ItemRenderOptions<T>) {
return (
<StyledItem {...props} isInverse={isInverse} ref={itemRef}>
<StyledItem
{...props}
isInverse={isInverse}
ref={itemRef}
isDisabled={isDisabled}
aria-disabled={isDisabled}
>
{itemString}
</StyledItem>
);
Expand Down
30 changes: 18 additions & 12 deletions packages/react-magma-dom/src/components/Select/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import * as React from 'react';
import {
useMultipleSelection,
UseMultipleSelectionProps,
useSelect,
UseSelectProps,
} from 'downshift';
import {
AlignedPlacement,
autoUpdate,
flip,
useFloating,
} from '@floating-ui/react-dom';
import { ReferenceType } from '@floating-ui/react-dom/dist/floating-ui.react-dom';
import { Select as InternalSelect } from './Select';
import { MultiSelect } from './MultiSelect';
import { SelectComponents } from './components';
import {
useMultipleSelection,
UseMultipleSelectionProps,
useSelect,
UseSelectProps,
} from 'downshift';
import * as React from 'react';
import { useIsInverse } from '../../inverse';
import { Omit, useGenerateId, XOR } from '../../utils';
import { LabelPosition } from '../Label';
import { useIsInverse } from '../../inverse';
import { MultiSelect } from './MultiSelect';
import { Select as InternalSelect } from './Select';
import { SelectComponents } from './components';

export type SelectOptions =
| string
| { value: string; label: string; [key: string]: any }
| { value: string; label: string; [key: string]: any; disabled?: boolean }
| any;

export interface InternalSelectProps<T> {
Expand Down Expand Up @@ -214,6 +214,12 @@ export function instanceOfToBeCreatedItemObject(object: any): object is {
);
}

export function instanceOfItemWithOptionalDisabled(
object: any
): object is { label: string; value: string; disabled?: boolean } {
return typeof object !== 'string' && object && 'disabled' in object;
}

export type XORSelectProps<T> = XOR<SelectProps<T>, MultiSelectProps<T>>;

export const SelectStateChangeTypes = useSelect.stateChangeTypes;
Expand Down
37 changes: 26 additions & 11 deletions packages/react-magma-dom/src/components/Select/shared.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { inputBaseStyles } from '../InputBase';
import { Card } from '../Card';
import { transparentize } from 'polished';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { transparentize } from 'polished';
import { ThemeInterface } from '../../theme/magma';
import { css } from '@emotion/react';
import { Card } from '../Card';
import { inputBaseStyles } from '../InputBase';

function buildListHoverColor(props) {
if (props.isFocused) {
Expand All @@ -25,6 +25,19 @@ function buildListFocusColor(props) {
return 'transparent';
}

function buildListItemColor(props) {
if (props.isDisabled) {
if (props.isInverse) {
return transparentize(0.6, props.theme.colors.neutral100);
}
return transparentize(0.4, props.theme.colors.neutral500);
}
if (props.isInverse) {
return props.theme.colors.neutral100;
}
return props.theme.colors.neutral700;
}

export const SelectContainer = styled.div`
position: relative;
`;
Expand Down Expand Up @@ -55,9 +68,12 @@ export const SelectText = styled.span<{
: props.theme.colors.neutral500;
}
}};
${props => props.isDisabled && props.isShowPlaceholder && css`
opacity: ${props.isInverse ? 0.4 : 0.6}
`}
${props =>
props.isDisabled &&
props.isShowPlaceholder &&
css`
opacity: ${props.isInverse ? 0.4 : 0.6};
`}
`;

export const StyledCard = styled(Card)<{
Expand Down Expand Up @@ -91,19 +107,18 @@ export const StyledList = styled('ul')<{ isOpen?: boolean; maxHeight: string }>`
export const StyledItem = styled('li')<{
isInverse?: boolean;
isFocused?: boolean;
isDisabled?: boolean;
}>`
align-self: center;
background: ${props => buildListHoverColor(props)};
border: 2px solid;
border-color: ${props => buildListFocusColor(props)};
cursor: default;
color: ${props =>
props.isInverse
? props.theme.colors.neutral100
: props.theme.colors.neutral700};
color: ${props => buildListItemColor(props)};
line-height: 24px;
margin: 0;
padding: 8px 16px;
cursor: ${props => (props.isDisabled ? 'not-allowed' : 'pointer')};
&:hover {
background: ${props => buildListHoverColor(props)};
border-color: transparent;
Expand Down
23 changes: 23 additions & 0 deletions website/react-magma-docs/src/pages/api/select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,29 @@ export function Example() {
}
```

## Disabled Items

You can disable specific items in the `Select` component by adding a `disabled: true` property to the item object. For example:

```tsx
import React from 'react';
import { Select } from 'react-magma-dom';

export function Example() {
return (
<Select
id="disabledItemsSelectId"
labelText="Select with Disabled Items"
items={[
{ label: 'Red', value: 'red' },
{ label: 'Blue', value: 'blue', disabled: true },
{ label: 'Green', value: 'green' },
]}
/>
);
}
```

## Clearable

The optional `isClearable` prop allows the user to clear the field once a selection has been made.
Expand Down

0 comments on commit 42a91f7

Please sign in to comment.