From 85568ce0952007f94e0649d752c25e1c61c36d99 Mon Sep 17 00:00:00 2001 From: NiteshSingh17 <79739154+NiteshSingh17@users.noreply.github.com> Date: Tue, 8 Jul 2025 23:35:25 +0530 Subject: [PATCH] feat: add theme prop (#2651) Co-authored-by: Dylan Tientcheu --- packages/docsearch-react/src/DocSearch.tsx | 9 ++++--- .../docsearch-react/src/DocSearchButton.tsx | 7 ++++-- .../docsearch-react/src/DocSearchModal.tsx | 5 +++- .../src/__tests__/api.test.tsx | 12 ++++++++++ .../src/types/DocSearchTheme.ts | 1 + packages/docsearch-react/src/types/index.ts | 1 + packages/docsearch-react/src/useTheme.tsx | 23 ++++++++++++++++++ packages/website/docs/api.mdx | 24 +++++++++++++++++++ 8 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 packages/docsearch-react/src/types/DocSearchTheme.ts create mode 100644 packages/docsearch-react/src/useTheme.tsx diff --git a/packages/docsearch-react/src/DocSearch.tsx b/packages/docsearch-react/src/DocSearch.tsx index 4bf5a9f4..54668a90 100644 --- a/packages/docsearch-react/src/DocSearch.tsx +++ b/packages/docsearch-react/src/DocSearch.tsx @@ -1,12 +1,13 @@ -import type { AutocompleteState, AutocompleteOptions } from '@algolia/autocomplete-core'; +import type { AutocompleteOptions, AutocompleteState } from '@algolia/autocomplete-core'; import type { LiteClient, SearchParamsObject } from 'algoliasearch/lite'; import React, { type JSX } from 'react'; import { createPortal } from 'react-dom'; import { DocSearchButton } from './DocSearchButton'; import { DocSearchModal } from './DocSearchModal'; -import type { DocSearchHit, InternalDocSearchHit, StoredDocSearchHit } from './types'; +import type { DocSearchHit, DocSearchTheme, InternalDocSearchHit, StoredDocSearchHit } from './types'; import { useDocSearchKeyboardEvents } from './useDocSearchKeyboardEvents'; +import { useTheme } from './useTheme'; import type { ButtonTranslations, ModalTranslations } from '.'; @@ -26,6 +27,7 @@ export interface DocSearchProps { appId: string; apiKey: string; indexName: string; + theme?: DocSearchTheme; placeholder?: string; searchParameters?: SearchParamsObject; maxResultsPerGroup?: number; @@ -41,7 +43,7 @@ export interface DocSearchProps { insights?: AutocompleteOptions['insights']; } -export function DocSearch(props: DocSearchProps): JSX.Element { +export function DocSearch({ ...props }: DocSearchProps): JSX.Element { const searchButtonRef = React.useRef(null); const [isOpen, setIsOpen] = React.useState(false); const [initialQuery, setInitialQuery] = React.useState(props?.initialQuery || undefined); @@ -70,6 +72,7 @@ export function DocSearch(props: DocSearchProps): JSX.Element { onInput, searchButtonRef, }); + useTheme({ theme: props.theme }); return ( <> diff --git a/packages/docsearch-react/src/DocSearchButton.tsx b/packages/docsearch-react/src/DocSearchButton.tsx index 7e7e101d..50db9aa2 100644 --- a/packages/docsearch-react/src/DocSearchButton.tsx +++ b/packages/docsearch-react/src/DocSearchButton.tsx @@ -1,7 +1,9 @@ -import React, { type JSX, useEffect, useState } from 'react'; +import React, { useEffect, useState, type JSX } from 'react'; import { ControlKeyIcon } from './icons/ControlKeyIcon'; import { SearchIcon } from './icons/SearchIcon'; +import type { DocSearchTheme } from './types'; +import { useTheme } from './useTheme'; export type ButtonTranslations = Partial<{ buttonText: string; @@ -9,6 +11,7 @@ export type ButtonTranslations = Partial<{ }>; export type DocSearchButtonProps = React.ComponentProps<'button'> & { + theme?: DocSearchTheme; translations?: ButtonTranslations; }; @@ -24,7 +27,7 @@ export const DocSearchButton = React.forwardRef(null); - + useTheme({ theme: props.theme }); useEffect(() => { if (typeof navigator !== 'undefined') { isAppleDevice() ? setKey(ACTION_KEY_APPLE) : setKey(ACTION_KEY_DEFAULT); diff --git a/packages/docsearch-react/src/DocSearchModal.tsx b/packages/docsearch-react/src/DocSearchModal.tsx index 6fc99d52..ac758096 100644 --- a/packages/docsearch-react/src/DocSearchModal.tsx +++ b/packages/docsearch-react/src/DocSearchModal.tsx @@ -14,9 +14,10 @@ import { SearchBox } from './SearchBox'; import { createStoredSearches } from './stored-searches'; import type { DocSearchHit, DocSearchState, InternalDocSearchHit, StoredDocSearchHit } from './types'; import { useSearchClient } from './useSearchClient'; +import { useTheme } from './useTheme'; import { useTouchEvents } from './useTouchEvents'; import { useTrapFocus } from './useTrapFocus'; -import { groupBy, identity, noop, removeHighlightTags, isModifierEvent } from './utils'; +import { groupBy, identity, isModifierEvent, noop, removeHighlightTags } from './utils'; export type ModalTranslations = Partial<{ searchBox: SearchBoxTranslations; @@ -37,6 +38,7 @@ export function DocSearchModal({ placeholder = 'Search docs', searchParameters, maxResultsPerGroup, + theme, onClose = noop, transformItems = identity, hitComponent = Hit, @@ -335,6 +337,7 @@ export function DocSearchModal({ inputElement: inputRef.current, }); useTrapFocus({ container: containerRef.current }); + useTheme({ theme }); React.useEffect(() => { document.body.classList.add('DocSearch--active'); diff --git a/packages/docsearch-react/src/__tests__/api.test.tsx b/packages/docsearch-react/src/__tests__/api.test.tsx index 84439590..6cffdcf9 100644 --- a/packages/docsearch-react/src/__tests__/api.test.tsx +++ b/packages/docsearch-react/src/__tests__/api.test.tsx @@ -261,4 +261,16 @@ describe('api', () => { expect(link?.getAttribute('href')).toBe('https://github.com/algolia/docsearch/issues/new?title=q'); }); }); + + describe('Theme', () => { + const html = document.documentElement; + it('light theme', () => { + render(); + expect(html.getAttribute('data-theme')).toBe('light'); + }); + it('dark theme', () => { + render(); + expect(html.getAttribute('data-theme')).toBe('dark'); + }); + }); }); diff --git a/packages/docsearch-react/src/types/DocSearchTheme.ts b/packages/docsearch-react/src/types/DocSearchTheme.ts new file mode 100644 index 00000000..4de0b6e1 --- /dev/null +++ b/packages/docsearch-react/src/types/DocSearchTheme.ts @@ -0,0 +1 @@ +export type DocSearchTheme = 'dark' | 'light'; diff --git a/packages/docsearch-react/src/types/index.ts b/packages/docsearch-react/src/types/index.ts index 37ac8f9a..2922cbc2 100644 --- a/packages/docsearch-react/src/types/index.ts +++ b/packages/docsearch-react/src/types/index.ts @@ -1,4 +1,5 @@ export * from './DocSearchHit'; export * from './DocSearchState'; +export * from './DocSearchTheme'; export * from './InternalDocSearchHit'; export * from './StoredDocSearchHit'; diff --git a/packages/docsearch-react/src/useTheme.tsx b/packages/docsearch-react/src/useTheme.tsx new file mode 100644 index 00000000..905c795b --- /dev/null +++ b/packages/docsearch-react/src/useTheme.tsx @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; + +import type { DocSearchTheme } from './types'; + +export interface UseThemeProps { + theme?: DocSearchTheme; +} + +export const useTheme = ({ theme }: UseThemeProps): void => { + useEffect(() => { + if (theme) { + const previousTheme = document.documentElement.dataset.theme; + if (theme !== previousTheme) { + document.documentElement.dataset.theme = theme; + return (): void => { + if (previousTheme === undefined) delete document.documentElement.dataset.theme; + else document.documentElement.dataset.theme = previousTheme; + }; + } + } + return undefined; + }, [theme]); +}; diff --git a/packages/website/docs/api.mdx b/packages/website/docs/api.mdx index 09c08fcd..2736e180 100644 --- a/packages/website/docs/api.mdx +++ b/packages/website/docs/api.mdx @@ -349,6 +349,30 @@ docsearch({ +## `theme` + +> `type: DocSearchTheme` | `default: "light"` | **optional** + +Theme for DocSearch. Can be either `light` or `dark`. + + + + +```js +docsearch({ + // ... + theme: 'light', +}); +``` + + + [1]: https://www.algolia.com/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete/ [2]: https://github.com/algolia/docsearch/ [3]: https://github.com/algolia/docsearch/tree/master