Skip to content

Commit f8def51

Browse files
authored
Allow custom indicator on Combobox/Select (#983)
1 parent b2aba1f commit f8def51

File tree

21 files changed

+592
-160
lines changed

21 files changed

+592
-160
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- `Tree`, `TreeItem` components
1414
- Includes docs and test suite
1515
- `ActionListItem` accepts `actionsButtonLabel` prop to help with testing
16+
- `ComboboxList` and `ComboboxOption` now both support a custom `indicator`
17+
- `Select` and `SelectMulti` also support `indicator` at both the component and option level
1618

1719
### Changed
1820

packages/components/src/Form/Inputs/Combobox/Combobox.test.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
ComboboxInput,
3434
ComboboxList,
3535
ComboboxOption,
36+
ComboboxOptionIndicatorFunction,
3637
ComboboxMulti,
3738
ComboboxMultiInput,
3839
ComboboxMultiList,
@@ -193,6 +194,97 @@ describe('<Combobox/> with children', () => {
193194
fireEvent.click(document)
194195
})
195196

197+
const getIndicatorJSX = (listLevel: boolean) => (
198+
indicator: ComboboxOptionIndicatorFunction
199+
) => (
200+
<Combobox key="combobox" value={{ label: 'Foo', value: '101' }}>
201+
<ComboboxInput placeholder="Type here" />
202+
<ComboboxList indicator={listLevel ? indicator : undefined}>
203+
<ComboboxOption
204+
label="Foo"
205+
value="101"
206+
indicator={listLevel ? undefined : indicator}
207+
/>
208+
<ComboboxOption
209+
label="Bar"
210+
value="102"
211+
indicator={listLevel ? undefined : indicator}
212+
/>
213+
</ComboboxList>
214+
</Combobox>
215+
)
216+
217+
const getIndicatorJSXMulti = (listLevel: boolean) => (
218+
indicator: ComboboxOptionIndicatorFunction
219+
) => (
220+
<ComboboxMulti
221+
key="combobox-multi"
222+
values={[{ label: 'Foo', value: '101' }]}
223+
>
224+
<ComboboxMultiInput placeholder="Type here" />
225+
<ComboboxMultiList indicator={listLevel ? indicator : undefined}>
226+
<ComboboxMultiOption
227+
label="Foo"
228+
value="101"
229+
indicator={listLevel ? undefined : indicator}
230+
/>
231+
<ComboboxMultiOption
232+
label="Bar"
233+
value="102"
234+
indicator={listLevel ? undefined : indicator}
235+
/>
236+
</ComboboxMultiList>
237+
</ComboboxMulti>
238+
)
239+
240+
test.each([
241+
['list level (Combobox)', getIndicatorJSX(true)],
242+
['list level (ComboboxMulti)', getIndicatorJSXMulti(true)],
243+
['option level (Combobox)', getIndicatorJSX(false)],
244+
['option level (ComboboxMulti)', getIndicatorJSXMulti(false)],
245+
])('Customize the indicator at the %s', (_, getJSX) => {
246+
const indicator = jest.fn()
247+
const { queryByTitle, getByText, getByPlaceholderText } = renderWithTheme(
248+
getJSX(indicator)
249+
)
250+
251+
const input = getByPlaceholderText('Type here')
252+
fireEvent.click(input)
253+
254+
expect(indicator).toHaveBeenCalledTimes(2)
255+
expect(indicator).toHaveBeenNthCalledWith(1, {
256+
isActive: false,
257+
isSelected: true,
258+
label: 'Foo',
259+
value: '101',
260+
})
261+
expect(indicator).toHaveBeenNthCalledWith(2, {
262+
isActive: false,
263+
isSelected: false,
264+
label: 'Bar',
265+
value: '102',
266+
})
267+
268+
const check = queryByTitle('Check')
269+
expect(check).not.toBeInTheDocument()
270+
271+
indicator.mockClear()
272+
273+
const bar = getByText('Bar')
274+
fireEvent.mouseOver(bar)
275+
276+
expect(indicator).toHaveBeenCalledTimes(1)
277+
expect(indicator).toHaveBeenCalledWith({
278+
isActive: true,
279+
isSelected: false,
280+
label: 'Bar',
281+
value: '102',
282+
})
283+
284+
// Close popover to silence act() warning
285+
fireEvent.click(document)
286+
})
287+
196288
test('Does not highlight current selected value', () => {
197289
const { getByText, getByPlaceholderText } = renderWithTheme(
198290
<Combobox key="combobox" value={{ label: 'Foo', value: '101' }}>

packages/components/src/Form/Inputs/Combobox/ComboboxContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
Dispatch,
3434
SetStateAction,
3535
} from 'react'
36+
import { ComboboxOptionIndicatorProps } from './ComboboxOptionIndicator'
3637
import {
3738
ComboboxData,
3839
ComboboxMultiData,
@@ -67,6 +68,7 @@ export interface ComboboxContextProps<
6768
readOnlyPropRef?: MutableRefObject<boolean>
6869
windowedOptionsPropRef?: MutableRefObject<boolean>
6970
isAutoScrollingRef?: MutableRefObject<boolean>
71+
indicatorPropRef?: MutableRefObject<ComboboxOptionIndicatorProps['indicator']>
7072
isVisible?: boolean
7173
openOnFocus?: boolean
7274
listScrollPosition?: number

packages/components/src/Form/Inputs/Combobox/ComboboxList.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,15 @@ import once from 'lodash/once'
4949
import throttle from 'lodash/throttle'
5050
import { useForkedRef } from '../../../utils'
5151
import { usePopover } from '../../../Popover'
52+
import { ComboboxOptionIndicatorProps } from './ComboboxOptionIndicator'
5253
import { ComboboxContext, ComboboxMultiContext } from './ComboboxContext'
5354
import { useBlur } from './utils/useBlur'
5455
import { useKeyDown } from './utils/useKeyDown'
5556
import { ComboboxActionType } from './utils/state'
5657

5758
export interface ComboboxListProps
58-
extends SpaceProps,
59+
extends Pick<ComboboxOptionIndicatorProps, 'indicator'>,
60+
SpaceProps,
5961
LayoutProps,
6062
TypographyProps,
6163
CompatibleHTMLProps<HTMLUListElement> {
@@ -100,6 +102,7 @@ const ComboboxListInternal = forwardRef(
100102
closeOnSelect = true,
101103
// disables the optionsRef behavior, to be handled externally (support keyboard nav in long lists)
102104
windowedOptions = false,
105+
indicator,
103106
isMulti,
104107
...props
105108
}: ComboboxListInternalProps,
@@ -112,6 +115,7 @@ const ComboboxListInternal = forwardRef(
112115
persistSelectionPropRef,
113116
closeOnSelectPropRef,
114117
windowedOptionsPropRef,
118+
indicatorPropRef,
115119
transition,
116120
wrapperElement,
117121
isVisible,
@@ -126,6 +130,7 @@ const ComboboxListInternal = forwardRef(
126130
persistSelectionPropRef.current = persistSelection
127131
if (closeOnSelectPropRef) closeOnSelectPropRef.current = closeOnSelect
128132
if (windowedOptionsPropRef) windowedOptionsPropRef.current = windowedOptions
133+
if (indicatorPropRef) indicatorPropRef.current = indicator
129134

130135
// WEIRD? Reset the options ref every render so that they are always
131136
// accurate and ready for keyboard navigation handlers. Using layout

packages/components/src/Form/Inputs/Combobox/ComboboxMultiOption.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ import {
3737
} from './ComboboxContext'
3838
import {
3939
comboboxOptionDefaultProps,
40-
ComboboxOptionDetail,
4140
ComboboxOptionProps,
4241
comboboxOptionStyle,
4342
ComboboxOptionWrapper,
4443
ComboboxOptionText,
4544
} from './ComboboxOption'
45+
import { ComboboxOptionIndicator } from './ComboboxOptionIndicator'
4646
import { useAddOptionToContext } from './utils/useAddOptionToContext'
4747
import { useOptionEvents } from './utils/useOptionEvents'
4848
import { useOptionStatus } from './utils/useOptionStatus'
@@ -52,6 +52,7 @@ const ComboboxMultiOptionInternal = forwardRef(
5252
(
5353
{
5454
children,
55+
indicator,
5556
highlightText = true,
5657
scrollIntoView,
5758
hideCheckMark,
@@ -92,9 +93,14 @@ const ComboboxMultiOptionInternal = forwardRef(
9293
ref={ref}
9394
aria-selected={isActive}
9495
>
95-
<ComboboxOptionDetail>
96+
<ComboboxOptionIndicator
97+
indicator={indicator}
98+
isActive={isActive}
99+
isSelected={isSelected}
100+
isMulti={true}
101+
>
96102
{!hideCheckMark && <Checkbox checked={isSelected} />}
97-
</ComboboxOptionDetail>
103+
</ComboboxOptionIndicator>
98104
{children || <ComboboxOptionText highlightText={highlightText} />}
99105
</ComboboxOptionWrapper>
100106
)
@@ -105,8 +111,6 @@ ComboboxMultiOptionInternal.displayName = 'ComboboxMultiOptionInternal'
105111

106112
export const ComboboxMultiOption = styled(ComboboxMultiOptionInternal)`
107113
${comboboxOptionStyle}
108-
grid-template-columns: ${(props) =>
109-
props.hideCheckMark ? 0 : props.theme.space.xlarge} 1fr;
110114
`
111115

112116
ComboboxMultiOption.defaultProps = {

packages/components/src/Form/Inputs/Combobox/ComboboxOption.tsx

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
TypographyProps,
4343
} from '@looker/design-tokens'
4444
import omit from 'lodash/omit'
45-
import React, { forwardRef, useContext, Ref } from 'react'
45+
import React, { forwardRef, useContext, Ref, ReactNode } from 'react'
4646
import styled, { css } from 'styled-components'
4747
import { Icon } from '../../../Icon'
4848
import { ReplaceText, Text } from '../../../Text'
@@ -59,6 +59,10 @@ import { getComboboxText } from './utils/getComboboxText'
5959
import { useOptionEvents } from './utils/useOptionEvents'
6060
import { useOptionStatus } from './utils/useOptionStatus'
6161
import { useAddOptionToContext } from './utils/useAddOptionToContext'
62+
import {
63+
ComboboxOptionIndicator,
64+
ComboboxOptionIndicatorProps,
65+
} from './ComboboxOptionIndicator'
6266
import { useOptionScroll } from './utils/useOptionScroll'
6367

6468
export interface ComboboxOptionObject {
@@ -86,6 +90,7 @@ export interface HighlightTextProps {
8690

8791
export interface ComboboxOptionProps
8892
extends ComboboxOptionObject,
93+
Pick<ComboboxOptionIndicatorProps, 'indicator'>,
8994
HighlightTextProps,
9095
ColorProps,
9196
FlexboxProps,
@@ -105,7 +110,7 @@ export interface ComboboxOptionProps
105110
* 🍎 <ComboboxOptionText />
106111
* </ComboboxOption>
107112
*/
108-
children?: React.ReactNode
113+
children?: ReactNode
109114
}
110115

111116
export const ComboboxOptionWrapper = forwardRef(
@@ -136,6 +141,7 @@ const ComboboxOptionInternal = forwardRef(
136141
(
137142
{
138143
children,
144+
indicator,
139145
highlightText = true,
140146
scrollIntoView,
141147
...props
@@ -175,9 +181,13 @@ const ComboboxOptionInternal = forwardRef(
175181
ref={ref}
176182
aria-selected={isActive}
177183
>
178-
<ComboboxOptionDetail>
184+
<ComboboxOptionIndicator
185+
indicator={indicator}
186+
isActive={isActive}
187+
isSelected={isSelected}
188+
>
179189
{isSelected && <Icon name="Check" mr={0} />}
180-
</ComboboxOptionDetail>
190+
</ComboboxOptionIndicator>
181191
{children || <ComboboxOptionText highlightText={highlightText} />}
182192
</ComboboxOptionWrapper>
183193
)
@@ -186,19 +196,6 @@ const ComboboxOptionInternal = forwardRef(
186196

187197
ComboboxOptionInternal.displayName = 'ComboboxOptionInternal'
188198

189-
export const ComboboxOptionDetail = styled.div`
190-
display: flex;
191-
align-items: center;
192-
justify-content: center;
193-
height: ${(props) => props.theme.space.large};
194-
`
195-
196-
export const comboboxOptionGrid = css`
197-
display: grid;
198-
grid-gap: ${(props) => props.theme.space.xxsmall};
199-
grid-template-columns: ${(props) => props.theme.space.medium} 1fr;
200-
`
201-
202199
export const comboboxOptionStyle = css`
203200
${reset}
204201
${color}
@@ -207,8 +204,7 @@ export const comboboxOptionStyle = css`
207204
${space}
208205
${typography}
209206
cursor: default;
210-
align-items: flex-start;
211-
${comboboxOptionGrid}
207+
align-items: stretch;
212208
outline: none;
213209
214210
&[aria-selected='true'] {

0 commit comments

Comments
 (0)