diff --git a/.changeset/long-snippets-wrap.md b/.changeset/long-snippets-wrap.md new file mode 100644 index 00000000..28ece56d --- /dev/null +++ b/.changeset/long-snippets-wrap.md @@ -0,0 +1,6 @@ +--- +"@docsearch/react": patch +"@docsearch/css": patch +--- + +Stop over-truncating mobile snippets and allow long search hits to wrap. diff --git a/bundlesize.config.json b/bundlesize.config.json index 402f132a..3b4562c1 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -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", diff --git a/packages/docsearch-css/src/modal.css b/packages/docsearch-css/src/modal.css index 040f38bf..5d3fea32 100644 --- a/packages/docsearch-css/src/modal.css +++ b/packages/docsearch-css/src/modal.css @@ -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; diff --git a/packages/docsearch-react/src/DocSearchAskAiModal.tsx b/packages/docsearch-react/src/DocSearchAskAiModal.tsx index 10090858..999d0754 100644 --- a/packages/docsearch-react/src/DocSearchAskAiModal.tsx +++ b/packages/docsearch-react/src/DocSearchAskAiModal.tsx @@ -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, }); diff --git a/packages/docsearch-react/src/DocSearchModal.tsx b/packages/docsearch-react/src/DocSearchModal.tsx index 6299b7e6..b74482e7 100644 --- a/packages/docsearch-react/src/DocSearchModal.tsx +++ b/packages/docsearch-react/src/DocSearchModal.tsx @@ -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, }); diff --git a/packages/docsearch-react/src/constants.ts b/packages/docsearch-react/src/constants.ts index 3fb3702c..212fc789 100644 --- a/packages/docsearch-react/src/constants.ts +++ b/packages/docsearch-react/src/constants.ts @@ -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'; diff --git a/packages/docsearch-react/src/hooks/__tests__/modal.test.tsx b/packages/docsearch-react/src/hooks/__tests__/modal.test.tsx index 7b70e9ee..0d922d8c 100644 --- a/packages/docsearch-react/src/hooks/__tests__/modal.test.tsx +++ b/packages/docsearch-react/src/hooks/__tests__/modal.test.tsx @@ -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(); diff --git a/packages/docsearch-react/src/hooks/useDocSearchModalEffects.ts b/packages/docsearch-react/src/hooks/useDocSearchModalEffects.ts index e0e92f83..8e17991b 100644 --- a/packages/docsearch-react/src/hooks/useDocSearchModalEffects.ts +++ b/packages/docsearch-react/src/hooks/useDocSearchModalEffects.ts @@ -7,12 +7,10 @@ import { manageLocalStorageQuota } from '../utils/storage'; export function useDocSearchModalEffects({ initialScrollY, modalRef, - snippetLength, theme, }: { initialScrollY: number; modalRef: React.RefObject; - snippetLength: React.MutableRefObject; 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) { diff --git a/packages/docsearch-react/src/hooks/useModalEnvironment.ts b/packages/docsearch-react/src/hooks/useModalEnvironment.ts index 3ad094a9..e5de889f 100644 --- a/packages/docsearch-react/src/hooks/useModalEnvironment.ts +++ b/packages/docsearch-react/src/hooks/useModalEnvironment.ts @@ -15,7 +15,6 @@ export function useModalEnvironment({ inputRef, initialScrollY, modalRef, - snippetLength, theme, }: { getEnvironmentProps: AutocompleteApi< @@ -30,7 +29,6 @@ export function useModalEnvironment({ inputRef: React.RefObject; initialScrollY: number; modalRef: React.RefObject; - snippetLength: React.MutableRefObject; 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 }); } diff --git a/packages/docsearch-react/src/hooks/useModalRefs.ts b/packages/docsearch-react/src/hooks/useModalRefs.ts index ed3ebe67..ccc7c7c3 100644 --- a/packages/docsearch-react/src/hooks/useModalRefs.ts +++ b/packages/docsearch-react/src/hooks/useModalRefs.ts @@ -6,14 +6,12 @@ export function useModalRefs(): { formElementRef: React.RefObject; dropdownRef: React.RefObject; inputRef: React.RefObject; - snippetLength: React.MutableRefObject; } { const containerRef = React.useRef(null); const modalRef = React.useRef(null); const formElementRef = React.useRef(null); const dropdownRef = React.useRef(null); const inputRef = React.useRef(null); - const snippetLength = React.useRef(15); return { containerRef, @@ -21,6 +19,5 @@ export function useModalRefs(): { formElementRef, dropdownRef, inputRef, - snippetLength, }; } diff --git a/packages/docsearch-react/src/utils/__tests__/createDocSearchSources.test.ts b/packages/docsearch-react/src/utils/__tests__/createDocSearchSources.test.ts index 57860388..45b7f173 100644 --- a/packages/docsearch-react/src/utils/__tests__/createDocSearchSources.test.ts +++ b/packages/docsearch-react/src/utils/__tests__/createDocSearchSources.test.ts @@ -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', diff --git a/packages/docsearch-react/src/utils/createDocSearchSources.ts b/packages/docsearch-react/src/utils/createDocSearchSources.ts index 428bcb48..7d5dc226 100644 --- a/packages/docsearch-react/src/utils/createDocSearchSources.ts +++ b/packages/docsearch-react/src/utils/createDocSearchSources.ts @@ -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['status']) => void; searchClient: ReturnType; indexes: DocSearchIndex[]; - snippetLength: React.MutableRefObject; 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 ?? '',