diff --git a/src/components/DropList/DropList.Combobox.jsx b/src/components/DropList/DropList.Combobox.jsx index 2b6a1d475..81c92a535 100644 --- a/src/components/DropList/DropList.Combobox.jsx +++ b/src/components/DropList/DropList.Combobox.jsx @@ -84,6 +84,7 @@ function Combobox({ return stateReducerCommon({ changes, closeOnSelection, + items, selectedItems, state, type: `${VARIANTS.COMBOBOX}.${type}`, @@ -113,7 +114,17 @@ function Combobox({ key: generateListItemKey(item, index), withMultipleSelection, renderCustomListItem, - ...getItemProps({ item, index }), + isDisabled: item.isDisabled, + ...getItemProps({ + item, + index, + onClick: event => { + if (item.isDisabled) { + event.nativeEvent.preventDownshiftDefault = true + return + } + }, + }), } return diff --git a/src/components/DropList/DropList.ListItem.jsx b/src/components/DropList/DropList.ListItem.jsx index c8facd714..fefaba47a 100644 --- a/src/components/DropList/DropList.ListItem.jsx +++ b/src/components/DropList/DropList.ListItem.jsx @@ -22,6 +22,7 @@ const ListItem = forwardRef( withMultipleSelection, isSelected, renderCustomListItem, + isDisabled, ...itemProps }, ref @@ -49,6 +50,7 @@ const ListItem = forwardRef( return classNames( 'DropListItem', isSelected && 'is-selected', + isDisabled && 'is-disabled', highlightedIndex === index && 'is-highlighted', withMultipleSelection && 'with-multiple-selection', isString(extraClassNames) && extraClassNames @@ -67,6 +69,7 @@ const ListItem = forwardRef( isSelected, isHighlighted: highlightedIndex === index, withMultipleSelection, + isDisabled, })} ) diff --git a/src/components/DropList/DropList.Select.jsx b/src/components/DropList/DropList.Select.jsx index c995f48c2..ea73e661c 100644 --- a/src/components/DropList/DropList.Select.jsx +++ b/src/components/DropList/DropList.Select.jsx @@ -79,6 +79,7 @@ function Select({ return stateReducerCommon({ changes, closeOnSelection, + items, selectedItems, state, type: `${VARIANTS.SELECT}.${type}`, @@ -100,7 +101,17 @@ function Select({ key: generateListItemKey(item, index), withMultipleSelection, renderCustomListItem, - ...getItemProps({ item, index }), + isDisabled: item.isDisabled, + ...getItemProps({ + item, + index, + onClick: event => { + if (item.isDisabled) { + event.nativeEvent.preventDownshiftDefault = true + return + } + }, + }), } return diff --git a/src/components/DropList/DropList.css.js b/src/components/DropList/DropList.css.js index 5e5f818f0..9c8759382 100644 --- a/src/components/DropList/DropList.css.js +++ b/src/components/DropList/DropList.css.js @@ -113,6 +113,13 @@ export const ListItemUI = styled('li')` background-color: ${getColor('blue.100')}; } } + + &.is-disabled, + &.with-multiple-selection.is-disabled { + color: ${getColor('charcoal.200')}; + background-color: transparent; + cursor: default; + } ` export const EmptyListUI = styled('div')` diff --git a/src/components/DropList/DropList.downshift.common.js b/src/components/DropList/DropList.downshift.common.js index c4f147c16..7a6e4a698 100644 --- a/src/components/DropList/DropList.downshift.common.js +++ b/src/components/DropList/DropList.downshift.common.js @@ -1,6 +1,10 @@ import { useSelect, useCombobox } from 'downshift' import { isObject } from '../../utilities/is' -import { findItemInArray, getItemContentKeyName } from './DropList.utils' +import { + findItemInArray, + getEnabledItemIndex, + getItemContentKeyName, +} from './DropList.utils' import { OPEN_ACTION_ORIGIN, VARIANTS } from './DropList.constants' const { SELECT, COMBOBOX } = VARIANTS @@ -8,6 +12,7 @@ const { SELECT, COMBOBOX } = VARIANTS export function stateReducerCommon({ changes, closeOnSelection, + items, selectedItems, state, type, @@ -67,6 +72,34 @@ export function stateReducerCommon({ return { ...changes, inputValue: '' } } + case `${COMBOBOX}.${useCombobox.stateChangeTypes.InputKeyDownArrowUp}`: + case `${SELECT}.${useSelect.stateChangeTypes.MenuKeyDownArrowUp}`: { + const { highlightedIndex } = changes + + return { + ...changes, + highlightedIndex: getEnabledItemIndex({ + highlightedIndex, + items, + arrowKey: 'UP', + }), + } + } + + case `${COMBOBOX}.${useCombobox.stateChangeTypes.InputKeyDownArrowDown}`: + case `${SELECT}.${useSelect.stateChangeTypes.MenuKeyDownArrowDown}`: { + const { highlightedIndex } = changes + + return { + ...changes, + highlightedIndex: getEnabledItemIndex({ + highlightedIndex, + items, + arrowKey: 'DOWN', + }), + } + } + default: return changes } diff --git a/src/components/DropList/DropList.jsx b/src/components/DropList/DropList.jsx index d4f4e2937..ee329cbeb 100644 --- a/src/components/DropList/DropList.jsx +++ b/src/components/DropList/DropList.jsx @@ -153,6 +153,9 @@ function DropListManager({ } function handleSelectedItemChange({ selectedItem }) { + if (selectedItem.isDisabled) { + return + } if (withMultipleSelection) { if (selectedItem) { const { remove } = selectedItem @@ -251,6 +254,7 @@ const itemShape = PropTypes.shape({ label: requiredItemPropsCheck, value: requiredItemPropsCheck, id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + isDisabled: PropTypes.bool, }) const dividerShape = PropTypes.shape({ type: PropTypes.oneOf(['divider', 'Divider']).isRequired, diff --git a/src/components/DropList/DropList.stories.mdx b/src/components/DropList/DropList.stories.mdx index 284b3ca7b..2888adef1 100644 --- a/src/components/DropList/DropList.stories.mdx +++ b/src/components/DropList/DropList.stories.mdx @@ -15,6 +15,7 @@ import { plainItems, regularItems, simpleGroupedItems, + disabledItems, } from '../../utilities/specs/dropdown.specs' (
@@ -426,6 +429,77 @@ function renderCustomListItem({ +- Disabled List Items: Sometimes you might need to render the list items that would be disabled, for that you can pass `isDisbaled` flag with an item. + + + +
+ } + onSelect={selection => { + console.log('selection', selection) + }} + /> +
+
+
+ + + +
+ } + onSelect={selection => { + console.log('selection', selection) + }} + /> +
+
+
+ + + +
+ { + console.log('selection', selection) + }} + toggler={} + withMultipleSelection + /> +
+
+
+ ### Multiple selection Depending on your use case, you might want to set `closeOnSelection = false` when multiple selection is enabled. diff --git a/src/components/DropList/DropList.test.js b/src/components/DropList/DropList.test.js index 386d41b2f..54580f30e 100644 --- a/src/components/DropList/DropList.test.js +++ b/src/components/DropList/DropList.test.js @@ -890,4 +890,157 @@ describe('Selection', () => { ).toBeTruthy() }) }) + + describe('disabled items', () => { + const items = dedupedRegularItems.map((item, index) => ({ + ...item, + isDisabled: index % 2 === 0, + })) + + test('should set an item as disabled and do not allow to select it (select)', async () => { + const onSelect = jest.fn() + const { getByText } = render( + } + isMenuOpen + /> + ) + + expect(getByText(regularItems[2].label).parentElement).toHaveClass( + 'is-disabled' + ) + expect(getByText(regularItems[1].label).parentElement).not.toHaveClass( + 'is-disabled' + ) + + user.click(getByText(regularItems[2].label)) + + await waitFor(() => { + expect(onSelect).not.toHaveBeenCalled() + }) + }) + + test('should set an item as disabled and do not allow to select it (combobox)', async () => { + const onSelect = jest.fn() + const { getByText } = render( + } + isMenuOpen + variant="combobox" + /> + ) + + expect(getByText(regularItems[2].label).parentElement).toHaveClass( + 'is-disabled' + ) + + user.click(getByText(regularItems[2].label)) + + await waitFor(() => { + expect(onSelect).not.toHaveBeenCalled() + }) + }) + + test('should set an item as disabled and do not allow to select it with custom list', async () => { + const onSelect = jest.fn() + const { getByText } = render( + } + isMenuOpen + renderCustomListItem={({ item, isDisabled }) => ( +
{item.label}
+ )} + /> + ) + + const exampleItem = getByText(regularItems[2].label) + + expect(exampleItem.parentElement).toHaveClass('is-disabled') + + user.click(exampleItem) + + await waitFor(() => { + expect(onSelect).not.toHaveBeenCalled() + expect(exampleItem).toHaveClass('is-disabled') + }) + }) + + test('should skip disabled items when navigating down', async () => { + const { getByPlaceholderText, getByText } = render( + } + isMenuOpen + variant="combobox" + /> + ) + + user.type(getByPlaceholderText('Search'), '{arrowdown}') + + await waitFor(() => { + expect(getByText(regularItems[0].label).parentElement).not.toHaveClass( + 'is-highlighted' + ) + expect(getByText(regularItems[1].label).parentElement).toHaveClass( + 'is-highlighted' + ) + }) + + user.type(getByPlaceholderText('Search'), '{arrowdown}') + + await waitFor(() => { + expect(getByText(regularItems[1].label).parentElement).not.toHaveClass( + 'is-highlighted' + ) + expect(getByText(regularItems[2].label).parentElement).not.toHaveClass( + 'is-highlighted' + ) + expect(getByText(regularItems[3].label).parentElement).toHaveClass( + 'is-highlighted' + ) + }) + }) + + test('should skip disabled items when navigating up', async () => { + const { getByPlaceholderText, getByText } = render( + } + isMenuOpen + variant="combobox" + /> + ) + + user.type(getByPlaceholderText('Search'), '{arrowup}') + + await waitFor(() => { + expect(getByText(regularItems[0].label).parentElement).not.toHaveClass( + 'is-highlighted' + ) + expect( + getByText(regularItems[regularItems.length - 2].label).parentElement + ).toHaveClass('is-highlighted') + }) + + user.type(getByPlaceholderText('Search'), '{arrowup}') + + await waitFor(() => { + expect( + getByText(regularItems[regularItems.length - 2].label).parentElement + ).not.toHaveClass('is-highlighted') + expect( + getByText(regularItems[regularItems.length - 3].label).parentElement + ).not.toHaveClass('is-highlighted') + expect( + getByText(regularItems[regularItems.length - 4].label).parentElement + ).toHaveClass('is-highlighted') + }) + }) + }) }) diff --git a/src/components/DropList/DropList.utils.js b/src/components/DropList/DropList.utils.js index 79bab2f37..c518a6efc 100644 --- a/src/components/DropList/DropList.utils.js +++ b/src/components/DropList/DropList.utils.js @@ -188,3 +188,33 @@ export function requiredItemPropsCheck(props, propName, componentName) { ) } } + +export function getEnabledItemIndex({ highlightedIndex, items, arrowKey }) { + let enabledItemIndex = 0 + + if (arrowKey === 'UP') { + for (let index = items.length - 1; index >= 0; index--) { + if ( + (highlightedIndex === 0 && !items[index].isDisabled) || + (index <= highlightedIndex && !items[index].isDisabled) + ) { + enabledItemIndex = index + break + } + } + } + + if (arrowKey === 'DOWN') { + for (let index = 0; index < items.length; index++) { + if ( + (highlightedIndex === items.length - 1 && !items[index].isDisabled) || + (index >= highlightedIndex && !items[index].isDisabled) + ) { + enabledItemIndex = index + break + } + } + } + + return enabledItemIndex +} diff --git a/src/utilities/specs/dropdown.specs.js b/src/utilities/specs/dropdown.specs.js index b9a9db241..13e4686e7 100644 --- a/src/utilities/specs/dropdown.specs.js +++ b/src/utilities/specs/dropdown.specs.js @@ -74,6 +74,14 @@ export const groupAndDividerItems = [ export const regularItems = ItemSpec.generate(15) +export const disabledItems = ItemSpec.generate(10).map((item, index) => { + if (index % 2 === 0) { + item.isDisabled = true + } + + return item +}) + export const plainItems = [ 'hello', 'hola',