1
0
Fork 0

fix(v5): stop truncating mobile snippets (#2958)

* fix(v5): stop truncating mobile snippets

Backport of #2907.\n\nOriginal commit: 9ad6d169fe

* fix(v5): allow mobile hit text to wrap

Completes the v5 adaptation of #2907 by overriding later v5 child-level truncation rules.\n\nOriginal commit: 9ad6d169fe

* chore(v5): account for mobile wrapping CSS

Updates the CSS size budget for the v5 adaptation of #2907.\n\nOriginal commit: 9ad6d169fe

---------

Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com>
This commit is contained in:
Paul Jankowski 2026-08-05 10:18:58 -04:00 committed by GitHub
parent 9a1b3e4c6c
commit 4e44b558e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 88 additions and 80 deletions

View file

@ -0,0 +1,6 @@
---
"@docsearch/react": patch
"@docsearch/css": patch
---
Stop over-truncating mobile snippets and allow long search hits to wrap.

View file

@ -2,7 +2,7 @@
"files": [ "files": [
{ {
"path": "packages/docsearch-css/dist/style.css", "path": "packages/docsearch-css/dist/style.css",
"maxSize": "8.05 kB" "maxSize": "8.1 kB"
}, },
{ {
"path": "packages/docsearch-react/dist/umd/index.js", "path": "packages/docsearch-react/dist/umd/index.js",

View file

@ -1135,10 +1135,26 @@ assistive tech users */
.DocSearch-Hit-content-wrapper { .DocSearch-Hit-content-wrapper {
display: flex; display: flex;
overflow-wrap: break-word;
position: relative; position: relative;
white-space: normal;
width: 80%; width: 80%;
} }
.DocSearch-Hit-title,
.DocSearch-Hit-path {
max-width: 100%;
overflow: visible;
text-overflow: clip;
white-space: normal;
}
.DocSearch-Hit-Container {
height: auto;
min-height: var(--docsearch-hit-height);
padding-block: 8px;
}
.DocSearch-Modal { .DocSearch-Modal {
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;

View file

@ -142,14 +142,8 @@ export function DocSearchAskAiModal({
'Ask another question...'; 'Ask another question...';
} }
const { const { containerRef, modalRef, formElementRef, dropdownRef, inputRef } =
containerRef, useModalRefs();
modalRef,
formElementRef,
dropdownRef,
inputRef,
snippetLength,
} = useModalRefs();
const { initialQuery, initialQueryFromSelection } = const { initialQuery, initialQueryFromSelection } =
useInitialModalQuery(initialQueryFromProp); useInitialModalQuery(initialQueryFromProp);
@ -417,7 +411,6 @@ export function DocSearchAskAiModal({
setStatus, setStatus,
searchClient, searchClient,
indexes, indexes,
snippetLength,
insights: Boolean(insights), insights: Boolean(insights),
appId, appId,
apiKey, apiKey,
@ -460,7 +453,6 @@ export function DocSearchAskAiModal({
inputRef, inputRef,
initialScrollY, initialScrollY,
modalRef, modalRef,
snippetLength,
theme, theme,
}); });

View file

@ -95,14 +95,8 @@ export function DocSearchModal({
props.placeholder || props.placeholder ||
'Search docs'; 'Search docs';
const { const { containerRef, modalRef, formElementRef, dropdownRef, inputRef } =
containerRef, useModalRefs();
modalRef,
formElementRef,
dropdownRef,
inputRef,
snippetLength,
} = useModalRefs();
const { initialQuery, initialQueryFromSelection } = const { initialQuery, initialQueryFromSelection } =
useInitialModalQuery(initialQueryFromProp); useInitialModalQuery(initialQueryFromProp);
@ -193,7 +187,6 @@ export function DocSearchModal({
setStatus, setStatus,
searchClient, searchClient,
indexes, indexes,
snippetLength,
insights: Boolean(insights), insights: Boolean(insights),
appId, appId,
apiKey, apiKey,
@ -221,7 +214,6 @@ export function DocSearchModal({
inputRef, inputRef,
initialScrollY, initialScrollY,
modalRef, modalRef,
snippetLength,
theme, theme,
}); });

View file

@ -1,3 +1,4 @@
export const MAX_QUERY_SIZE = 512; export const MAX_QUERY_SIZE = 512;
export const SNIPPET_LENGTH = 15;
export const SUGGESTED_QUETIONS_INDEX_NAME = export const SUGGESTED_QUETIONS_INDEX_NAME =
'algolia_ask_ai_suggested_questions'; 'algolia_ask_ai_suggested_questions';

View file

@ -117,7 +117,6 @@ describe('modal hooks', () => {
expect(firstResult.formElementRef.current).toBeNull(); expect(firstResult.formElementRef.current).toBeNull();
expect(firstResult.dropdownRef.current).toBeNull(); expect(firstResult.dropdownRef.current).toBeNull();
expect(firstResult.inputRef.current).toBeNull(); expect(firstResult.inputRef.current).toBeNull();
expect(firstResult.snippetLength.current).toBe(15);
rerender(<TestComponent onResult={onResult} />); rerender(<TestComponent onResult={onResult} />);

View file

@ -7,12 +7,10 @@ import { manageLocalStorageQuota } from '../utils/storage';
export function useDocSearchModalEffects({ export function useDocSearchModalEffects({
initialScrollY, initialScrollY,
modalRef, modalRef,
snippetLength,
theme, theme,
}: { }: {
initialScrollY: number; initialScrollY: number;
modalRef: React.RefObject<HTMLDivElement | null>; modalRef: React.RefObject<HTMLDivElement | null>;
snippetLength: React.MutableRefObject<number>;
theme?: DocSearchTheme; theme?: DocSearchTheme;
}): void { }): void {
useTheme({ theme }); useTheme({ theme });
@ -39,12 +37,6 @@ export function useDocSearchModalEffects({
}; };
}, []); }, []);
React.useEffect(() => {
if (window.matchMedia('(max-width: 768px)').matches) {
snippetLength.current = 5;
}
}, [snippetLength]);
React.useEffect(() => { React.useEffect(() => {
function setFullViewportHeight(): void { function setFullViewportHeight(): void {
if (modalRef.current) { if (modalRef.current) {

View file

@ -15,7 +15,6 @@ export function useModalEnvironment({
inputRef, inputRef,
initialScrollY, initialScrollY,
modalRef, modalRef,
snippetLength,
theme, theme,
}: { }: {
getEnvironmentProps: AutocompleteApi< getEnvironmentProps: AutocompleteApi<
@ -30,7 +29,6 @@ export function useModalEnvironment({
inputRef: React.RefObject<HTMLInputElement | null>; inputRef: React.RefObject<HTMLInputElement | null>;
initialScrollY: number; initialScrollY: number;
modalRef: React.RefObject<HTMLDivElement | null>; modalRef: React.RefObject<HTMLDivElement | null>;
snippetLength: React.MutableRefObject<number>;
theme?: DocSearchTheme; theme?: DocSearchTheme;
}): void { }): void {
useTouchEvents({ useTouchEvents({
@ -40,5 +38,5 @@ export function useModalEnvironment({
inputElement: inputRef.current, inputElement: inputRef.current,
}); });
useTrapFocus({ container: containerRef.current }); useTrapFocus({ container: containerRef.current });
useDocSearchModalEffects({ initialScrollY, modalRef, snippetLength, theme }); useDocSearchModalEffects({ initialScrollY, modalRef, theme });
} }

View file

@ -6,14 +6,12 @@ export function useModalRefs(): {
formElementRef: React.RefObject<HTMLDivElement | null>; formElementRef: React.RefObject<HTMLDivElement | null>;
dropdownRef: React.RefObject<HTMLDivElement | null>; dropdownRef: React.RefObject<HTMLDivElement | null>;
inputRef: React.RefObject<HTMLInputElement | null>; inputRef: React.RefObject<HTMLInputElement | null>;
snippetLength: React.MutableRefObject<number>;
} { } {
const containerRef = React.useRef<HTMLDivElement | null>(null); const containerRef = React.useRef<HTMLDivElement | null>(null);
const modalRef = React.useRef<HTMLDivElement | null>(null); const modalRef = React.useRef<HTMLDivElement | null>(null);
const formElementRef = React.useRef<HTMLDivElement | null>(null); const formElementRef = React.useRef<HTMLDivElement | null>(null);
const dropdownRef = React.useRef<HTMLDivElement | null>(null); const dropdownRef = React.useRef<HTMLDivElement | null>(null);
const inputRef = React.useRef<HTMLInputElement | null>(null); const inputRef = React.useRef<HTMLInputElement | null>(null);
const snippetLength = React.useRef<number>(15);
return { return {
containerRef, containerRef,
@ -21,6 +19,5 @@ export function useModalRefs(): {
formElementRef, formElementRef,
dropdownRef, dropdownRef,
inputRef, inputRef,
snippetLength,
}; };
} }

View file

@ -63,55 +63,71 @@ function createHit({
describe('buildQuerySources', () => { describe('buildQuerySources', () => {
it('creates a source per lvl0 group and scopes parents to that group', async () => { it('creates a source per lvl0 group and scopes parents to that group', async () => {
const search = vi.fn().mockResolvedValue({
results: [
{
index: 'docs',
nbHits: 4,
hits: [
createHit({
objectID: 'guide-install',
lvl0: 'Guides',
lvl1: 'Installation',
type: 'lvl1',
}),
createHit({
objectID: 'guide-install-content',
lvl0: 'Guides',
lvl1: 'Installation',
type: 'content',
}),
createHit({
objectID: 'reference-install',
lvl0: 'Reference',
lvl1: 'Installation',
type: 'lvl1',
}),
createHit({
objectID: 'reference-install-content',
lvl0: 'Reference',
lvl1: 'Installation',
type: 'content',
}),
],
},
],
});
const sources = await buildQuerySources({ const sources = await buildQuerySources({
query: 'install', query: 'install',
state: { context: { searchSuggestions: [] } }, state: { context: { searchSuggestions: [] } },
setContext: vi.fn(), setContext: vi.fn(),
setStatus: vi.fn(), setStatus: vi.fn(),
searchClient: { searchClient: {
search: vi.fn().mockResolvedValue({ search,
results: [
{
index: 'docs',
nbHits: 4,
hits: [
createHit({
objectID: 'guide-install',
lvl0: 'Guides',
lvl1: 'Installation',
type: 'lvl1',
}),
createHit({
objectID: 'guide-install-content',
lvl0: 'Guides',
lvl1: 'Installation',
type: 'content',
}),
createHit({
objectID: 'reference-install',
lvl0: 'Reference',
lvl1: 'Installation',
type: 'lvl1',
}),
createHit({
objectID: 'reference-install-content',
lvl0: 'Reference',
lvl1: 'Installation',
type: 'content',
}),
],
},
],
}),
} as any, } as any,
indexes: [{ name: 'docs' }], indexes: [{ name: 'docs' }],
snippetLength: { current: 15 },
insights: false, insights: false,
saveRecentSearch: vi.fn(), saveRecentSearch: vi.fn(),
onClose: vi.fn(), onClose: vi.fn(),
facetSelections: { current: {} }, facetSelections: { current: {} },
}); });
expect(search).toHaveBeenCalledWith({
requests: [
expect.objectContaining({
attributesToSnippet: [
'hierarchy.lvl1:15',
'hierarchy.lvl2:15',
'hierarchy.lvl3:15',
'hierarchy.lvl4:15',
'hierarchy.lvl5:15',
'hierarchy.lvl6:15',
'content:15',
],
}),
],
});
expect(sources.map((source) => source.sourceId)).toEqual([ expect(sources.map((source) => source.sourceId)).toEqual([
'hits_docs_0', 'hits_docs_0',
'hits_docs_1', 'hits_docs_1',

View file

@ -9,6 +9,7 @@ import type {
} from 'algoliasearch/lite'; } from 'algoliasearch/lite';
import type React from 'react'; import type React from 'react';
import { SNIPPET_LENGTH } from '../constants';
import type { DocSearchIndex, DocSearchProps } from '../DocSearch'; import type { DocSearchIndex, DocSearchProps } from '../DocSearch';
import type { import type {
DocSearchHit, DocSearchHit,
@ -135,7 +136,6 @@ export async function buildQuerySources({
setStatus, setStatus,
searchClient, searchClient,
indexes, indexes,
snippetLength,
insights, insights,
appId, appId,
apiKey, apiKey,
@ -153,7 +153,6 @@ export async function buildQuerySources({
setStatus: (status: DocSearchState<InternalDocSearchHit>['status']) => void; setStatus: (status: DocSearchState<InternalDocSearchHit>['status']) => void;
searchClient: ReturnType<typeof useSearchClient>; searchClient: ReturnType<typeof useSearchClient>;
indexes: DocSearchIndex[]; indexes: DocSearchIndex[];
snippetLength: React.MutableRefObject<number>;
insights: boolean; insights: boolean;
appId?: string; appId?: string;
apiKey?: string; apiKey?: string;
@ -191,13 +190,13 @@ export async function buildQuerySources({
'url', 'url',
], ],
attributesToSnippet: searchParams?.attributesToSnippet ?? [ attributesToSnippet: searchParams?.attributesToSnippet ?? [
`hierarchy.lvl1:${snippetLength.current}`, `hierarchy.lvl1:${SNIPPET_LENGTH}`,
`hierarchy.lvl2:${snippetLength.current}`, `hierarchy.lvl2:${SNIPPET_LENGTH}`,
`hierarchy.lvl3:${snippetLength.current}`, `hierarchy.lvl3:${SNIPPET_LENGTH}`,
`hierarchy.lvl4:${snippetLength.current}`, `hierarchy.lvl4:${SNIPPET_LENGTH}`,
`hierarchy.lvl5:${snippetLength.current}`, `hierarchy.lvl5:${SNIPPET_LENGTH}`,
`hierarchy.lvl6:${snippetLength.current}`, `hierarchy.lvl6:${SNIPPET_LENGTH}`,
`content:${snippetLength.current}`, `content:${SNIPPET_LENGTH}`,
], ],
snippetEllipsisText: searchParams?.snippetEllipsisText ?? '…', snippetEllipsisText: searchParams?.snippetEllipsisText ?? '…',
highlightPreTag: searchParams?.highlightPreTag ?? '<mark>', highlightPreTag: searchParams?.highlightPreTag ?? '<mark>',