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:
parent
9a1b3e4c6c
commit
4e44b558e1
12 changed files with 88 additions and 80 deletions
6
.changeset/long-snippets-wrap.md
Normal file
6
.changeset/long-snippets-wrap.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
"@docsearch/react": patch
|
||||||
|
"@docsearch/css": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Stop over-truncating mobile snippets and allow long search hits to wrap.
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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} />);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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>',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue