diff --git a/pages/dropdown/list-with-sticky-item.page.tsx b/pages/dropdown/list-with-sticky-item.page.tsx index 1b295f588e1..a4c220231b9 100644 --- a/pages/dropdown/list-with-sticky-item.page.tsx +++ b/pages/dropdown/list-with-sticky-item.page.tsx @@ -5,8 +5,8 @@ import React, { useContext, useRef, useState } from 'react'; import Box from '~components/box'; import Button from '~components/button'; import Dropdown from '~components/dropdown'; +import PlainList from '~components/internal/components/listbox/parts/plain-list'; import { DropdownOption } from '~components/internal/components/option/interfaces'; -import PlainList from '~components/select/parts/plain-list'; import VirtualList from '~components/select/parts/virtual-list'; import SpaceBetween from '~components/space-between'; @@ -95,7 +95,7 @@ export default function MultiselectPage() { filteringValue={''} firstOptionSticky={true} highlightType={{ type: 'mouse', moveFocus: false }} - useInteractiveGroups={true} + selectionType="multi" /> } /> diff --git a/pages/select/item.permutations.page.tsx b/pages/select/item.permutations.page.tsx index 586d836ac78..0945548ca8d 100644 --- a/pages/select/item.permutations.page.tsx +++ b/pages/select/item.permutations.page.tsx @@ -2,9 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; +import Item, { ItemProps } from '~components/internal/components/listbox/parts/item'; import { DropdownOption } from '~components/internal/components/option/interfaces'; -import Item from '~components/select/parts/item'; -import { ItemProps } from '~components/select/parts/item'; import createPermutations from '../utils/permutations'; import PermutationsView from '../utils/permutations-view'; @@ -100,7 +99,7 @@ const permutations = createPermutations>([ highlighted: [false, true], selected: [false, true], highlightType: ['keyboard'], - hasCheckbox: [true], + selectionType: ['multi'], }, { option: [options.complexOption], @@ -126,21 +125,21 @@ const permutations = createPermutations>([ }, { option: [options.complexDisabledOptionWithCustomSvg], - hasCheckbox: [true], + selectionType: ['multi'], }, { option: [options.simpleOption, options.complexOption], highlighted: [false, true], highlightType: ['keyboard'], selected: [true], - hasCheckbox: [false], + selectionType: ['single'], }, { option: [options.complexOptionGroup], highlighted: [false], highlightType: ['keyboard'], selected: [true], - hasCheckbox: [false, true], + selectionType: ['single', 'multi'], }, ]); diff --git a/src/internal/components/listbox/__tests__/get-item-props.test.ts b/src/internal/components/listbox/__tests__/get-item-props.test.ts index a8883b4d2b2..fb7f3a041a0 100644 --- a/src/internal/components/listbox/__tests__/get-item-props.test.ts +++ b/src/internal/components/listbox/__tests__/get-item-props.test.ts @@ -56,7 +56,6 @@ const defaultItemProps = { index: 1, getOptionProps: getOptionProps(false, false), filteringValue: 'child', - checkboxes: false, }; describe('getItemProps', () => { @@ -68,29 +67,6 @@ describe('getItemProps', () => { 'data-test-index': 1, 'data-mouse-target': 1, filteringValue: 'child', - hasCheckbox: false, - highlighted: false, - key: 1, - option: { - option: { label: 'Child 1' }, - type: 'child', - }, - selected: false, - }); - }); - - test('should return props for the first child in the first group with checkboxes and keyboard', () => { - const props = getItemProps({ - ...defaultItemProps, - checkboxes: true, - }); - expect(props).toEqual({ - 'data-child-index': 1, - 'data-group-index': 1, - 'data-test-index': 1, - 'data-mouse-target': 1, - filteringValue: 'child', - hasCheckbox: true, highlighted: false, key: 1, option: { @@ -112,7 +88,6 @@ describe('getItemProps', () => { 'data-test-index': 1, 'data-mouse-target': 1, filteringValue: 'child', - hasCheckbox: false, highlighted: true, key: 1, option: { @@ -136,7 +111,6 @@ describe('getItemProps', () => { 'data-test-index': 1, 'data-mouse-target': 1, filteringValue: 'child', - hasCheckbox: false, highlighted: false, key: 1, option: { @@ -159,7 +133,6 @@ describe('getItemProps', () => { 'data-mouse-target': 3, 'data-test-index': 3, filteringValue: 'child', - hasCheckbox: false, highlighted: false, key: 3, option: { diff --git a/src/internal/components/listbox/__tests__/navigation.test.ts b/src/internal/components/listbox/__tests__/navigation.test.ts new file mode 100644 index 00000000000..de91150d8a1 --- /dev/null +++ b/src/internal/components/listbox/__tests__/navigation.test.ts @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { OptionDefinition, OptionGroup } from '../../../../types/option'; +import { DropdownOption } from '../../option/interfaces'; +import { flattenOptions } from '../../option/utils/flatten-options'; +import { getListboxNavigation } from '../utils/navigation'; + +const flatten = (options: ReadonlyArray) => flattenOptions(options).flatOptions; + +const flat = flatten([{ value: '1' }, { value: '2' }, { value: '3' }]); +// [header A, a1, a2, header B, b1] +const grouped = flatten([ + { label: 'A', options: [{ value: 'a1' }, { value: 'a2' }] }, + { label: 'B', options: [{ value: 'b1' }] }, +]); +// [1, header Empty] +const endsWithEmptyGroup = flatten([{ value: '1' }, { label: 'Empty', options: [] }]); + +function setup(options: ReadonlyArray, highlightedIndex: number, interactiveGroups = false) { + const moveHighlight = jest.fn(); + const goHome = jest.fn(); + const goEnd = jest.fn(); + const navigation = getListboxNavigation({ + options, + highlightedIndex, + highlightedOption: options[highlightedIndex], + interactiveGroups, + moveHighlight, + goHome, + goEnd, + }); + return { ...navigation, moveHighlight, goHome, goEnd }; +} + +describe('getListboxNavigation', () => { + test('moves the highlight by one option', () => { + const { goUp, goDown, moveHighlight } = setup(flat, 1); + goDown(); + expect(moveHighlight).toHaveBeenLastCalledWith(1); + goUp(); + expect(moveHighlight).toHaveBeenLastCalledWith(-1); + }); + + test('wraps from the first option to the end and from the last option to the start', () => { + const first = setup(flat, 0); + first.goUp(); + expect(first.goEnd).toHaveBeenCalledTimes(1); + expect(first.moveHighlight).not.toHaveBeenCalled(); + + const last = setup(flat, 2); + last.goDown(); + expect(last.goHome).toHaveBeenCalledTimes(1); + expect(last.moveHighlight).not.toHaveBeenCalled(); + }); + + test('wraps up from the first child of a leading group when groups are not interactive', () => { + const { goUp, goEnd, moveHighlight } = setup(grouped, 1); + goUp(); + expect(goEnd).toHaveBeenCalledTimes(1); + expect(moveHighlight).not.toHaveBeenCalled(); + }); + + test('moves up to the leading group header when groups are interactive', () => { + const { goUp, goEnd, moveHighlight } = setup(grouped, 1, true); + goUp(); + expect(goEnd).not.toHaveBeenCalled(); + expect(moveHighlight).toHaveBeenCalledWith(-1); + }); + + test('wraps down only from the last row, even when it is not highlightable', () => { + const { goDown, goHome, moveHighlight } = setup(endsWithEmptyGroup, 0); + goDown(); + expect(goHome).not.toHaveBeenCalled(); + expect(moveHighlight).toHaveBeenCalledWith(1); + }); + + test('moves down from no highlight', () => { + const { goDown, goHome, moveHighlight } = setup(flat, -1); + goDown(); + expect(goHome).not.toHaveBeenCalled(); + expect(moveHighlight).toHaveBeenCalledWith(1); + }); +}); diff --git a/src/internal/components/listbox/__tests__/option-props.test.ts b/src/internal/components/listbox/__tests__/option-props.test.ts new file mode 100644 index 00000000000..f4aead4f198 --- /dev/null +++ b/src/internal/components/listbox/__tests__/option-props.test.ts @@ -0,0 +1,168 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { OptionDefinition, OptionGroup } from '../../../../types/option'; +import { DropdownOption } from '../../option/interfaces'; +import { flattenOptions } from '../../option/utils/flatten-options'; +import { getOptionId } from '../../options-list/utils/use-ids'; +import { multiSelection } from '../selection/multi-selection'; +import { singleSelection } from '../selection/single-selection'; +import { connectOptionsByValue } from '../utils/connect-options'; +import { + getListboxOptionProps, + getListMouseHandlers, + getSelectableOption, + isAnnouncedSelected, + isHighlightable, +} from '../utils/option-props'; + +const flatten = (options: ReadonlyArray) => flattenOptions(options).flatOptions; +const selectedSetOf = (options: ReadonlyArray, selected: ReadonlyArray) => + new Set(connectOptionsByValue(options, selected)); + +describe('isHighlightable', () => { + test('group headers are highlightable only with interactive groups', () => { + const header: DropdownOption = { type: 'parent', option: { label: 'Group', options: [] } }; + expect(isHighlightable(header, false)).toBe(false); + expect(isHighlightable(header, true)).toBe(true); + expect(isHighlightable({ option: { value: '1' } }, false)).toBe(true); + expect(isHighlightable(undefined, true)).toBe(false); + }); +}); + +describe('getListboxOptionProps', () => { + test('single selection marks the selected option and highlight', () => { + const options = flatten([{ value: '1' }, { value: '2' }]); + const selected = [{ value: '2' }]; + const context = { + options, + selectedSet: selectedSetOf(options, selected), + selection: singleSelection(selected), + highlightedOption: options[0], + interactiveGroups: false, + listId: 'list', + }; + expect(getListboxOptionProps(options[0], 0, context)).toEqual({ + key: 0, + option: options[0], + highlighted: true, + selected: false, + isNextSelected: true, + isPreviousSelected: false, + indeterminate: false, + 'data-mouse-target': 0, + id: getOptionId('list', 0), + }); + expect(getListboxOptionProps(options[1], 1, context)).toMatchObject({ highlighted: false, selected: true }); + }); + + test('multi selection uses group state for headers and group neighbours', () => { + const options = flatten([ + { label: 'A', options: [{ value: 'a1' }, { value: 'a2' }] }, + { label: 'B', options: [{ value: 'b1' }] }, + ]); + const selected = [{ value: 'a1' }, { value: 'b1' }]; + const context = { + options, + selectedSet: selectedSetOf(options, selected), + selection: multiSelection(selected, { options, interactiveGroups: true }), + highlightedOption: undefined, + interactiveGroups: true, + listId: 'list', + }; + // [A, a1, a2, B, b1] + expect(getListboxOptionProps(options[0], 0, context)).toMatchObject({ selected: false, indeterminate: true }); + expect(getListboxOptionProps(options[3], 3, context)).toMatchObject({ selected: true, indeterminate: false }); + // a2's next neighbour is the fully selected group B; its previous neighbour a1 is selected. + expect(getListboxOptionProps(options[2], 2, context)).toMatchObject({ + selected: false, + isNextSelected: true, + isPreviousSelected: true, + }); + }); + + test('select-all gets group state but never counts as a selected neighbour', () => { + const items = flatten([{ value: '1' }]); + const selectAll: DropdownOption = { type: 'select-all', option: { label: 'Select all' } }; + const options = [selectAll, ...items]; + const selected = [{ value: '1' }]; + const context = { + options, + selectedSet: selectedSetOf(items, selected), + selection: multiSelection(selected, { options: items }), + highlightedOption: undefined, + interactiveGroups: true, + listId: 'list', + }; + expect(getListboxOptionProps(selectAll, 0, context)).toMatchObject({ selected: true, isNextSelected: true }); + expect(getListboxOptionProps(options[1], 1, context)).toMatchObject({ selected: true, isPreviousSelected: false }); + }); + + test('non-highlightable group headers get data-mouse-target -1', () => { + const options = flatten([{ label: 'A', options: [{ value: 'a1' }] }]); + const context = { + options, + selectedSet: new Set(), + selection: singleSelection([]), + highlightedOption: undefined, + interactiveGroups: false, + listId: 'list', + }; + expect(getListboxOptionProps(options[0], 0, context)['data-mouse-target']).toBe(-1); + expect(getListboxOptionProps(options[1], 1, context)['data-mouse-target']).toBe(1); + }); +}); + +describe('isAnnouncedSelected', () => { + test('announces selected options and fully selected groups', () => { + const options = flatten([{ label: 'A', options: [{ value: 'a1' }] }, { value: '2' }]); + const selected = [{ value: 'a1' }]; + const selectedSet = selectedSetOf(options, selected); + const selection = multiSelection(selected, { options, interactiveGroups: true }); + expect(isAnnouncedSelected(undefined, selectedSet, selection)).toBe(false); + expect(isAnnouncedSelected(options[0], selectedSet, selection)).toBe(true); + expect(isAnnouncedSelected(options[1], selectedSet, selection)).toBe(true); + expect(isAnnouncedSelected(options[2], selectedSet, selection)).toBe(false); + }); +}); + +describe('getSelectableOption', () => { + const isEnabled = (option: DropdownOption) => !option.option.disabled; + const first: DropdownOption = { option: { value: '1' } }; + const highlighted: DropdownOption = { option: { value: '2' } }; + const disabled: DropdownOption = { option: { value: '3', disabled: true } }; + + test('returns the given option, or the highlighted option when omitted', () => { + expect(getSelectableOption(first, highlighted, isEnabled)).toBe(first); + expect(getSelectableOption(undefined, highlighted, isEnabled)).toBe(highlighted); + }); + + test('returns undefined when there is nothing to choose or it is not interactive', () => { + expect(getSelectableOption(undefined, undefined, isEnabled)).toBeUndefined(); + expect(getSelectableOption(disabled, highlighted, isEnabled)).toBeUndefined(); + expect(getSelectableOption(undefined, disabled, isEnabled)).toBeUndefined(); + }); +}); + +describe('getListMouseHandlers', () => { + const options = flatten([{ value: '1' }, { value: '2' }]); + + test('selects and highlights the option under the pointer', () => { + const selectOption = jest.fn(); + const setHighlightedIndexWithMouse = jest.fn(); + const handlers = getListMouseHandlers({ options, selectOption, setHighlightedIndexWithMouse }); + handlers.onMouseUp(1); + handlers.onMouseMove(0); + expect(selectOption).toHaveBeenCalledWith(options[1]); + expect(setHighlightedIndexWithMouse).toHaveBeenCalledWith(0); + }); + + test('ignores pointer events outside a highlightable option', () => { + const selectOption = jest.fn(); + const setHighlightedIndexWithMouse = jest.fn(); + const handlers = getListMouseHandlers({ options, selectOption, setHighlightedIndexWithMouse }); + handlers.onMouseUp(-1); + handlers.onMouseMove(-1); + expect(selectOption).not.toHaveBeenCalled(); + expect(setHighlightedIndexWithMouse).not.toHaveBeenCalled(); + }); +}); diff --git a/src/select/__tests__/use-native-search.test.ts b/src/internal/components/listbox/__tests__/use-type-ahead.test.ts similarity index 80% rename from src/select/__tests__/use-native-search.test.ts rename to src/internal/components/listbox/__tests__/use-type-ahead.test.ts index 717360cec76..01f9a14d6b4 100644 --- a/src/select/__tests__/use-native-search.test.ts +++ b/src/internal/components/listbox/__tests__/use-type-ahead.test.ts @@ -1,9 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { act, renderHook } from '../../__tests__/render-hook'; -import type { DropdownOption } from '../../internal/components/option/interfaces'; -import { useNativeSearch } from '../utils/use-native-search'; +import { act, renderHook } from '../../../../__tests__/render-hook'; +import type { DropdownOption } from '../../option/interfaces'; +import { useTypeAhead } from '../use-type-ahead'; const options: DropdownOption[] = [ { option: { label: 'option 1', value: 'option1' } }, @@ -19,8 +19,8 @@ const initialProps = { highlightedOption: null, }; -describe('useNativeSearch', () => { - const hook = renderHook(useNativeSearch, { +describe('useTypeAhead', () => { + const hook = renderHook(useTypeAhead, { initialProps: initialProps, }); @@ -47,7 +47,7 @@ describe('useNativeSearch', () => { }); test('should not highlight an option when isEnabled is false', () => { - const hook = renderHook(useNativeSearch, { + const hook = renderHook(useTypeAhead, { initialProps: { ...initialProps, isEnabled: false, diff --git a/src/internal/components/listbox/parts/get-item-props.ts b/src/internal/components/listbox/parts/get-item-props.ts index 03400ddf8ca..2c48211187f 100644 --- a/src/internal/components/listbox/parts/get-item-props.ts +++ b/src/internal/components/listbox/parts/get-item-props.ts @@ -8,16 +8,14 @@ interface ItemProps { index: number; getOptionProps: any; filteringValue: string; - checkboxes: boolean; } -export const getItemProps = ({ option, index, getOptionProps, filteringValue, checkboxes = false }: ItemProps) => { +export const getItemProps = ({ option, index, getOptionProps, filteringValue }: ItemProps) => { const optionProps = getOptionProps(option, index); optionProps.filteringValue = filteringValue; const { inGroupIndex, groupIndex, throughIndex } = getTestOptionIndexes(option) || {}; return { ...optionProps, - hasCheckbox: checkboxes, ['data-group-index']: groupIndex, ['data-child-index']: inGroupIndex, ['data-test-index']: throughIndex, diff --git a/src/internal/components/listbox/parts/item.tsx b/src/internal/components/listbox/parts/item.tsx new file mode 100644 index 00000000000..8a0f5b4c4f2 --- /dev/null +++ b/src/internal/components/listbox/parts/item.tsx @@ -0,0 +1,371 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useEffect, useRef, useState } from 'react'; +import clsx from 'clsx'; + +import { useMergeRefs } from '@cloudscape-design/component-toolkit/internal'; + +import InternalIcon from '../../../../icon/internal.js'; +import { MultiselectProps } from '../../../../multiselect/interfaces'; +import { SelectProps } from '../../../../select/interfaces.js'; +import Tooltip from '../../../../tooltip/internal.js'; +import { OptionDefinition, OptionGroup } from '../../../../types/option'; +import { getBaseProps } from '../../../base-component'; +import useHiddenDescription from '../../../hooks/use-hidden-description'; +import CheckboxIcon from '../../checkbox-icon'; +import Option from '../../option'; +import { DropdownOption } from '../../option/interfaces'; +import { HighlightType } from '../../options-list/utils/use-highlight-option.js'; +import SelectableItem from '../../selectable-item'; +import { SelectionType } from '../selection/interfaces'; + +import styles from '../../../../select/parts/styles.css.js'; + +export interface ItemProps { + index: number; + virtualIndex?: number; + option: DropdownOption; + /** + * `single` renders a check mark on the selected option (Select). + * `multi` renders a checkbox, interactive groups and the select-all row (Multiselect). + */ + selectionType?: SelectionType; + highlighted?: boolean; + selected?: boolean; + indeterminate?: boolean; + filteringValue?: string; + virtualPosition?: number; + padBottom?: boolean; + isNextSelected?: boolean; + isPreviousSelected?: boolean; + screenReaderContent?: string; + ariaPosinset?: number; + ariaSetsize?: number; + highlightType?: HighlightType['type']; + withScrollbar?: boolean; + sticky?: boolean; + renderOption?: SelectProps.SelectOptionItemRenderer | MultiselectProps.MultiselectOptionItemRenderer; + parentProps?: ItemParentProps; +} + +export interface ItemParentProps { + index: number; + virtualIndex?: number; + option: DropdownOption; + disabled: boolean; + indeterminate?: boolean; + highlighted?: boolean; + selected?: boolean; +} + +interface MultiselectItemParentProps extends ItemParentProps { + indeterminate: boolean; + highlighted: boolean; + selected: boolean; +} + +const toSelectOptionGroupItem = (props: ItemParentProps): SelectProps.SelectOptionGroupItem => { + return { + type: 'group', + index: props.virtualIndex ?? props.index, + option: props.option.option as OptionGroup, + disabled: props.disabled, + }; +}; + +const toSelectOptionItem = (props: { + index: number; + virtualIndex?: number; + option: DropdownOption; + disabled: boolean; + selected: boolean; + highlighted: boolean; + parentProps?: ItemParentProps; +}): SelectProps.SelectOptionItem => { + return { + type: 'item', + index: props.virtualIndex ?? props.index, + option: props.option.option as OptionDefinition, + selected: props.selected, + highlighted: props.highlighted, + disabled: props.disabled, + parent: props.parentProps + ? toSelectOptionGroupItem({ + index: props.parentProps?.index, + virtualIndex: props.parentProps?.virtualIndex, + option: props.parentProps?.option, + disabled: props.disabled, + }) + : null, + }; +}; + +const toMultiselectOptionGroupItem = ( + props: MultiselectItemParentProps +): MultiselectProps.MultiselectOptionGroupItem => { + return { + type: 'group', + index: props.virtualIndex ?? props.index, + option: props.option.option as OptionGroup, + indeterminate: props.indeterminate ?? false, + selected: props.selected, + highlighted: props.highlighted, + disabled: props.disabled, + }; +}; + +const toMultiselectOptionItem = (props: { + index: number; + virtualIndex?: number; + option: DropdownOption; + disabled: boolean; + selected: boolean; + highlighted: boolean; + parentProps?: ItemParentProps; +}): MultiselectProps.MultiselectOptionItem => { + return { + type: 'item', + index: props.virtualIndex ?? props.index, + option: props.option.option as OptionDefinition, + selected: props.selected, + highlighted: props.highlighted, + disabled: props.disabled, + parent: props.parentProps + ? toMultiselectOptionGroupItem({ + index: props.parentProps?.index, + virtualIndex: props.parentProps?.virtualIndex, + option: props.parentProps?.option, + disabled: props.disabled, + highlighted: props.parentProps?.highlighted ?? false, + indeterminate: props.parentProps?.indeterminate ?? false, + selected: props.parentProps?.selected ?? false, + }) + : null, + }; +}; + +const Item = ( + { + index, + virtualIndex, + option, + selectionType = 'single', + highlighted, + selected, + indeterminate, + filteringValue, + virtualPosition, + padBottom, + isNextSelected, + isPreviousSelected, + screenReaderContent, + ariaPosinset, + ariaSetsize, + highlightType, + withScrollbar, + sticky, + renderOption, + parentProps, + ...restProps + }: ItemProps, + ref: React.Ref +) => { + const baseProps = getBaseProps(restProps); + const isMulti = selectionType === 'multi'; + const isParent = option.type === 'parent'; + const isChild = option.type === 'child'; + const isSelectAll = option.type === 'select-all'; + const wrappedOption: OptionDefinition = option.option; + const disabled = option.disabled || wrappedOption.disabled; + const disabledReasonSource = isMulti + ? option.disabledReason || wrappedOption.disabledReason + : wrappedOption.disabledReason; + const disabledReason = disabled && disabledReasonSource ? disabledReasonSource : ''; + const isDisabledWithReason = !!disabledReason; + const internalRef = useRef(null); + const { descriptionEl, descriptionId } = useHiddenDescription(disabledReason); + const [canShowTooltip, setCanShowTooltip] = useState(true); + useEffect(() => setCanShowTooltip(true), [highlighted]); + const mergedRef = useMergeRefs(ref, internalRef); + + const getSelectItemProps = (option: DropdownOption): SelectProps.SelectItem => { + if (option.type === 'parent') { + return toSelectOptionGroupItem({ + option: option, + index: index, + virtualIndex: virtualIndex, + disabled: !!disabled, + }); + } else { + return toSelectOptionItem({ + option: option, + index: index, + virtualIndex: virtualIndex, + disabled: !!disabled, + highlighted: !!highlighted, + selected: !!selected, + parentProps: parentProps, + }); + } + }; + + const getMultiselectItemProps = (option: DropdownOption): MultiselectProps.MultiselectItem => { + if (option.type === 'parent') { + return toMultiselectOptionGroupItem({ + option: option, + index: index, + virtualIndex: virtualIndex, + disabled: !!disabled, + highlighted: !!highlighted, + selected: !!selected, + indeterminate: indeterminate ?? false, + }); + } else if (option.type === 'select-all') { + return { + type: 'select-all', + option: option.option as OptionDefinition, + indeterminate: indeterminate ?? false, + selected: !!selected, + highlighted: !!highlighted, + }; + } else { + return toMultiselectOptionItem({ + option: option, + index: index, + virtualIndex: virtualIndex, + disabled: !!disabled, + highlighted: !!highlighted, + selected: !!selected, + parentProps: parentProps, + }); + } + }; + + const renderOptionWrapper = (option: DropdownOption) => { + if (!renderOption) { + return null; + } + if (isMulti) { + return (renderOption as MultiselectProps.MultiselectOptionItemRenderer)({ + item: getMultiselectItemProps(option), + filterText: filteringValue, + }); + } + return (renderOption as SelectProps.SelectOptionItemRenderer)({ + item: getSelectItemProps(option), + filterText: filteringValue, + }); + }; + + const renderResult = renderOptionWrapper(option); + + const optionElement = ( +