Skip to content

Commit 5e9bdfe

Browse files
authored
feat(composable-api): Introduce new Composable API (#2779)
* feat(composable-api): Step 1, new core package, pull basic state up higer (#2771) * feat(composable-api): Step 1, new core package, pull basic state up higher * Package cleanup, update core Readme * Cleanup to fix weird import behavior * Fix dependencies * Try fixing deps again * Track new package dist in CI * Make sure to restore core package * feat(composable-api): Step 2, composable DocSearchButton (#2772) * feat(composable-api): Step 1, new core package, pull basic state up higher * Package cleanup, update core Readme * Cleanup to fix weird import behavior * Fix dependencies * Try fixing deps again * Track new package dist in CI * Make sure to restore core package * feat(composable-api): Step 2, move DocSearchButton to be composable * Implement keyboard shortcuts props * Set modal package version * Render new DocSearchButton in react package * Bump allowed bundle size for now * feat(composable-api): Step 3, composable DocSearchModal (#2774) * feat(composable-api): Step 1, new core package, pull basic state up higher * Package cleanup, update core Readme * Cleanup to fix weird import behavior * Fix dependencies * Try fixing deps again * Track new package dist in CI * Make sure to restore core package * feat(composable-api): Step 2, move DocSearchButton to be composable * Implement keyboard shortcuts props * Set modal package version * Render new DocSearchButton in react package * Bump allowed bundle size for now * feat(composable-api): Step 3, make DocSearchModal composable * Clean up needed dependencies, add more bundle checks * fix: CI * add test cases to core package * add test cases to modal package * fixes after rebase * fix: lint * Memoize modal props to stabalize rendering * Dont track isModalActive in props * bump @docsearch/modal bundlesize * remove icons dir from @docsearch-modal * update version definition for @docsearch-react and @docsearch-modal * fix: lint
1 parent 5381e76 commit 5e9bdfe

40 files changed

Lines changed: 1946 additions & 410 deletions

.circleci/config.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ aliases:
2525
mkdir -p packages/docsearch-react/dist
2626
mkdir -p packages/docsearch-js/dist
2727
mkdir -p packages/docsearch-css/dist
28+
mkdir -p packages/docsearch-core/dist
29+
mkdir -p packages/docsearch-modal/dist
2830
2931
cp -R /tmp/workspace/packages/docsearch-react/dist packages/docsearch-react
3032
cp -R /tmp/workspace/packages/docsearch-js/dist packages/docsearch-js
3133
cp -R /tmp/workspace/packages/docsearch-css/dist packages/docsearch-css
34+
cp -R /tmp/workspace/packages/docsearch-core/dist packages/docsearch-core
35+
cp -R /tmp/workspace/packages/docsearch-modal/dist packages/docsearch-modal
3236
3337
defaults: &defaults
3438
working_directory: ~/docsearch
@@ -69,10 +73,14 @@ jobs:
6973
mkdir -p /tmp/workspace/packages/docsearch-react/dist
7074
mkdir -p /tmp/workspace/packages/docsearch-js/dist
7175
mkdir -p /tmp/workspace/packages/docsearch-css/dist
76+
mkdir -p /tmp/workspace/packages/docsearch-core/dist
77+
mkdir -p /tmp/workspace/packages/docsearch-modal/dist
7278
7379
cp -R packages/docsearch-react/dist /tmp/workspace/packages/docsearch-react
7480
cp -R packages/docsearch-js/dist /tmp/workspace/packages/docsearch-js
7581
cp -R packages/docsearch-css/dist /tmp/workspace/packages/docsearch-css
82+
cp -R packages/docsearch-core/dist /tmp/workspace/packages/docsearch-core
83+
cp -R packages/docsearch-modal/dist /tmp/workspace/packages/docsearch-modal
7684
- persist_to_workspace:
7785
root: *workspace_root
7886
paths:

bundlesize.config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
{
1212
"path": "packages/docsearch-js/dist/umd/index.js",
1313
"maxSize": "128 kB"
14+
},
15+
{
16+
"path": "packages/docsearch-core/dist/umd/index.js",
17+
"maxSize": "3 kB"
18+
},
19+
{
20+
"path": "packages/docsearch-modal/dist/umd/index.js",
21+
"maxSize": "113 kB"
1422
}
1523
]
1624
}

examples/demo-react/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"preview": "vite preview"
1212
},
1313
"dependencies": {
14+
"@docsearch/core": "workspace:*",
1415
"@docsearch/css": "workspace:*",
16+
"@docsearch/modal": "workspace:*",
1517
"@docsearch/react": "workspace:*",
1618
"react": "^19.0.0",
1719
"react-dom": "^19.0.0"

examples/demo-react/src/App.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import '@docsearch/css/dist/style.css';
77

88
import Basic from './examples/basic';
99
import BasicAskAI from './examples/basic-askai';
10+
import Composable from './examples/composable';
11+
import DynamicImportModal from './examples/dynamic-import-modal';
1012
import MultiIndex from './examples/multi-index';
1113
import WHitComponent from './examples/w-hit-component';
1214
import WTransformItems from './examples/w-hit-transformItems';
@@ -57,6 +59,20 @@ function App(): JSX.Element {
5759
</div>
5860
</section>
5961

62+
<section className="demo-section">
63+
<p className="section-description">composable</p>
64+
<div className="search-wrapper">
65+
<Composable />
66+
</div>
67+
</section>
68+
69+
<section className="demo-section">
70+
<p className="section-description">dynamically imported modal</p>
71+
<div className="search-wrapper">
72+
<DynamicImportModal />
73+
</div>
74+
</section>
75+
6076
<section className="demo-section">
6177
<p className="section-description">results footer component</p>
6278
<div className="search-wrapper">
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* eslint-disable react/react-in-jsx-scope */
2+
import { DocSearch } from '@docsearch/core';
3+
import { DocSearchButton, DocSearchModal } from '@docsearch/modal';
4+
import { type JSX } from 'react';
5+
6+
export default function Composable(): JSX.Element {
7+
return (
8+
<DocSearch>
9+
<DocSearchButton translations={{ buttonText: 'Composable API' }} />
10+
<DocSearchModal
11+
indexName="docsearch"
12+
appId="PMZUYBQDAK"
13+
apiKey="24b09689d5b4223813d9b8e48563c8f6"
14+
askAi={{
15+
assistantId: 'askAIDemo',
16+
searchParameters: {
17+
facetFilters: ['language:en'],
18+
},
19+
}}
20+
/>
21+
</DocSearch>
22+
);
23+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/* eslint-disable react/react-in-jsx-scope */
2+
import { useDocSearchKeyboardEvents } from '@docsearch/core/useDocSearchKeyboardEvents';
3+
import { DocSearchButton } from '@docsearch/react/button';
4+
import type { DocSearchModal as DocSearchModalType } from '@docsearch/react/modal';
5+
import { useCallback, useRef, useState, type JSX } from 'react';
6+
import { createPortal } from 'react-dom';
7+
8+
let DocSearchModal: typeof DocSearchModalType | null = null;
9+
10+
function importDocSearchModalIfNeeded(): Promise<void> {
11+
if (DocSearchModal) {
12+
return Promise.resolve();
13+
}
14+
// eslint-disable-next-line import/dynamic-import-chunkname
15+
return Promise.all([import('@docsearch/react/modal')]).then(([{ DocSearchModal: Modal }]) => {
16+
DocSearchModal = Modal;
17+
});
18+
}
19+
20+
function DocSearch(): JSX.Element {
21+
const [isOpen, setIsOpen] = useState(false);
22+
const [initialQuery, setInitialQuery] = useState<string | undefined>(undefined);
23+
const [isAskAiActive, setIsAskAiActive] = useState(false);
24+
const searchContainer = useRef<HTMLDivElement | null>(null);
25+
const searchButtonRef = useRef<HTMLButtonElement | null>(null);
26+
27+
const prepareSearchContainer = useCallback(() => {
28+
if (!searchContainer.current) {
29+
const divElement = document.createElement('div');
30+
searchContainer.current = divElement;
31+
document.body.insertBefore(divElement, document.body.firstChild);
32+
}
33+
}, []);
34+
35+
const openModal = useCallback(() => {
36+
prepareSearchContainer();
37+
importDocSearchModalIfNeeded().then(() => setIsOpen(true));
38+
}, [prepareSearchContainer]);
39+
40+
const closeModal = useCallback(() => {
41+
setIsOpen(false);
42+
searchButtonRef.current?.focus();
43+
setInitialQuery(undefined);
44+
}, []);
45+
46+
const handleInput = useCallback(
47+
(event: KeyboardEvent) => {
48+
if (event.key === 'f' && (event.metaKey || event.ctrlKey)) {
49+
// ignore browser's ctrl+f
50+
return;
51+
}
52+
// prevents duplicate key insertion in the modal input
53+
event.preventDefault();
54+
setInitialQuery(event.key);
55+
openModal();
56+
},
57+
[openModal],
58+
);
59+
60+
const toggleAskAi = (active: boolean): void => {
61+
setIsAskAiActive(active);
62+
};
63+
64+
useDocSearchKeyboardEvents({
65+
isOpen,
66+
onOpen: openModal,
67+
onClose: closeModal,
68+
onInput: handleInput,
69+
searchButtonRef,
70+
isAskAiActive,
71+
onAskAiToggle: toggleAskAi,
72+
});
73+
74+
return (
75+
<>
76+
<DocSearchButton
77+
ref={searchButtonRef}
78+
translations={{ buttonText: 'Dynamic modal search' }}
79+
onTouchStart={importDocSearchModalIfNeeded}
80+
onFocus={importDocSearchModalIfNeeded}
81+
onMouseOver={importDocSearchModalIfNeeded}
82+
onClick={openModal}
83+
/>
84+
85+
{isOpen &&
86+
DocSearchModal &&
87+
searchContainer.current &&
88+
createPortal(
89+
<DocSearchModal
90+
indexName="docsearch"
91+
appId="PMZUYBQDAK"
92+
apiKey="24b09689d5b4223813d9b8e48563c8f6"
93+
askAi={{
94+
assistantId: 'askAIDemo',
95+
searchParameters: {
96+
facetFilters: ['language:en'],
97+
},
98+
}}
99+
initialScrollY={window.scrollY}
100+
initialQuery={initialQuery}
101+
isAskAiActive={isAskAiActive}
102+
canHandleAskAi={true}
103+
onClose={closeModal}
104+
onAskAiToggle={toggleAskAi}
105+
/>,
106+
searchContainer.current,
107+
)}
108+
</>
109+
);
110+
}
111+
112+
export default function DynamicImportModal(): JSX.Element {
113+
return <DocSearch />;
114+
}

packages/docsearch-core/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# @docsearch/core
2+
3+
Core logic and state package for [DocSearch](http://docsearch.algolia.com/), the best search experience for docs.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export default (api) => {
2+
const isTest = api.env('test');
3+
const targets = {};
4+
5+
if (isTest) {
6+
targets.node = true;
7+
} else {
8+
targets.browsers = ['last 2 versions', 'ie >= 9'];
9+
}
10+
11+
return {
12+
presets: [
13+
'@babel/preset-typescript',
14+
[
15+
'@babel/preset-env',
16+
{
17+
targets,
18+
},
19+
],
20+
],
21+
plugins: [['@babel/plugin-transform-react-jsx']],
22+
};
23+
};

packages/docsearch-core/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { DocSearch, useDocSearch } from './dist/esm/index.js';
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"name": "@docsearch/core",
3+
"description": "Core package logic for DocSearch",
4+
"version": "1.0.0",
5+
"license": "MIT",
6+
"homepage": "https://docsearch.algolia.com",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/algolia/docsearch.git",
10+
"directory": "packages/docsearch-react"
11+
},
12+
"author": {
13+
"name": "Algolia, Inc.",
14+
"url": "https://www.algolia.com"
15+
},
16+
"sideEffects": false,
17+
"files": [
18+
"dist/",
19+
"index.js",
20+
"useTheme.js",
21+
"useDocSearchKeyboardEvents.js",
22+
"useKeyboardShortcuts.js"
23+
],
24+
"exports": {
25+
".": "./dist/esm/index.js",
26+
"./useTheme": "./dist/esm/useTheme.js",
27+
"./useDocSearchKeyboardEvents": "./dist/esm/useDocSearchKeyboardEvents.js",
28+
"./useKeyboardShortcuts": "./dist/esm/useKeyboardShortcuts.js"
29+
},
30+
"source": "src/index.ts",
31+
"types": "dist/esm/index.d.ts",
32+
"module": "dist/esm/index.js",
33+
"main": "dist/umd/index.js",
34+
"umd:main": "dist/umd/index.js",
35+
"unpkg": "dist/umd/index.js",
36+
"jsdelivr": "dist/umd/index.js",
37+
"scripts": {
38+
"build:clean": "rm -rf ./dist",
39+
"build:clean-types": "rm -rf ./dist/esm/types",
40+
"build:types": "tsc -p ./tsconfig.declaration.json --outDir ./dist/esm/types",
41+
"build": "yarn build:clean && yarn build:types && rollup --config --bundleConfigAsCjs && yarn build:clean-types",
42+
"on:change": "yarn build",
43+
"watch": "nodemon --watch src --ext ts,tsx,js,jsx,json --ignore dist/ --ignore node_modules/ --verbose --delay 250ms --exec \"yarn on:change\""
44+
},
45+
"devDependencies": {
46+
"@rollup/plugin-replace": "6.0.2",
47+
"@testing-library/jest-dom": "6.6.3",
48+
"@testing-library/react": "16.2.0",
49+
"nodemon": "^3.1.0",
50+
"rollup-plugin-dts": "^6.2.1",
51+
"vitest": "3.0.2"
52+
},
53+
"peerDependencies": {
54+
"@types/react": ">= 16.8.0 < 20.0.0",
55+
"react": ">= 16.8.0 < 20.0.0",
56+
"react-dom": ">= 16.8.0 < 20.0.0"
57+
},
58+
"peerDependenciesMeta": {
59+
"@types/react": {
60+
"optional": true
61+
},
62+
"react": {
63+
"optional": true
64+
},
65+
"react-dom": {
66+
"optional": true
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)