Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e54ef67
feat(useEventLocker): implement hook that provide functions to lock e…
Tanney-102 Dec 20, 2022
318595f
feat(useKeyboardActionLockerWhileComposing): implement hook
Tanney-102 Dec 20, 2022
b9dc4ff
feat(TextField): wrap keyboard event handlers with composing locker
Tanney-102 Dec 20, 2022
f52de23
fix(TestField.test): correct fired event for lock keyboard event test
Tanney-102 Dec 20, 2022
cea3508
fix(TextField.test): move tests for composing into new decribe context
Tanney-102 Dec 20, 2022
b33855f
feat(TextArea): wrap keyboard event handlers with composing locker
Tanney-102 Dec 20, 2022
d513c25
docs: add change set
Tanney-102 Dec 21, 2022
0351092
fix(useKeyboardActionLockerWhileComposing): merge logics from useEven…
Tanney-102 Dec 21, 2022
22fa9f5
fix(useKeyboardActionLockerWhileComposing): remove key===enter condit…
Tanney-102 Dec 21, 2022
2523d76
refactor(useKeyboardActionLockerWhileComposing): replace isComposingR…
Tanney-102 Dec 21, 2022
2886919
fix(TextField.test): force fired key events to notice isComposing state
Tanney-102 Dec 21, 2022
333b0be
fix(TextArea.test): force fired key events to notice isComposing state
Tanney-102 Dec 21, 2022
63073db
fix(useKeyboardActionLockerWhileComposing): remove onKeyPress from ar…
Tanney-102 Dec 22, 2022
593a988
fix: case sensitive import
Tanney-102 Dec 22, 2022
abc7927
docs(changeset): add details to changeset document
Tanney-102 Jan 2, 2023
f3aac26
test(TextArea): rewrite test case for keyboard locker in English
Tanney-102 Jan 2, 2023
461a50a
test(TextField): rewrite test case for keyboard locker
Tanney-102 Jan 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/lovely-planets-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@channel.io/bezier-react": minor
---

- keyboard event locker added.
- TextField and TextArea use keyboard event locker, so that they can block keyboard event handling for IME control keys while composing.


Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from 'react'
import { LightFoundation } from 'Foundation'
import disabledOpacity from 'Constants/DisabledOpacity'
import { render } from 'Utils/testUtils'
import { COMMON_IME_CONTROL_KEYS } from 'Components/Forms/Inputs/constants/CommonImeControlKeys'
import TextArea, { TEXT_AREA_TEST_ID } from './TextArea'
import TextAreaProps from './TextArea.types'
import { getTextAreaBgColorSemanticName } from './utils'
Expand Down Expand Up @@ -184,6 +185,34 @@ describe('TextArea 테스트 >', () => {
expect(textareaElement.selectionEnd).toEqual(TEST_INITIAL_VALUE.length)
})
})

describe('Keyboard event handlers for common ime control keys should not be called while composing ', () => {
it('onKeyDown', () => {
const onKeyDown = jest.fn()
const { getByTestId } = renderComponent({ onKeyDown })
const rendered = getByTestId(TEXT_AREA_TEST_ID)
const textareaElement = rendered.getElementsByTagName('textarea')[0]

COMMON_IME_CONTROL_KEYS.forEach((key) => {
const isCompositionStartFired = fireEvent.compositionStart(textareaElement)
fireEvent.keyDown(textareaElement, { key, isComposing: isCompositionStartFired })
expect(onKeyDown).not.toBeCalled()
})
})

it('onKeyUp', () => {
const onKeyUp = jest.fn()
const { getByTestId } = renderComponent({ onKeyUp })
const rendered = getByTestId(TEXT_AREA_TEST_ID)
const textareaElement = rendered.getElementsByTagName('textarea')[0]

COMMON_IME_CONTROL_KEYS.forEach((key) => {
const isCompositionStartFired = fireEvent.compositionStart(textareaElement)
fireEvent.keyUp(textareaElement, { key, isComposing: isCompositionStartFired })
expect(onKeyUp).not.toBeCalled()
})
})
})
})

describe('TextArea util test >', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import React, { forwardRef, Ref, useRef, useCallback, useState, useLayoutEffect,
/* Internal dependencies */
import useMergeRefs from 'Hooks/useMergeRefs'
import useFormFieldProps from 'Components/Forms/useFormFieldProps'
import useKeyboardActionLockerWhileComposing from 'Components/Forms/useKeyboardActionLockerWhileComposing'
import { COMMON_IME_CONTROL_KEYS } from 'Components/Forms/Inputs/constants/CommonImeControlKeys'
import Styled from './TextArea.styled'
import { getTextAreaBgColorSemanticName } from './utils'
import TextAreaProps, { TextAreaHeight } from './TextArea.types'
Expand All @@ -25,6 +27,8 @@ function TextArea({
onFocus,
onBlur,
onChange,
onKeyDown,
onKeyUp,
...rest
}: TextAreaProps,
forwardedRef: Ref<HTMLTextAreaElement>,
Expand Down Expand Up @@ -69,6 +73,15 @@ forwardedRef: Ref<HTMLTextAreaElement>,
onBlur?.(event)
}, [onBlur])

const {
handleKeyDown,
handleKeyUp,
} = useKeyboardActionLockerWhileComposing({
keysToLock: COMMON_IME_CONTROL_KEYS,
onKeyDown,
onKeyUp,
})

// eslint-disable-next-line prefer-arrow-callback
useLayoutEffect(function initialAutoFocus() {
function setSelectionToEnd() {
Expand Down Expand Up @@ -103,6 +116,8 @@ forwardedRef: Ref<HTMLTextAreaElement>,
onChange={onChange}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
/>
</Styled.Wrapper>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { fireEvent } from '@testing-library/dom'
/* Internal dependencies */
import { LightFoundation } from 'Foundation'
import { render } from 'Utils/testUtils'
import { COMMON_IME_CONTROL_KEYS } from 'Components/Forms/Inputs/constants/CommonImeControlKeys'
import TextField, { TEXT_INPUT_TEST_ID } from './TextField'
import { TextFieldProps, TextFieldVariant } from './TextField.types'
import { getProperTextFieldBgColor } from './TextFieldUtils'
Expand Down Expand Up @@ -251,4 +252,32 @@ describe('TextField', () => {
expect(onKeyUp).not.toBeCalled()
})
})

describe('Keyboard event handlers for common ime control keys should not be called while composing', () => {
it('onKeyDown', async () => {
const onKeyDown = jest.fn()
const { getByTestId } = renderComponent({ onKeyDown })
const rendered = getByTestId(TEXT_INPUT_TEST_ID)
const input = rendered.getElementsByTagName('input')[0]

COMMON_IME_CONTROL_KEYS.forEach(async (key) => {
const isCompositionStartFired = fireEvent.compositionStart(input)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fireEvent 대신 userEvent 를 통해 테스트 가능할까요? (관련 링크)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userEvent에서는 composition을 mocking할 수 있는 방법을 따로 지원하지 않습니다 😢

fireEvent.keyDown(input, { key, isComposing: isCompositionStartFired })
expect(onKeyDown).not.toBeCalled()
})
})

it('onKeyUp', () => {
const onKeyUp = jest.fn()
const { getByTestId } = renderComponent({ onKeyUp })
const rendered = getByTestId(TEXT_INPUT_TEST_ID)
const input = rendered.getElementsByTagName('input')[0]

COMMON_IME_CONTROL_KEYS.forEach((key) => {
const isCompositionStartFired = fireEvent.compositionStart(input)
fireEvent.keyUp(input, { key, isComposing: isCompositionStartFired })
expect(onKeyUp).not.toBeCalled()
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { v4 as uuid } from 'uuid'
import { window } from 'Utils/domUtils'
import { LegacyIcon, Icon, IconSize, CancelCircleFilledIcon } from 'Components/Icon'
import useFormFieldProps from 'Components/Forms/useFormFieldProps'
import useKeyboardActionLockerWhileComposing from 'Components/Forms/useKeyboardActionLockerWhileComposing'
import { COMMON_IME_CONTROL_KEYS } from 'Components/Forms/Inputs/constants/CommonImeControlKeys'
import Styled from './TextField.styled'
import {
TextFieldItemProps,
Expand Down Expand Up @@ -206,22 +208,31 @@ forwardedRef: Ref<TextFieldRef>,
onChange,
])

const {
handleKeyDown: handleKeyDownWrappedWithComposingLocker,
handleKeyUp: handleKeyUpWrappedWithComposingLocker,
} = useKeyboardActionLockerWhileComposing({
keysToLock: COMMON_IME_CONTROL_KEYS,
onKeyDown,
onKeyUp,
})

const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
if (activeInput && onKeyDown) {
onKeyDown(event)
if (activeInput && handleKeyDownWrappedWithComposingLocker) {
handleKeyDownWrappedWithComposingLocker(event)
}
}, [
activeInput,
onKeyDown,
handleKeyDownWrappedWithComposingLocker,
])

const handleKeyUp = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
if (activeInput && onKeyUp) {
onKeyUp(event)
if (activeInput && handleKeyUpWrappedWithComposingLocker) {
handleKeyUpWrappedWithComposingLocker(event)
}
}, [
activeInput,
onKeyUp,
handleKeyUpWrappedWithComposingLocker,
])

const handleClear = useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const COMMON_IME_CONTROL_KEYS = ['Enter', 'Escape', 'Tab', ' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
9 changes: 8 additions & 1 deletion packages/bezier-react/src/components/Forms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import type { FormComponentProps } from './Form.types'
import FormFieldSize from './FormFieldSize'
import useFormControlContext from './useFormControlContext'
import useFormFieldProps from './useFormFieldProps'
import useKeyboardActionLockerWhileComposing from './useKeyboardActionLockerWhileComposing'

export type { FormComponentProps }
export { FormFieldSize, useFormControlContext, useFormFieldProps }
export * from './Inputs/constants/CommonImeControlKeys'
export {
FormFieldSize,
useFormControlContext,
useFormFieldProps,
useKeyboardActionLockerWhileComposing,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* External dependencies */
import React, { useCallback, useRef } from 'react'

type HandlerCache<TargetElement extends HTMLElement = HTMLInputElement> =
Map<React.KeyboardEventHandler<TargetElement>, React.KeyboardEventHandler<TargetElement>>
interface UseKeyboardActionLockerWhileComposingProps<TargetElement extends HTMLElement = HTMLInputElement> {
keysToLock?: string[]
onKeyDown?: React.KeyboardEventHandler<TargetElement>
onKeyUp?: React.KeyboardEventHandler<TargetElement>
}

const isSafari = () => window.navigator.userAgent.search('Safari') >= 0 && window.navigator.userAgent.search('Chrome') < 0

function useKeyboardActionLockerWhileComposing<TargetElement extends HTMLElement = HTMLInputElement>({
keysToLock,
onKeyDown,
onKeyUp,
}: UseKeyboardActionLockerWhileComposingProps<TargetElement>) {
const handlerCache = useRef<HandlerCache<TargetElement>>(new Map())

const wrapHandler = useCallback((handler?: React.KeyboardEventHandler<TargetElement>) => {
if (!handler) { return undefined }
if (handlerCache.current.has(handler)) { return handlerCache.current.get(handler) }

const wrappedHandler = (event: React.KeyboardEvent<TargetElement>) => {
// NOTE: If keysToLock is not provided, lock all keys.
const isKeyLocked =
event.nativeEvent.isComposing &&
(!keysToLock || keysToLock.some(controlKey => event.key === controlKey))
/**
* NOTE
* According to the spec(https://www.w3.org/TR/uievents/#events-composition-key-events),
* keyDown event that exit composition should be fired before compositionEnd event.
* However, Safari has different behavior.
* In Safari, keyDown event that exit composition is fired after compositionEnd event.
* So, we need to prevent keyDown event that exit composition in Safari.
* Browser fires keydown event with keyCode 229 when user is composing.
* An event that exits composition is also fired with keyCode 229, even though it has fired after compositionEnd event.
* Therefore, we need to check if the event is fired with keyCode 229 in Safari.
*/
const isSafariKeydownWhileComposing =
isSafari() &&
event.type === 'keydown' &&
event.keyCode === 229

if (isKeyLocked || isSafariKeydownWhileComposing) {
event.stopPropagation()
return
}
handler?.(event)
}

handlerCache.current.set(handler, wrappedHandler)
return wrappedHandler
}, [keysToLock])

return {
handleKeyDown: wrapHandler(onKeyDown),
handleKeyUp: wrapHandler(onKeyUp),
}
}

export default useKeyboardActionLockerWhileComposing