@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.
- package/lib/components/CodeBlock/CodeBlockContainer.js +4 -0
- package/lib/components/CodeBlock/variables.js +1 -0
- package/lib/components/Feedback/Mood.js +13 -2
- package/lib/components/Feedback/Rating.js +13 -2
- package/lib/components/Feedback/Scale.js +13 -2
- package/lib/components/Feedback/Sentiment.js +13 -2
- package/lib/components/Image/Image.d.ts +1 -0
- package/lib/components/Image/Image.js +66 -16
- package/lib/components/Search/SearchDialog.js +19 -9
- package/lib/components/Search/SearchInput.js +7 -9
- package/lib/core/constants/search.d.ts +1 -0
- package/lib/core/constants/search.js +2 -1
- package/lib/core/hooks/__mocks__/index.d.ts +1 -0
- package/lib/core/hooks/__mocks__/index.js +1 -0
- package/lib/core/hooks/__mocks__/use-input-key-commands.d.ts +3 -0
- package/lib/core/hooks/__mocks__/use-input-key-commands.js +7 -0
- package/lib/core/hooks/index.d.ts +1 -0
- package/lib/core/hooks/index.js +1 -0
- package/lib/core/hooks/menu/use-nested-menu.js +7 -1
- package/lib/core/hooks/search/use-recent-searches.js +48 -25
- package/lib/core/hooks/use-input-key-commands.d.ts +8 -0
- package/lib/core/hooks/use-input-key-commands.js +71 -0
- package/lib/markdoc/tags/img.js +3 -0
- package/package.json +2 -2
- package/src/components/CodeBlock/CodeBlockContainer.tsx +4 -0
- package/src/components/CodeBlock/variables.ts +1 -0
- package/src/components/Feedback/Mood.tsx +15 -2
- package/src/components/Feedback/Rating.tsx +15 -2
- package/src/components/Feedback/Scale.tsx +15 -2
- package/src/components/Feedback/Sentiment.tsx +15 -4
- package/src/components/Image/Image.tsx +72 -20
- package/src/components/Search/SearchDialog.tsx +83 -58
- package/src/components/Search/SearchInput.tsx +9 -12
- package/src/core/constants/search.ts +2 -0
- package/src/core/hooks/__mocks__/index.ts +1 -0
- package/src/core/hooks/__mocks__/use-input-key-commands.ts +3 -0
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/menu/use-nested-menu.ts +9 -1
- package/src/core/hooks/search/use-recent-searches.ts +57 -24
- package/src/core/hooks/use-input-key-commands.ts +98 -0
- 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
|
package/lib/markdoc/tags/img.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/theme",
|
|
3
|
-
"version": "0.54.0-next.
|
|
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.
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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 } =
|
|
31
|
+
const { src, srcSet, alt, className, width, height, border, style, withLightbox, lightboxStyle } =
|
|
32
|
+
props;
|
|
20
33
|
|
|
21
|
-
const
|
|
22
|
-
const
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
const handleLightboxKeyDown = useCallback((e: KeyboardEvent) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
if (e.key === 'Escape') {
|
|
43
|
+
setLightboxImage(undefined);
|
|
29
44
|
}
|
|
30
|
-
|
|
31
|
-
};
|
|
45
|
+
}, []);
|
|
32
46
|
|
|
33
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 {
|
|
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
|
-
|
|
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 = (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 = (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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={
|
|
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
|
-
|
|
72
|
+
onKeyDown={onKeyDown}
|
|
76
73
|
/>
|
|
77
74
|
{!!value && (
|
|
78
75
|
<ResetButton
|