Skip to content

Commit 00d7b9c

Browse files
authored
InputChipsBase: fix click/mousedown/focus issues (#1305)
1 parent 6c6abc7 commit 00d7b9c

File tree

5 files changed

+109
-13
lines changed

5 files changed

+109
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
### Fixed
1818

19+
- `SelectMulti` and `InputChips` issues clicking on icons and chips
1920
- `Select` display value now properly updates when the option label for the current value changes
2021
- `InputChips` and `SelectMulti` overflow when a fixed height is used
2122
- `InputChips` and `SelectMulti` long values breaking out of the input

packages/components/src/Form/Inputs/AdvancedInputControls.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,17 @@ import flatMap from 'lodash/flatMap'
2929
import tail from 'lodash/tail'
3030
import compact from 'lodash/compact'
3131
import { Icon } from '../../Icon'
32-
import { InputSearchControls } from './InputSearch/InputSearchControls'
32+
import {
33+
InputSearchControls,
34+
InputSearchControlsProps,
35+
} from './InputSearch/InputSearchControls'
3336

34-
interface AdvancedInputControlsProps {
37+
export interface AdvancedInputControlsProps
38+
extends Omit<InputSearchControlsProps, 'height' | 'showClear'> {
3539
validationType?: 'error'
3640
renderSearchControls?: boolean
3741
isVisibleOptions?: boolean
3842
hasOptions?: boolean
39-
disabled?: boolean
40-
onClear: () => void
41-
summary?: string
4243
}
4344

4445
// inserts a divider line between each control element (item1 | item2 | item3)
@@ -50,11 +51,10 @@ const intersperseDivider = (children: ReactElement[]) =>
5051
export const AdvancedInputControls: FC<AdvancedInputControlsProps> = ({
5152
validationType,
5253
renderSearchControls,
53-
onClear,
5454
disabled,
5555
isVisibleOptions,
56-
summary,
5756
hasOptions = true,
57+
...rest
5858
}) => {
5959
const children = intersperseDivider(
6060
compact([
@@ -70,10 +70,9 @@ export const AdvancedInputControls: FC<AdvancedInputControlsProps> = ({
7070
renderSearchControls && (
7171
<InputSearchControls
7272
key="search-controls"
73-
onClear={onClear}
7473
showClear={true}
7574
disabled={disabled}
76-
summary={summary}
75+
{...rest}
7776
/>
7877
),
7978
hasOptions && (

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ tag2`
148148
expect(onChangeMock).toHaveBeenCalledWith([])
149149
})
150150

151+
test('all values are removed by clicking clear field', () => {
152+
const onChangeMock = jest.fn()
153+
renderWithTheme(
154+
<InputChips onChange={onChangeMock} values={['tag1', 'tag2']} />
155+
)
156+
const clear = screen.getByText('Clear Field')
157+
158+
fireEvent.click(clear)
159+
expect(onChangeMock).toHaveBeenCalledTimes(1)
160+
expect(onChangeMock).toHaveBeenCalledWith([])
161+
})
162+
151163
test('values are removed by clicking remove on the chip', () => {
152164
const onChangeMock = jest.fn()
153165
renderWithTheme(<InputChips onChange={onChangeMock} values={['tag1']} />)
@@ -262,4 +274,57 @@ tag2`
262274
expect(onChangeMock).not.toHaveBeenCalled()
263275
})
264276
})
277+
278+
test('mousedown on a chip does not focus the inner input', () => {
279+
const rafSpy = jest
280+
.spyOn(window, 'requestAnimationFrame')
281+
.mockImplementation((cb: any) => cb())
282+
283+
renderWithTheme(
284+
<InputChips
285+
onChange={() => null}
286+
values={['foo', 'bar']}
287+
placeholder="type here"
288+
/>
289+
)
290+
291+
const chip = screen.getByText('bar')
292+
const deleteButton = screen.getAllByText('Delete')[0]
293+
const input = screen.getByPlaceholderText('type here')
294+
295+
fireEvent.mouseDown(chip)
296+
expect(document.activeElement).not.toEqual(input)
297+
298+
// Focus should move _after_ delete button is clicked
299+
fireEvent.click(deleteButton)
300+
expect(document.activeElement).toEqual(input)
301+
302+
rafSpy.mockRestore()
303+
})
304+
305+
test('mousedown on clear button does not focus the inner input', () => {
306+
const rafSpy = jest
307+
.spyOn(window, 'requestAnimationFrame')
308+
.mockImplementation((cb: any) => cb())
309+
310+
renderWithTheme(
311+
<InputChips
312+
onChange={() => null}
313+
values={['foo', 'bar']}
314+
placeholder="type here"
315+
/>
316+
)
317+
318+
const clear = screen.getByText('Clear Field')
319+
const input = screen.getByPlaceholderText('type here')
320+
321+
fireEvent.mouseDown(clear)
322+
expect(document.activeElement).not.toEqual(input)
323+
324+
// Focus should move _after_ clear button is clicked
325+
fireEvent.click(clear)
326+
expect(document.activeElement).toEqual(input)
327+
328+
rafSpy.mockRestore()
329+
})
265330
})

packages/components/src/Form/Inputs/InputChips/InputChipsBase.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,21 @@
2323
SOFTWARE.
2424
2525
*/
26-
import React, { FormEvent, forwardRef, KeyboardEvent, Ref } from 'react'
26+
import React, {
27+
FormEvent,
28+
forwardRef,
29+
KeyboardEvent,
30+
MouseEvent,
31+
Ref,
32+
useRef,
33+
} from 'react'
2734
import styled, { css } from 'styled-components'
2835
import { MaxHeightProps } from 'styled-system'
2936
import { Chip } from '../../../Chip'
3037
import { inputHeight } from '../height'
3138
import { InputTextContent, InputText, InputTextBaseProps } from '../InputText'
3239
import { AdvancedInputControls } from '../AdvancedInputControls'
40+
import { useForkedRef } from '../../../utils'
3341

3442
export interface InputChipsInputControlProps {
3543
/**
@@ -93,11 +101,25 @@ export const InputChipsBaseInternal = forwardRef(
93101
removeOnBackspace = true,
94102
...props
95103
}: InputChipsBaseProps & InputChipsInputControlProps,
96-
ref: Ref<HTMLInputElement>
104+
forwardedRef: Ref<HTMLInputElement>
97105
) => {
106+
const internalRef = useRef<HTMLInputElement>(null)
107+
const ref = useForkedRef(forwardedRef, internalRef)
108+
109+
// Prevent the default InputText behavior of moving focus to the internal input just after mousedown
110+
// on Chip and clear button and instead focus after onChipDelete / onClear
111+
// If mousedown/click is elsewhere on Chip, don't move focus b/c user is trying to select the Chip itself
112+
function stopPropagation(e: MouseEvent) {
113+
e.stopPropagation()
114+
}
115+
function focusInput() {
116+
internalRef.current && internalRef.current.focus()
117+
}
118+
98119
function handleDeleteChip(value: string) {
99120
const newValues = values.filter((v) => value !== v)
100121
onChange(newValues)
122+
focusInput()
101123
}
102124

103125
function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
@@ -112,14 +134,20 @@ export const InputChipsBaseInternal = forwardRef(
112134
onChange([])
113135
onInputChange('')
114136
onClear && onClear()
137+
focusInput()
115138
}
116139

117140
const chips = values.map((value) => {
118141
function onChipDelete() {
119142
handleDeleteChip(value)
120143
}
121144
return (
122-
<Chip onDelete={onChipDelete} key={value}>
145+
<Chip
146+
onDelete={onChipDelete}
147+
onMouseDown={stopPropagation}
148+
onClick={stopPropagation}
149+
key={value}
150+
>
123151
{value}
124152
</Chip>
125153
)
@@ -143,6 +171,7 @@ export const InputChipsBaseInternal = forwardRef(
143171
disabled={disabled}
144172
summary={summary}
145173
hasOptions={hasOptions}
174+
onMouseDown={stopPropagation}
146175
/>
147176
)
148177
}

packages/components/src/Form/Inputs/InputSearch/InputSearchControls.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@
2424
2525
*/
2626

27+
import { CompatibleHTMLProps } from '@looker/design-tokens'
2728
import omit from 'lodash/omit'
2829
import React, { forwardRef, MouseEvent, Ref } from 'react'
2930
import styled from 'styled-components'
3031
import { Box, Space } from '../../../Layout'
3132
import { IconButton } from '../../../Button'
3233
import { Text } from '../../../Text'
3334

34-
export interface InputSearchControlsProps {
35+
export interface InputSearchControlsProps
36+
extends CompatibleHTMLProps<HTMLDivElement> {
3537
summary?: string
3638
showClear: boolean
3739
onClear: (e: MouseEvent<HTMLButtonElement>) => void

0 commit comments

Comments
 (0)