@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
|
@@ -25,3 +25,4 @@ export * from '@redocly/theme/core/hooks/search/use-search-dialog';
|
|
|
25
25
|
export * from '@redocly/theme/core/hooks/use-language-picker';
|
|
26
26
|
export * from '@redocly/theme/core/hooks/__mocks__/use-element-size';
|
|
27
27
|
export * from '@redocly/theme/core/hooks/__mocks__/use-time-ago';
|
|
28
|
+
export * from '@redocly/theme/core/hooks/__mocks__/use-input-key-commands';
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -35,3 +35,4 @@ export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-panel';
|
|
|
35
35
|
export * from '@redocly/theme/core/hooks/code-walkthrough/use-renderable-files';
|
|
36
36
|
export * from '@redocly/theme/core/hooks/use-element-size';
|
|
37
37
|
export * from '@redocly/theme/core/hooks/use-time-ago';
|
|
38
|
+
export * from '@redocly/theme/core/hooks/use-input-key-commands';
|
|
@@ -37,8 +37,16 @@ export function useNestedMenu({ item, labelRef, nestedMenuRef }: NestedMenuProps
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
function scrollIfNeeded(el: Element, centerIfNeeded: boolean = false) {
|
|
40
|
+
const rect = el.getBoundingClientRect();
|
|
41
|
+
const isInViewport =
|
|
42
|
+
rect.top >= 0 &&
|
|
43
|
+
rect.left >= 0 &&
|
|
44
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
45
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
|
46
|
+
|
|
47
|
+
// Only scroll if element is in viewport to prevent page jumping
|
|
40
48
|
// @ts-ignore
|
|
41
|
-
if (typeof el.scrollIntoViewIfNeeded === 'function') {
|
|
49
|
+
if (isInViewport && typeof el.scrollIntoViewIfNeeded === 'function') {
|
|
42
50
|
// @ts-ignore
|
|
43
51
|
el.scrollIntoViewIfNeeded(centerIfNeeded);
|
|
44
52
|
}
|
|
@@ -1,49 +1,82 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
2
2
|
|
|
3
3
|
import { isBrowser } from '@redocly/theme/core/utils';
|
|
4
4
|
|
|
5
5
|
const RECENT_SEARCHES_KEY = 'recentSearches';
|
|
6
6
|
const RECENT_SEARCHES_LIMIT = 5;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const createRecentSearchesStore = () => {
|
|
9
|
+
const subscribers = new Set<() => void>();
|
|
10
|
+
let cachedSnapshot: string[];
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const getSnapshot = (): string[] => {
|
|
13
|
+
if (!isBrowser()) return [];
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
if (cachedSnapshot) return cachedSnapshot;
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
try {
|
|
18
|
+
const stored = localStorage.getItem(RECENT_SEARCHES_KEY);
|
|
19
|
+
cachedSnapshot = stored ? JSON.parse(stored) : [];
|
|
20
|
+
return cachedSnapshot;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
cachedSnapshot = [];
|
|
23
|
+
return cachedSnapshot;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
const updateItems = (value: string, isAdd: boolean) => {
|
|
28
|
+
if (!isBrowser()) return;
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
const currentItems = getSnapshot();
|
|
31
|
+
const valueIndex = currentItems.indexOf(value);
|
|
22
32
|
|
|
23
|
-
|
|
33
|
+
if (valueIndex !== -1) {
|
|
34
|
+
currentItems.splice(valueIndex, 1);
|
|
35
|
+
}
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
if (isAdd) {
|
|
38
|
+
currentItems.unshift(value);
|
|
39
|
+
}
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
if (isAdd) recentSearches.unshift(value);
|
|
41
|
+
const limitedItems = currentItems.slice(0, RECENT_SEARCHES_LIMIT);
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(limitedItems));
|
|
44
|
+
cachedSnapshot = limitedItems;
|
|
45
|
+
|
|
46
|
+
subscribers.forEach((callback) => callback());
|
|
47
|
+
};
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
const subscribe = (callback: () => void) => {
|
|
50
|
+
subscribers.add(callback);
|
|
51
|
+
return () => subscribers.delete(callback);
|
|
52
|
+
};
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
return {
|
|
55
|
+
getSnapshot,
|
|
56
|
+
subscribe,
|
|
57
|
+
updateItems,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const recentSearchesStore = createRecentSearchesStore();
|
|
62
|
+
|
|
63
|
+
export const useRecentSearches = (): {
|
|
64
|
+
items: string[];
|
|
65
|
+
addSearchHistoryItem: (value: string) => void;
|
|
66
|
+
removeSearchHistoryItem: (value: string) => void;
|
|
67
|
+
} => {
|
|
68
|
+
const items = useSyncExternalStore(
|
|
69
|
+
recentSearchesStore.subscribe,
|
|
70
|
+
recentSearchesStore.getSnapshot,
|
|
71
|
+
() => [],
|
|
72
|
+
);
|
|
40
73
|
|
|
41
74
|
const addSearchHistoryItem = useCallback((value: string) => {
|
|
42
|
-
|
|
75
|
+
recentSearchesStore.updateItems(value, true);
|
|
43
76
|
}, []);
|
|
44
77
|
|
|
45
78
|
const removeSearchHistoryItem = useCallback((value: string) => {
|
|
46
|
-
|
|
79
|
+
recentSearchesStore.updateItems(value, false);
|
|
47
80
|
}, []);
|
|
48
81
|
|
|
49
82
|
return { items, addSearchHistoryItem, removeSearchHistoryItem };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
type Action = 'selectAll' | 'escape' | 'clear' | 'enter' | 'paste';
|
|
4
|
+
type ActionHandlers = {
|
|
5
|
+
[key in `on${Capitalize<Action>}`]?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
6
|
+
};
|
|
7
|
+
type KeyboardCommand = {
|
|
8
|
+
match: (event: React.KeyboardEvent<HTMLInputElement>) => boolean;
|
|
9
|
+
execute: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function useInputKeyCommands(actionHandlers?: ActionHandlers) {
|
|
13
|
+
// MacOS uses Command key instead of Ctrl
|
|
14
|
+
const ctrlKey = useMemo(() => (navigator.userAgent.includes('Mac') ? 'metaKey' : 'ctrlKey'), []);
|
|
15
|
+
|
|
16
|
+
const isSelectAll = useCallback(
|
|
17
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
18
|
+
return event.key === 'a' && event[ctrlKey];
|
|
19
|
+
},
|
|
20
|
+
[ctrlKey],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const isPaste = useCallback(
|
|
24
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
25
|
+
return event.key === 'v' && event[ctrlKey];
|
|
26
|
+
},
|
|
27
|
+
[ctrlKey],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const commands = useMemo<KeyboardCommand[]>(
|
|
31
|
+
() => [
|
|
32
|
+
{
|
|
33
|
+
match: (event) => event.key === 'Enter',
|
|
34
|
+
execute: (event) => actionHandlers?.onEnter?.(event),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
match: (event) => event.key === 'Escape',
|
|
38
|
+
execute: (event) => {
|
|
39
|
+
actionHandlers?.onEscape?.(event);
|
|
40
|
+
|
|
41
|
+
if (event.currentTarget?.selectionStart !== event.currentTarget?.selectionEnd) {
|
|
42
|
+
event.stopPropagation();
|
|
43
|
+
removeSelection(event);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
match: isSelectAll,
|
|
49
|
+
execute: (event) => actionHandlers?.onSelectAll?.(event),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
match: isPaste,
|
|
53
|
+
execute: (event) => actionHandlers?.onPaste?.(event),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
match: (event) => {
|
|
57
|
+
if (!event.currentTarget?.value) return false;
|
|
58
|
+
|
|
59
|
+
const selectionLength =
|
|
60
|
+
(event.currentTarget?.selectionEnd ?? 0) - (event.currentTarget?.selectionStart ?? 0);
|
|
61
|
+
const isFullValueSelected = event.currentTarget?.value.length === selectionLength;
|
|
62
|
+
const isModifyAction = isPrintableCharacter(event) || isPaste(event) || isDelete(event);
|
|
63
|
+
|
|
64
|
+
return isFullValueSelected && isModifyAction;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
execute: (event) => actionHandlers?.onClear?.(event),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
[actionHandlers, isPaste, isSelectAll],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const onKeyDown = useCallback(
|
|
74
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
75
|
+
for (const command of commands) {
|
|
76
|
+
if (command.match(event)) {
|
|
77
|
+
command.execute(event);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[commands],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return { onKeyDown };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function removeSelection(event: React.KeyboardEvent<HTMLInputElement>) {
|
|
88
|
+
const selectionEnd = event.currentTarget.selectionEnd ?? 0;
|
|
89
|
+
event.currentTarget.setSelectionRange(selectionEnd, selectionEnd);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPrintableCharacter(event: React.KeyboardEvent<HTMLInputElement>) {
|
|
93
|
+
return event.key.length === 1 && !event.ctrlKey && !event.metaKey;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isDelete(event: React.KeyboardEvent<HTMLInputElement>) {
|
|
97
|
+
return event.key === 'Backspace' || event.key === 'Delete';
|
|
98
|
+
}
|
package/src/markdoc/tags/img.ts
CHANGED