@redocly/theme 0.54.0-next.0 → 0.54.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/lib/components/CodeBlock/CodeBlockContainer.js +4 -0
  2. package/lib/components/CodeBlock/variables.js +1 -0
  3. package/lib/components/Feedback/Mood.js +13 -2
  4. package/lib/components/Feedback/Rating.js +13 -2
  5. package/lib/components/Feedback/Scale.js +13 -2
  6. package/lib/components/Feedback/Sentiment.js +13 -2
  7. package/lib/components/Image/Image.d.ts +1 -0
  8. package/lib/components/Image/Image.js +66 -16
  9. package/lib/components/Search/SearchDialog.js +19 -9
  10. package/lib/components/Search/SearchInput.js +7 -9
  11. package/lib/core/constants/search.d.ts +1 -0
  12. package/lib/core/constants/search.js +2 -1
  13. package/lib/core/hooks/__mocks__/index.d.ts +1 -0
  14. package/lib/core/hooks/__mocks__/index.js +1 -0
  15. package/lib/core/hooks/__mocks__/use-input-key-commands.d.ts +3 -0
  16. package/lib/core/hooks/__mocks__/use-input-key-commands.js +7 -0
  17. package/lib/core/hooks/index.d.ts +1 -0
  18. package/lib/core/hooks/index.js +1 -0
  19. package/lib/core/hooks/menu/use-nested-menu.js +7 -1
  20. package/lib/core/hooks/search/use-recent-searches.js +48 -25
  21. package/lib/core/hooks/use-input-key-commands.d.ts +8 -0
  22. package/lib/core/hooks/use-input-key-commands.js +71 -0
  23. package/lib/markdoc/tags/img.js +3 -0
  24. package/package.json +2 -2
  25. package/src/components/CodeBlock/CodeBlockContainer.tsx +4 -0
  26. package/src/components/CodeBlock/variables.ts +1 -0
  27. package/src/components/Feedback/Mood.tsx +15 -2
  28. package/src/components/Feedback/Rating.tsx +15 -2
  29. package/src/components/Feedback/Scale.tsx +15 -2
  30. package/src/components/Feedback/Sentiment.tsx +15 -4
  31. package/src/components/Image/Image.tsx +72 -20
  32. package/src/components/Search/SearchDialog.tsx +83 -58
  33. package/src/components/Search/SearchInput.tsx +9 -12
  34. package/src/core/constants/search.ts +2 -0
  35. package/src/core/hooks/__mocks__/index.ts +1 -0
  36. package/src/core/hooks/__mocks__/use-input-key-commands.ts +3 -0
  37. package/src/core/hooks/index.ts +1 -0
  38. package/src/core/hooks/menu/use-nested-menu.ts +9 -1
  39. package/src/core/hooks/search/use-recent-searches.ts +57 -24
  40. package/src/core/hooks/use-input-key-commands.ts +98 -0
  41. package/src/markdoc/tags/img.ts +3 -0
@@ -176,6 +176,10 @@ const CodeBlockContainerComponent = styled_components_1.default.pre `
176
176
  display: flex;
177
177
  }
178
178
 
179
+ .tree-view-comment {
180
+ color: var(--code-block-tree-view-comment-color);
181
+ }
182
+
179
183
  .tree-view-branch {
180
184
  color: var(--code-block-tree-view-lines-color);
181
185
  }
@@ -132,6 +132,7 @@ exports.code = (0, styled_components_1.css) `
132
132
  */
133
133
  --code-block-tree-view-icon-font-family: 'TreeViewIcons'; // @presenter FontFamily
134
134
  --code-block-tree-view-lines-color: var(--border-color-primary); // @presenter Color
135
+ --code-block-tree-view-comment-color: var(--input-content-placeholder-color); // @presenter Color
135
136
 
136
137
  // @tokens End
137
138
  `;
@@ -157,10 +157,15 @@ function Mood({ settings, onSubmit, className }) {
157
157
  }, onChange: setReasons })),
158
158
  displayComment && (React.createElement(Comment_1.Comment, { standAlone: false, onSubmit: ({ comment }) => setComment(comment), settings: { label: renderCommentLabel(score) } })))),
159
159
  displayFeedbackEmail && (React.createElement(StyledFormOptionalFields, null,
160
- React.createElement(Label, { "data-translation-key": "feedback.settings.optionalEmail.label" }, (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.label) ||
160
+ React.createElement(InputLabel, { "data-translation-key": "feedback.settings.optionalEmail.label" }, (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.label) ||
161
161
  translate('feedback.settings.optionalEmail.label', 'Your email (optional, for follow-up)')),
162
162
  React.createElement(EmailInput, { onChange: onEmailChange, placeholder: (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.placeholder) ||
163
- translate('feedback.settings.optionalEmail.placeholder', 'yourname@example.com'), type: "email", required: !!email }))),
163
+ translate('feedback.settings.optionalEmail.placeholder', 'yourname@example.com'), type: "email", required: !!email, onKeyDown: (e) => {
164
+ if (e.key === 'Enter') {
165
+ e.preventDefault();
166
+ onSubmitMoodForm();
167
+ }
168
+ } }))),
164
169
  displaySubmitBnt && (React.createElement(ButtonsContainer, null,
165
170
  React.createElement(Button_1.Button, { onClick: onCancelMoodForm, variant: "text", size: "small" }, "Cancel"),
166
171
  React.createElement(Button_1.Button, { type: "submit", variant: "secondary", size: "small" }, "Submit"))))));
@@ -199,6 +204,12 @@ const Label = styled_components_1.default.h4 `
199
204
 
200
205
  margin: 0;
201
206
  `;
207
+ const InputLabel = styled_components_1.default.h4 `
208
+ font-weight: var(--font-weight-regular);
209
+ font-size: var(--feedback-font-size);
210
+ line-height: var(--feedback-line-height);
211
+ margin: 0;
212
+ `;
202
213
  const ButtonsContainer = styled_components_1.default.div `
203
214
  display: flex;
204
215
  justify-content: end;
@@ -102,10 +102,15 @@ function Rating({ settings, onSubmit, className }) {
102
102
  translate('feedback.settings.comment.label', 'Please share your feedback with us.'),
103
103
  } })))),
104
104
  displayFeedbackEmail && (React.createElement(StyledFormOptionalFields, null,
105
- React.createElement(Label, { "data-translation-key": "feedback.settings.optionalEmail.label" }, (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.label) ||
105
+ React.createElement(InputLabel, { "data-translation-key": "feedback.settings.optionalEmail.label" }, (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.label) ||
106
106
  translate('feedback.settings.optionalEmail.label', 'Your email (optional, for follow-up)')),
107
107
  React.createElement(EmailInput, { onChange: onEmailChange, placeholder: (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.placeholder) ||
108
- translate('feedback.settings.optionalEmail.placeholder', 'yourname@example.com'), type: "email", required: !!email }))),
108
+ translate('feedback.settings.optionalEmail.placeholder', 'yourname@example.com'), type: "email", required: !!email, onKeyDown: (e) => {
109
+ if (e.key === 'Enter') {
110
+ e.preventDefault();
111
+ onSubmitRatingForm();
112
+ }
113
+ } }))),
109
114
  displaySubmitBnt && (React.createElement(ButtonsContainer, null,
110
115
  React.createElement(Button_1.Button, { onClick: onCancelRatingForm, variant: "text", size: "small" }, "Cancel"),
111
116
  React.createElement(Button_1.Button, { onClick: onSubmitRatingForm, variant: "secondary", size: "small" }, "Submit"))))));
@@ -143,6 +148,12 @@ const Label = styled_components_1.default.h4 `
143
148
  color: var(--feedback-header-text-color);
144
149
  margin: 0;
145
150
  `;
151
+ const InputLabel = styled_components_1.default.h4 `
152
+ font-weight: var(--font-weight-regular);
153
+ font-size: var(--feedback-font-size);
154
+ line-height: var(--feedback-line-height);
155
+ margin: 0;
156
+ `;
146
157
  const ButtonsContainer = styled_components_1.default.div `
147
158
  display: flex;
148
159
  justify-content: end;
@@ -109,10 +109,15 @@ function Scale({ settings, onSubmit, className }) {
109
109
  translate('feedback.settings.comment.label', 'Please share your feedback with us.'),
110
110
  } }))),
111
111
  displayFeedbackEmail && (React.createElement(StyledFormOptionalFields, null,
112
- React.createElement(Label, { "data-translation-key": "feedback.settings.optionalEmail.label" }, (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.label) ||
112
+ React.createElement(InputLabel, { "data-translation-key": "feedback.settings.optionalEmail.label" }, (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.label) ||
113
113
  translate('feedback.settings.optionalEmail.label', 'Your email (optional, for follow-up)')),
114
114
  React.createElement(EmailInput, { onChange: onEmailChange, placeholder: (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.placeholder) ||
115
- translate('feedback.settings.optionalEmail.placeholder', 'yourname@example.com'), type: "email", required: !!email }))),
115
+ translate('feedback.settings.optionalEmail.placeholder', 'yourname@example.com'), type: "email", required: !!email, onKeyDown: (e) => {
116
+ if (e.key === 'Enter') {
117
+ e.preventDefault();
118
+ onSubmitScaleForm();
119
+ }
120
+ } }))),
116
121
  displaySubmitBnt && (React.createElement(ButtonsContainer, null,
117
122
  React.createElement(Button_1.Button, { "data-translation-key": "feedback.settings.comment.cancel", onClick: handleCancel, variant: "text", size: "small" }, translate('feedback.settings.comment.cancel', 'Cancel')),
118
123
  React.createElement(Button_1.Button, { "data-translation-key": "feedback.settings.scale.send", type: "submit", variant: "secondary", size: "small" }, translate('feedback.settings.scale.send', 'Submit')))))));
@@ -132,6 +137,12 @@ const Label = styled_components_1.default.h4 `
132
137
  margin: 0;
133
138
  width: 100%;
134
139
  `;
140
+ const InputLabel = styled_components_1.default.h4 `
141
+ font-weight: var(--font-weight-regular);
142
+ font-size: var(--feedback-font-size);
143
+ line-height: var(--feedback-line-height);
144
+ margin: 0;
145
+ `;
135
146
  const SubLabelContainer = styled_components_1.default.div `
136
147
  display: flex;
137
148
  justify-content: space-between;
@@ -141,10 +141,15 @@ function Sentiment({ settings, onSubmit, className }) {
141
141
  }, onChange: setReasons })),
142
142
  displayComment && (React.createElement(Comment_1.Comment, { standAlone: false, onSubmit: ({ comment }) => setComment(comment), settings: { label: commentLabel } })))),
143
143
  displayFeedbackEmail && (React.createElement(StyledFormOptionalFields, null,
144
- React.createElement(Label, { "data-translation-key": "feedback.settings.optionalEmail.label" }, (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.label) ||
144
+ React.createElement(InputLabel, { "data-translation-key": "feedback.settings.optionalEmail.label" }, (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.label) ||
145
145
  translate('feedback.settings.optionalEmail.label', 'Your email (optional, for follow-up)')),
146
146
  React.createElement(EmailInput, { onChange: onEmailChange, placeholder: (optionalEmailSettings === null || optionalEmailSettings === void 0 ? void 0 : optionalEmailSettings.placeholder) ||
147
- translate('feedback.settings.optionalEmail.placeholder', 'yourname@example.com'), type: "email", required: !!email }))),
147
+ translate('feedback.settings.optionalEmail.placeholder', 'yourname@example.com'), type: "email", required: !!email, onKeyDown: (e) => {
148
+ if (e.key === 'Enter') {
149
+ e.preventDefault();
150
+ onSubmitSentimentForm();
151
+ }
152
+ } }))),
148
153
  displaySubmitBnt && (React.createElement(ButtonsContainer, null,
149
154
  React.createElement(Button_1.Button, { onClick: onCancelSentimentForm, variant: "text", size: "small" }, "Cancel"),
150
155
  React.createElement(Button_1.Button, { onClick: onSubmitSentimentForm, variant: "secondary", size: "small" }, "Submit"))))));
@@ -163,6 +168,12 @@ const Label = styled_components_1.default.h4 `
163
168
  color: var(--feedback-header-text-color);
164
169
  margin: 0;
165
170
  `;
171
+ const InputLabel = styled_components_1.default.h4 `
172
+ font-weight: var(--font-weight-regular);
173
+ font-size: var(--feedback-font-size);
174
+ line-height: var(--feedback-line-height);
175
+ margin: 0;
176
+ `;
166
177
  const StyledForm = styled_components_1.default.form `
167
178
  width: 100%;
168
179
  gap: var(--spacing-sm);
@@ -8,6 +8,7 @@ export type ImageProps = {
8
8
  height?: string | number;
9
9
  border?: string;
10
10
  withLightbox?: boolean;
11
+ lightboxStyle?: React.CSSProperties | string;
11
12
  style?: React.CSSProperties | string;
12
13
  };
13
14
  export declare function Image(props: ImageProps): JSX.Element;
@@ -1,30 +1,68 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
27
  };
5
28
  Object.defineProperty(exports, "__esModule", { value: true });
6
29
  exports.Image = Image;
7
- const react_1 = __importDefault(require("react"));
30
+ const react_1 = __importStar(require("react"));
8
31
  const styled_components_1 = __importDefault(require("styled-components"));
9
32
  const utils_1 = require("../../core/utils");
10
33
  function Image(props) {
11
- const { src, srcSet, alt, className, width, height, border, style, withLightbox } = props;
12
- const [lightboxImage, setLightboxImage] = react_1.default.useState(undefined);
13
- const parsedSourceSetMap = react_1.default.useMemo(() => {
34
+ const { src, srcSet, alt, className, width, height, border, style, withLightbox, lightboxStyle } = props;
35
+ const lightboxContainerRef = (0, react_1.useRef)(null);
36
+ const [lightboxImage, setLightboxImage] = (0, react_1.useState)(undefined);
37
+ const parsedSourceSetMap = (0, react_1.useMemo)(() => {
14
38
  return srcSet ? (0, utils_1.parseSrcSet)(srcSet) : new Map();
15
39
  }, [srcSet]);
16
- const handleImageClick = (src) => {
40
+ const handleLightboxKeyDown = (0, react_1.useCallback)((e) => {
41
+ e.stopPropagation();
42
+ if (e.key === 'Escape') {
43
+ setLightboxImage(undefined);
44
+ }
45
+ }, []);
46
+ const handleImageClick = (0, react_1.useCallback)((src) => {
17
47
  if (!withLightbox || lightboxImage) {
18
48
  return;
19
49
  }
20
50
  setLightboxImage(src);
21
- };
22
- const handleCloseLightbox = () => {
51
+ }, [withLightbox, lightboxImage]);
52
+ const handleCloseLightbox = (0, react_1.useCallback)(() => {
23
53
  setLightboxImage(undefined);
24
- };
54
+ }, []);
55
+ (0, react_1.useEffect)(() => {
56
+ var _a;
57
+ if (lightboxImage) {
58
+ (_a = lightboxContainerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
59
+ }
60
+ }, [lightboxImage]);
25
61
  const combinedStyles = Object.assign(Object.assign(Object.assign({}, (withLightbox && { cursor: 'pointer' })), (border && { border })), (typeof style === 'string' ? { cssText: style } : style));
62
+ const lightboxOverlayStyles = typeof lightboxStyle === 'string' ? { cssText: lightboxStyle } : lightboxStyle;
26
63
  return (react_1.default.createElement(react_1.default.Fragment, null,
27
- lightboxImage ? (react_1.default.createElement(LightboxContainer, { onClick: handleCloseLightbox },
64
+ lightboxImage ? (react_1.default.createElement(LightboxContainer, { ref: lightboxContainerRef, onClick: handleCloseLightbox, onKeyDown: handleLightboxKeyDown, tabIndex: 0 },
65
+ react_1.default.createElement(Overlay, { style: lightboxOverlayStyles }),
28
66
  react_1.default.createElement(Image, { src: lightboxImage, alt: alt }))) : null,
29
67
  src ? (react_1.default.createElement("img", { src: src, alt: alt, className: className, width: width, height: height, style: combinedStyles, onClick: () => handleImageClick(src) })) : (Array.from(parsedSourceSetMap).map(([key, value]) => (react_1.default.createElement(ColorModeAwareImage, { key: key, colorMode: key, src: value, alt: alt, className: className, width: width, height: height, "$withLightbox": withLightbox, style: combinedStyles, onClick: () => handleImageClick(value) }))))));
30
68
  }
@@ -37,20 +75,32 @@ const ColorModeAwareImage = styled_components_1.default.img `
37
75
  cursor: pointer;
38
76
  `}
39
77
  `;
78
+ const Overlay = styled_components_1.default.div `
79
+ background-color: var(--bg-color-modal-overlay);
80
+ grid-column: 1 / 2;
81
+ grid-row: 1 / 2;
82
+ height: 100%;
83
+ width: 100%;
84
+ z-index: -1;
85
+ `;
40
86
  const LightboxContainer = styled_components_1.default.div `
87
+ display: grid;
88
+ height: 100vh;
89
+ left: 0;
41
90
  position: fixed;
42
91
  top: 0;
43
- left: 0;
44
- width: 100%;
45
- height: 100%;
46
- background-color: var(--bg-color-modal-overlay);
92
+ width: 100vw;
47
93
  z-index: var(--z-index-overlay);
48
- display: flex;
49
- justify-content: center;
50
- align-items: center;
94
+
95
+ &:focus {
96
+ outline: none;
97
+ }
51
98
 
52
99
  img {
53
100
  cursor: pointer;
101
+ grid-column: 1 / 2;
102
+ grid-row: 1 / 2;
103
+ margin: auto;
54
104
  max-width: 90%;
55
105
  max-height: 90%;
56
106
  }
@@ -59,6 +59,7 @@ function SearchDialog({ onClose, className }) {
59
59
  const autoSearchDisabled = mode !== 'search';
60
60
  const { query, setQuery, filter, setFilter, items, isSearchLoading, facets, setLoadMore, advancedSearch, askAi, groupField, } = useSearch(product === null || product === void 0 ? void 0 : product.name, autoSearchDisabled);
61
61
  const { isFilterOpen, onFilterToggle, onFilterChange, onFilterReset, onFacetReset, onQuickFilterReset, } = (0, hooks_1.useSearchFilter)(filter, setFilter);
62
+ const { addSearchHistoryItem } = (0, hooks_1.useRecentSearches)();
62
63
  const aiSearch = useAiSearch({ filter });
63
64
  const searchInputRef = (0, react_1.useRef)(null);
64
65
  const modalRef = (0, react_1.useRef)(null);
@@ -66,7 +67,15 @@ function SearchDialog({ onClose, className }) {
66
67
  const firstSearchResultRef = (0, react_1.useRef)(null);
67
68
  const searchKeysWithResults = items ? Object.keys(items).filter((key) => { var _a; return (_a = items[key]) === null || _a === void 0 ? void 0 : _a.length; }) : [];
68
69
  const { translate } = useTranslate();
69
- (0, hooks_1.useDialogHotKeys)(modalRef, onClose);
70
+ const handleClose = (0, react_1.useCallback)(() => {
71
+ var _a;
72
+ const value = (_a = searchInputRef === null || searchInputRef === void 0 ? void 0 : searchInputRef.current) === null || _a === void 0 ? void 0 : _a.value;
73
+ if (value) {
74
+ addSearchHistoryItem(value);
75
+ }
76
+ onClose();
77
+ }, [addSearchHistoryItem, onClose]);
78
+ (0, hooks_1.useDialogHotKeys)(modalRef, handleClose);
70
79
  const focusSearchInput = () => {
71
80
  requestAnimationFrame(() => {
72
81
  var _a;
@@ -79,16 +88,16 @@ function SearchDialog({ onClose, className }) {
79
88
  }
80
89
  }, [mode, aiSearch.isGeneratingResponse, setQuery]);
81
90
  (0, react_1.useEffect)(focusSearchInput, []);
82
- const handleOverlayClick = (event) => {
91
+ const handleOverlayClick = (0, react_1.useCallback)((event) => {
83
92
  var _a;
84
93
  const target = event.target;
85
94
  if (typeof target.className !== 'string')
86
95
  return;
87
96
  if ((_a = target.className) === null || _a === void 0 ? void 0 : _a.includes(' overlay')) {
88
- onClose();
97
+ handleClose();
89
98
  }
90
- };
91
- const mapItem = (item, index, results, innerRef) => {
99
+ }, [handleClose]);
100
+ const mapItem = (0, react_1.useCallback)((item, index, results, innerRef) => {
92
101
  var _a;
93
102
  let itemProduct;
94
103
  if (!product && item.document.product) {
@@ -99,6 +108,7 @@ function SearchDialog({ onClose, className }) {
99
108
  : undefined;
100
109
  }
101
110
  return (react_1.default.createElement(SearchItem_1.SearchItem, { key: `${index}-${item.document.id}`, item: item, product: itemProduct, innerRef: innerRef, onClick: () => {
111
+ addSearchHistoryItem(query);
102
112
  otelTelemetry.send('search.result.clicked', {
103
113
  query,
104
114
  url: item.document.url,
@@ -107,8 +117,8 @@ function SearchDialog({ onClose, className }) {
107
117
  search_engine: mode,
108
118
  });
109
119
  } }));
110
- };
111
- const showLoadMore = (groupKey, currentCount = 0) => {
120
+ }, [product, products, addSearchHistoryItem, query, otelTelemetry, mode]);
121
+ const showLoadMore = (0, react_1.useCallback)((groupKey, currentCount = 0) => {
112
122
  const groupFacet = facets.find((facet) => facet.field === groupField);
113
123
  let needLoadMore = false;
114
124
  if (groupFacet) {
@@ -122,7 +132,7 @@ function SearchDialog({ onClose, className }) {
122
132
  needLoadMore = groupValue ? groupValue.count > currentCount : false;
123
133
  }
124
134
  return needLoadMore;
125
- };
135
+ }, [facets, groupField]);
126
136
  const showResults = !!((filter && filter.length) || query);
127
137
  const showSearchFilterButton = advancedSearch && mode === 'search';
128
138
  const showAiSearchButton = askAi && mode === 'search';
@@ -220,7 +230,7 @@ function SearchDialog({ onClose, className }) {
220
230
  isSearchLoading && (react_1.default.createElement(SearchLoading, null,
221
231
  react_1.default.createElement(SpinnerLoader_1.SpinnerLoader, { size: "16px", color: "var(--search-input-icon-color)" }),
222
232
  translate('search.loading', 'Loading...'))),
223
- react_1.default.createElement(SearchCancelButton, { "data-translation-key": "search.cancel", variant: "secondary", size: "small", onClick: onClose }, translate('search.cancel', 'Cancel'))))))));
233
+ react_1.default.createElement(SearchCancelButton, { "data-translation-key": "search.cancel", variant: "secondary", size: "small", onClick: handleClose }, translate('search.cancel', 'Cancel'))))))));
224
234
  }
225
235
  const SearchOverlay = styled_components_1.default.div `
226
236
  position: fixed;
@@ -14,26 +14,24 @@ const CloseFilledIcon_1 = require("../../icons/CloseFilledIcon/CloseFilledIcon")
14
14
  const ChevronLeftIcon_1 = require("../../icons/ChevronLeftIcon/ChevronLeftIcon");
15
15
  function SearchInput({ placeholder, value, onChange, isLoading, showReturnButton, inputRef, onReturn, onSubmit, className, }) {
16
16
  const { useTelemetry } = (0, hooks_1.useThemeHooks)();
17
+ const { addSearchHistoryItem } = (0, hooks_1.useRecentSearches)();
17
18
  const telemetry = useTelemetry();
19
+ const { onKeyDown } = (0, hooks_1.useInputKeyCommands)({
20
+ onEnter: (event) => onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(event),
21
+ onClear: () => addSearchHistoryItem(value),
22
+ });
18
23
  const stopPropagation = (event) => event.stopPropagation();
19
24
  const handleOnChange = (event) => {
20
25
  onChange(event.target.value);
21
26
  };
22
27
  const handleOnReset = () => {
23
28
  onChange('');
29
+ addSearchHistoryItem(value);
24
30
  telemetry.send('search_input_reset_button_clicked', {});
25
31
  };
26
- const handleOnKeyUp = (e) => {
27
- if (!onSubmit) {
28
- return;
29
- }
30
- if (e.key === 'Enter') {
31
- onSubmit(e);
32
- }
33
- };
34
32
  return (react_1.default.createElement(SearchInputWrapper, { "data-component-name": "Search/SearchInput", className: className },
35
33
  showReturnButton ? (react_1.default.createElement(Button_1.Button, { icon: react_1.default.createElement(ChevronLeftIcon_1.ChevronLeftIcon, null), onClick: onReturn })) : value && isLoading ? (react_1.default.createElement(Spinner_1.Spinner, { size: "24px", color: "--search-input-icon-color" })) : (react_1.default.createElement(SearchIcon_1.SearchIcon, { size: "24px", color: "--search-input-icon-color" })),
36
- react_1.default.createElement(SearchInputField, { value: value, ref: inputRef, placeholder: placeholder, onChange: handleOnChange, onClick: stopPropagation, onKeyUp: handleOnKeyUp }),
34
+ react_1.default.createElement(SearchInputField, { value: value, ref: inputRef, placeholder: placeholder, onChange: handleOnChange, onClick: stopPropagation, onKeyDown: onKeyDown }),
37
35
  !!value && (react_1.default.createElement(ResetButton, { variant: "ghost", onClick: handleOnReset, icon: react_1.default.createElement(CloseFilledIcon_1.CloseFilledIcon, null), tabIndex: -1 }))));
38
36
  }
39
37
  const SearchInputWrapper = styled_components_1.default.div `
@@ -11,3 +11,4 @@ export declare const enum AiSearchConversationRole {
11
11
  ASSISTANT = "assistant"
12
12
  }
13
13
  export declare const AI_SEARCH_ERROR_CONFIG: Record<AiSearchError, AiSearchErrorConfig>;
14
+ export declare const SEARCH_DEBOUNCE_TIME_MS = 300;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AI_SEARCH_ERROR_CONFIG = exports.AiSearchConversationRole = exports.AiSearchError = void 0;
3
+ exports.SEARCH_DEBOUNCE_TIME_MS = exports.AI_SEARCH_ERROR_CONFIG = exports.AiSearchConversationRole = exports.AiSearchError = void 0;
4
4
  var AiSearchError;
5
5
  (function (AiSearchError) {
6
6
  AiSearchError["Unauthorized"] = "ai_search_unauthorized";
@@ -37,4 +37,5 @@ exports.AI_SEARCH_ERROR_CONFIG = {
37
37
  [AiSearchError.EmptyResponse]: defaultErrorConfig,
38
38
  [AiSearchError.ErrorProcessingResponse]: defaultErrorConfig,
39
39
  };
40
+ exports.SEARCH_DEBOUNCE_TIME_MS = 300;
40
41
  //# sourceMappingURL=search.js.map
@@ -25,3 +25,4 @@ export * from '../../../core/hooks/search/use-search-dialog';
25
25
  export * from '../../../core/hooks/use-language-picker';
26
26
  export * from '../../../core/hooks/__mocks__/use-element-size';
27
27
  export * from '../../../core/hooks/__mocks__/use-time-ago';
28
+ export * from '../../../core/hooks/__mocks__/use-input-key-commands';
@@ -41,4 +41,5 @@ __exportStar(require("../../../core/hooks/search/use-search-dialog"), exports);
41
41
  __exportStar(require("../../../core/hooks/use-language-picker"), exports);
42
42
  __exportStar(require("../../../core/hooks/__mocks__/use-element-size"), exports);
43
43
  __exportStar(require("../../../core/hooks/__mocks__/use-time-ago"), exports);
44
+ __exportStar(require("../../../core/hooks/__mocks__/use-input-key-commands"), exports);
44
45
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ export declare const useInputKeyCommands: jest.Mock<{
2
+ onKeyDown: jest.Mock<any, any, any>;
3
+ }, [], any>;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useInputKeyCommands = void 0;
4
+ exports.useInputKeyCommands = jest.fn(() => ({
5
+ onKeyDown: jest.fn(),
6
+ }));
7
+ //# sourceMappingURL=use-input-key-commands.js.map
@@ -35,3 +35,4 @@ export * from '../../core/hooks/code-walkthrough/use-code-panel';
35
35
  export * from '../../core/hooks/code-walkthrough/use-renderable-files';
36
36
  export * from '../../core/hooks/use-element-size';
37
37
  export * from '../../core/hooks/use-time-ago';
38
+ export * from '../../core/hooks/use-input-key-commands';
@@ -51,4 +51,5 @@ __exportStar(require("../../core/hooks/code-walkthrough/use-code-panel"), export
51
51
  __exportStar(require("../../core/hooks/code-walkthrough/use-renderable-files"), exports);
52
52
  __exportStar(require("../../core/hooks/use-element-size"), exports);
53
53
  __exportStar(require("../../core/hooks/use-time-ago"), exports);
54
+ __exportStar(require("../../core/hooks/use-input-key-commands"), exports);
54
55
  //# sourceMappingURL=index.js.map
@@ -38,8 +38,14 @@ function useNestedMenu({ item, labelRef, nestedMenuRef }) {
38
38
  },
39
39
  });
40
40
  function scrollIfNeeded(el, centerIfNeeded = false) {
41
+ const rect = el.getBoundingClientRect();
42
+ const isInViewport = rect.top >= 0 &&
43
+ rect.left >= 0 &&
44
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
45
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth);
46
+ // Only scroll if element is in viewport to prevent page jumping
41
47
  // @ts-ignore
42
- if (typeof el.scrollIntoViewIfNeeded === 'function') {
48
+ if (isInViewport && typeof el.scrollIntoViewIfNeeded === 'function') {
43
49
  // @ts-ignore
44
50
  el.scrollIntoViewIfNeeded(centerIfNeeded);
45
51
  }
@@ -5,35 +5,58 @@ const react_1 = require("react");
5
5
  const utils_1 = require("../../../core/utils");
6
6
  const RECENT_SEARCHES_KEY = 'recentSearches';
7
7
  const RECENT_SEARCHES_LIMIT = 5;
8
- function getRecentSearches() {
9
- if (!(0, utils_1.isBrowser)())
10
- return [];
11
- const recentSearchesStr = window.localStorage.getItem(RECENT_SEARCHES_KEY);
12
- if (!recentSearchesStr)
13
- return [];
14
- return JSON.parse(recentSearchesStr);
15
- }
16
- function updateRecentSearches(value, isAdd) {
17
- if (!(0, utils_1.isBrowser)())
18
- return [];
19
- const recentSearches = getRecentSearches();
20
- if (value === '')
21
- return recentSearches;
22
- const valueIndex = recentSearches.indexOf(value);
23
- if (valueIndex !== -1)
24
- recentSearches.splice(valueIndex, 1);
25
- if (isAdd)
26
- recentSearches.unshift(value);
27
- localStorage === null || localStorage === void 0 ? void 0 : localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(recentSearches.slice(0, RECENT_SEARCHES_LIMIT)));
28
- return recentSearches;
29
- }
8
+ const createRecentSearchesStore = () => {
9
+ const subscribers = new Set();
10
+ let cachedSnapshot;
11
+ const getSnapshot = () => {
12
+ if (!(0, utils_1.isBrowser)())
13
+ return [];
14
+ if (cachedSnapshot)
15
+ return cachedSnapshot;
16
+ try {
17
+ const stored = localStorage.getItem(RECENT_SEARCHES_KEY);
18
+ cachedSnapshot = stored ? JSON.parse(stored) : [];
19
+ return cachedSnapshot;
20
+ }
21
+ catch (e) {
22
+ cachedSnapshot = [];
23
+ return cachedSnapshot;
24
+ }
25
+ };
26
+ const updateItems = (value, isAdd) => {
27
+ if (!(0, utils_1.isBrowser)())
28
+ return;
29
+ const currentItems = getSnapshot();
30
+ const valueIndex = currentItems.indexOf(value);
31
+ if (valueIndex !== -1) {
32
+ currentItems.splice(valueIndex, 1);
33
+ }
34
+ if (isAdd) {
35
+ currentItems.unshift(value);
36
+ }
37
+ const limitedItems = currentItems.slice(0, RECENT_SEARCHES_LIMIT);
38
+ localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(limitedItems));
39
+ cachedSnapshot = limitedItems;
40
+ subscribers.forEach((callback) => callback());
41
+ };
42
+ const subscribe = (callback) => {
43
+ subscribers.add(callback);
44
+ return () => subscribers.delete(callback);
45
+ };
46
+ return {
47
+ getSnapshot,
48
+ subscribe,
49
+ updateItems,
50
+ };
51
+ };
52
+ const recentSearchesStore = createRecentSearchesStore();
30
53
  const useRecentSearches = () => {
31
- const [items, setItems] = (0, react_1.useState)(getRecentSearches());
54
+ const items = (0, react_1.useSyncExternalStore)(recentSearchesStore.subscribe, recentSearchesStore.getSnapshot, () => []);
32
55
  const addSearchHistoryItem = (0, react_1.useCallback)((value) => {
33
- setItems(updateRecentSearches(value, true));
56
+ recentSearchesStore.updateItems(value, true);
34
57
  }, []);
35
58
  const removeSearchHistoryItem = (0, react_1.useCallback)((value) => {
36
- setItems(updateRecentSearches(value, false));
59
+ recentSearchesStore.updateItems(value, false);
37
60
  }, []);
38
61
  return { items, addSearchHistoryItem, removeSearchHistoryItem };
39
62
  };
@@ -0,0 +1,8 @@
1
+ type Action = 'selectAll' | 'escape' | 'clear' | 'enter' | 'paste';
2
+ type ActionHandlers = {
3
+ [key in `on${Capitalize<Action>}`]?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
4
+ };
5
+ export declare function useInputKeyCommands(actionHandlers?: ActionHandlers): {
6
+ onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
7
+ };
8
+ export {};