@redocly/theme
Version:
Shared UI components lib
448 lines (434 loc) • 24 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SearchDialog = SearchDialog;
const react_1 = __importStar(require("react"));
const styled_components_1 = __importDefault(require("styled-components"));
const SearchInput_1 = require("../../components/Search/SearchInput");
const SearchShortcut_1 = require("../../components/Search/SearchShortcut");
const Button_1 = require("../../components/Button/Button");
const utils_1 = require("../../core/utils");
const SearchItem_1 = require("../../components/Search/SearchItem");
const SearchRecent_1 = require("../../components/Search/SearchRecent");
const SearchSuggestedPages_1 = require("../../components/Search/SearchSuggestedPages");
const hooks_1 = require("../../core/hooks");
const Tag_1 = require("../../components/Tag/Tag");
const CloseIcon_1 = require("../../icons/CloseIcon/CloseIcon");
const SearchFilter_1 = require("../../components/Search/SearchFilter");
const SearchGroups_1 = require("../../components/Search/SearchGroups");
const Typography_1 = require("../../components/Typography/Typography");
const SpinnerLoader_1 = require("../../components/Loaders/SpinnerLoader");
const SearchAiDialog_1 = require("../../components/Search/SearchAiDialog");
const SettingsIcon_1 = require("../../icons/SettingsIcon/SettingsIcon");
const AiStarsIcon_1 = require("../../icons/AiStarsIcon/AiStarsIcon");
const ReturnKeyIcon_1 = require("../../icons/ReturnKeyIcon/ReturnKeyIcon");
const ChevronLeftIcon_1 = require("../../icons/ChevronLeftIcon/ChevronLeftIcon");
const EditIcon_1 = require("../../icons/EditIcon/EditIcon");
const AiStarsGradientIcon_1 = require("../../icons/AiStarsGradientIcon/AiStarsGradientIcon");
function SearchDialog({ onClose, className }) {
const { useTranslate, useCurrentProduct, useSearch, useProducts, useAiSearch, useOtelTelemetry } = (0, hooks_1.useThemeHooks)();
const otelTelemetry = useOtelTelemetry();
const products = useProducts();
const currentProduct = useCurrentProduct();
const [product, setProduct] = (0, react_1.useState)(currentProduct);
const [mode, setMode] = (0, react_1.useState)('search');
const autoSearchDisabled = mode !== 'search';
const { query, setQuery, filter, setFilter, items, isSearchLoading, facets, setLoadMore, advancedSearch, askAi, groupField, } = useSearch(product === null || product === void 0 ? void 0 : product.name, autoSearchDisabled);
const { isFilterOpen, onFilterToggle, onFilterChange, onFilterReset, onFacetReset, onQuickFilterReset, } = (0, hooks_1.useSearchFilter)(filter, setFilter);
const { addSearchHistoryItem } = (0, hooks_1.useRecentSearches)();
const aiSearch = useAiSearch({ filter });
const searchInputRef = (0, react_1.useRef)(null);
const modalRef = (0, react_1.useRef)(null);
const aiQueryRef = (0, react_1.useRef)(null);
const firstSearchResultRef = (0, react_1.useRef)(null);
const searchKeysWithResults = items ? Object.keys(items).filter((key) => { var _a; return (_a = items[key]) === null || _a === void 0 ? void 0 : _a.length; }) : [];
const { translate } = useTranslate();
const handleClose = (0, react_1.useCallback)(() => {
var _a;
const value = (_a = searchInputRef === null || searchInputRef === void 0 ? void 0 : searchInputRef.current) === null || _a === void 0 ? void 0 : _a.value;
if (value) {
addSearchHistoryItem(value);
}
onClose();
}, [addSearchHistoryItem, onClose]);
(0, hooks_1.useDialogHotKeys)(modalRef, handleClose);
const focusSearchInput = () => {
requestAnimationFrame(() => {
var _a;
(_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
});
};
(0, react_1.useEffect)(() => {
if (mode === 'ai-dialog' && aiSearch.isGeneratingResponse) {
setQuery('');
}
}, [mode, aiSearch.isGeneratingResponse, setQuery]);
(0, react_1.useEffect)(focusSearchInput, []);
const handleOverlayClick = (0, react_1.useCallback)((event) => {
var _a;
const target = event.target;
if (typeof target.className !== 'string')
return;
if ((_a = target.className) === null || _a === void 0 ? void 0 : _a.includes(' overlay')) {
handleClose();
}
}, [handleClose]);
const mapItem = (0, react_1.useCallback)((item, index, results, innerRef) => {
var _a;
let itemProduct;
if (!product && item.document.product) {
const folder = (_a = item.document.product) === null || _a === void 0 ? void 0 : _a.folder;
const resolvedProduct = products.find((product) => product.slug.match(`/${folder.startsWith('./') ? folder.slice(2) : folder}/`));
itemProduct = resolvedProduct
? { name: resolvedProduct.name, icon: resolvedProduct.icon }
: undefined;
}
return (react_1.default.createElement(SearchItem_1.SearchItem, { key: `${index}-${item.document.id}`, item: item, product: itemProduct, innerRef: innerRef, onClick: () => {
addSearchHistoryItem(query);
otelTelemetry.send({
type: 'search.result.clicked',
payload: {
query,
url: item.document.url,
total_results: results.length.toString(),
index: index.toString(),
search_engine: mode,
},
});
onClose();
} }));
}, [onClose, product, products, addSearchHistoryItem, query, otelTelemetry, mode]);
const showLoadMore = (0, react_1.useCallback)((groupKey, currentCount = 0) => {
const groupFacet = facets.find((facet) => facet.field === groupField);
let needLoadMore = false;
if (groupFacet) {
const groupValue = groupFacet.values.find((value) => {
if (typeof value === 'object') {
return value.value === groupKey;
}
else
return false;
});
needLoadMore = groupValue ? groupValue.count > currentCount : false;
}
return needLoadMore;
}, [facets, groupField]);
const showResults = !!((filter && filter.length) || query);
const showSearchFilterButton = advancedSearch && mode === 'search';
const showAiSearchButton = askAi && mode === 'search';
const showAiSearchItem = showAiSearchButton && query;
const showHeaderButtons = showSearchFilterButton || showAiSearchButton;
return (react_1.default.createElement(SearchOverlay, { "data-component-name": "Search/SearchDialog", ref: modalRef, onClick: handleOverlayClick, className: (0, utils_1.concatClassNames)('overlay', className) },
react_1.default.createElement(SearchDialogWrapper, { className: "scroll-lock", role: "dialog" },
react_1.default.createElement(SearchDialogHeader, null,
product && (react_1.default.createElement(SearchProductTag, { color: "product" },
product.name,
react_1.default.createElement(CloseIcon_1.CloseIcon, { onClick: () => setProduct(undefined), color: "--icon-color-additional" }))),
mode === 'search' ? (react_1.default.createElement(react_1.default.Fragment, null,
react_1.default.createElement(SearchInput_1.SearchInput, { value: query, onChange: setQuery, placeholder: translate('search.label', 'Search docs...'), isLoading: isSearchLoading, inputRef: searchInputRef, onSubmit: () => {
var _a;
if (isSearchLoading)
return;
if (showAiSearchButton && aiQueryRef.current) {
aiQueryRef.current.focus();
}
else {
(_a = firstSearchResultRef.current) === null || _a === void 0 ? void 0 : _a.focus();
}
}, "data-translation-key": "search.label" }),
showHeaderButtons && (react_1.default.createElement(SearchHeaderButtons, null,
showAiSearchButton ? (react_1.default.createElement(SearchAiButton, { icon: react_1.default.createElement(AiStarsGradientIcon_1.AiStarsGradientIcon, { color: "var(--search-ai-button-icon-color)" }), onClick: () => {
setMode('ai-dialog');
if (query.trim()) {
aiSearch.askQuestion(query);
}
} }, translate('search.ai.button', 'Search with AI'))) : null,
showSearchFilterButton && (react_1.default.createElement(SearchFilterToggleButton, { icon: react_1.default.createElement(SettingsIcon_1.SettingsIcon, null), onClick: onFilterToggle })))))) : (react_1.default.createElement(AiDialogHeaderWrapper, null,
react_1.default.createElement(Button_1.Button, { variant: "secondary", onClick: () => {
setMode('search');
aiSearch.clearConversation();
focusSearchInput();
}, tabIndex: 0, icon: react_1.default.createElement(ChevronLeftIcon_1.ChevronLeftIcon, null) }, translate('search.ai.backToSearch', 'Back to search')),
react_1.default.createElement(Button_1.Button, { variant: "secondary", disabled: !aiSearch.conversation.length, onClick: () => aiSearch.clearConversation(), tabIndex: 0, icon: react_1.default.createElement(EditIcon_1.EditIcon, null) }, translate('search.ai.newConversation', 'New conversation'))))),
react_1.default.createElement(SearchDialogBody, null, mode === 'search' ? (react_1.default.createElement(react_1.default.Fragment, null,
advancedSearch && isFilterOpen && (react_1.default.createElement(SearchDialogBodyFilterView, null,
react_1.default.createElement(SearchFilter_1.SearchFilter, { facets: facets, filter: filter, query: query, quickFilterFields: [groupField], onFilterChange: onFilterChange, onFilterReset: onFilterReset, onFacetReset: onFacetReset }))),
react_1.default.createElement(SearchDialogBodyMainView, null,
react_1.default.createElement(SearchGroups_1.SearchGroups, { facets: facets, searchFilter: filter, onFilterChange: onFilterChange, onQuickFilterReset: onQuickFilterReset, groupField: groupField }),
showAiSearchItem && (react_1.default.createElement(SearchWithAI, { onClick: () => {
setMode('ai-dialog');
if (query.trim()) {
aiSearch.askQuestion(query);
}
}, onKeyDown: (e) => {
if (e.key === 'Enter') {
setMode('ai-dialog');
if (query.trim()) {
aiSearch.askQuestion(query);
}
}
}, ref: aiQueryRef, tabIndex: 0, role: "option", "aria-selected": "true" },
react_1.default.createElement(AiStarsIcon_1.AiStarsIcon, { color: "var(--search-ai-icon-color)", size: "36px", background: "var(--search-ai-icon-bg-color)", margin: "0 var(--spacing-md) 0 0", borderRadius: "var(--border-radius-lg)" }),
react_1.default.createElement(Typography_1.Typography, { fontWeight: "var(--font-weight-semibold)" }, query),
react_1.default.createElement(Typography_1.Typography, null,
"- ",
translate('search.ai.label', 'Ask AI assistant')),
react_1.default.createElement(ReturnKeyIcon_1.ReturnKeyIcon, { color: "var(--search-item-text-color)" }))),
showResults ? (searchKeysWithResults.length ? (searchKeysWithResults.map((key, searchGroupKeyIdx) => {
const searchResultItems = items[key];
if (searchResultItems === null || searchResultItems === void 0 ? void 0 : searchResultItems.length) {
return (react_1.default.createElement(react_1.Fragment, { key: key },
react_1.default.createElement(SearchGroupTitle, { "data-testid": "search-group-title" }, key),
searchResultItems.map((item, idx, resultsArr) => mapItem(item, idx, resultsArr, searchGroupKeyIdx === 0 ? firstSearchResultRef : undefined)),
showLoadMore(key, searchResultItems.length || 0) && (react_1.default.createElement(SearchGroupFooter, { tabIndex: 0, "data-translation-key": "search.showMore", onKeyDown: (e) => {
if (e.key === 'Enter') {
setLoadMore({
groupKey: key,
offset: searchResultItems.length || 0,
});
}
}, onClick: () => setLoadMore({
groupKey: key,
offset: searchResultItems.length || 0,
}) }, translate('search.showMore', 'Show more')))));
}
return null;
})) : isSearchLoading ? (react_1.default.createElement(SearchMessage, { "data-translation-key": "search.loading" },
react_1.default.createElement(SpinnerLoader_1.SpinnerLoader, { size: "26px", color: "var(--search-input-icon-color)" }),
translate('search.loading', 'Loading...'))) : (react_1.default.createElement(SearchMessage, { "data-translation-key": "search.noResults" },
react_1.default.createElement("b", null, translate('search.noResults.title', 'No results'))))) : (react_1.default.createElement(react_1.default.Fragment, null,
react_1.default.createElement(SearchRecent_1.SearchRecent, { onSelect: (query, index) => {
otelTelemetry.send({
type: 'search.recent.clicked',
payload: {
query,
index: index.toString(),
},
});
setQuery(query);
focusSearchInput();
} }),
react_1.default.createElement(SearchSuggestedPages_1.SearchSuggestedPages, null)))))) : (react_1.default.createElement(SearchAiDialog_1.SearchAiDialog, { initialMessage: query, response: aiSearch.response, isGeneratingResponse: aiSearch.isGeneratingResponse, error: aiSearch.error, resources: aiSearch.resources, conversation: aiSearch.conversation, setConversation: aiSearch.setConversation, onMessageSent: aiSearch.askQuestion }))),
react_1.default.createElement(SearchDialogFooter, null, mode === 'ai-dialog' ? (react_1.default.createElement(AiDisclaimer, null, translate('search.ai.disclaimer', 'AI search might provide incomplete or incorrect results. Verify important information.'))) : (react_1.default.createElement(react_1.default.Fragment, null,
react_1.default.createElement(SearchShortcuts, null,
react_1.default.createElement(SearchShortcut_1.SearchShortcut, { "data-translation-key": "search.keys.navigate", combination: "Tab", text: translate('search.keys.navigate', 'to navigate') }),
react_1.default.createElement(SearchShortcut_1.SearchShortcut, { "data-translation-key": "search.keys.select", combination: "\u23CE", text: translate('search.keys.select', 'to select') }),
react_1.default.createElement(SearchShortcut_1.SearchShortcut, { "data-translation-key": "search.keys.exit", combination: "Esc", text: translate('search.keys.exit', 'to exit') })),
isSearchLoading && (react_1.default.createElement(SearchLoading, null,
react_1.default.createElement(SpinnerLoader_1.SpinnerLoader, { size: "16px", color: "var(--search-input-icon-color)" }),
translate('search.loading', 'Loading...'))),
react_1.default.createElement(SearchCancelButton, { "data-translation-key": "search.cancel", variant: "secondary", size: "small", onClick: handleClose }, translate('search.cancel', 'Cancel'))))))));
}
const SearchOverlay = styled_components_1.default.div `
position: fixed;
display: flex;
align-items: center;
justify-content: center;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: var(--bg-color-modal-overlay);
z-index: var(--z-index-overlay);
@media screen and (max-width: ${utils_1.breakpoints.small}) {
align-items: start;
position: fixed;
overflow: hidden;
overscroll-behavior: none;
}
`;
const SearchDialogWrapper = styled_components_1.default.div `
display: flex;
flex-direction: column;
overflow: auto;
width: 100vw;
height: 100vh;
background: var(--search-modal-bg-color);
box-shadow: var(--search-modal-box-shadow);
border-radius: 0;
@media screen and (max-width: ${utils_1.breakpoints.small}) {
min-height: -webkit-fill-available !important;
min-height: 100dvh !important;
height: auto !important;
width: 100vw !important;
}
@media screen and (min-width: ${utils_1.breakpoints.small}) {
border-radius: var(--search-modal-border-radius);
width: var(--search-modal-width);
min-height: var(--search-modal-min-height);
min-width: var(--search-modal-min-width);
max-width: 95vw;
max-height: 95vh;
height: var(--search-modal-min-height);
resize: both;
}
`;
const SearchDialogHeader = styled_components_1.default.header `
display: flex;
align-items: center;
border-bottom: var(--search-modal-border);
background-color: var(--search-modal-header-bg-color);
padding: var(--search-modal-header-padding);
`;
const AiDialogHeaderWrapper = styled_components_1.default.div `
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
`;
const SearchDialogBody = styled_components_1.default.div `
display: flex;
flex-direction: row-reverse;
flex: 1;
min-height: 0;
overflow: hidden;
@media screen and (max-width: ${utils_1.breakpoints.small}) {
min-height: 0;
}
`;
const SearchDialogBodyMainView = styled_components_1.default.div `
flex: 2;
flex-grow: 2;
overflow-y: scroll;
overscroll-behavior: contain;
border-right: var(--search-modal-border);
`;
const SearchDialogBodyFilterView = styled_components_1.default.div `
overflow: scroll;
`;
const SearchDialogFooter = styled_components_1.default.footer `
display: flex;
gap: var(--search-modal-footer-gap);
padding: var(--search-modal-footer-padding);
border-top: var(--search-modal-border);
`;
const SearchShortcuts = styled_components_1.default.div `
display: none;
justify-content: flex-start;
align-items: center;
gap: var(--search-shortcuts-gap);
@media screen and (min-width: ${utils_1.breakpoints.small}) {
display: flex;
}
`;
const SearchMessage = styled_components_1.default.div `
display: flex;
height: 40%;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: var(--search-message-font-size);
font-weight: var(--search-message-font-weight);
line-height: var(--search-message-line-height);
color: var(--search-message-text-color);
gap: var(--search-message-gap);
`;
const SearchProductTag = (0, styled_components_1.default)(Tag_1.Tag) `
--tag-border-radius: var(--border-radius);
border: none;
margin: var(--spacing-xs) var(--spacing-sm) !important;
`;
const SearchFilterToggleButton = (0, styled_components_1.default)(Button_1.Button) `
margin-left: 0;
`;
const SearchAiButton = (0, styled_components_1.default)(Button_1.Button) `
margin-left: 0;
`;
const SearchCancelButton = (0, styled_components_1.default)(Button_1.Button) `
width: 100%;
@media screen and (min-width: ${utils_1.breakpoints.small}) {
display: none;
}
`;
const SearchGroupTitle = styled_components_1.default.div `
border-bottom: var(--search-modal-border);
padding: var(--search-group-title-padding);
background-color: var(--search-group-title-bg-color);
`;
const SearchGroupFooter = styled_components_1.default.div `
display: flex;
justify-content: center;
padding: var(--search-group-footer-padding);
color: var(--search-group-footer-text-color);
cursor: pointer;
`;
const SearchLoading = styled_components_1.default.div `
display: none;
align-items: center;
gap: var(--spacing-xs);
@media screen and (min-width: ${utils_1.breakpoints.small}) {
display: flex;
}
`;
const SearchHeaderButtons = styled_components_1.default.div `
display: flex;
gap: var(--search-header-buttons-gap);
padding-left: var(--search-header-buttons-padding-left);
border-left: var(--search-header-buttons-border-left);
`;
const AiDisclaimer = styled_components_1.default.div `
font-size: var(--search-ai-disclaimer-font-size);
line-height: var(--search-ai-disclaimer-line-height);
color: var(--search-ai-disclaimer-text-color);
margin: 0 auto;
`;
const SearchWithAI = styled_components_1.default.div `
display: flex;
justify-content: flex-start;
align-items: center;
cursor: pointer;
gap: var(--spacing-unit);
padding: var(--spacing-md);
color: var(--search-item-text-color);
background-color: var(--search-item-bg-color);
text-decoration: none;
white-space: normal;
outline: none;
border-top: 1px solid var(--search-item-border-color);
border-bottom: 1px solid var(--search-item-border-color);
transition: all 0.3s ease;
${ReturnKeyIcon_1.ReturnKeyIcon} {
opacity: 0;
}
&:focus,
&:hover {
color: var(--search-item-text-color-hover);
background-color: var(--search-item-bg-color-hover);
${ReturnKeyIcon_1.ReturnKeyIcon} {
opacity: 1;
}
}
&:focus {
border-top: 1px solid var(--search-item-border-color-focused);
border-bottom: 1px solid var(--search-item-border-color-focused);
}
& > :first-child {
margin-right: var(--spacing-xs);
}
`;
//# sourceMappingURL=SearchDialog.js.map