Skip to content

Commit 9d131c3

Browse files
committed
fix: modal search focus trap
1 parent 760b468 commit 9d131c3

File tree

9 files changed

+84
-31
lines changed

9 files changed

+84
-31
lines changed

apps/site/components/Common/Searchbox/ChatActions/index.module.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
text-neutral-800
2222
duration-300
2323
hover:bg-neutral-300
24+
focus:bg-neutral-300
25+
focus:outline-none
2426
motion-safe:transition-colors
2527
dark:text-neutral-400
26-
dark:hover:bg-neutral-900;
28+
dark:hover:bg-neutral-900
29+
dark:focus:bg-neutral-900;
2730

2831
svg {
2932
@apply size-4;

apps/site/components/Common/Searchbox/ChatInput/index.module.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@
3434
p-2
3535
text-white
3636
duration-300
37+
focus:bg-green-600/75
38+
focus:outline-none
3739
disabled:cursor-not-allowed
3840
disabled:bg-neutral-200/60
3941
disabled:text-neutral-800
4042
motion-safe:transition-colors
4143
dark:bg-green-400
4244
dark:text-neutral-400
45+
focus:dark:bg-green-400/75
4346
disabled:dark:bg-neutral-900/60;
4447

4548
svg {
@@ -85,9 +88,12 @@
8588
text-neutral-900
8689
duration-300
8790
hover:bg-neutral-300
91+
focus:bg-neutral-300
92+
focus:outline-none
8893
motion-safe:transition-colors
8994
dark:border-neutral-900
9095
dark:bg-neutral-950
9196
dark:text-neutral-200
92-
dark:hover:bg-neutral-900;
97+
dark:hover:bg-neutral-900
98+
dark:focus:bg-neutral-900;
9399
}

apps/site/components/Common/Searchbox/ChatInput/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { PromptTextArea, Suggestions } from '@orama/ui/components';
66
import { useChat } from '@orama/ui/hooks';
77
import { useTranslations } from 'next-intl';
88
import type { FC } from 'react';
9+
import { useEffect, useRef } from 'react';
910

1011
import styles from './index.module.css';
1112

@@ -14,6 +15,7 @@ export const ChatInput: FC = () => {
1415
const {
1516
context: { interactions },
1617
} = useChat();
18+
const textareaRef = useRef<HTMLTextAreaElement>(null);
1719

1820
const suggestions = [
1921
t('components.search.suggestionOne'),
@@ -23,6 +25,12 @@ export const ChatInput: FC = () => {
2325

2426
const hasInteractions = !!interactions?.length;
2527

28+
useEffect(() => {
29+
setTimeout(() => {
30+
textareaRef.current?.focus();
31+
}, 100);
32+
}, []);
33+
2634
return (
2735
<>
2836
{!hasInteractions && (
@@ -40,12 +48,13 @@ export const ChatInput: FC = () => {
4048
<div className={styles.textareaContainer}>
4149
<PromptTextArea.Wrapper className={styles.textareaWrapper}>
4250
<PromptTextArea.Field
43-
id="chat-input"
51+
id="orama-chat-input"
4452
name="chat-input"
4553
placeholder={t('components.search.chatPlaceholder')}
4654
rows={1}
4755
maxLength={500}
4856
autoFocus
57+
ref={textareaRef}
4958
className={styles.textareaField}
5059
/>
5160
<PromptTextArea.Button

apps/site/components/Common/Searchbox/ChatInteractions/index.module.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@
5050
p-2
5151
text-neutral-900
5252
duration-300
53+
focus:bg-neutral-300
5354
focus:outline-none
5455
motion-safe:transition-colors
5556
lg:bottom-28
5657
dark:bg-neutral-900
57-
dark:text-neutral-200;
58+
dark:text-neutral-200
59+
focus:dark:bg-neutral-800;
5860

5961
svg {
6062
@apply size-4;

apps/site/components/Common/Searchbox/ChatSources/index.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
duration-300
3535
hover:bg-neutral-200
3636
focus:bg-neutral-200
37+
focus:outline-none
3738
motion-safe:transition-colors
3839
dark:bg-neutral-950
3940
dark:text-neutral-200

apps/site/components/Common/Searchbox/Search/index.module.css

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
text-xs
106106
font-semibold
107107
uppercase
108-
text-neutral-700
108+
text-neutral-800
109109
dark:text-neutral-500;
110110
}
111111

@@ -161,7 +161,7 @@
161161
justify-center
162162
pt-10
163163
text-sm
164-
text-neutral-700
164+
text-neutral-800
165165
dark:text-neutral-500;
166166
}
167167

@@ -170,7 +170,7 @@
170170
overflow-x-auto;
171171

172172
&::-webkit-scrollbar {
173-
display: none;
173+
@apply hidden;
174174
}
175175
}
176176

@@ -204,11 +204,14 @@
204204
items-center
205205
gap-2
206206
overflow-x-auto;
207+
208+
&::-webkit-scrollbar {
209+
@apply hidden;
210+
}
207211
}
208212

209213
.facetTabItemCount {
210-
@apply text-neutral-700
211-
dark:text-neutral-700;
214+
@apply text-neutral-700;
212215
}
213216

214217
.searchResultsGroupWrapper {

apps/site/components/Common/Searchbox/Search/index.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,28 @@ import { useSearch } from '@orama/ui/hooks/useSearch';
1313
import classNames from 'classnames';
1414
import { useTranslations } from 'next-intl';
1515
import type { FC, PropsWithChildren } from 'react';
16-
import { useEffect, useCallback } from 'react';
16+
import { useEffect, useCallback, useRef } from 'react';
1717

1818
import { DEFAULT_ORAMA_QUERY_PARAMS } from '#site/next.constants.mjs';
1919

2020
import styles from './index.module.css';
2121
import { getFormattedPath } from './utils';
22-
import { DocumentLink, type Document } from '../DocumentLink';
22+
import { DocumentLink } from '../DocumentLink';
23+
import type { Document } from '../DocumentLink';
24+
import { Footer } from '../Footer';
2325

2426
type SearchProps = PropsWithChildren<{
2527
onChatTrigger: () => void;
28+
mode?: 'search' | 'chat';
2629
}>;
2730

28-
export const Search: FC<SearchProps> = ({ onChatTrigger }) => {
31+
export const Search: FC<SearchProps> = ({ onChatTrigger, mode = 'search' }) => {
2932
const t = useTranslations();
3033
const {
3134
dispatch,
3235
context: { searchTerm, selectedFacet },
3336
} = useSearch();
37+
const containerRef = useRef<HTMLDivElement>(null);
3438

3539
const clearAll = useCallback(() => {
3640
dispatch({ type: 'SET_SEARCH_TERM', payload: { searchTerm: '' } });
@@ -45,12 +49,28 @@ export const Search: FC<SearchProps> = ({ onChatTrigger }) => {
4549
};
4650
}, [clearAll]);
4751

52+
useEffect(() => {
53+
const interactiveElements = containerRef.current?.querySelectorAll(
54+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
55+
);
56+
57+
if (mode === 'search') {
58+
interactiveElements?.forEach(el => {
59+
el.removeAttribute('tabindex');
60+
});
61+
} else {
62+
interactiveElements?.forEach(el => {
63+
el.setAttribute('tabindex', '-1');
64+
});
65+
}
66+
}, [mode]);
67+
4868
return (
49-
<div className={styles.searchContainer}>
69+
<div className={styles.searchContainer} ref={containerRef}>
5070
<SearchInput.Wrapper className={styles.searchInputWrapper}>
5171
<MagnifyingGlassIcon />
5272
<SearchInput.Input
53-
inputId="doc-search"
73+
inputId="orama-doc-search"
5474
ariaLabel={t('components.search.searchPlaceholder')}
5575
placeholder={t('components.search.searchPlaceholder')}
5676
className={styles.searchInput}
@@ -232,6 +252,7 @@ export const Search: FC<SearchProps> = ({ onChatTrigger }) => {
232252
</SearchResults.Wrapper>
233253
</div>
234254
</div>
255+
<Footer />
235256
</div>
236257
);
237258
};

apps/site/components/Common/Searchbox/SlidingChatPanel/index.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
text-neutral-700
1212
duration-300
1313
hover:bg-white/20
14+
focus:bg-white/20
1415
focus:outline-none
1516
motion-safe:transition-colors
1617
dark:text-white;

apps/site/components/Common/Searchbox/index.tsx

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -134,30 +134,37 @@ const InnerSearchBox: FC<PropsWithChildren<{ onClose: () => void }>> = ({
134134
/>
135135
)}
136136
{displaySearch && (
137-
<Search
138-
onChatTrigger={() => {
139-
setAutoTriggerValue(searchTerm ?? null);
140-
handleSelectMode('chat');
141-
}}
142-
/>
137+
<>
138+
<Search
139+
mode={mode}
140+
onChatTrigger={() => {
141+
setAutoTriggerValue(searchTerm ?? null);
142+
handleSelectMode('chat');
143+
}}
144+
/>
145+
</>
143146
)}
144147
{isMobileScreen && mode === 'chat' && (
145-
<div className={styles.mobileChatContainer}>
146-
<div className={styles.mobileChatTop}>
147-
<ChatInteractionsContainer />
148+
<>
149+
<div className={styles.mobileChatContainer}>
150+
<div className={styles.mobileChatTop}>
151+
<ChatInteractionsContainer />
152+
</div>
153+
<div className={styles.mobileChatBottom}>
154+
<ChatInput />
155+
</div>
148156
</div>
149-
<div className={styles.mobileChatBottom}>
150-
<ChatInput />
151-
</div>
152-
</div>
157+
<Footer />
158+
</>
153159
)}
154-
<Footer />
155-
{!isMobileScreen && (
160+
{!isMobileScreen && mode === 'chat' && (
156161
<SlidingChatPanel
157162
open={isChatOpen}
158163
onClose={() => {
159164
setIsChatOpen(false);
160165
setMode('search');
166+
const searchInput = document.getElementById('orama-doc-search');
167+
searchInput?.focus();
161168
dispatch({ type: 'CLEAR_INTERACTIONS' });
162169
dispatch({ type: 'CLEAR_USER_PROMPT' });
163170
}}
@@ -210,8 +217,8 @@ const SearchWithModal: FC = () => {
210217
<Modal.Wrapper
211218
open={open}
212219
onModalClosed={() => setOpen(false)}
213-
closeOnOutsideClick={true}
214-
closeOnEscape={true}
220+
closeOnOutsideClick
221+
closeOnEscape
215222
className={styles.modalWrapper}
216223
>
217224
<Modal.Inner className={styles.modalInner}>

0 commit comments

Comments
 (0)