@redocly/theme 0.70.0-next.4 → 0.70.0-next.5
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/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.d.ts +1 -1
- package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +30 -11
- package/lib/components/Dropdown/DropdownMenu.d.ts +1 -0
- package/lib/components/Dropdown/DropdownMenu.js +1 -1
- package/lib/components/Feedback/Reasons.js +3 -2
- package/lib/components/Filter/FilterCheckboxes.js +30 -11
- package/lib/components/Image/Image.js +24 -4
- package/lib/components/Segmented/Segmented.js +23 -3
- package/lib/components/Select/Select.js +10 -3
- package/lib/components/Select/SelectInput.d.ts +1 -0
- package/lib/components/Select/SelectInput.js +3 -3
- package/lib/core/hooks/index.d.ts +1 -0
- package/lib/core/hooks/index.js +1 -0
- package/lib/core/hooks/use-tab-list.d.ts +31 -0
- package/lib/core/hooks/use-tab-list.js +49 -0
- package/lib/core/utils/Dynamic.d.ts +8 -1
- package/lib/core/utils/Dynamic.js +34 -3
- package/package.json +2 -2
- package/src/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.tsx +37 -22
- package/src/components/Dropdown/DropdownMenu.tsx +7 -1
- package/src/components/Feedback/Reasons.tsx +3 -2
- package/src/components/Filter/FilterCheckboxes.tsx +33 -18
- package/src/components/Image/Image.tsx +26 -5
- package/src/components/Segmented/Segmented.tsx +25 -16
- package/src/components/Select/Select.tsx +12 -6
- package/src/components/Select/SelectInput.tsx +8 -10
- package/src/core/hooks/__mocks__/use-theme-hooks.ts +1 -0
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/use-tab-list.ts +63 -0
- package/src/core/utils/Dynamic.tsx +67 -35
- package/src/icons/CheckboxIcon/CheckboxIcon.tsx +1 -0
- package/src/icons/ChevronSortIcon/ChevronSortIcon.tsx +1 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CatalogEntityRelationsGraphProps } from '../../../../components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph';
|
|
2
|
-
export declare const CatalogEntityRelationsGraph: import("
|
|
2
|
+
export declare const CatalogEntityRelationsGraph: import("../../../../core/utils").DynamicComponent<CatalogEntityRelationsGraphProps>;
|
|
@@ -101,17 +101,20 @@ function CatalogFilterCheckboxes({ filter, filterValuesCasing, showCounter = tru
|
|
|
101
101
|
react_1.default.createElement(FilterInput_1.FilterInput, { value: searchTerm, onChange: setSearchTerm })),
|
|
102
102
|
react_1.default.createElement(FilterOptions_1.FilterOptions, null, filteredOptions.map(({ value, count }) => {
|
|
103
103
|
const id = 'filter--' + filter.property + '--' + value;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
react_1.default.createElement(
|
|
111
|
-
|
|
112
|
-
filter.
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
const checked = filter.selectedOptions instanceof Set
|
|
105
|
+
? filter.selectedOptions.has(value) ||
|
|
106
|
+
filter.selectedOptions.has(value === null || value === void 0 ? void 0 : value.toLowerCase())
|
|
107
|
+
: false;
|
|
108
|
+
return (react_1.default.createElement(FilterCheckboxOption, { key: id },
|
|
109
|
+
react_1.default.createElement(CheckboxIcon_1.CheckboxIcon, { checked: checked }),
|
|
110
|
+
react_1.default.createElement(FilterOptionLabel_1.FilterOptionLabel, { "data-translation-key": value },
|
|
111
|
+
react_1.default.createElement(FilterCheckboxInput, { checked: checked, onChange: () => {
|
|
112
|
+
filter.toggleOption(value);
|
|
113
|
+
telemetry.sendCatalogEntitiesFilterCheckboxToggledMessage([
|
|
114
|
+
Object.assign(Object.assign({}, (0, utils_1.getBaseDataAttributes)('catalogEntitiesFilterCheckboxId', 'checkbox')), { category: filter.property, value: value }),
|
|
115
|
+
]);
|
|
116
|
+
} }),
|
|
117
|
+
highlightText((0, utils_1.changeTextCasing)(translate(value), filterValuesCasing) || '')),
|
|
115
118
|
showCounter && react_1.default.createElement(CounterTag_1.CounterTag, { borderless: true }, count)));
|
|
116
119
|
})))));
|
|
117
120
|
}
|
|
@@ -147,6 +150,22 @@ const HighlightedText = styled_components_1.default.span `
|
|
|
147
150
|
`;
|
|
148
151
|
const FilterCheckboxOption = (0, styled_components_1.default)(FilterOption_1.FilterOption) `
|
|
149
152
|
padding-left: var(--filter-option-checkbox-padding-left);
|
|
153
|
+
|
|
154
|
+
&:has(input:focus-visible) {
|
|
155
|
+
outline: 1px solid var(--button-border-color-focused);
|
|
156
|
+
outline-offset: -1px;
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
const FilterCheckboxInput = styled_components_1.default.input.attrs({ type: 'checkbox' }) `
|
|
160
|
+
position: absolute;
|
|
161
|
+
width: 1px;
|
|
162
|
+
height: 1px;
|
|
163
|
+
margin: -1px;
|
|
164
|
+
padding: 0;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
clip: rect(0 0 0 0);
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
border: 0;
|
|
150
169
|
`;
|
|
151
170
|
const FilterSearchWrapper = styled_components_1.default.div `
|
|
152
171
|
padding-bottom: var(--spacing-xs);
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import type { ReactNode, JSX } from 'react';
|
|
3
3
|
import type { DropdownMenuItemProps } from '../../components/Dropdown/DropdownMenuItem';
|
|
4
4
|
export type DropdownMenuProps = {
|
|
5
|
+
id?: string;
|
|
5
6
|
className?: string;
|
|
6
7
|
role?: string;
|
|
7
8
|
footer?: React.ReactNode;
|
|
@@ -15,7 +15,7 @@ function DropdownMenu(props) {
|
|
|
15
15
|
if ('items' in props) {
|
|
16
16
|
content = props.items.map((item, idx) => (react_1.default.createElement(DropdownMenuItem_1.DropdownMenuItem, Object.assign({ key: idx }, item), item.content)));
|
|
17
17
|
}
|
|
18
|
-
return (react_1.default.createElement(DropdownMenuWrapper, { "data-component-name": "Dropdown/DropdownMenu", "data-testid": "dropdown-menu", className: props.className, role: props.role || 'menu' },
|
|
18
|
+
return (react_1.default.createElement(DropdownMenuWrapper, { "data-component-name": "Dropdown/DropdownMenu", "data-testid": "dropdown-menu", id: props.id, className: props.className, role: props.role || 'menu' },
|
|
19
19
|
content,
|
|
20
20
|
props.footer || null));
|
|
21
21
|
}
|
|
@@ -43,6 +43,7 @@ const hooks_1 = require("../../core/hooks");
|
|
|
43
43
|
function Reasons({ settings, onChange, className }) {
|
|
44
44
|
const { label, component = 'checkbox', items = [] } = settings;
|
|
45
45
|
const [checkedState, setCheckedState] = React.useState(new Array(items.length).fill(false));
|
|
46
|
+
const idPrefix = React.useId();
|
|
46
47
|
const { useTranslate } = (0, hooks_1.useThemeHooks)();
|
|
47
48
|
const { translate } = useTranslate();
|
|
48
49
|
if (!items.length) {
|
|
@@ -59,8 +60,8 @@ function Reasons({ settings, onChange, className }) {
|
|
|
59
60
|
React.createElement(Label, null, label ||
|
|
60
61
|
translate('feedback.settings.reasons.label', 'Which statement describes your thoughts about this page?')),
|
|
61
62
|
items.map((reason, idx) => (React.createElement(OptionWrapper, { key: reason },
|
|
62
|
-
React.createElement("input", { type: component, value: reason, checked: checkedState[idx], name: "reasons", onChange: () => handleOptionChange(idx) }),
|
|
63
|
-
React.createElement("label", { "data-translation-key": `feedback.settings.reasons.items.${idx + 1}`,
|
|
63
|
+
React.createElement("input", { id: `${idPrefix}-${idx}`, type: component, value: reason, checked: checkedState[idx], name: "reasons", onChange: () => handleOptionChange(idx) }),
|
|
64
|
+
React.createElement("label", { "data-translation-key": `feedback.settings.reasons.items.${idx + 1}`, htmlFor: `${idPrefix}-${idx}` }, translate(`feedback.settings.reasons.items.${idx + 1}`, reason)))))));
|
|
64
65
|
}
|
|
65
66
|
const ReasonsWrapper = styled_components_1.default.div `
|
|
66
67
|
font-family: var(--feedback-font-family);
|
|
@@ -22,22 +22,41 @@ function FilterCheckboxes({ filter, filterValuesCasing, showCounter = true, }) {
|
|
|
22
22
|
react_1.default.createElement(FilterTitle_1.FilterTitle, { "data-translation-key": filter.titleTranslationKey }, translate(filter.titleTranslationKey, filter.title)),
|
|
23
23
|
react_1.default.createElement(FilterOptions_1.FilterOptions, null, (filter.filteredOptions || filter.options).map(({ value, count }) => {
|
|
24
24
|
const id = 'filter--' + filter.property + '--' + value;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
react_1.default.createElement(
|
|
32
|
-
|
|
33
|
-
filter.
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
const checked = filter.selectedOptions instanceof Set
|
|
26
|
+
? filter.selectedOptions.has(value) ||
|
|
27
|
+
filter.selectedOptions.has(value === null || value === void 0 ? void 0 : value.toLowerCase())
|
|
28
|
+
: false;
|
|
29
|
+
return (react_1.default.createElement(FilterCheckboxOption, { key: id },
|
|
30
|
+
react_1.default.createElement(CheckboxIcon_1.CheckboxIcon, { checked: checked }),
|
|
31
|
+
react_1.default.createElement(FilterOptionLabel_1.FilterOptionLabel, { "data-translation-key": value },
|
|
32
|
+
react_1.default.createElement(FilterCheckboxInput, { checked: checked, onChange: () => {
|
|
33
|
+
filter.toggleOption(value);
|
|
34
|
+
telemetry.sendFilterCheckboxToggledMessage([
|
|
35
|
+
(0, utils_1.getBaseDataAttributes)('filterId', 'checkbox'),
|
|
36
|
+
]);
|
|
37
|
+
} }),
|
|
38
|
+
(0, utils_1.changeTextCasing)(translate(value), filterValuesCasing)),
|
|
36
39
|
showCounter && react_1.default.createElement(CounterTag_1.CounterTag, { borderless: true }, count)));
|
|
37
40
|
}))));
|
|
38
41
|
}
|
|
39
42
|
const FilterCheckboxOption = (0, styled_components_1.default)(FilterOption_1.FilterOption) `
|
|
40
43
|
padding-left: var(--filter-option-checkbox-padding-left);
|
|
44
|
+
|
|
45
|
+
&:has(input:focus-visible) {
|
|
46
|
+
outline: 1px solid var(--button-border-color-focused);
|
|
47
|
+
outline-offset: -1px;
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
const FilterCheckboxInput = styled_components_1.default.input.attrs({ type: 'checkbox' }) `
|
|
51
|
+
position: absolute;
|
|
52
|
+
width: 1px;
|
|
53
|
+
height: 1px;
|
|
54
|
+
margin: -1px;
|
|
55
|
+
padding: 0;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
clip: rect(0 0 0 0);
|
|
58
|
+
white-space: nowrap;
|
|
59
|
+
border: 0;
|
|
41
60
|
`;
|
|
42
61
|
const FilterCheckboxesWrapper = styled_components_1.default.div `
|
|
43
62
|
display: flex;
|
|
@@ -43,6 +43,7 @@ const Button_1 = require("../../components/Button/Button");
|
|
|
43
43
|
function Image(props) {
|
|
44
44
|
const { src, srcSet, images: galleryImages, alt, className, width, height, border, caption, framed, style, withLightbox, lightboxStyle, } = props;
|
|
45
45
|
const lightboxContainerRef = (0, react_1.useRef)(null);
|
|
46
|
+
const lightboxTriggerRef = (0, react_1.useRef)(null);
|
|
46
47
|
const [lightboxImage, setLightboxImage] = (0, react_1.useState)(undefined);
|
|
47
48
|
const parsedSourceSetMap = (0, react_1.useMemo)(() => {
|
|
48
49
|
return srcSet ? (0, utils_1.parseSrcSet)(srcSet) : new Map();
|
|
@@ -53,28 +54,47 @@ function Image(props) {
|
|
|
53
54
|
setLightboxImage(undefined);
|
|
54
55
|
}
|
|
55
56
|
}, []);
|
|
56
|
-
const handleImageClick = (0, react_1.useCallback)((src) => {
|
|
57
|
+
const handleImageClick = (0, react_1.useCallback)((src, trigger) => {
|
|
57
58
|
if (!withLightbox || lightboxImage) {
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
61
|
+
lightboxTriggerRef.current = trigger;
|
|
60
62
|
setLightboxImage(src);
|
|
61
63
|
}, [withLightbox, lightboxImage]);
|
|
64
|
+
const getLightboxTriggerProps = (imageSrc) => withLightbox
|
|
65
|
+
? {
|
|
66
|
+
role: 'button',
|
|
67
|
+
tabIndex: 0,
|
|
68
|
+
'aria-label': alt ? undefined : 'Open image',
|
|
69
|
+
onClick: (e) => handleImageClick(imageSrc, e.currentTarget),
|
|
70
|
+
onKeyDown: (e) => {
|
|
71
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
handleImageClick(imageSrc, e.currentTarget);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
: {};
|
|
62
78
|
const handleCloseLightbox = (0, react_1.useCallback)(() => {
|
|
63
79
|
setLightboxImage(undefined);
|
|
64
80
|
}, []);
|
|
65
81
|
(0, hooks_1.useModalScrollLock)(!!lightboxImage);
|
|
66
82
|
(0, react_1.useEffect)(() => {
|
|
67
|
-
var _a;
|
|
83
|
+
var _a, _b;
|
|
68
84
|
if (lightboxImage) {
|
|
69
85
|
(_a = lightboxContainerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
70
86
|
}
|
|
87
|
+
else {
|
|
88
|
+
(_b = lightboxTriggerRef.current) === null || _b === void 0 ? void 0 : _b.focus();
|
|
89
|
+
lightboxTriggerRef.current = null;
|
|
90
|
+
}
|
|
71
91
|
}, [lightboxImage]);
|
|
72
92
|
const combinedStyles = Object.assign(Object.assign(Object.assign({}, (withLightbox && { cursor: 'pointer' })), (border && { border })), (typeof style === 'string' ? (0, utils_1.parseStyleString)(style) : style));
|
|
73
93
|
const lightboxOverlayStyles = typeof lightboxStyle === 'string' ? (0, utils_1.parseStyleString)(lightboxStyle) : lightboxStyle;
|
|
74
|
-
const images = src ? (react_1.default.createElement("img", { src: src, alt: alt, className: className, width: width, height: height, style: combinedStyles,
|
|
94
|
+
const images = src ? (react_1.default.createElement("img", Object.assign({ src: src, alt: alt, className: className, width: width, height: height, style: combinedStyles }, getLightboxTriggerProps(src)))) : (Array.from(parsedSourceSetMap).map(([key, value]) => (react_1.default.createElement(ColorModeAwareImage, Object.assign({ key: key, $colorMode: key, src: value, alt: alt, className: className, width: width, height: height, $withLightbox: withLightbox, style: combinedStyles }, getLightboxTriggerProps(value))))));
|
|
75
95
|
const hasGallery = Array.isArray(galleryImages) && galleryImages.length > 0;
|
|
76
96
|
const visibleGalleryImages = hasGallery ? galleryImages.slice(0, 3) : [];
|
|
77
|
-
const gallery = hasGallery ? (react_1.default.createElement(GalleryRow, null, visibleGalleryImages.map((galleryImageSrc, index) => (react_1.default.createElement("img", { key: `${galleryImageSrc}-${index}`, src: galleryImageSrc, alt: alt, style: withLightbox ? { cursor: 'pointer' } : undefined,
|
|
97
|
+
const gallery = hasGallery ? (react_1.default.createElement(GalleryRow, null, visibleGalleryImages.map((galleryImageSrc, index) => (react_1.default.createElement("img", Object.assign({ key: `${galleryImageSrc}-${index}`, src: galleryImageSrc, alt: alt, style: withLightbox ? { cursor: 'pointer' } : undefined }, getLightboxTriggerProps(galleryImageSrc))))))) : null;
|
|
78
98
|
const hasCaption = Boolean(caption);
|
|
79
99
|
const hasWrapper = Boolean(framed || hasCaption || hasGallery);
|
|
80
100
|
const wrapperContent = hasGallery ? gallery : images;
|
|
@@ -32,15 +32,35 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
36
|
+
var t = {};
|
|
37
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
38
|
+
t[p] = s[p];
|
|
39
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
40
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
41
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
42
|
+
t[p[i]] = s[p[i]];
|
|
43
|
+
}
|
|
44
|
+
return t;
|
|
45
|
+
};
|
|
35
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
47
|
exports.Segmented = void 0;
|
|
37
48
|
const react_1 = __importStar(require("react"));
|
|
38
49
|
const styled_components_1 = __importStar(require("styled-components"));
|
|
39
50
|
const typed_memo_1 = require("../../core/hoc/typed-memo");
|
|
51
|
+
const hooks_1 = require("../../core/hooks");
|
|
40
52
|
function SegmentedComponent({ options, onChange, value, className = '', size = 'regular' }, ref) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
const { tabListProps, getTabProps } = (0, hooks_1.useTabList)({
|
|
54
|
+
count: options.length,
|
|
55
|
+
activeIndex: options.findIndex((opt) => value == opt.value),
|
|
56
|
+
onChange: (index) => onChange(options[index]),
|
|
57
|
+
});
|
|
58
|
+
return (react_1.default.createElement(SegmentedGroup, Object.assign({ ref: ref, "data-component-name": "Segmented/Segmented", className: `tag-grey ${size} ${className}` }, tabListProps, { $size: size }), options.map((opt, index) => {
|
|
59
|
+
const _a = getTabProps(index), { 'aria-controls': _noPanel } = _a, tabProps = __rest(_a, ['aria-controls']);
|
|
60
|
+
return (react_1.default.createElement(react_1.default.Fragment, { key: index },
|
|
61
|
+
opt.divider,
|
|
62
|
+
react_1.default.createElement(SegmentedItem, Object.assign({ key: opt.label }, tabProps, { title: opt.label, $isActive: value == opt.value, $size: size }), opt.element || opt.label)));
|
|
63
|
+
})));
|
|
44
64
|
}
|
|
45
65
|
exports.Segmented = (0, typed_memo_1.typedMemo)((0, react_1.forwardRef)(SegmentedComponent));
|
|
46
66
|
const SegmentedGroup = styled_components_1.default.div `
|
|
@@ -68,6 +68,7 @@ function Select(props) {
|
|
|
68
68
|
const [filteredOptions, setFilteredOptions] = (0, react_1.useState)(options);
|
|
69
69
|
const [stickyInputValue, setStickyInputValue] = (0, react_1.useState)(placeholder || '');
|
|
70
70
|
const inputId = (0, react_1.useId)();
|
|
71
|
+
const listboxId = `${inputId}-listbox`;
|
|
71
72
|
(0, hooks_1.useOutsideClick)(selectRef, () => {
|
|
72
73
|
setDropdownActive(false);
|
|
73
74
|
});
|
|
@@ -169,14 +170,20 @@ function Select(props) {
|
|
|
169
170
|
return !!selectedOptions.find((selectOption) => selectOption.value === option.value || selectOption.value === option.label);
|
|
170
171
|
};
|
|
171
172
|
const renderDefaultInput = (inputRef) => {
|
|
172
|
-
return (react_1.default.createElement(SelectInput_1.SelectInput, { id: inputId, listboxId:
|
|
173
|
+
return (react_1.default.createElement(SelectInput_1.SelectInput, { id: inputId, listboxId: listboxId, expanded: dropdownActive, selectedOptions: selectedOptions, searchValue: searchValue, placeholder: placeholder, stickyValue: stickyInputValue, multiple: multiple, searchable: searchable, clearable: clearable, customIcon: icon, inputRef: inputRef, onlyIcon: onlyIcon, clearHandler: clearHandler, searchHandler: searchHandler, inputBlurHandler: inputBlurHandler, inputFocusHandler: inputFocusHandler, clickHandler: inputClickHandler }));
|
|
173
174
|
};
|
|
174
175
|
return (react_1.default.createElement(exports.SelectWrapper, Object.assign({ "data-component-name": "Select/Select", ref: selectRef }, dataAttributes, { disabled: disabled, "data-testid": dataTestId, className: className, onKeyDown: (e) => {
|
|
175
176
|
if (e.key === 'Enter' && document.activeElement === selectRef.current) {
|
|
176
177
|
e.preventDefault();
|
|
177
178
|
setDropdownActive(!dropdownActive);
|
|
178
179
|
}
|
|
179
|
-
}, tabIndex:
|
|
180
|
+
}, tabIndex: searchable ? undefined : 0 }, (!searchable && {
|
|
181
|
+
role: 'combobox',
|
|
182
|
+
'aria-haspopup': 'listbox',
|
|
183
|
+
'aria-expanded': dropdownActive,
|
|
184
|
+
'aria-controls': listboxId,
|
|
185
|
+
'aria-label': 'Select option',
|
|
186
|
+
}), { onFocus: (e) => {
|
|
180
187
|
var _a;
|
|
181
188
|
if (e.target === selectRef.current) {
|
|
182
189
|
(_a = selectInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
@@ -185,7 +192,7 @@ function Select(props) {
|
|
|
185
192
|
e.preventDefault();
|
|
186
193
|
} }),
|
|
187
194
|
react_1.default.createElement(SelectDropdown, { closeOnClick: !multiple, withArrow: withArrow, trigger: renderInput ? renderInput() : renderDefaultInput(selectInputRef), triggerEvent: triggerEvent, placement: placement, alignment: alignment, active: !renderInput ? dropdownActive : undefined },
|
|
188
|
-
react_1.default.createElement(SelectDropdownMenu, { role: "listbox", footer: footer }, filteredOptions.length === 0 ? (react_1.default.createElement(DropdownMenuItem_1.DropdownMenuItem, { disabled: true }, translate('select.noResults', 'No results'))) : (filteredOptions.map((option, index) => {
|
|
195
|
+
react_1.default.createElement(SelectDropdownMenu, { id: listboxId, role: "listbox", footer: footer }, filteredOptions.length === 0 ? (react_1.default.createElement(DropdownMenuItem_1.DropdownMenuItem, { disabled: true }, translate('select.noResults', 'No results'))) : (filteredOptions.map((option, index) => {
|
|
189
196
|
return (react_1.default.createElement(react_1.Fragment, { key: index },
|
|
190
197
|
react_1.default.createElement(DropdownMenuItem_1.DropdownMenuItem, { onAction: () => selectHandler(option), prefix: !hideCheckmarkIcon &&
|
|
191
198
|
(checkmarkIconPosition ? checkmarkIconPosition === 'start' : false) &&
|
|
@@ -44,7 +44,7 @@ const Tag_1 = require("../../components/Tag/Tag");
|
|
|
44
44
|
const CloseIcon_1 = require("../../icons/CloseIcon/CloseIcon");
|
|
45
45
|
const Button_1 = require("../../components/Button/Button");
|
|
46
46
|
function SelectInput(props) {
|
|
47
|
-
const { id, listboxId, onlyIcon, icon, customIcon, selectedOptions, placeholder, stickyValue, multiple, searchable, clearable, clearHandler, searchHandler, clickHandler, searchValue, inputBlurHandler, inputFocusHandler, inputRef: forwardedInputRef, } = props;
|
|
47
|
+
const { id, listboxId, onlyIcon, icon, customIcon, selectedOptions, placeholder, stickyValue, multiple, searchable, expanded, clearable, clearHandler, searchHandler, clickHandler, searchValue, inputBlurHandler, inputFocusHandler, inputRef: forwardedInputRef, } = props;
|
|
48
48
|
const inputRef = (0, react_1.useRef)(null);
|
|
49
49
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
50
50
|
const onChangeHandler = (e) => {
|
|
@@ -95,14 +95,14 @@ function SelectInput(props) {
|
|
|
95
95
|
else {
|
|
96
96
|
forwardedInputRef.current = input;
|
|
97
97
|
}
|
|
98
|
-
}, $width: multiple ? (!searchValue && selectedOptions.length ? '10px' : 'auto') : '100%', tabIndex: 0, onFocus: (e) => {
|
|
98
|
+
}, $width: multiple ? (!searchValue && selectedOptions.length ? '10px' : 'auto') : '100%', tabIndex: 0, role: "combobox", "aria-expanded": !!expanded, "aria-controls": listboxId, "aria-autocomplete": "list", "aria-label": "Select option", onFocus: (e) => {
|
|
99
99
|
e.stopPropagation();
|
|
100
100
|
}, onMouseDown: (e) => {
|
|
101
101
|
e.preventDefault();
|
|
102
102
|
} }));
|
|
103
103
|
const simpleValue = selectedOptions.length ? (selectedOptions[0].label || selectedOptions[0].element || selectedOptions[0].value) : (react_1.default.createElement(SelectInternalInputPlaceholder, null, placeholder));
|
|
104
104
|
const multipleValues = selectedOptions.length ? (selectTags) : (react_1.default.createElement(SelectInternalInputPlaceholder, null, placeholder));
|
|
105
|
-
return (react_1.default.createElement(exports.SelectInputWrapper, { id: id, onFocus: onFocusHandler, onClick: onClickHandler
|
|
105
|
+
return (react_1.default.createElement(exports.SelectInputWrapper, { id: id, onFocus: onFocusHandler, onClick: onClickHandler },
|
|
106
106
|
!onlyIcon && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
107
107
|
react_1.default.createElement(SelectInputValue, null, multiple ? (searchable ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
108
108
|
selectTags,
|
|
@@ -21,6 +21,7 @@ export * from './menu/use-mobile-menu-levels';
|
|
|
21
21
|
export * from './menu/use-collapse';
|
|
22
22
|
export * from './feedback/use-report-dialog';
|
|
23
23
|
export * from './use-tabs';
|
|
24
|
+
export * from './use-tab-list';
|
|
24
25
|
export * from './use-focus-trap';
|
|
25
26
|
export * from './use-consent-dialog';
|
|
26
27
|
export * from './use-consent-telemetry';
|
package/lib/core/hooks/index.js
CHANGED
|
@@ -37,6 +37,7 @@ __exportStar(require("./menu/use-mobile-menu-levels"), exports);
|
|
|
37
37
|
__exportStar(require("./menu/use-collapse"), exports);
|
|
38
38
|
__exportStar(require("./feedback/use-report-dialog"), exports);
|
|
39
39
|
__exportStar(require("./use-tabs"), exports);
|
|
40
|
+
__exportStar(require("./use-tab-list"), exports);
|
|
40
41
|
__exportStar(require("./use-focus-trap"), exports);
|
|
41
42
|
__exportStar(require("./use-consent-dialog"), exports);
|
|
42
43
|
__exportStar(require("./use-consent-telemetry"), exports);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { KeyboardEvent } from 'react';
|
|
2
|
+
type UseTabListProps = {
|
|
3
|
+
count: number;
|
|
4
|
+
activeIndex: number;
|
|
5
|
+
onChange: (index: number) => void;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Props for a WAI-ARIA tab list with automatic activation: only the active tab is a tab stop,
|
|
9
|
+
* and Left/Right/Home/End select and focus another tab.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useTabList({ count, activeIndex, onChange }: UseTabListProps): {
|
|
12
|
+
tabListProps: {
|
|
13
|
+
role: string;
|
|
14
|
+
};
|
|
15
|
+
getTabProps: (index: number) => {
|
|
16
|
+
id: string;
|
|
17
|
+
role: string;
|
|
18
|
+
'aria-selected': boolean;
|
|
19
|
+
'aria-controls': string;
|
|
20
|
+
tabIndex: number;
|
|
21
|
+
ref: (element: HTMLElement | null) => void;
|
|
22
|
+
onClick: () => void;
|
|
23
|
+
onKeyDown: (event: KeyboardEvent) => void;
|
|
24
|
+
};
|
|
25
|
+
getTabPanelProps: (index: number) => {
|
|
26
|
+
id: string;
|
|
27
|
+
role: string;
|
|
28
|
+
'aria-labelledby': string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useTabList = useTabList;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/**
|
|
6
|
+
* Props for a WAI-ARIA tab list with automatic activation: only the active tab is a tab stop,
|
|
7
|
+
* and Left/Right/Home/End select and focus another tab.
|
|
8
|
+
*/
|
|
9
|
+
function useTabList({ count, activeIndex, onChange }) {
|
|
10
|
+
const idPrefix = (0, react_1.useId)();
|
|
11
|
+
const focusableIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
12
|
+
const tabRefs = (0, react_1.useRef)([]);
|
|
13
|
+
const select = (0, react_1.useCallback)((index) => {
|
|
14
|
+
var _a;
|
|
15
|
+
onChange(index);
|
|
16
|
+
(_a = tabRefs.current[index]) === null || _a === void 0 ? void 0 : _a.focus();
|
|
17
|
+
}, [onChange]);
|
|
18
|
+
const onKeyDown = (0, react_1.useCallback)((event, index) => {
|
|
19
|
+
const target = {
|
|
20
|
+
ArrowRight: (index + 1) % count,
|
|
21
|
+
ArrowLeft: (index - 1 + count) % count,
|
|
22
|
+
Home: 0,
|
|
23
|
+
End: count - 1,
|
|
24
|
+
}[event.key];
|
|
25
|
+
if (target === undefined)
|
|
26
|
+
return;
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
select(target);
|
|
29
|
+
}, [count, select]);
|
|
30
|
+
const getTabProps = (index) => ({
|
|
31
|
+
id: `${idPrefix}-tab-${index}`,
|
|
32
|
+
role: 'tab',
|
|
33
|
+
'aria-selected': index === activeIndex,
|
|
34
|
+
'aria-controls': `${idPrefix}-panel-${index}`,
|
|
35
|
+
tabIndex: index === focusableIndex ? 0 : -1,
|
|
36
|
+
ref: (element) => {
|
|
37
|
+
tabRefs.current[index] = element;
|
|
38
|
+
},
|
|
39
|
+
onClick: () => select(index),
|
|
40
|
+
onKeyDown: (event) => onKeyDown(event, index),
|
|
41
|
+
});
|
|
42
|
+
const getTabPanelProps = (index) => ({
|
|
43
|
+
id: `${idPrefix}-panel-${index}`,
|
|
44
|
+
role: 'tabpanel',
|
|
45
|
+
'aria-labelledby': `${idPrefix}-tab-${index}`,
|
|
46
|
+
});
|
|
47
|
+
return { tabListProps: { role: 'tablist' }, getTabProps, getTabPanelProps };
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=use-tab-list.js.map
|
|
@@ -3,7 +3,14 @@ interface DynamicOptions<TProps extends Record<string, unknown>> {
|
|
|
3
3
|
ssr?: boolean;
|
|
4
4
|
loading?: React.ComponentType<TProps>;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* A component from `dynamic()`. After `preload()` resolves, the component renders without a
|
|
8
|
+
* loading state. On the server, `preload()` does not load a client-only component (`ssr: false`).
|
|
9
|
+
*/
|
|
10
|
+
export type DynamicComponent<TProps> = React.ComponentType<TProps> & {
|
|
11
|
+
preload(): Promise<void>;
|
|
12
|
+
};
|
|
6
13
|
export declare function dynamic<TProps extends Record<string, unknown>>(factory: () => Promise<React.ComponentType<TProps> | {
|
|
7
14
|
default: React.ComponentType<TProps>;
|
|
8
|
-
} | Record<string, unknown>>, { ssr, loading }?: DynamicOptions<TProps>):
|
|
15
|
+
} | Record<string, unknown>>, { ssr, loading }?: DynamicOptions<TProps>): DynamicComponent<TProps>;
|
|
9
16
|
export {};
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
13
|
};
|
|
@@ -6,7 +15,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
15
|
exports.dynamic = dynamic;
|
|
7
16
|
const react_1 = __importDefault(require("react"));
|
|
8
17
|
function dynamic(factory, { ssr = true, loading } = {}) {
|
|
9
|
-
|
|
18
|
+
let loaded;
|
|
19
|
+
let pending;
|
|
20
|
+
const load = () => (pending !== null && pending !== void 0 ? pending : (pending = factory()
|
|
21
|
+
.then((mod) => {
|
|
10
22
|
// Case 1: factory() => import('./SomeModule') - direct import
|
|
11
23
|
// Case 2: factory() => import('./SomeModule').then(mod => mod.default) - extract default
|
|
12
24
|
if (typeof mod === 'function') {
|
|
@@ -38,10 +50,21 @@ function dynamic(factory, { ssr = true, loading } = {}) {
|
|
|
38
50
|
}
|
|
39
51
|
}
|
|
40
52
|
throw new Error('Dynamic import did not return a valid React component');
|
|
41
|
-
})
|
|
53
|
+
})
|
|
54
|
+
.then((module) => {
|
|
55
|
+
loaded = module.default;
|
|
56
|
+
return module;
|
|
57
|
+
}, (error) => {
|
|
58
|
+
// A failed load is not kept, so the next preload tries again.
|
|
59
|
+
pending = undefined;
|
|
60
|
+
throw error;
|
|
61
|
+
})));
|
|
62
|
+
const LazyComponent = react_1.default.lazy(load);
|
|
42
63
|
const LoadingComponent = loading || (() => null);
|
|
43
|
-
|
|
64
|
+
const DynamicComponent = (props) => {
|
|
44
65
|
const [hasMounted, setHasMounted] = react_1.default.useState(false);
|
|
66
|
+
// Fixed per instance: a change of the element type would remount the component.
|
|
67
|
+
const [Component] = react_1.default.useState(() => loaded !== null && loaded !== void 0 ? loaded : LazyComponent);
|
|
45
68
|
react_1.default.useEffect(() => {
|
|
46
69
|
setHasMounted(true);
|
|
47
70
|
}, []);
|
|
@@ -55,5 +78,13 @@ function dynamic(factory, { ssr = true, loading } = {}) {
|
|
|
55
78
|
return (react_1.default.createElement(react_1.default.Suspense, { fallback: react_1.default.createElement(LoadingComponent, Object.assign({}, props)) },
|
|
56
79
|
react_1.default.createElement(Component, Object.assign({}, props))));
|
|
57
80
|
};
|
|
81
|
+
return Object.assign(DynamicComponent, {
|
|
82
|
+
preload() {
|
|
83
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
84
|
+
if (ssr || typeof window !== 'undefined')
|
|
85
|
+
yield load();
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
});
|
|
58
89
|
}
|
|
59
90
|
//# sourceMappingURL=Dynamic.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/theme",
|
|
3
|
-
"version": "0.70.0-next.
|
|
3
|
+
"version": "0.70.0-next.5",
|
|
4
4
|
"description": "Shared UI components lib",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"theme",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"webpack": "5.105.2"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@redocly/config": "0.61.
|
|
74
|
+
"@redocly/config": "0.61.1",
|
|
75
75
|
"@tanstack/react-query": "5.62.3",
|
|
76
76
|
"@tanstack/react-virtual": "3.13.0",
|
|
77
77
|
"@xyflow/react": "12.8.3",
|
|
@@ -96,30 +96,28 @@ export function CatalogFilterCheckboxes({
|
|
|
96
96
|
<FilterOptions>
|
|
97
97
|
{filteredOptions.map(({ value, count }) => {
|
|
98
98
|
const id = 'filter--' + filter.property + '--' + value;
|
|
99
|
+
const checked =
|
|
100
|
+
filter.selectedOptions instanceof Set
|
|
101
|
+
? filter.selectedOptions.has(value) ||
|
|
102
|
+
filter.selectedOptions.has(value?.toLowerCase())
|
|
103
|
+
: false;
|
|
99
104
|
return (
|
|
100
|
-
<FilterCheckboxOption
|
|
101
|
-
|
|
102
|
-
role="link"
|
|
103
|
-
onClick={() => {
|
|
104
|
-
filter.toggleOption(value);
|
|
105
|
-
telemetry.sendCatalogEntitiesFilterCheckboxToggledMessage([
|
|
106
|
-
{
|
|
107
|
-
...getBaseDataAttributes('catalogEntitiesFilterCheckboxId', 'checkbox'),
|
|
108
|
-
category: filter.property,
|
|
109
|
-
value: value,
|
|
110
|
-
},
|
|
111
|
-
]);
|
|
112
|
-
}}
|
|
113
|
-
>
|
|
114
|
-
<CheckboxIcon
|
|
115
|
-
checked={
|
|
116
|
-
filter.selectedOptions instanceof Set
|
|
117
|
-
? filter.selectedOptions.has(value) ||
|
|
118
|
-
filter.selectedOptions.has(value?.toLowerCase())
|
|
119
|
-
: false
|
|
120
|
-
}
|
|
121
|
-
/>
|
|
105
|
+
<FilterCheckboxOption key={id}>
|
|
106
|
+
<CheckboxIcon checked={checked} />
|
|
122
107
|
<FilterOptionLabel data-translation-key={value}>
|
|
108
|
+
<FilterCheckboxInput
|
|
109
|
+
checked={checked}
|
|
110
|
+
onChange={() => {
|
|
111
|
+
filter.toggleOption(value);
|
|
112
|
+
telemetry.sendCatalogEntitiesFilterCheckboxToggledMessage([
|
|
113
|
+
{
|
|
114
|
+
...getBaseDataAttributes('catalogEntitiesFilterCheckboxId', 'checkbox'),
|
|
115
|
+
category: filter.property,
|
|
116
|
+
value: value,
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
123
121
|
{highlightText(changeTextCasing(translate(value), filterValuesCasing) || '')}
|
|
124
122
|
</FilterOptionLabel>
|
|
125
123
|
{showCounter && <CounterTag borderless>{count}</CounterTag>}
|
|
@@ -168,6 +166,23 @@ const HighlightedText = styled.span`
|
|
|
168
166
|
|
|
169
167
|
const FilterCheckboxOption = styled(FilterOption)`
|
|
170
168
|
padding-left: var(--filter-option-checkbox-padding-left);
|
|
169
|
+
|
|
170
|
+
&:has(input:focus-visible) {
|
|
171
|
+
outline: 1px solid var(--button-border-color-focused);
|
|
172
|
+
outline-offset: -1px;
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const FilterCheckboxInput = styled.input.attrs({ type: 'checkbox' })`
|
|
177
|
+
position: absolute;
|
|
178
|
+
width: 1px;
|
|
179
|
+
height: 1px;
|
|
180
|
+
margin: -1px;
|
|
181
|
+
padding: 0;
|
|
182
|
+
overflow: hidden;
|
|
183
|
+
clip: rect(0 0 0 0);
|
|
184
|
+
white-space: nowrap;
|
|
185
|
+
border: 0;
|
|
171
186
|
`;
|
|
172
187
|
|
|
173
188
|
const FilterSearchWrapper = styled.div`
|
|
@@ -6,7 +6,12 @@ import type { DropdownMenuItemProps } from '@redocly/theme/components/Dropdown/D
|
|
|
6
6
|
|
|
7
7
|
import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
|
|
8
8
|
|
|
9
|
-
export type DropdownMenuProps = {
|
|
9
|
+
export type DropdownMenuProps = {
|
|
10
|
+
id?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
role?: string;
|
|
13
|
+
footer?: React.ReactNode;
|
|
14
|
+
} & (
|
|
10
15
|
| {
|
|
11
16
|
items: DropdownMenuItemProps[];
|
|
12
17
|
}
|
|
@@ -32,6 +37,7 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
|
|
|
32
37
|
<DropdownMenuWrapper
|
|
33
38
|
data-component-name="Dropdown/DropdownMenu"
|
|
34
39
|
data-testid="dropdown-menu"
|
|
40
|
+
id={props.id}
|
|
35
41
|
className={props.className}
|
|
36
42
|
role={props.role || 'menu'}
|
|
37
43
|
>
|
|
@@ -18,6 +18,7 @@ export type ReasonsProps = {
|
|
|
18
18
|
export function Reasons({ settings, onChange, className }: ReasonsProps): JSX.Element | null {
|
|
19
19
|
const { label, component = 'checkbox', items = [] } = settings;
|
|
20
20
|
const [checkedState, setCheckedState] = React.useState(new Array(items.length).fill(false));
|
|
21
|
+
const idPrefix = React.useId();
|
|
21
22
|
const { useTranslate } = useThemeHooks();
|
|
22
23
|
const { translate } = useTranslate();
|
|
23
24
|
|
|
@@ -51,6 +52,7 @@ export function Reasons({ settings, onChange, className }: ReasonsProps): JSX.El
|
|
|
51
52
|
{items.map((reason, idx) => (
|
|
52
53
|
<OptionWrapper key={reason}>
|
|
53
54
|
<input
|
|
55
|
+
id={`${idPrefix}-${idx}`}
|
|
54
56
|
type={component}
|
|
55
57
|
value={reason}
|
|
56
58
|
checked={checkedState[idx]}
|
|
@@ -59,8 +61,7 @@ export function Reasons({ settings, onChange, className }: ReasonsProps): JSX.El
|
|
|
59
61
|
/>
|
|
60
62
|
<label
|
|
61
63
|
data-translation-key={`feedback.settings.reasons.items.${idx + 1}`}
|
|
62
|
-
|
|
63
|
-
onClick={() => handleOptionChange(idx)}
|
|
64
|
+
htmlFor={`${idPrefix}-${idx}`}
|
|
64
65
|
>
|
|
65
66
|
{translate(`feedback.settings.reasons.items.${idx + 1}`, reason)}
|
|
66
67
|
</label>
|
|
@@ -30,26 +30,24 @@ export function FilterCheckboxes({
|
|
|
30
30
|
<FilterOptions>
|
|
31
31
|
{(filter.filteredOptions || filter.options).map(({ value, count }) => {
|
|
32
32
|
const id = 'filter--' + filter.property + '--' + value;
|
|
33
|
+
const checked =
|
|
34
|
+
filter.selectedOptions instanceof Set
|
|
35
|
+
? filter.selectedOptions.has(value) ||
|
|
36
|
+
filter.selectedOptions.has(value?.toLowerCase())
|
|
37
|
+
: false;
|
|
33
38
|
return (
|
|
34
|
-
<FilterCheckboxOption
|
|
35
|
-
|
|
36
|
-
role="link"
|
|
37
|
-
onClick={() => {
|
|
38
|
-
filter.toggleOption(value);
|
|
39
|
-
telemetry.sendFilterCheckboxToggledMessage([
|
|
40
|
-
getBaseDataAttributes('filterId', 'checkbox'),
|
|
41
|
-
]);
|
|
42
|
-
}}
|
|
43
|
-
>
|
|
44
|
-
<CheckboxIcon
|
|
45
|
-
checked={
|
|
46
|
-
filter.selectedOptions instanceof Set
|
|
47
|
-
? filter.selectedOptions.has(value) ||
|
|
48
|
-
filter.selectedOptions.has(value?.toLowerCase())
|
|
49
|
-
: false
|
|
50
|
-
}
|
|
51
|
-
/>
|
|
39
|
+
<FilterCheckboxOption key={id}>
|
|
40
|
+
<CheckboxIcon checked={checked} />
|
|
52
41
|
<FilterOptionLabel data-translation-key={value}>
|
|
42
|
+
<FilterCheckboxInput
|
|
43
|
+
checked={checked}
|
|
44
|
+
onChange={() => {
|
|
45
|
+
filter.toggleOption(value);
|
|
46
|
+
telemetry.sendFilterCheckboxToggledMessage([
|
|
47
|
+
getBaseDataAttributes('filterId', 'checkbox'),
|
|
48
|
+
]);
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
53
51
|
{changeTextCasing(translate(value), filterValuesCasing)}
|
|
54
52
|
</FilterOptionLabel>
|
|
55
53
|
{showCounter && <CounterTag borderless>{count}</CounterTag>}
|
|
@@ -63,6 +61,23 @@ export function FilterCheckboxes({
|
|
|
63
61
|
|
|
64
62
|
const FilterCheckboxOption = styled(FilterOption)`
|
|
65
63
|
padding-left: var(--filter-option-checkbox-padding-left);
|
|
64
|
+
|
|
65
|
+
&:has(input:focus-visible) {
|
|
66
|
+
outline: 1px solid var(--button-border-color-focused);
|
|
67
|
+
outline-offset: -1px;
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
const FilterCheckboxInput = styled.input.attrs({ type: 'checkbox' })`
|
|
72
|
+
position: absolute;
|
|
73
|
+
width: 1px;
|
|
74
|
+
height: 1px;
|
|
75
|
+
margin: -1px;
|
|
76
|
+
padding: 0;
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
clip: rect(0 0 0 0);
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
border: 0;
|
|
66
81
|
`;
|
|
67
82
|
|
|
68
83
|
const FilterCheckboxesWrapper = styled.div`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import styled, { css } from 'styled-components';
|
|
3
3
|
|
|
4
|
-
import type { KeyboardEvent, JSX } from 'react';
|
|
4
|
+
import type { KeyboardEvent, JSX, MouseEvent } from 'react';
|
|
5
5
|
|
|
6
6
|
import { parseSrcSet, parseStyleString } from '@redocly/theme/core/utils';
|
|
7
7
|
import { useModalScrollLock } from '@redocly/theme/core/hooks';
|
|
@@ -42,6 +42,7 @@ export function Image(props: ImageProps): JSX.Element {
|
|
|
42
42
|
} = props;
|
|
43
43
|
|
|
44
44
|
const lightboxContainerRef = useRef<HTMLDivElement>(null);
|
|
45
|
+
const lightboxTriggerRef = useRef<HTMLElement | null>(null);
|
|
45
46
|
const [lightboxImage, setLightboxImage] = useState<string | undefined>(undefined);
|
|
46
47
|
const parsedSourceSetMap = useMemo(() => {
|
|
47
48
|
return srcSet ? parseSrcSet(srcSet) : new Map();
|
|
@@ -55,15 +56,32 @@ export function Image(props: ImageProps): JSX.Element {
|
|
|
55
56
|
}, []);
|
|
56
57
|
|
|
57
58
|
const handleImageClick = useCallback(
|
|
58
|
-
(src: string) => {
|
|
59
|
+
(src: string, trigger: HTMLElement) => {
|
|
59
60
|
if (!withLightbox || lightboxImage) {
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
63
|
+
lightboxTriggerRef.current = trigger;
|
|
62
64
|
setLightboxImage(src);
|
|
63
65
|
},
|
|
64
66
|
[withLightbox, lightboxImage],
|
|
65
67
|
);
|
|
66
68
|
|
|
69
|
+
const getLightboxTriggerProps = (imageSrc: string) =>
|
|
70
|
+
withLightbox
|
|
71
|
+
? {
|
|
72
|
+
role: 'button',
|
|
73
|
+
tabIndex: 0,
|
|
74
|
+
'aria-label': alt ? undefined : 'Open image',
|
|
75
|
+
onClick: (e: MouseEvent<HTMLElement>) => handleImageClick(imageSrc, e.currentTarget),
|
|
76
|
+
onKeyDown: (e: KeyboardEvent<HTMLElement>) => {
|
|
77
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
handleImageClick(imageSrc, e.currentTarget);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
: {};
|
|
84
|
+
|
|
67
85
|
const handleCloseLightbox = useCallback(() => {
|
|
68
86
|
setLightboxImage(undefined);
|
|
69
87
|
}, []);
|
|
@@ -72,6 +90,9 @@ export function Image(props: ImageProps): JSX.Element {
|
|
|
72
90
|
useEffect(() => {
|
|
73
91
|
if (lightboxImage) {
|
|
74
92
|
lightboxContainerRef.current?.focus();
|
|
93
|
+
} else {
|
|
94
|
+
lightboxTriggerRef.current?.focus();
|
|
95
|
+
lightboxTriggerRef.current = null;
|
|
75
96
|
}
|
|
76
97
|
}, [lightboxImage]);
|
|
77
98
|
|
|
@@ -92,7 +113,7 @@ export function Image(props: ImageProps): JSX.Element {
|
|
|
92
113
|
width={width}
|
|
93
114
|
height={height}
|
|
94
115
|
style={combinedStyles}
|
|
95
|
-
|
|
116
|
+
{...getLightboxTriggerProps(src)}
|
|
96
117
|
/>
|
|
97
118
|
) : (
|
|
98
119
|
Array.from(parsedSourceSetMap).map(([key, value]) => (
|
|
@@ -106,7 +127,7 @@ export function Image(props: ImageProps): JSX.Element {
|
|
|
106
127
|
height={height}
|
|
107
128
|
$withLightbox={withLightbox}
|
|
108
129
|
style={combinedStyles}
|
|
109
|
-
|
|
130
|
+
{...getLightboxTriggerProps(value)}
|
|
110
131
|
/>
|
|
111
132
|
))
|
|
112
133
|
);
|
|
@@ -122,7 +143,7 @@ export function Image(props: ImageProps): JSX.Element {
|
|
|
122
143
|
src={galleryImageSrc}
|
|
123
144
|
alt={alt}
|
|
124
145
|
style={withLightbox ? { cursor: 'pointer' } : undefined}
|
|
125
|
-
|
|
146
|
+
{...getLightboxTriggerProps(galleryImageSrc)}
|
|
126
147
|
/>
|
|
127
148
|
))}
|
|
128
149
|
</GalleryRow>
|
|
@@ -5,34 +5,43 @@ import type { ForwardedRef, ReactElement } from 'react';
|
|
|
5
5
|
import type { SegmentedProps } from '@redocly/theme/core/types';
|
|
6
6
|
|
|
7
7
|
import { typedMemo } from '@redocly/theme/core/hoc/typed-memo';
|
|
8
|
+
import { useTabList } from '@redocly/theme/core/hooks';
|
|
8
9
|
|
|
9
10
|
function SegmentedComponent<T>(
|
|
10
11
|
{ options, onChange, value, className = '', size = 'regular' }: SegmentedProps<T>,
|
|
11
12
|
ref?: ForwardedRef<HTMLDivElement>,
|
|
12
13
|
): ReactElement {
|
|
14
|
+
const { tabListProps, getTabProps } = useTabList({
|
|
15
|
+
count: options.length,
|
|
16
|
+
activeIndex: options.findIndex((opt) => value == opt.value),
|
|
17
|
+
onChange: (index) => onChange(options[index]),
|
|
18
|
+
});
|
|
19
|
+
|
|
13
20
|
return (
|
|
14
21
|
<SegmentedGroup
|
|
15
22
|
ref={ref}
|
|
16
23
|
data-component-name="Segmented/Segmented"
|
|
17
24
|
className={`tag-grey ${size} ${className}`}
|
|
18
|
-
|
|
25
|
+
{...tabListProps}
|
|
19
26
|
$size={size}
|
|
20
27
|
>
|
|
21
|
-
{options.map((opt, index) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
{options.map((opt, index) => {
|
|
29
|
+
const { 'aria-controls': _noPanel, ...tabProps } = getTabProps(index);
|
|
30
|
+
return (
|
|
31
|
+
<React.Fragment key={index}>
|
|
32
|
+
{opt.divider}
|
|
33
|
+
<SegmentedItem
|
|
34
|
+
key={opt.label}
|
|
35
|
+
{...tabProps}
|
|
36
|
+
title={opt.label}
|
|
37
|
+
$isActive={value == opt.value}
|
|
38
|
+
$size={size}
|
|
39
|
+
>
|
|
40
|
+
{opt.element || opt.label}
|
|
41
|
+
</SegmentedItem>
|
|
42
|
+
</React.Fragment>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
36
45
|
</SegmentedGroup>
|
|
37
46
|
);
|
|
38
47
|
}
|
|
@@ -63,6 +63,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
|
|
|
63
63
|
const [filteredOptions, setFilteredOptions] = useState<SelectOption<T>[]>(options);
|
|
64
64
|
const [stickyInputValue, setStickyInputValue] = useState<string>(placeholder || '');
|
|
65
65
|
const inputId = useId();
|
|
66
|
+
const listboxId = `${inputId}-listbox`;
|
|
66
67
|
|
|
67
68
|
useOutsideClick(selectRef, () => {
|
|
68
69
|
setDropdownActive(false);
|
|
@@ -189,7 +190,8 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
|
|
|
189
190
|
return (
|
|
190
191
|
<SelectInput
|
|
191
192
|
id={inputId}
|
|
192
|
-
listboxId={
|
|
193
|
+
listboxId={listboxId}
|
|
194
|
+
expanded={dropdownActive}
|
|
193
195
|
selectedOptions={selectedOptions}
|
|
194
196
|
searchValue={searchValue}
|
|
195
197
|
placeholder={placeholder}
|
|
@@ -223,10 +225,14 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
|
|
|
223
225
|
setDropdownActive(!dropdownActive);
|
|
224
226
|
}
|
|
225
227
|
}}
|
|
226
|
-
tabIndex={0}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
tabIndex={searchable ? undefined : 0}
|
|
229
|
+
{...(!searchable && {
|
|
230
|
+
role: 'combobox',
|
|
231
|
+
'aria-haspopup': 'listbox' as const,
|
|
232
|
+
'aria-expanded': dropdownActive,
|
|
233
|
+
'aria-controls': listboxId,
|
|
234
|
+
'aria-label': 'Select option',
|
|
235
|
+
})}
|
|
230
236
|
onFocus={(e) => {
|
|
231
237
|
if (e.target === selectRef.current) {
|
|
232
238
|
selectInputRef.current?.focus();
|
|
@@ -245,7 +251,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
|
|
|
245
251
|
alignment={alignment}
|
|
246
252
|
active={!renderInput ? dropdownActive : undefined}
|
|
247
253
|
>
|
|
248
|
-
<SelectDropdownMenu role="listbox" footer={footer}>
|
|
254
|
+
<SelectDropdownMenu id={listboxId} role="listbox" footer={footer}>
|
|
249
255
|
{filteredOptions.length === 0 ? (
|
|
250
256
|
<DropdownMenuItem disabled>
|
|
251
257
|
{translate('select.noResults', 'No results')}
|
|
@@ -22,6 +22,7 @@ type SelectInputProps<T> = {
|
|
|
22
22
|
placeholder?: string;
|
|
23
23
|
multiple?: boolean;
|
|
24
24
|
searchable?: boolean;
|
|
25
|
+
expanded?: boolean;
|
|
25
26
|
clearable?: boolean;
|
|
26
27
|
inputRef?: React.ForwardedRef<HTMLInputElement>;
|
|
27
28
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
@@ -48,6 +49,7 @@ export function SelectInput<T>(props: SelectInputProps<T>): React.ReactNode {
|
|
|
48
49
|
stickyValue,
|
|
49
50
|
multiple,
|
|
50
51
|
searchable,
|
|
52
|
+
expanded,
|
|
51
53
|
clearable,
|
|
52
54
|
clearHandler,
|
|
53
55
|
searchHandler,
|
|
@@ -134,6 +136,11 @@ export function SelectInput<T>(props: SelectInputProps<T>): React.ReactNode {
|
|
|
134
136
|
}}
|
|
135
137
|
$width={multiple ? (!searchValue && selectedOptions.length ? '10px' : 'auto') : '100%'}
|
|
136
138
|
tabIndex={0}
|
|
139
|
+
role="combobox"
|
|
140
|
+
aria-expanded={!!expanded}
|
|
141
|
+
aria-controls={listboxId}
|
|
142
|
+
aria-autocomplete="list"
|
|
143
|
+
aria-label="Select option"
|
|
137
144
|
onFocus={(e) => {
|
|
138
145
|
e.stopPropagation();
|
|
139
146
|
}}
|
|
@@ -156,16 +163,7 @@ export function SelectInput<T>(props: SelectInputProps<T>): React.ReactNode {
|
|
|
156
163
|
);
|
|
157
164
|
|
|
158
165
|
return (
|
|
159
|
-
<SelectInputWrapper
|
|
160
|
-
id={id}
|
|
161
|
-
onFocus={onFocusHandler}
|
|
162
|
-
onClick={onClickHandler}
|
|
163
|
-
role="button"
|
|
164
|
-
aria-haspopup="listbox"
|
|
165
|
-
aria-controls={listboxId}
|
|
166
|
-
aria-owns={listboxId}
|
|
167
|
-
aria-label="Select option"
|
|
168
|
-
>
|
|
166
|
+
<SelectInputWrapper id={id} onFocus={onFocusHandler} onClick={onClickHandler}>
|
|
169
167
|
{!onlyIcon && (
|
|
170
168
|
<>
|
|
171
169
|
<SelectInputValue>
|
|
@@ -20,6 +20,7 @@ export const useThemeHooks = vi.fn(() => ({
|
|
|
20
20
|
sendPageActionsButtonClickedMessage: vi.fn(),
|
|
21
21
|
sendColorModeSwitchedMessage: vi.fn(),
|
|
22
22
|
sendConsentChangedMessage: vi.fn(),
|
|
23
|
+
sendFilterCheckboxToggledMessage: vi.fn(),
|
|
23
24
|
})),
|
|
24
25
|
useBreadcrumbs: vi.fn().mockReturnValue({ breadcrumbs: [], siblings: undefined }),
|
|
25
26
|
useBanner: vi.fn(() => ({
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './menu/use-mobile-menu-levels';
|
|
|
21
21
|
export * from './menu/use-collapse';
|
|
22
22
|
export * from './feedback/use-report-dialog';
|
|
23
23
|
export * from './use-tabs';
|
|
24
|
+
export * from './use-tab-list';
|
|
24
25
|
export * from './use-focus-trap';
|
|
25
26
|
export * from './use-consent-dialog';
|
|
26
27
|
export * from './use-consent-telemetry';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback, useId, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { KeyboardEvent } from 'react';
|
|
4
|
+
|
|
5
|
+
type UseTabListProps = {
|
|
6
|
+
count: number;
|
|
7
|
+
activeIndex: number;
|
|
8
|
+
onChange: (index: number) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Props for a WAI-ARIA tab list with automatic activation: only the active tab is a tab stop,
|
|
13
|
+
* and Left/Right/Home/End select and focus another tab.
|
|
14
|
+
*/
|
|
15
|
+
export function useTabList({ count, activeIndex, onChange }: UseTabListProps) {
|
|
16
|
+
const idPrefix = useId();
|
|
17
|
+
const focusableIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
18
|
+
const tabRefs = useRef<(HTMLElement | null)[]>([]);
|
|
19
|
+
|
|
20
|
+
const select = useCallback(
|
|
21
|
+
(index: number) => {
|
|
22
|
+
onChange(index);
|
|
23
|
+
tabRefs.current[index]?.focus();
|
|
24
|
+
},
|
|
25
|
+
[onChange],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const onKeyDown = useCallback(
|
|
29
|
+
(event: KeyboardEvent, index: number) => {
|
|
30
|
+
const target = (<Record<string, number>>{
|
|
31
|
+
ArrowRight: (index + 1) % count,
|
|
32
|
+
ArrowLeft: (index - 1 + count) % count,
|
|
33
|
+
Home: 0,
|
|
34
|
+
End: count - 1,
|
|
35
|
+
})[event.key];
|
|
36
|
+
if (target === undefined) return;
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
select(target);
|
|
39
|
+
},
|
|
40
|
+
[count, select],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const getTabProps = (index: number) => ({
|
|
44
|
+
id: `${idPrefix}-tab-${index}`,
|
|
45
|
+
role: 'tab',
|
|
46
|
+
'aria-selected': index === activeIndex,
|
|
47
|
+
'aria-controls': `${idPrefix}-panel-${index}`,
|
|
48
|
+
tabIndex: index === focusableIndex ? 0 : -1,
|
|
49
|
+
ref: (element: HTMLElement | null) => {
|
|
50
|
+
tabRefs.current[index] = element;
|
|
51
|
+
},
|
|
52
|
+
onClick: () => select(index),
|
|
53
|
+
onKeyDown: (event: KeyboardEvent) => onKeyDown(event, index),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const getTabPanelProps = (index: number) => ({
|
|
57
|
+
id: `${idPrefix}-panel-${index}`,
|
|
58
|
+
role: 'tabpanel',
|
|
59
|
+
'aria-labelledby': `${idPrefix}-tab-${index}`,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { tabListProps: { role: 'tablist' }, getTabProps, getTabPanelProps };
|
|
63
|
+
}
|
|
@@ -5,60 +5,86 @@ interface DynamicOptions<TProps extends Record<string, unknown>> {
|
|
|
5
5
|
loading?: React.ComponentType<TProps>;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A component from `dynamic()`. After `preload()` resolves, the component renders without a
|
|
10
|
+
* loading state. On the server, `preload()` does not load a client-only component (`ssr: false`).
|
|
11
|
+
*/
|
|
12
|
+
export type DynamicComponent<TProps> = React.ComponentType<TProps> & {
|
|
13
|
+
preload(): Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
|
|
8
16
|
export function dynamic<TProps extends Record<string, unknown>>(
|
|
9
17
|
factory: () => Promise<
|
|
10
18
|
React.ComponentType<TProps> | { default: React.ComponentType<TProps> } | Record<string, unknown>
|
|
11
19
|
>,
|
|
12
20
|
{ ssr = true, loading }: DynamicOptions<TProps> = {},
|
|
13
|
-
):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (mod && typeof mod === 'object' && 'default' in mod) {
|
|
25
|
-
const defaultExport = mod.default;
|
|
26
|
-
|
|
27
|
-
// Handle both function components and React component objects (e.g., memo/forwardRef)
|
|
28
|
-
if (typeof defaultExport === 'function') {
|
|
29
|
-
return { default: defaultExport as React.ComponentType<TProps> };
|
|
21
|
+
): DynamicComponent<TProps> {
|
|
22
|
+
let loaded: React.ComponentType<TProps> | undefined;
|
|
23
|
+
let pending: Promise<{ default: React.ComponentType<TProps> }> | undefined;
|
|
24
|
+
|
|
25
|
+
const load = () =>
|
|
26
|
+
(pending ??= factory()
|
|
27
|
+
.then((mod) => {
|
|
28
|
+
// Case 1: factory() => import('./SomeModule') - direct import
|
|
29
|
+
// Case 2: factory() => import('./SomeModule').then(mod => mod.default) - extract default
|
|
30
|
+
if (typeof mod === 'function') {
|
|
31
|
+
return { default: mod };
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
// Case 3: factory() => import('./SomeModule').then(mod => ({ default: mod.default })) - manual wrap
|
|
35
|
+
// Case 4: factory() => import('./SomeModule').then(mod => ({ default: mod.Named })) - manual wrap named
|
|
36
|
+
if (mod && typeof mod === 'object' && 'default' in mod) {
|
|
37
|
+
const defaultExport = mod.default;
|
|
38
|
+
|
|
39
|
+
// Handle both function components and React component objects (e.g., memo/forwardRef)
|
|
40
|
+
if (typeof defaultExport === 'function') {
|
|
41
|
+
return { default: defaultExport as React.ComponentType<TProps> };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (defaultExport && typeof defaultExport === 'object' && '$$typeof' in defaultExport) {
|
|
45
|
+
return { default: defaultExport as React.ComponentType<TProps> };
|
|
46
|
+
}
|
|
34
47
|
}
|
|
35
|
-
}
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
// Case 5: factory() => import('./SomeModule').then(mod => mod.Named) - find named export
|
|
50
|
+
if (mod && typeof mod === 'object') {
|
|
51
|
+
const modObj = mod as Record<string, unknown>;
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
for (const key in modObj) {
|
|
54
|
+
const value = modObj[key];
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
if (typeof value === 'function') {
|
|
57
|
+
return { default: value as React.ComponentType<TProps> };
|
|
58
|
+
}
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
if (value && typeof value === 'object' && '$$typeof' in value) {
|
|
61
|
+
return { default: value as React.ComponentType<TProps> };
|
|
62
|
+
}
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
|
-
}
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
throw new Error('Dynamic import did not return a valid React component');
|
|
67
|
+
})
|
|
68
|
+
.then(
|
|
69
|
+
(module) => {
|
|
70
|
+
loaded = module.default;
|
|
71
|
+
return module;
|
|
72
|
+
},
|
|
73
|
+
(error: unknown) => {
|
|
74
|
+
// A failed load is not kept, so the next preload tries again.
|
|
75
|
+
pending = undefined;
|
|
76
|
+
throw error;
|
|
77
|
+
},
|
|
78
|
+
));
|
|
79
|
+
|
|
80
|
+
const LazyComponent = React.lazy(load);
|
|
57
81
|
|
|
58
82
|
const LoadingComponent: React.ComponentType<TProps> = loading || (() => null);
|
|
59
83
|
|
|
60
|
-
|
|
84
|
+
const DynamicComponent = (props: TProps) => {
|
|
61
85
|
const [hasMounted, setHasMounted] = React.useState(false);
|
|
86
|
+
// Fixed per instance: a change of the element type would remount the component.
|
|
87
|
+
const [Component] = React.useState(() => loaded ?? LazyComponent);
|
|
62
88
|
|
|
63
89
|
React.useEffect(() => {
|
|
64
90
|
setHasMounted(true);
|
|
@@ -82,4 +108,10 @@ export function dynamic<TProps extends Record<string, unknown>>(
|
|
|
82
108
|
</React.Suspense>
|
|
83
109
|
);
|
|
84
110
|
};
|
|
111
|
+
|
|
112
|
+
return Object.assign(DynamicComponent, {
|
|
113
|
+
async preload() {
|
|
114
|
+
if (ssr || typeof window !== 'undefined') await load();
|
|
115
|
+
},
|
|
116
|
+
});
|
|
85
117
|
}
|
|
@@ -11,6 +11,7 @@ const Icon = ({
|
|
|
11
11
|
checked?: boolean;
|
|
12
12
|
onClick?: () => void;
|
|
13
13
|
}) => (
|
|
14
|
+
// oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- the control that renders the icon handles the keyboard
|
|
14
15
|
<span className={className} onClick={onClick}>
|
|
15
16
|
{checked ? (
|
|
16
17
|
<svg
|
|
@@ -6,6 +6,7 @@ import type { IconProps } from '@redocly/theme/icons/types';
|
|
|
6
6
|
import { getCssColorVariable } from '@redocly/theme/core/utils';
|
|
7
7
|
|
|
8
8
|
const Icon = ({ className, onClick }: IconProps & { onClick?: () => void }) => (
|
|
9
|
+
// oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- the control that renders the icon handles the keyboard
|
|
9
10
|
<svg
|
|
10
11
|
className={className}
|
|
11
12
|
onClick={onClick}
|