Skip to content

Commit 8c99baf

Browse files
authoredAug 30, 2024
[v3] enhance search input to better support CJK language users (#3156)
* [v3] enhance search input to better support CJK language users * update code formatting * fix summary typo * update code formatting
1 parent 8ffe2fe commit 8c99baf

File tree

2 files changed

+56
-24
lines changed

2 files changed

+56
-24
lines changed
 

‎.changeset/gorgeous-crabs-laugh.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'nextra-theme-docs': patch
3+
---
4+
5+
enhance search input to better support CJK language users

‎packages/nextra-theme-docs/src/components/search.tsx

+51-24
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useRouter } from 'next/router'
66
import { useMounted } from 'nextra/hooks'
77
import { InformationCircleIcon, SpinnerIcon } from 'nextra/icons'
88
import type {
9+
ChangeEvent,
910
CompositionEvent,
1011
FocusEventHandler,
1112
KeyboardEvent,
@@ -34,7 +35,7 @@ export function Search({
3435
className,
3536
overlayClassName,
3637
value,
37-
onChange,
38+
onChange: onChangeProp,
3839
onActive,
3940
loading,
4041
error,
@@ -48,8 +49,6 @@ export function Search({
4849
const input = useRef<HTMLInputElement>(null)
4950
const ulRef = useRef<HTMLUListElement>(null)
5051
const [focused, setFocused] = useState(false)
51-
// Trigger the search after the Input is complete for languages like Chinese
52-
const [composition, setComposition] = useState(true)
5352

5453
useEffect(() => {
5554
setActive(0)
@@ -86,12 +85,15 @@ export function Search({
8685
}
8786
}, [])
8887

88+
const clearValue = useCallback(() => {
89+
onChangeProp('')
90+
}, [onChangeProp])
8991
const finishSearch = useCallback(() => {
9092
input.current?.blur()
91-
onChange('')
93+
clearValue()
9294
setShow(false)
9395
setMenu(false)
94-
}, [onChange, setMenu])
96+
}, [clearValue, setMenu])
9597

9698
const handleActive = useCallback(
9799
(e: { currentTarget: { dataset: DOMStringMap } }) => {
@@ -103,6 +105,9 @@ export function Search({
103105

104106
const handleKeyDown = useCallback(
105107
<T,>(e: KeyboardEvent<T>) => {
108+
// skip the character selection process in the IME for CJK language users.
109+
if (e.nativeEvent.isComposing) return
110+
106111
switch (e.key) {
107112
case 'ArrowDown': {
108113
if (active + 1 < results.length) {
@@ -132,7 +137,7 @@ export function Search({
132137
}
133138
case 'Enter': {
134139
const result = results[active]
135-
if (result && composition) {
140+
if (result) {
136141
void router.push(result.route)
137142
finishSearch()
138143
}
@@ -145,7 +150,7 @@ export function Search({
145150
}
146151
}
147152
},
148-
[active, results, router, finishSearch, handleActive, composition]
153+
[active, results, router, finishSearch, handleActive]
149154
)
150155

151156
const mounted = useMounted()
@@ -174,9 +179,7 @@ export function Search({
174179
: '_pointer-events-none max-sm:_hidden'
175180
)}
176181
title={value ? 'Clear' : undefined}
177-
onClick={() => {
178-
onChange('')
179-
}}
182+
onClick={clearValue}
180183
>
181184
{value && focused
182185
? 'ESC'
@@ -191,12 +194,6 @@ export function Search({
191194
</kbd>
192195
</Transition>
193196
)
194-
const handleComposition = useCallback(
195-
(e: CompositionEvent<HTMLInputElement>) => {
196-
setComposition(e.type === 'compositionend')
197-
},
198-
[]
199-
)
200197

201198
const handleFocus = useCallback<FocusEventHandler>(
202199
event => {
@@ -210,6 +207,41 @@ export function Search({
210207
[onActive]
211208
)
212209

210+
// To handle CJK language users, refer to the following approach: https://github.com/SukkaW/foxact/commit/fe17304b410cddc4803d4fdbebf3cd5ecb070618
211+
// An explicit explanation can be found here: https://github.com/SukkaW/foxact/blob/2b54157187d33ef873c1ab4a9b8700dfdb2e7288/docs/src/pages/use-composition-input.mdx
212+
const compositionStateRef = useRef({
213+
compositioning: false,
214+
emitted: false
215+
})
216+
const handleChange = useCallback(
217+
(e: ChangeEvent<HTMLInputElement> | CompositionEvent<HTMLInputElement>) => {
218+
if (!compositionStateRef.current.compositioning) {
219+
const { value } = e.target as HTMLInputElement
220+
onChangeProp(value)
221+
setShow(Boolean(value))
222+
compositionStateRef.current.emitted = true
223+
return
224+
}
225+
compositionStateRef.current.emitted = false
226+
},
227+
[onChangeProp]
228+
)
229+
230+
const handleCompositionStart = useCallback(() => {
231+
compositionStateRef.current.compositioning = true
232+
compositionStateRef.current.emitted = false
233+
}, [])
234+
235+
const handleCompositionEnd = useCallback(
236+
(e: CompositionEvent<HTMLInputElement>) => {
237+
compositionStateRef.current.compositioning = false
238+
if (!compositionStateRef.current.emitted) {
239+
handleChange(e)
240+
}
241+
},
242+
[handleChange]
243+
)
244+
213245
return (
214246
<div className={cn('nextra-search _relative md:_w-64', className)}>
215247
{renderList && (
@@ -218,16 +250,11 @@ export function Search({
218250

219251
<Input
220252
ref={input}
221-
value={value}
222-
onChange={e => {
223-
const { value } = e.target
224-
onChange(value)
225-
setShow(Boolean(value))
226-
}}
253+
onCompositionStart={handleCompositionStart}
254+
onCompositionEnd={handleCompositionEnd}
255+
onChange={handleChange}
227256
onFocus={handleFocus}
228257
onBlur={handleFocus}
229-
onCompositionStart={handleComposition}
230-
onCompositionEnd={handleComposition}
231258
type="search"
232259
placeholder={renderString(themeConfig.search.placeholder)}
233260
onKeyDown={handleKeyDown}

0 commit comments

Comments
 (0)