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": [
{
"path": "packages/docsearch-css/dist/style.css",
"maxSize": "8.05 kB"
"maxSize": "8.1 kB"
},
{
"path": "packages/docsearch-react/dist/umd/index.js",

View file

@ -1135,10 +1135,26 @@ assistive tech users */
.DocSearch-Hit-content-wrapper {
display: flex;
overflow-wrap: break-word;
position: relative;
white-space: normal;
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 {
border-radius: 0;
box-shadow: none;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,6 @@ export function useModalEnvironment({
inputRef,
initialScrollY,
modalRef,
snippetLength,
theme,
}: {
getEnvironmentProps: AutocompleteApi<
@ -30,7 +29,6 @@ export function useModalEnvironment({
inputRef: React.RefObject<HTMLInputElement | null>;
initialScrollY: number;
modalRef: React.RefObject<HTMLDivElement | null>;
snippetLength: React.MutableRefObject<number>;
theme?: DocSearchTheme;
}): void {
useTouchEvents({
@ -40,5 +38,5 @@ export function useModalEnvironment({
inputElement: inputRef.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>;
dropdownRef: React.RefObject<HTMLDivElement | null>;
inputRef: React.RefObject<HTMLInputElement | null>;
snippetLength: React.MutableRefObject<number>;
} {
const containerRef = React.useRef<HTMLDivElement | null>(null);
const modalRef = React.useRef<HTMLDivElement | null>(null);
const formElementRef = React.useRef<HTMLDivElement | null>(null);
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
const inputRef = React.useRef<HTMLInputElement | null>(null);
const snippetLength = React.useRef<number>(15);
return {
containerRef,
@ -21,6 +19,5 @@ export function useModalRefs(): {
formElementRef,
dropdownRef,
inputRef,
snippetLength,
};
}

View file

@ -63,55 +63,71 @@ function createHit({
describe('buildQuerySources', () => {
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({
query: 'install',
state: { context: { searchSuggestions: [] } },
setContext: vi.fn(),
setStatus: vi.fn(),
searchClient: {
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',
}),
],
},
],
}),
search,
} as any,
indexes: [{ name: 'docs' }],
snippetLength: { current: 15 },
insights: false,
saveRecentSearch: vi.fn(),
onClose: vi.fn(),
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([
'hits_docs_0',
'hits_docs_1',

View file

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