@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
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useInputKeyCommands = useInputKeyCommands;
4
+ const react_1 = require("react");
5
+ function useInputKeyCommands(actionHandlers) {
6
+ // MacOS uses Command key instead of Ctrl
7
+ const ctrlKey = (0, react_1.useMemo)(() => (navigator.userAgent.includes('Mac') ? 'metaKey' : 'ctrlKey'), []);
8
+ const isSelectAll = (0, react_1.useCallback)((event) => {
9
+ return event.key === 'a' && event[ctrlKey];
10
+ }, [ctrlKey]);
11
+ const isPaste = (0, react_1.useCallback)((event) => {
12
+ return event.key === 'v' && event[ctrlKey];
13
+ }, [ctrlKey]);
14
+ const commands = (0, react_1.useMemo)(() => [
15
+ {
16
+ match: (event) => event.key === 'Enter',
17
+ execute: (event) => { var _a; return (_a = actionHandlers === null || actionHandlers === void 0 ? void 0 : actionHandlers.onEnter) === null || _a === void 0 ? void 0 : _a.call(actionHandlers, event); },
18
+ },
19
+ {
20
+ match: (event) => event.key === 'Escape',
21
+ execute: (event) => {
22
+ var _a, _b, _c;
23
+ (_a = actionHandlers === null || actionHandlers === void 0 ? void 0 : actionHandlers.onEscape) === null || _a === void 0 ? void 0 : _a.call(actionHandlers, event);
24
+ if (((_b = event.currentTarget) === null || _b === void 0 ? void 0 : _b.selectionStart) !== ((_c = event.currentTarget) === null || _c === void 0 ? void 0 : _c.selectionEnd)) {
25
+ event.stopPropagation();
26
+ removeSelection(event);
27
+ }
28
+ },
29
+ },
30
+ {
31
+ match: isSelectAll,
32
+ execute: (event) => { var _a; return (_a = actionHandlers === null || actionHandlers === void 0 ? void 0 : actionHandlers.onSelectAll) === null || _a === void 0 ? void 0 : _a.call(actionHandlers, event); },
33
+ },
34
+ {
35
+ match: isPaste,
36
+ execute: (event) => { var _a; return (_a = actionHandlers === null || actionHandlers === void 0 ? void 0 : actionHandlers.onPaste) === null || _a === void 0 ? void 0 : _a.call(actionHandlers, event); },
37
+ },
38
+ {
39
+ match: (event) => {
40
+ var _a, _b, _c, _d, _e, _f;
41
+ if (!((_a = event.currentTarget) === null || _a === void 0 ? void 0 : _a.value))
42
+ return false;
43
+ const selectionLength = ((_c = (_b = event.currentTarget) === null || _b === void 0 ? void 0 : _b.selectionEnd) !== null && _c !== void 0 ? _c : 0) - ((_e = (_d = event.currentTarget) === null || _d === void 0 ? void 0 : _d.selectionStart) !== null && _e !== void 0 ? _e : 0);
44
+ const isFullValueSelected = ((_f = event.currentTarget) === null || _f === void 0 ? void 0 : _f.value.length) === selectionLength;
45
+ const isModifyAction = isPrintableCharacter(event) || isPaste(event) || isDelete(event);
46
+ return isFullValueSelected && isModifyAction;
47
+ },
48
+ execute: (event) => { var _a; return (_a = actionHandlers === null || actionHandlers === void 0 ? void 0 : actionHandlers.onClear) === null || _a === void 0 ? void 0 : _a.call(actionHandlers, event); },
49
+ },
50
+ ], [actionHandlers, isPaste, isSelectAll]);
51
+ const onKeyDown = (0, react_1.useCallback)((event) => {
52
+ for (const command of commands) {
53
+ if (command.match(event)) {
54
+ command.execute(event);
55
+ }
56
+ }
57
+ }, [commands]);
58
+ return { onKeyDown };
59
+ }
60
+ function removeSelection(event) {
61
+ var _a;
62
+ const selectionEnd = (_a = event.currentTarget.selectionEnd) !== null && _a !== void 0 ? _a : 0;
63
+ event.currentTarget.setSelectionRange(selectionEnd, selectionEnd);
64
+ }
65
+ function isPrintableCharacter(event) {
66
+ return event.key.length === 1 && !event.ctrlKey && !event.metaKey;
67
+ }
68
+ function isDelete(event) {
69
+ return event.key === 'Backspace' || event.key === 'Delete';
70
+ }
71
+ //# sourceMappingURL=use-input-key-commands.js.map
@@ -39,6 +39,9 @@ exports.img = {
39
39
  style: {
40
40
  type: [Object, String],
41
41
  },
42
+ lightboxStyle: {
43
+ type: [Object, String],
44
+ },
42
45
  },
43
46
  validate: (node) => {
44
47
  if (!node.attributes.src && !node.attributes.srcSet) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.54.0-next.0",
3
+ "version": "0.54.0-next.2",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -47,7 +47,7 @@
47
47
  "@types/jest-when": "3.5.5",
48
48
  "@types/lodash.debounce": "4.0.9",
49
49
  "@types/lodash.throttle": "4.1.9",
50
- "@types/node": "22.10.5",
50
+ "@types/node": "22.15.3",
51
51
  "@types/nprogress": "0.2.3",
52
52
  "@types/react": "18.3.9",
53
53
  "@types/react-dom": "18.3.5",
@@ -167,6 +167,10 @@ const CodeBlockContainerComponent = styled.pre<CodeBlockContainerProps>`
167
167
  display: flex;
168
168
  }
169
169
 
170
+ .tree-view-comment {
171
+ color: var(--code-block-tree-view-comment-color);
172
+ }
173
+
170
174
  .tree-view-branch {
171
175
  color: var(--code-block-tree-view-lines-color);
172
176
  }
@@ -130,6 +130,7 @@ export const code = css`
130
130
  */
131
131
  --code-block-tree-view-icon-font-family: 'TreeViewIcons'; // @presenter FontFamily
132
132
  --code-block-tree-view-lines-color: var(--border-color-primary); // @presenter Color
133
+ --code-block-tree-view-comment-color: var(--input-content-placeholder-color); // @presenter Color
133
134
 
134
135
  // @tokens End
135
136
  `;
@@ -240,13 +240,13 @@ export function Mood({ settings, onSubmit, className }: MoodProps): JSX.Element
240
240
 
241
241
  {displayFeedbackEmail && (
242
242
  <StyledFormOptionalFields>
243
- <Label data-translation-key="feedback.settings.optionalEmail.label">
243
+ <InputLabel data-translation-key="feedback.settings.optionalEmail.label">
244
244
  {optionalEmailSettings?.label ||
245
245
  translate(
246
246
  'feedback.settings.optionalEmail.label',
247
247
  'Your email (optional, for follow-up)',
248
248
  )}
249
- </Label>
249
+ </InputLabel>
250
250
  <EmailInput
251
251
  onChange={onEmailChange}
252
252
  placeholder={
@@ -255,6 +255,12 @@ export function Mood({ settings, onSubmit, className }: MoodProps): JSX.Element
255
255
  }
256
256
  type="email"
257
257
  required={!!email}
258
+ onKeyDown={(e) => {
259
+ if (e.key === 'Enter') {
260
+ e.preventDefault();
261
+ onSubmitMoodForm();
262
+ }
263
+ }}
258
264
  />
259
265
  </StyledFormOptionalFields>
260
266
  )}
@@ -312,6 +318,13 @@ const Label = styled.h4<{ standAlone?: boolean }>`
312
318
  margin: 0;
313
319
  `;
314
320
 
321
+ const InputLabel = styled.h4`
322
+ font-weight: var(--font-weight-regular);
323
+ font-size: var(--feedback-font-size);
324
+ line-height: var(--feedback-line-height);
325
+ margin: 0;
326
+ `;
327
+
315
328
  const ButtonsContainer = styled.div`
316
329
  display: flex;
317
330
  justify-content: end;
@@ -153,13 +153,13 @@ export function Rating({ settings, onSubmit, className }: RatingProps): JSX.Elem
153
153
 
154
154
  {displayFeedbackEmail && (
155
155
  <StyledFormOptionalFields>
156
- <Label data-translation-key="feedback.settings.optionalEmail.label">
156
+ <InputLabel data-translation-key="feedback.settings.optionalEmail.label">
157
157
  {optionalEmailSettings?.label ||
158
158
  translate(
159
159
  'feedback.settings.optionalEmail.label',
160
160
  'Your email (optional, for follow-up)',
161
161
  )}
162
- </Label>
162
+ </InputLabel>
163
163
  <EmailInput
164
164
  onChange={onEmailChange}
165
165
  placeholder={
@@ -168,6 +168,12 @@ export function Rating({ settings, onSubmit, className }: RatingProps): JSX.Elem
168
168
  }
169
169
  type="email"
170
170
  required={!!email}
171
+ onKeyDown={(e) => {
172
+ if (e.key === 'Enter') {
173
+ e.preventDefault();
174
+ onSubmitRatingForm();
175
+ }
176
+ }}
171
177
  />
172
178
  </StyledFormOptionalFields>
173
179
  )}
@@ -226,6 +232,13 @@ const Label = styled.h4`
226
232
  margin: 0;
227
233
  `;
228
234
 
235
+ const InputLabel = styled.h4`
236
+ font-weight: var(--font-weight-regular);
237
+ font-size: var(--feedback-font-size);
238
+ line-height: var(--feedback-line-height);
239
+ margin: 0;
240
+ `;
241
+
229
242
  const ButtonsContainer = styled.div`
230
243
  display: flex;
231
244
  justify-content: end;
@@ -178,13 +178,13 @@ export function Scale({ settings, onSubmit, className }: ScaleProps): JSX.Elemen
178
178
 
179
179
  {displayFeedbackEmail && (
180
180
  <StyledFormOptionalFields>
181
- <Label data-translation-key="feedback.settings.optionalEmail.label">
181
+ <InputLabel data-translation-key="feedback.settings.optionalEmail.label">
182
182
  {optionalEmailSettings?.label ||
183
183
  translate(
184
184
  'feedback.settings.optionalEmail.label',
185
185
  'Your email (optional, for follow-up)',
186
186
  )}
187
- </Label>
187
+ </InputLabel>
188
188
  <EmailInput
189
189
  onChange={onEmailChange}
190
190
  placeholder={
@@ -193,6 +193,12 @@ export function Scale({ settings, onSubmit, className }: ScaleProps): JSX.Elemen
193
193
  }
194
194
  type="email"
195
195
  required={!!email}
196
+ onKeyDown={(e) => {
197
+ if (e.key === 'Enter') {
198
+ e.preventDefault();
199
+ onSubmitScaleForm();
200
+ }
201
+ }}
196
202
  />
197
203
  </StyledFormOptionalFields>
198
204
  )}
@@ -240,6 +246,13 @@ const Label = styled.h4`
240
246
  width: 100%;
241
247
  `;
242
248
 
249
+ const InputLabel = styled.h4`
250
+ font-weight: var(--font-weight-regular);
251
+ font-size: var(--feedback-font-size);
252
+ line-height: var(--feedback-line-height);
253
+ margin: 0;
254
+ `;
255
+
243
256
  const SubLabelContainer = styled.div`
244
257
  display: flex;
245
258
  justify-content: space-between;
@@ -210,16 +210,15 @@ export function Sentiment({ settings, onSubmit, className }: SentimentProps): JS
210
210
  )}
211
211
  </StyledFormOptionalFields>
212
212
  )}
213
-
214
213
  {displayFeedbackEmail && (
215
214
  <StyledFormOptionalFields>
216
- <Label data-translation-key="feedback.settings.optionalEmail.label">
215
+ <InputLabel data-translation-key="feedback.settings.optionalEmail.label">
217
216
  {optionalEmailSettings?.label ||
218
217
  translate(
219
218
  'feedback.settings.optionalEmail.label',
220
219
  'Your email (optional, for follow-up)',
221
220
  )}
222
- </Label>
221
+ </InputLabel>
223
222
  <EmailInput
224
223
  onChange={onEmailChange}
225
224
  placeholder={
@@ -228,10 +227,15 @@ export function Sentiment({ settings, onSubmit, className }: SentimentProps): JS
228
227
  }
229
228
  type="email"
230
229
  required={!!email}
230
+ onKeyDown={(e) => {
231
+ if (e.key === 'Enter') {
232
+ e.preventDefault();
233
+ onSubmitSentimentForm();
234
+ }
235
+ }}
231
236
  />
232
237
  </StyledFormOptionalFields>
233
238
  )}
234
-
235
239
  {displaySubmitBnt && (
236
240
  <ButtonsContainer>
237
241
  <Button onClick={onCancelSentimentForm} variant="text" size="small">
@@ -263,6 +267,13 @@ const Label = styled.h4`
263
267
  margin: 0;
264
268
  `;
265
269
 
270
+ const InputLabel = styled.h4`
271
+ font-weight: var(--font-weight-regular);
272
+ font-size: var(--feedback-font-size);
273
+ line-height: var(--feedback-line-height);
274
+ margin: 0;
275
+ `;
276
+
266
277
  const StyledForm = styled.form`
267
278
  width: 100%;
268
279
  gap: var(--spacing-sm);
@@ -1,8 +1,19 @@
1
- import React from 'react';
1
+ import React, {
2
+ type KeyboardEvent,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
2
9
  import styled from 'styled-components';
3
10
 
4
11
  import { parseSrcSet } from '@redocly/theme/core/utils';
5
12
 
13
+ interface CSSProperties extends React.CSSProperties {
14
+ cssText?: string;
15
+ }
16
+
6
17
  export type ImageProps = {
7
18
  src?: string;
8
19
  srcSet?: string;
@@ -12,38 +23,66 @@ export type ImageProps = {
12
23
  height?: string | number;
13
24
  border?: string;
14
25
  withLightbox?: boolean;
26
+ lightboxStyle?: React.CSSProperties | string;
15
27
  style?: React.CSSProperties | string;
16
28
  };
17
29
 
18
30
  export function Image(props: ImageProps): JSX.Element {
19
- const { src, srcSet, alt, className, width, height, border, style, withLightbox } = props;
31
+ const { src, srcSet, alt, className, width, height, border, style, withLightbox, lightboxStyle } =
32
+ props;
20
33
 
21
- const [lightboxImage, setLightboxImage] = React.useState<string | undefined>(undefined);
22
- const parsedSourceSetMap = React.useMemo(() => {
34
+ const lightboxContainerRef = useRef<HTMLDivElement>(null);
35
+ const [lightboxImage, setLightboxImage] = useState<string | undefined>(undefined);
36
+ const parsedSourceSetMap = useMemo(() => {
23
37
  return srcSet ? parseSrcSet(srcSet) : new Map();
24
38
  }, [srcSet]);
25
39
 
26
- const handleImageClick = (src: string) => {
27
- if (!withLightbox || lightboxImage) {
28
- return;
40
+ const handleLightboxKeyDown = useCallback((e: KeyboardEvent) => {
41
+ e.stopPropagation();
42
+ if (e.key === 'Escape') {
43
+ setLightboxImage(undefined);
29
44
  }
30
- setLightboxImage(src);
31
- };
45
+ }, []);
32
46
 
33
- const handleCloseLightbox = () => {
47
+ const handleImageClick = useCallback(
48
+ (src: string) => {
49
+ if (!withLightbox || lightboxImage) {
50
+ return;
51
+ }
52
+ setLightboxImage(src);
53
+ },
54
+ [withLightbox, lightboxImage],
55
+ );
56
+
57
+ const handleCloseLightbox = useCallback(() => {
34
58
  setLightboxImage(undefined);
35
- };
59
+ }, []);
36
60
 
37
- const combinedStyles: React.CSSProperties = {
61
+ useEffect(() => {
62
+ if (lightboxImage) {
63
+ lightboxContainerRef.current?.focus();
64
+ }
65
+ }, [lightboxImage]);
66
+
67
+ const combinedStyles: CSSProperties = {
38
68
  ...(withLightbox && { cursor: 'pointer' }),
39
69
  ...(border && { border }),
40
70
  ...(typeof style === 'string' ? { cssText: style } : style),
41
71
  };
42
72
 
73
+ const lightboxOverlayStyles: CSSProperties | undefined =
74
+ typeof lightboxStyle === 'string' ? { cssText: lightboxStyle } : lightboxStyle;
75
+
43
76
  return (
44
77
  <>
45
78
  {lightboxImage ? (
46
- <LightboxContainer onClick={handleCloseLightbox}>
79
+ <LightboxContainer
80
+ ref={lightboxContainerRef}
81
+ onClick={handleCloseLightbox}
82
+ onKeyDown={handleLightboxKeyDown}
83
+ tabIndex={0}
84
+ >
85
+ <Overlay style={lightboxOverlayStyles} />
47
86
  <Image src={lightboxImage} alt={alt} />
48
87
  </LightboxContainer>
49
88
  ) : null}
@@ -88,20 +127,33 @@ const ColorModeAwareImage = styled.img<{ colorMode: string; $withLightbox?: bool
88
127
  `}
89
128
  `;
90
129
 
130
+ const Overlay = styled.div`
131
+ background-color: var(--bg-color-modal-overlay);
132
+ grid-column: 1 / 2;
133
+ grid-row: 1 / 2;
134
+ height: 100%;
135
+ width: 100%;
136
+ z-index: -1;
137
+ `;
138
+
91
139
  const LightboxContainer = styled.div`
140
+ display: grid;
141
+ height: 100vh;
142
+ left: 0;
92
143
  position: fixed;
93
144
  top: 0;
94
- left: 0;
95
- width: 100%;
96
- height: 100%;
97
- background-color: var(--bg-color-modal-overlay);
145
+ width: 100vw;
98
146
  z-index: var(--z-index-overlay);
99
- display: flex;
100
- justify-content: center;
101
- align-items: center;
147
+
148
+ &:focus {
149
+ outline: none;
150
+ }
102
151
 
103
152
  img {
104
153
  cursor: pointer;
154
+ grid-column: 1 / 2;
155
+ grid-row: 1 / 2;
156
+ margin: auto;
105
157
  max-width: 90%;
106
158
  max-height: 90%;
107
159
  }
@@ -1,4 +1,4 @@
1
- import React, { Fragment, useRef, useState, useEffect } from 'react';
1
+ import React, { Fragment, useRef, useState, useEffect, useCallback } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { MouseEvent } from 'react';
@@ -11,7 +11,12 @@ import { breakpoints, concatClassNames } from '@redocly/theme/core/utils';
11
11
  import { SearchItem } from '@redocly/theme/components/Search/SearchItem';
12
12
  import { SearchRecent } from '@redocly/theme/components/Search/SearchRecent';
13
13
  import { SearchSuggestedPages } from '@redocly/theme/components/Search/SearchSuggestedPages';
14
- import { useThemeHooks, useDialogHotKeys, useSearchFilter } from '@redocly/theme/core/hooks';
14
+ import {
15
+ useThemeHooks,
16
+ useDialogHotKeys,
17
+ useSearchFilter,
18
+ useRecentSearches,
19
+ } from '@redocly/theme/core/hooks';
15
20
  import { Tag } from '@redocly/theme/components/Tag/Tag';
16
21
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
17
22
  import { SearchFilter } from '@redocly/theme/components/Search/SearchFilter';
@@ -60,6 +65,7 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
60
65
  onFacetReset,
61
66
  onQuickFilterReset,
62
67
  } = useSearchFilter(filter, setFilter);
68
+ const { addSearchHistoryItem } = useRecentSearches();
63
69
  const aiSearch = useAiSearch({ filter });
64
70
 
65
71
  const searchInputRef = useRef<HTMLInputElement>(null);
@@ -70,7 +76,16 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
70
76
 
71
77
  const { translate } = useTranslate();
72
78
 
73
- useDialogHotKeys(modalRef, onClose);
79
+ const handleClose = useCallback(() => {
80
+ const value = searchInputRef?.current?.value;
81
+ if (value) {
82
+ addSearchHistoryItem(value);
83
+ }
84
+
85
+ onClose();
86
+ }, [addSearchHistoryItem, onClose]);
87
+
88
+ useDialogHotKeys(modalRef, handleClose);
74
89
 
75
90
  const focusSearchInput = () => {
76
91
  requestAnimationFrame(() => {
@@ -86,63 +101,73 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
86
101
 
87
102
  useEffect(focusSearchInput, []);
88
103
 
89
- const handleOverlayClick = (event: MouseEvent<HTMLElement>) => {
90
- const target = event.target as HTMLElement;
91
- if (typeof target.className !== 'string') return;
92
- if (target.className?.includes(' overlay')) {
93
- onClose();
94
- }
95
- };
104
+ const handleOverlayClick = useCallback(
105
+ (event: MouseEvent<HTMLElement>) => {
106
+ const target = event.target as HTMLElement;
107
+ if (typeof target.className !== 'string') return;
108
+ if (target.className?.includes(' overlay')) {
109
+ handleClose();
110
+ }
111
+ },
112
+ [handleClose],
113
+ );
96
114
 
97
- const mapItem = (
98
- item: SearchItemData,
99
- index: number,
100
- results: SearchItemData[],
101
- innerRef?: React.Ref<HTMLAnchorElement>,
102
- ) => {
103
- let itemProduct;
104
- if (!product && item.document.product) {
105
- const folder = item.document.product?.folder;
106
- const resolvedProduct = products.find((product) =>
107
- product.slug.match(`/${folder.startsWith('./') ? folder.slice(2) : folder}/`),
115
+ const mapItem = useCallback(
116
+ (
117
+ item: SearchItemData,
118
+ index: number,
119
+ results: SearchItemData[],
120
+ innerRef?: React.Ref<HTMLAnchorElement>,
121
+ ) => {
122
+ let itemProduct;
123
+ if (!product && item.document.product) {
124
+ const folder = item.document.product?.folder;
125
+ const resolvedProduct = products.find((product) =>
126
+ product.slug.match(`/${folder.startsWith('./') ? folder.slice(2) : folder}/`),
127
+ );
128
+ itemProduct = resolvedProduct
129
+ ? { name: resolvedProduct.name, icon: resolvedProduct.icon }
130
+ : undefined;
131
+ }
132
+
133
+ return (
134
+ <SearchItem
135
+ key={`${index}-${item.document.id}`}
136
+ item={item}
137
+ product={itemProduct}
138
+ innerRef={innerRef}
139
+ onClick={() => {
140
+ addSearchHistoryItem(query);
141
+ otelTelemetry.send('search.result.clicked', {
142
+ query,
143
+ url: item.document.url,
144
+ total_results: results.length.toString(),
145
+ index: index.toString(),
146
+ search_engine: mode,
147
+ });
148
+ }}
149
+ />
108
150
  );
109
- itemProduct = resolvedProduct
110
- ? { name: resolvedProduct.name, icon: resolvedProduct.icon }
111
- : undefined;
112
- }
113
-
114
- return (
115
- <SearchItem
116
- key={`${index}-${item.document.id}`}
117
- item={item}
118
- product={itemProduct}
119
- innerRef={innerRef}
120
- onClick={() => {
121
- otelTelemetry.send('search.result.clicked', {
122
- query,
123
- url: item.document.url,
124
- total_results: results.length.toString(),
125
- index: index.toString(),
126
- search_engine: mode,
127
- });
128
- }}
129
- />
130
- );
131
- };
151
+ },
152
+ [product, products, addSearchHistoryItem, query, otelTelemetry, mode],
153
+ );
132
154
 
133
- const showLoadMore = (groupKey: string, currentCount: number = 0) => {
134
- const groupFacet = facets.find((facet) => facet.field === groupField);
135
- let needLoadMore = false;
136
- if (groupFacet) {
137
- const groupValue = groupFacet.values.find((value) => {
138
- if (typeof value === 'object') {
139
- return value.value === groupKey;
140
- } else return false;
141
- }) as SearchFacetCount;
142
- needLoadMore = groupValue ? groupValue.count > currentCount : false;
143
- }
144
- return needLoadMore;
145
- };
155
+ const showLoadMore = useCallback(
156
+ (groupKey: string, currentCount: number = 0) => {
157
+ const groupFacet = facets.find((facet) => facet.field === groupField);
158
+ let needLoadMore = false;
159
+ if (groupFacet) {
160
+ const groupValue = groupFacet.values.find((value) => {
161
+ if (typeof value === 'object') {
162
+ return value.value === groupKey;
163
+ } else return false;
164
+ }) as SearchFacetCount;
165
+ needLoadMore = groupValue ? groupValue.count > currentCount : false;
166
+ }
167
+ return needLoadMore;
168
+ },
169
+ [facets, groupField],
170
+ );
146
171
 
147
172
  const showResults = !!((filter && filter.length) || query);
148
173
  const showSearchFilterButton = advancedSearch && mode === 'search';
@@ -410,7 +435,7 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
410
435
  data-translation-key="search.cancel"
411
436
  variant="secondary"
412
437
  size="small"
413
- onClick={onClose}
438
+ onClick={handleClose}
414
439
  >
415
440
  {translate('search.cancel', 'Cancel')}
416
441
  </SearchCancelButton>
@@ -6,7 +6,7 @@ import type { ChangeEvent, SyntheticEvent } from 'react';
6
6
  import { SearchIcon } from '@redocly/theme/icons/SearchIcon/SearchIcon';
7
7
  import { Spinner } from '@redocly/theme/icons/Spinner/Spinner';
8
8
  import { Button } from '@redocly/theme/components/Button/Button';
9
- import { useThemeHooks } from '@redocly/theme/core/hooks';
9
+ import { useInputKeyCommands, useRecentSearches, useThemeHooks } from '@redocly/theme/core/hooks';
10
10
  import { CloseFilledIcon } from '@redocly/theme/icons/CloseFilledIcon/CloseFilledIcon';
11
11
  import { ChevronLeftIcon } from '@redocly/theme/icons/ChevronLeftIcon/ChevronLeftIcon';
12
12
 
@@ -34,8 +34,14 @@ export function SearchInput({
34
34
  className,
35
35
  }: SearchInputProps): JSX.Element {
36
36
  const { useTelemetry } = useThemeHooks();
37
+ const { addSearchHistoryItem } = useRecentSearches();
37
38
  const telemetry = useTelemetry();
38
39
 
40
+ const { onKeyDown } = useInputKeyCommands({
41
+ onEnter: (event) => onSubmit?.(event),
42
+ onClear: () => addSearchHistoryItem(value),
43
+ });
44
+
39
45
  const stopPropagation = (event: SyntheticEvent) => event.stopPropagation();
40
46
 
41
47
  const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
@@ -44,19 +50,10 @@ export function SearchInput({
44
50
 
45
51
  const handleOnReset = () => {
46
52
  onChange('');
53
+ addSearchHistoryItem(value);
47
54
  telemetry.send('search_input_reset_button_clicked', {});
48
55
  };
49
56
 
50
- const handleOnKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
51
- if (!onSubmit) {
52
- return;
53
- }
54
-
55
- if (e.key === 'Enter') {
56
- onSubmit(e);
57
- }
58
- };
59
-
60
57
  return (
61
58
  <SearchInputWrapper data-component-name="Search/SearchInput" className={className}>
62
59
  {showReturnButton ? (
@@ -72,7 +69,7 @@ export function SearchInput({
72
69
  placeholder={placeholder}
73
70
  onChange={handleOnChange}
74
71
  onClick={stopPropagation}
75
- onKeyUp={handleOnKeyUp}
72
+ onKeyDown={onKeyDown}
76
73
  />
77
74
  {!!value && (
78
75
  <ResetButton