Skip to content

Commit ec84a21

Browse files
committed
Use in SearchInput and PageFeedbackForm
1 parent 43eb4a3 commit ec84a21

File tree

6 files changed

+205
-176
lines changed

6 files changed

+205
-176
lines changed

packages/gitbook/src/components/AIChat/AIChatInput.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,6 @@ export function AIChatInput(props: {
2424

2525
const inputRef = useRef<HTMLTextAreaElement>(null);
2626

27-
const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
28-
const textarea = event.currentTarget;
29-
onChange(textarea.value);
30-
31-
// Auto-resize
32-
textarea.style.height = 'auto';
33-
textarea.style.height = `${textarea.scrollHeight}px`;
34-
};
35-
3627
useEffect(() => {
3728
if (chat.opened && !disabled && !loading) {
3829
// Add a small delay to ensure the input is rendered before focusing
@@ -61,9 +52,10 @@ export function AIChatInput(props: {
6152
data-testid="ai-chat-input"
6253
name="ai-chat-input"
6354
multiline
55+
resize
6456
label="Assistant chat input"
6557
placeholder={tString(language, 'ai_chat_input_placeholder')}
66-
onChange={handleInput}
58+
onChange={(event) => onChange(event.target.value)}
6759
onSubmit={() => onSubmit(value)}
6860
value={value}
6961
submitButton={{

packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import React, { type ButtonHTMLAttributes } from 'react';
66
import { useLanguage } from '@/intl/client';
77
import { t, tString } from '@/intl/translate';
88
import { tcls } from '@/lib/tailwind';
9-
109
import { useTrackEvent } from '../Insights';
1110
import { Button, ButtonGroup } from '../primitives';
11+
import { Input } from '../primitives/Input';
1212

13+
const MIN_COMMENT_LENGTH = 3;
1314
const MAX_COMMENT_LENGTH = 512;
1415

1516
/**
@@ -24,7 +25,6 @@ export function PageFeedbackForm(props: {
2425
const trackEvent = useTrackEvent();
2526
const inputRef = React.useRef<HTMLTextAreaElement>(null);
2627
const [rating, setRating] = React.useState<PageFeedbackRating>();
27-
const [comment, setComment] = React.useState('');
2828
const [submitted, setSubmitted] = React.useState(false);
2929

3030
const onSubmitRating = (rating: PageFeedbackRating) => {
@@ -86,43 +86,20 @@ export function PageFeedbackForm(props: {
8686
</ButtonGroup>
8787
</div>
8888
{rating ? (
89-
<div className="flex flex-col gap-2">
90-
{!submitted ? (
91-
<>
92-
<textarea
93-
ref={inputRef}
94-
name="comment"
95-
className="mx-0.5 max-h-40 min-h-16 grow rounded-sm straight-corners:rounded-none bg-tint-base p-2 ring-1 ring-tint ring-inset placeholder:text-sm placeholder:text-tint contrast-more:ring-tint-12 contrast-more:placeholder:text-tint-strong"
96-
placeholder={tString(languages, 'was_this_helpful_comment')}
97-
aria-label={tString(languages, 'was_this_helpful_comment')}
98-
onChange={(e) => setComment(e.target.value)}
99-
value={comment}
100-
rows={3}
101-
maxLength={MAX_COMMENT_LENGTH}
102-
/>
103-
<div className="flex items-center justify-between gap-4">
104-
<Button
105-
size="small"
106-
onClick={() => onSubmitComment(rating, comment)}
107-
label={tString(languages, 'submit')}
108-
/>
109-
{comment.length > MAX_COMMENT_LENGTH * 0.8 ? (
110-
<span
111-
className={
112-
comment.length === MAX_COMMENT_LENGTH
113-
? 'text-red-500'
114-
: ''
115-
}
116-
>
117-
{comment.length} / {MAX_COMMENT_LENGTH}
118-
</span>
119-
) : null}
120-
</div>
121-
</>
122-
) : (
123-
<p>{t(languages, 'was_this_helpful_thank_you')}</p>
124-
)}
125-
</div>
89+
<Input
90+
label={tString(languages, 'was_this_helpful_comment')}
91+
multiline
92+
submitButton
93+
rows={3}
94+
name="page-feedback-comment"
95+
onSubmit={(comment) => onSubmitComment(rating, comment as string)}
96+
maxLength={MAX_COMMENT_LENGTH}
97+
minLength={MIN_COMMENT_LENGTH}
98+
disabled={submitted}
99+
submitMessage={tString(languages, 'was_this_helpful_thank_you')}
100+
className="animate-blur-in"
101+
resize
102+
/>
126103
) : null}
127104
</div>
128105
);
Lines changed: 29 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
'use client';
2-
import React from 'react';
3-
import { useEffect, useRef } from 'react';
2+
import React, { useEffect, useRef } from 'react';
43

54
import { tString, useLanguage } from '@/intl/client';
6-
import { tcls } from '@/lib/tailwind';
7-
import { Icon } from '@gitbook/icons';
8-
import { Button, variantClasses } from '../primitives';
5+
import { Input } from '../primitives/Input';
96
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
10-
import { useClassnames } from '../primitives/StyleProvider';
117

128
interface SearchInputProps {
139
onChange: (value: string) => void;
@@ -20,14 +16,11 @@ interface SearchInputProps {
2016
children?: React.ReactNode;
2117
}
2218

23-
// Size classes for medium size button
24-
const sizeClasses = ['text-sm', 'px-3.5', 'py-1.5', '@2xl:circular-corners:px-4'];
25-
2619
/**
2720
* Input to trigger search.
2821
*/
2922
export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
30-
function SearchInput(props, ref) {
23+
function SearchInput(props, containerRef) {
3124
const {
3225
onChange,
3326
onKeyDown,
@@ -42,7 +35,6 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
4235
const inputRef = useRef<HTMLInputElement>(null);
4336

4437
const language = useLanguage();
45-
const buttonStyles = useClassnames(['ButtonStyles']);
4638

4739
useEffect(() => {
4840
if (isOpen) {
@@ -58,75 +50,32 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
5850
}, [isOpen, value]);
5951

6052
return (
61-
<div className={tcls('relative flex size-9 grow', className)}>
62-
{/* biome-ignore lint/a11y/useKeyWithClickEvents: this div needs an onClick to show the input on mobile, where it's normally hidden.
63-
Normally you'd also need to add a keyboard trigger to do the same without a pointer, but in this case the input already be focused on its own. */}
64-
<div
65-
ref={ref}
66-
onClick={onFocus}
67-
className={tcls(
68-
// Apply button styles
69-
buttonStyles,
70-
variantClasses.header,
71-
sizeClasses,
72-
// Additional custom styles
73-
'has-[input:focus]:-translate-y-px h-9 grow @2xl:cursor-text cursor-pointer px-2.5 has-[input:focus]:bg-tint-base has-[input:focus]:depth-subtle:shadow-lg has-[input:focus]:depth-subtle:shadow-primary-subtle has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-primary-hover',
74-
'theme-bold:border-header-link/3 has-[input:focus-visible]:theme-bold:border-header-link/5 has-[input:focus-visible]:theme-bold:bg-header-link/3 has-[input:focus-visible]:theme-bold:ring-header-link/5',
75-
'theme-bold:before:absolute theme-bold:before:inset-0 theme-bold:before:bg-header-background/7 theme-bold:before:backdrop-blur-xl ', // Special overlay to make the transparent colors of theme-bold visible.
76-
'@max-2xl:absolute relative @max-2xl:right-0 z-30 max-w-none shrink grow justify-start',
77-
isOpen ? '@max-2xl:w-56' : '@max-2xl:w-[38px]'
78-
)}
79-
>
80-
{value && isOpen ? (
81-
<Button
82-
variant="blank"
83-
label={tString(language, 'clear')}
84-
size="medium"
85-
iconOnly
86-
icon="circle-xmark"
87-
className="-ml-1.5 -mr-1 animate-scale-in px-1.5 theme-bold:text-header-link theme-bold:hover:bg-header-link/3"
88-
onClick={() => {
89-
onChange('');
90-
inputRef.current?.focus();
91-
}}
92-
/>
93-
) : (
94-
<Icon
95-
icon="magnifying-glass"
96-
className="size-4 shrink-0 animate-scale-in"
97-
/>
98-
)}
99-
{children}
100-
<input
101-
{...rest}
102-
type="text"
103-
onFocus={onFocus}
104-
onKeyDown={onKeyDown}
105-
onChange={(event) => onChange(event.target.value)}
106-
value={value}
107-
// We only show "search or ask" if the search input actually handles both search and ask.
108-
placeholder={`${tString(language, withAI ? 'search_or_ask' : 'search')}…`}
109-
maxLength={512}
110-
size={10}
111-
data-testid="search-input"
112-
className={tcls(
113-
'peer z-10 min-w-0 grow bg-transparent py-0.5 text-tint-strong theme-bold:text-header-link outline-hidden transition-[width] duration-300 contain-paint placeholder:text-tint theme-bold:placeholder:text-current theme-bold:placeholder:opacity-7',
114-
isOpen ? '' : '@max-2xl:opacity-0'
115-
)}
116-
role="combobox"
117-
autoComplete="off"
118-
aria-autocomplete="list"
119-
aria-haspopup="listbox"
120-
aria-expanded={value && isOpen ? 'true' : 'false'}
121-
// Forward
122-
ref={inputRef}
123-
/>
124-
<KeyboardShortcut
125-
keys={isOpen ? ['esc'] : ['mod', 'k']}
126-
className="last:-mr-1 theme-bold:border-header-link/5 theme-bold:bg-header-background theme-bold:text-header-link"
127-
/>
128-
</div>
129-
</div>
53+
<Input
54+
data-testid="search-input"
55+
name="search-input"
56+
ref={inputRef}
57+
containerRef={containerRef as React.RefObject<HTMLDivElement | null>}
58+
sizing="small"
59+
label={tString(language, withAI ? 'search_or_ask' : 'search')}
60+
className="grow"
61+
placeholder={`${tString(language, withAI ? 'search_or_ask' : 'search')}…`}
62+
onFocus={onFocus}
63+
onKeyDown={onKeyDown}
64+
leading="magnifying-glass"
65+
onChange={(event) => {
66+
onChange(event.target.value);
67+
}}
68+
value={value}
69+
maxLength={512}
70+
autoComplete="off"
71+
aria-autocomplete="list"
72+
aria-haspopup="listbox"
73+
aria-expanded={value && isOpen ? 'true' : 'false'}
74+
clearButton
75+
keyboardShortcut={<KeyboardShortcut keys={isOpen ? ['esc'] : ['mod', 'k']} />}
76+
{...rest}
77+
type="text"
78+
/>
13079
);
13180
}
13281
);

packages/gitbook/src/components/Search/useSearchResultsCursor.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ export function useSearchResultsCursor(props: { query: string; results: ResultTy
1212
}
1313
}, [query]);
1414

15-
React.useEffect(() => {
16-
if (results.length > 0) {
17-
// Auto-focus the first result
18-
setCursor(0);
19-
}
20-
}, [results]);
15+
// TODO: `results` is getting updated too often, causing the cursor to reset to 0 for every render.
16+
// Reimplement this once we have fixed that issue.
17+
18+
// React.useEffect(() => {
19+
// if (results.length > 0) {
20+
// // Auto-focus the first result
21+
// setCursor(0);
22+
// }
23+
// }, [results]);
2124

2225
const moveBy = React.useCallback(
2326
(delta: number) => {

0 commit comments

Comments
 (0)