@redocly/theme 0.69.0-next.8 → 0.69.0-next.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/lib/components/Accordion/Accordion.js +5 -1
  2. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.js +5 -3
  3. package/lib/components/Catalog/CatalogViewModeToggle.d.ts +1 -1
  4. package/lib/components/Catalog/CatalogViewModeToggle.js +2 -43
  5. package/lib/components/Filter/FilterInput.js +4 -2
  6. package/lib/components/JsonViewer/Helpers.js +12 -42
  7. package/lib/components/Panel/Panel.js +5 -1
  8. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +22 -32
  9. package/lib/components/Search/SearchItem.js +5 -3
  10. package/lib/components/Search/variables.js +1 -0
  11. package/lib/components/Select/Select.js +10 -5
  12. package/lib/components/Sidebar/Sidebar.js +2 -38
  13. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-controls.js +5 -17
  14. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.js +1 -0
  15. package/lib/core/hooks/menu/use-menu-item-expanded.js +1 -0
  16. package/lib/core/hooks/use-local-state.js +1 -0
  17. package/lib/core/hooks/use-mobile-menu.js +10 -2
  18. package/package.json +1 -1
  19. package/src/components/Accordion/Accordion.tsx +7 -3
  20. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.tsx +6 -4
  21. package/src/components/Catalog/CatalogViewModeToggle.tsx +2 -17
  22. package/src/components/Filter/FilterInput.tsx +5 -3
  23. package/src/components/JsonViewer/Helpers.tsx +13 -9
  24. package/src/components/Panel/Panel.tsx +7 -3
  25. package/src/components/Search/FilterFields/SearchFilterFieldSelect.tsx +32 -40
  26. package/src/components/Search/SearchItem.tsx +6 -1
  27. package/src/components/Search/variables.ts +1 -0
  28. package/src/components/Select/Select.tsx +12 -6
  29. package/src/components/Sidebar/Sidebar.tsx +2 -6
  30. package/src/core/hooks/code-walkthrough/use-code-walkthrough-controls.ts +6 -20
  31. package/src/core/hooks/code-walkthrough/use-code-walkthrough-steps.ts +1 -0
  32. package/src/core/hooks/menu/use-menu-item-expanded.ts +1 -0
  33. package/src/core/hooks/use-local-state.ts +1 -0
  34. package/src/core/hooks/use-mobile-menu.ts +11 -3
@@ -47,6 +47,11 @@ const ChevronRightIcon_1 = require("../../icons/ChevronRightIcon/ChevronRightIco
47
47
  function Accordion({ expanded = true, isExpandable = true, renderChildrenHidden = false, header, className, children, onToggle, dataTestId, }) {
48
48
  const [isExpanded, setExpanded] = (0, react_1.useState)(expanded);
49
49
  const [animate, setAnimate] = (0, react_1.useState)(false);
50
+ const [prevExpanded, setPrevExpanded] = (0, react_1.useState)(expanded);
51
+ if (prevExpanded !== expanded) {
52
+ setPrevExpanded(expanded);
53
+ setExpanded(expanded);
54
+ }
50
55
  const toggle = ({ target }) => {
51
56
  if (target instanceof HTMLAnchorElement || !isExpandable)
52
57
  return;
@@ -54,7 +59,6 @@ function Accordion({ expanded = true, isExpandable = true, renderChildrenHidden
54
59
  setExpanded(!isExpanded);
55
60
  onToggle === null || onToggle === void 0 ? void 0 : onToggle(!isExpanded);
56
61
  };
57
- (0, react_1.useEffect)(() => setExpanded(expanded), [expanded]);
58
62
  return (react_1.default.createElement(AccordionWrapper, { className: className, "data-testid": dataTestId, "data-component-name": "Accordion/Accordion" },
59
63
  header &&
60
64
  (typeof header === 'function' ? (header({ expanded: isExpanded })) : (react_1.default.createElement(AccordionHeader_1.AccordionHeader, { "data-testid": "accordion-header", onClick: toggle, expanded: isExpanded, isExpandable: isExpandable },
@@ -48,9 +48,11 @@ function CatalogEntityRelations({ entity, entitiesCatalogConfig, initialRelation
48
48
  const { useCatalogSort, useFetchCatalogEntitiesRelations } = (0, hooks_1.useThemeHooks)();
49
49
  const { sortOption, setSortOption, handleSortClick, isColumnSorted } = useCatalogSort();
50
50
  const [filter, setFilter] = (0, react_1.useState)(ENTITY_DEFAULT_FILTERS[entity.type]);
51
- (0, react_1.useEffect)(() => {
52
- setFilter(ENTITY_DEFAULT_FILTERS[entity.key]);
53
- }, [entity.key]);
51
+ const [prevEntityKey, setPrevEntityKey] = (0, react_1.useState)(entity.key);
52
+ if (prevEntityKey !== entity.key) {
53
+ setPrevEntityKey(entity.key);
54
+ setFilter(ENTITY_DEFAULT_FILTERS[entity.type]);
55
+ }
54
56
  const combinedFilter = (0, react_1.useMemo)(() => {
55
57
  const baseFilter = filter !== null && filter !== void 0 ? filter : ENTITY_DEFAULT_FILTERS[entity.type];
56
58
  if (entity.version && entity.revision) {
@@ -4,4 +4,4 @@ export type CatalogViewModeToggleProps = {
4
4
  viewMode: CatalogViewMode;
5
5
  onViewModeChange: (mode: CatalogViewMode) => void;
6
6
  };
7
- export declare function CatalogViewModeToggle({ viewMode: initialViewMode, onViewModeChange, }: CatalogViewModeToggleProps): React.JSX.Element;
7
+ export declare function CatalogViewModeToggle({ viewMode, onViewModeChange }: CatalogViewModeToggleProps): React.JSX.Element;
@@ -1,55 +1,14 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
6
  exports.CatalogViewModeToggle = CatalogViewModeToggle;
40
- const react_1 = __importStar(require("react"));
7
+ const react_1 = __importDefault(require("react"));
41
8
  const styled_components_1 = __importDefault(require("styled-components"));
42
9
  const TableIcon_1 = require("../../icons/TableIcon/TableIcon");
43
10
  const CardsIcon_1 = require("../../icons/CardsIcon/CardsIcon");
44
- function useViewMode(initialViewMode) {
45
- const [viewMode, setViewMode] = (0, react_1.useState)(null);
46
- (0, react_1.useEffect)(() => {
47
- setViewMode(initialViewMode);
48
- }, [initialViewMode]);
49
- return viewMode;
50
- }
51
- function CatalogViewModeToggle({ viewMode: initialViewMode, onViewModeChange, }) {
52
- const viewMode = useViewMode(initialViewMode);
11
+ function CatalogViewModeToggle({ viewMode, onViewModeChange }) {
53
12
  return (react_1.default.createElement(ViewModeToggleWrapper, { "data-component-name": "Catalog/CatalogViewModeToggle" },
54
13
  react_1.default.createElement(ViewModeButton, { className: viewMode === 'table' ? 'active' : '', onClick: () => onViewModeChange('table') },
55
14
  react_1.default.createElement(TableIcon_1.TableIcon, { size: "var(--catalog-view-mode-table-icon-size)" })),
@@ -44,9 +44,11 @@ const SearchIcon_1 = require("../../icons/SearchIcon/SearchIcon");
44
44
  function FilterInput(props) {
45
45
  const { value: initialValue, onChange, dataTestId } = props;
46
46
  const [value, setValue] = (0, react_1.useState)(initialValue);
47
- (0, react_1.useEffect)(() => {
47
+ const [prevInitialValue, setPrevInitialValue] = (0, react_1.useState)(initialValue);
48
+ if (prevInitialValue !== initialValue) {
49
+ setPrevInitialValue(initialValue);
48
50
  setValue(initialValue);
49
- }, [initialValue]);
51
+ }
50
52
  const handleChange = (e) => {
51
53
  const newValue = e.target.value;
52
54
  setValue(newValue);
@@ -1,43 +1,10 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
6
  exports.JsonValue = JsonValue;
40
- const react_1 = __importStar(require("react"));
7
+ const react_1 = __importDefault(require("react"));
41
8
  const styled_components_1 = __importDefault(require("styled-components"));
42
9
  const Button_1 = require("../../components/Button/Button");
43
10
  const ShowMoreToggleButton_1 = require("../../components/Button/ShowMoreToggleButton");
@@ -47,14 +14,15 @@ function JsonValue(props) {
47
14
  const indent = ' '.repeat(level * 2);
48
15
  const [isExpandedState, setExpanded] = react_1.default.useState(expandAllSignal !== null && expandAllSignal !== void 0 ? expandAllSignal : level < defaultExpandLevel);
49
16
  const isExpanded = expandAllSignal !== undefined && level > 0 ? expandAllSignal : isExpandedState;
50
- (0, react_1.useEffect)(() => {
51
- if (expandAllSignal !== undefined) {
52
- if (expandAllSignal === false && level === 0) {
53
- return;
54
- }
17
+ const [prevExpandAllSignal, setPrevExpandAllSignal] = react_1.default.useState(expandAllSignal);
18
+ const [prevLevel, setPrevLevel] = react_1.default.useState(level);
19
+ if (prevExpandAllSignal !== expandAllSignal || prevLevel !== level) {
20
+ setPrevExpandAllSignal(expandAllSignal);
21
+ setPrevLevel(level);
22
+ if (expandAllSignal !== undefined && !(expandAllSignal === false && level === 0)) {
55
23
  setExpanded(expandAllSignal);
56
24
  }
57
- }, [expandAllSignal, level]);
25
+ }
58
26
  const keys = (value && Object.keys(value).filter((key) => value[key] !== undefined)) || []; // filter out undefined properties
59
27
  const valueType = typeof value;
60
28
  const isPrimitive = (valueType !== 'object' && value !== null) || keys.length === 0;
@@ -94,11 +62,13 @@ function JsonKeyValue(props) {
94
62
  const isCollapsible = typeof value === 'object' && value !== null && keys.length > 0;
95
63
  const [isExpandedState, setExpanded] = react_1.default.useState(expandAllSignal !== null && expandAllSignal !== void 0 ? expandAllSignal : level < defaultExpandLevel);
96
64
  const isExpanded = expandAllSignal !== undefined ? expandAllSignal : isExpandedState;
97
- (0, react_1.useEffect)(() => {
65
+ const [prevExpandAllSignal, setPrevExpandAllSignal] = react_1.default.useState(expandAllSignal);
66
+ if (prevExpandAllSignal !== expandAllSignal) {
67
+ setPrevExpandAllSignal(expandAllSignal);
98
68
  if (expandAllSignal !== undefined) {
99
69
  setExpanded(expandAllSignal);
100
70
  }
101
- }, [expandAllSignal]);
71
+ }
102
72
  const openingPunctuation = Array.isArray(value) ? '[' : '{';
103
73
  const closingPunctuation = Array.isArray(value) ? ']' : '}';
104
74
  const valueElement = isCollapsible ? (react_1.default.createElement(react_1.default.Fragment, null,
@@ -47,6 +47,11 @@ const ChevronRightIcon_1 = require("../../icons/ChevronRightIcon/ChevronRightIco
47
47
  function Panel({ expanded = true, isExpandable = true, renderChildrenHidden = false, header, footer, className = 'panel-default', children, onToggle, description, }) {
48
48
  const [isExpanded, setExpanded] = (0, react_1.useState)(expanded);
49
49
  const [animate, setAnimate] = (0, react_1.useState)(false);
50
+ const [prevExpanded, setPrevExpanded] = (0, react_1.useState)(expanded);
51
+ if (prevExpanded !== expanded) {
52
+ setPrevExpanded(expanded);
53
+ setExpanded(expanded);
54
+ }
50
55
  const toggle = ({ target }) => {
51
56
  if (target instanceof HTMLAnchorElement || !isExpandable)
52
57
  return;
@@ -54,7 +59,6 @@ function Panel({ expanded = true, isExpandable = true, renderChildrenHidden = fa
54
59
  setExpanded(!isExpanded);
55
60
  onToggle === null || onToggle === void 0 ? void 0 : onToggle(!isExpanded);
56
61
  };
57
- (0, react_1.useEffect)(() => setExpanded(expanded), [expanded]);
58
62
  return (react_1.default.createElement(PanelWrapper, { className: className, "data-testid": typeof header === 'string' ? ('panel-' + header).replace(/ /g, '-').toLowerCase() : null, "data-component-name": "Panel/Panel" },
59
63
  header &&
60
64
  (typeof header === 'function' ? (header({ expanded: isExpanded, toggle })) : (react_1.default.createElement(PanelHeader_1.PanelHeader, { "data-testid": "panel-header", toggle: toggle, expanded: isExpanded, isExpandable: isExpandable },
@@ -47,13 +47,7 @@ function SearchFilterFieldSelect({ facet, filter, query, selectedValues, onChang
47
47
  const { useFacetQuery, useTranslate } = (0, hooks_1.useThemeHooks)();
48
48
  const { translate } = useTranslate();
49
49
  const { searchFacet, setSearchFacet, setSearchFacetQuery } = useFacetQuery(facet.field);
50
- /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
51
- const [selectOptions, setSelectOptions] = (0, react_1.useState)([]);
52
50
  const isMultiple = facet.type === 'multi-select';
53
- (0, react_1.useEffect)(() => {
54
- setSelectOptions(getSelectOptions());
55
- // eslint-disable-next-line react-hooks/exhaustive-deps
56
- }, [searchFacet, facet]);
57
51
  const onSearch = (value) => {
58
52
  if (value === null) {
59
53
  setSearchFacet(null);
@@ -67,38 +61,34 @@ function SearchFilterFieldSelect({ facet, filter, query, selectedValues, onChang
67
61
  });
68
62
  }
69
63
  };
70
- const getSelectOptions = () => {
71
- return searchFacet
72
- ? searchFacet.values.map(facetValueToSelectOption)
73
- : facet.values.map(facetValueToSelectOption);
74
- };
75
- const facetValueToSelectOption = (facetValue) => {
76
- if (!facetValue) {
77
- return { element: null, value: '' };
78
- }
79
- if (typeof facetValue === 'string') {
80
- return {
81
- element: (react_1.default.createElement(FilterSelectOptionWrapper, null,
82
- react_1.default.createElement(FilterSelectOptionText, null, facetValue))),
83
- value: facetValue,
84
- };
85
- }
86
- else {
87
- const { value, count, isCounterVisible } = facetValue;
88
- return {
89
- element: (react_1.default.createElement(FilterSelectOptionWrapper, null,
90
- react_1.default.createElement(FilterSelectOptionText, null, value),
91
- isCounterVisible && react_1.default.createElement(CounterTag_1.CounterTag, null, count))),
92
- value: value,
93
- };
94
- }
95
- };
64
+ const selectOptions = (0, react_1.useMemo)(() => (searchFacet ? searchFacet.values : facet.values).map(facetValueToSelectOption), [searchFacet, facet]);
96
65
  return (react_1.default.createElement(Select_1.Select, { value: selectedValues.length
97
66
  ? isMultiple
98
67
  ? selectedValues.map((value) => facetValueToSelectOption(value))
99
68
  : facetValueToSelectOption(selectedValues[0])
100
69
  : undefined, options: selectOptions, onChange: onChange, placeholder: `${translate('search.filter.field.placeholder', 'Search')} ${facet.name.toLowerCase()}...`, alignment: "start", multiple: isMultiple, searchable: true, footer: selectOptions.length === MAX_SELECT_OPTIONS ? (react_1.default.createElement(FilterSelectFooter, null, translate('search.filter.field.footer', 'Search to show more items...'))) : null, onSearch: onSearch, checkmarkIconPosition: "start" }));
101
70
  }
71
+ function facetValueToSelectOption(facetValue) {
72
+ if (!facetValue) {
73
+ return { element: null, value: '' };
74
+ }
75
+ if (typeof facetValue === 'string') {
76
+ return {
77
+ element: (react_1.default.createElement(FilterSelectOptionWrapper, null,
78
+ react_1.default.createElement(FilterSelectOptionText, null, facetValue))),
79
+ value: facetValue,
80
+ };
81
+ }
82
+ else {
83
+ const { value, count, isCounterVisible } = facetValue;
84
+ return {
85
+ element: (react_1.default.createElement(FilterSelectOptionWrapper, null,
86
+ react_1.default.createElement(FilterSelectOptionText, null, value),
87
+ isCounterVisible && react_1.default.createElement(CounterTag_1.CounterTag, null, count))),
88
+ value: value,
89
+ };
90
+ }
91
+ }
102
92
  const FilterSelectOptionWrapper = styled_components_1.default.div `
103
93
  width: 100%;
104
94
  display: flex;
@@ -48,7 +48,7 @@ const utils_1 = require("../../core/utils");
48
48
  const ReturnKeyIcon_1 = require("../../icons/ReturnKeyIcon/ReturnKeyIcon");
49
49
  const hooks_1 = require("../../core/hooks");
50
50
  function SearchItem({ onClick, item, className, product, innerRef, }) {
51
- var _a, _b, _c, _d;
51
+ var _a, _b, _c;
52
52
  const ref = (0, react_1.useRef)(null);
53
53
  const { useTranslate } = (0, hooks_1.useThemeHooks)();
54
54
  const { translate } = useTranslate();
@@ -95,11 +95,13 @@ function SearchItem({ onClick, item, className, product, innerRef, }) {
95
95
  react_1.default.createElement("div", null,
96
96
  (0, SearchHighlight_1.searchHighlight)(parameter.place),
97
97
  ` → `,
98
- ((_b = parameter.path) === null || _b === void 0 ? void 0 : _b.length) ? (0, SearchHighlight_1.searchHighlight)((_c = parameter.path) === null || _c === void 0 ? void 0 : _c.join(' → ')) + ' → ' : '',
98
+ ((_b = parameter.path) === null || _b === void 0 ? void 0 : _b.length) ? (react_1.default.createElement(react_1.default.Fragment, null,
99
+ (0, SearchHighlight_1.searchHighlight)(parameter.path.join(' → ')),
100
+ ` → `)) : null,
99
101
  (0, SearchHighlight_1.searchHighlight)(parameter.name)),
100
102
  react_1.default.createElement("div", null, (0, SearchHighlight_1.searchHighlight)(parameter.description)))) : (shouldShowPath && (react_1.default.createElement(SearchItemPath, null, highlight.path && highlight.path.length
101
103
  ? (0, SearchHighlight_1.searchHighlight)(highlight.path.join(' → '))
102
- : (_d = document.path) === null || _d === void 0 ? void 0 : _d.join(' → '))))),
104
+ : (_c = document.path) === null || _c === void 0 ? void 0 : _c.join(' → '))))),
103
105
  react_1.default.createElement(ReturnKeyIcon_1.ReturnKeyIcon, { color: "var(--search-item-text-color)" })));
104
106
  }
105
107
  const SearchItemWrapper = (0, styled_components_1.default)(Link_1.Link) `
@@ -98,6 +98,7 @@ exports.search = (0, styled_components_1.css) `
98
98
  --search-message-line-height: var(--line-height-lg); // @presenter LineHeight
99
99
  --search-message-text-color: var(--text-color-secondary); // @presenter Color
100
100
  --search-message-gap: var(--spacing-md);
101
+ --search-message-padding: var(--spacing-md);
101
102
 
102
103
  --search-header-buttons-gap: var(--spacing-sm);
103
104
  --search-header-buttons-padding-left: var(--spacing-sm);
@@ -71,13 +71,18 @@ function Select(props) {
71
71
  (0, hooks_1.useOutsideClick)(selectRef, () => {
72
72
  setDropdownActive(false);
73
73
  });
74
- (0, react_1.useEffect)(() => {
74
+ const [prevOptions, setPrevOptions] = (0, react_1.useState)(options);
75
+ const [prevValue, setPrevValue] = (0, react_1.useState)(value);
76
+ const [prevMultiple, setPrevMultiple] = (0, react_1.useState)(multiple);
77
+ if (prevOptions !== options) {
78
+ setPrevOptions(options);
75
79
  setFilteredOptions(options);
76
- }, [options]);
77
- (0, react_1.useEffect)(() => {
80
+ }
81
+ if (prevValue !== value || prevMultiple !== multiple) {
82
+ setPrevValue(value);
83
+ setPrevMultiple(multiple);
78
84
  setSelectedOptions(getSelectedOptionsFromPropsValue());
79
- // eslint-disable-next-line react-hooks/exhaustive-deps
80
- }, [multiple, value]);
85
+ }
81
86
  const changeSearchValue = (newSearchValue) => {
82
87
  if (newSearchValue === searchValue) {
83
88
  return;
@@ -1,54 +1,18 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
6
  exports.SidebarHeader = void 0;
40
7
  exports.Sidebar = Sidebar;
41
- const react_1 = __importStar(require("react"));
8
+ const react_1 = __importDefault(require("react"));
42
9
  const styled_components_1 = __importDefault(require("styled-components"));
43
10
  const utils_1 = require("../../core/utils");
44
11
  const hooks_1 = require("../../core/hooks");
45
12
  const MenuContainer_1 = require("../../components/Menu/MenuContainer");
46
13
  function Sidebar({ versions, menu, footer, header, growContent, collapsed, className, menuItemsAnimation, }) {
47
14
  const { isOpen } = (0, hooks_1.useMobileMenu)();
48
- const [mappedCollapsed, setMappedCollapsed] = react_1.default.useState(collapsed);
49
- (0, react_1.useEffect)(() => {
50
- setMappedCollapsed(collapsed && !isOpen);
51
- }, [isOpen, collapsed, setMappedCollapsed]);
15
+ const mappedCollapsed = collapsed && !isOpen;
52
16
  const { sidebar } = (0, hooks_1.useThemeConfig)();
53
17
  if (sidebar === null || sidebar === void 0 ? void 0 : sidebar.hide) {
54
18
  return null;
@@ -16,9 +16,6 @@ function useCodeWalkthroughControls(filters, inputs, toggles, enableDeepLink) {
16
16
  const location = (0, react_router_1.useLocation)();
17
17
  const navigate = (0, react_router_1.useNavigate)();
18
18
  const searchParams = (0, react_1.useMemo)(() => new URLSearchParams(location.search), [location.search]);
19
- const filtersRef = (0, react_1.useRef)(filters);
20
- const inputsRef = (0, react_1.useRef)(inputs);
21
- const togglesRef = (0, react_1.useRef)(toggles);
22
19
  const getInitialState = () => {
23
20
  var _a, _b;
24
21
  var _c, _d;
@@ -36,18 +33,10 @@ function useCodeWalkthroughControls(filters, inputs, toggles, enableDeepLink) {
36
33
  return state;
37
34
  };
38
35
  const [controlsState, setControlsState] = (0, react_1.useState)(getInitialState);
39
- (0, react_1.useEffect)(() => {
40
- const sameProps = [
41
- JSON.stringify(filters) === JSON.stringify(filtersRef.current),
42
- JSON.stringify(inputs) === JSON.stringify(inputsRef.current),
43
- JSON.stringify(toggles) === JSON.stringify(togglesRef.current),
44
- ];
45
- if (sameProps.every(Boolean)) {
46
- return;
47
- }
48
- filtersRef.current = filters;
49
- inputsRef.current = inputs;
50
- togglesRef.current = toggles;
36
+ const controlsKey = JSON.stringify([filters, inputs, toggles]);
37
+ const [prevControlsKey, setPrevControlsKey] = (0, react_1.useState)(controlsKey);
38
+ if (prevControlsKey !== controlsKey) {
39
+ setPrevControlsKey(controlsKey);
51
40
  const newState = getInitialState();
52
41
  // Preserve existing values where control type hasn't changed
53
42
  Object.entries(newState).forEach(([id, control]) => {
@@ -58,8 +47,7 @@ function useCodeWalkthroughControls(filters, inputs, toggles, enableDeepLink) {
58
47
  }
59
48
  });
60
49
  setControlsState(newState);
61
- // eslint-disable-next-line react-hooks/exhaustive-deps
62
- }, [filters, inputs, toggles, enableDeepLink]);
50
+ }
63
51
  const changeControlState = (id, value) => {
64
52
  setControlsState((prev) => {
65
53
  const control = prev[id];
@@ -171,6 +171,7 @@ function useCodeWalkthroughSteps({ steps, enableDeepLink, root, }) {
171
171
  }
172
172
  const rootTopOffset = (_c = (_a = root.current) === null || _a === void 0 ? void 0 : _a.offsetTop) !== null && _c !== void 0 ? _c : 0;
173
173
  if (!activeStep && rootTopOffset <= options.navbarHeight) {
174
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- seeded from root.current.offsetTop, a post-layout measurement
174
175
  setActiveStep(((_b = visibleSteps[0]) === null || _b === void 0 ? void 0 : _b.id) || null);
175
176
  }
176
177
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -11,6 +11,7 @@ function useMenuItemExpanded(item) {
11
11
  // it's important for transitions
12
12
  const [controlledValue, setControlledValue] = (0, react_1.useState)(isItemExpanded(item));
13
13
  (0, react_1.useEffect)(() => {
14
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- deferred a commit on purpose so CSS transitions still run
14
15
  setIsExpanded(controlledValue);
15
16
  }, [controlledValue]);
16
17
  (0, react_1.useEffect)(() => {
@@ -22,6 +22,7 @@ function useLocalState(key, initialValue) {
22
22
  (0, react_1.useEffect)(() => {
23
23
  if (!(0, js_utils_1.isBrowser)())
24
24
  return;
25
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- localStorage is read after mount so SSR and client hydrate from the same value
25
26
  setValue(getStoredValue(key, initialValue));
26
27
  // eslint-disable-next-line react-hooks/exhaustive-deps
27
28
  }, [key]);
@@ -7,9 +7,17 @@ const use_modal_scroll_lock_1 = require("./use-modal-scroll-lock");
7
7
  function useMobileMenu(initialState = false) {
8
8
  const location = (0, react_router_1.useLocation)();
9
9
  const [isOpen, setIsOpen] = (0, react_1.useState)(initialState);
10
- (0, react_1.useEffect)(() => setIsOpen(false), [location.pathname, location.hash]);
10
+ const locationKey = `${location.pathname}${location.hash}`;
11
+ const [prevLocationKey, setPrevLocationKey] = (0, react_1.useState)(locationKey);
12
+ if (isOpen && prevLocationKey !== locationKey) {
13
+ setPrevLocationKey(locationKey);
14
+ setIsOpen(false);
15
+ }
11
16
  (0, use_modal_scroll_lock_1.useModalScrollLock)(isOpen);
12
- const openMobileMenu = () => setIsOpen(true);
17
+ const openMobileMenu = () => {
18
+ setPrevLocationKey(locationKey);
19
+ setIsOpen(true);
20
+ };
13
21
  const closeMobileMenu = () => setIsOpen(false);
14
22
  return { isOpen, openMobileMenu, closeMobileMenu };
15
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.69.0-next.8",
3
+ "version": "0.69.0-next.9",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { BaseSyntheticEvent, PropsWithChildren, ReactNode, JSX } from 'react';
@@ -32,6 +32,12 @@ export function Accordion({
32
32
  }: PropsWithChildren<AccordionProps>): JSX.Element {
33
33
  const [isExpanded, setExpanded] = useState(expanded);
34
34
  const [animate, setAnimate] = useState(false);
35
+ const [prevExpanded, setPrevExpanded] = useState(expanded);
36
+
37
+ if (prevExpanded !== expanded) {
38
+ setPrevExpanded(expanded);
39
+ setExpanded(expanded);
40
+ }
35
41
 
36
42
  const toggle = ({ target }: BaseSyntheticEvent): void => {
37
43
  if (target instanceof HTMLAnchorElement || !isExpandable) return;
@@ -40,8 +46,6 @@ export function Accordion({
40
46
  onToggle?.(!isExpanded);
41
47
  };
42
48
 
43
- useEffect(() => setExpanded(expanded), [expanded]);
44
-
45
49
  return (
46
50
  <AccordionWrapper
47
51
  className={className}
@@ -1,4 +1,4 @@
1
- import React, { JSX, useState, useEffect, useMemo } from 'react';
1
+ import React, { JSX, useState, useMemo } from 'react';
2
2
 
3
3
  import type { CatalogEntityConfig, EntitiesCatalogConfig } from '@redocly/config';
4
4
  import type { BffCatalogEntity, BffCatalogRelatedEntityList } from '@redocly/theme/core/types';
@@ -35,10 +35,12 @@ export function CatalogEntityRelations({
35
35
  const { sortOption, setSortOption, handleSortClick, isColumnSorted } = useCatalogSort();
36
36
 
37
37
  const [filter, setFilter] = useState<string | undefined>(ENTITY_DEFAULT_FILTERS[entity.type]);
38
+ const [prevEntityKey, setPrevEntityKey] = useState(entity.key);
38
39
 
39
- useEffect(() => {
40
- setFilter(ENTITY_DEFAULT_FILTERS[entity.key]);
41
- }, [entity.key]);
40
+ if (prevEntityKey !== entity.key) {
41
+ setPrevEntityKey(entity.key);
42
+ setFilter(ENTITY_DEFAULT_FILTERS[entity.type]);
43
+ }
42
44
 
43
45
  const combinedFilter = useMemo(() => {
44
46
  const baseFilter = filter ?? ENTITY_DEFAULT_FILTERS[entity.type];
@@ -1,31 +1,16 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import { TableIcon } from '@redocly/theme/icons/TableIcon/TableIcon';
5
5
  import { CardsIcon } from '@redocly/theme/icons/CardsIcon/CardsIcon';
6
6
  import { CatalogViewMode } from '@redocly/theme/core/types';
7
7
 
8
- function useViewMode(initialViewMode: CatalogViewMode) {
9
- const [viewMode, setViewMode] = useState<CatalogViewMode | null>(null);
10
-
11
- useEffect(() => {
12
- setViewMode(initialViewMode);
13
- }, [initialViewMode]);
14
-
15
- return viewMode;
16
- }
17
-
18
8
  export type CatalogViewModeToggleProps = {
19
9
  viewMode: CatalogViewMode;
20
10
  onViewModeChange: (mode: CatalogViewMode) => void;
21
11
  };
22
12
 
23
- export function CatalogViewModeToggle({
24
- viewMode: initialViewMode,
25
- onViewModeChange,
26
- }: CatalogViewModeToggleProps) {
27
- const viewMode = useViewMode(initialViewMode);
28
-
13
+ export function CatalogViewModeToggle({ viewMode, onViewModeChange }: CatalogViewModeToggleProps) {
29
14
  return (
30
15
  <ViewModeToggleWrapper data-component-name="Catalog/CatalogViewModeToggle">
31
16
  <ViewModeButton
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
@@ -16,10 +16,12 @@ export function FilterInput(props: FilterInputProps): JSX.Element {
16
16
  const { value: initialValue, onChange, dataTestId } = props;
17
17
 
18
18
  const [value, setValue] = useState(initialValue);
19
+ const [prevInitialValue, setPrevInitialValue] = useState(initialValue);
19
20
 
20
- useEffect(() => {
21
+ if (prevInitialValue !== initialValue) {
22
+ setPrevInitialValue(initialValue);
21
23
  setValue(initialValue);
22
- }, [initialValue]);
24
+ }
23
25
 
24
26
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
25
27
  const newValue = e.target.value;
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from 'react';
1
+ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
@@ -23,14 +23,16 @@ export function JsonValue(props: {
23
23
  expandAllSignal ?? level < defaultExpandLevel,
24
24
  );
25
25
  const isExpanded = expandAllSignal !== undefined && level > 0 ? expandAllSignal : isExpandedState;
26
- useEffect(() => {
27
- if (expandAllSignal !== undefined) {
28
- if (expandAllSignal === false && level === 0) {
29
- return;
30
- }
26
+ const [prevExpandAllSignal, setPrevExpandAllSignal] = React.useState(expandAllSignal);
27
+ const [prevLevel, setPrevLevel] = React.useState(level);
28
+
29
+ if (prevExpandAllSignal !== expandAllSignal || prevLevel !== level) {
30
+ setPrevExpandAllSignal(expandAllSignal);
31
+ setPrevLevel(level);
32
+ if (expandAllSignal !== undefined && !(expandAllSignal === false && level === 0)) {
31
33
  setExpanded(expandAllSignal);
32
34
  }
33
- }, [expandAllSignal, level]);
35
+ }
34
36
 
35
37
  const keys: string[] =
36
38
  (value && Object.keys(value).filter((key) => value[key] !== undefined)) || []; // filter out undefined properties
@@ -131,12 +133,14 @@ function JsonKeyValue(props: {
131
133
  expandAllSignal ?? level < defaultExpandLevel,
132
134
  );
133
135
  const isExpanded = expandAllSignal !== undefined ? expandAllSignal : isExpandedState;
136
+ const [prevExpandAllSignal, setPrevExpandAllSignal] = React.useState(expandAllSignal);
134
137
 
135
- useEffect(() => {
138
+ if (prevExpandAllSignal !== expandAllSignal) {
139
+ setPrevExpandAllSignal(expandAllSignal);
136
140
  if (expandAllSignal !== undefined) {
137
141
  setExpanded(expandAllSignal);
138
142
  }
139
- }, [expandAllSignal]);
143
+ }
140
144
 
141
145
  const openingPunctuation = Array.isArray(value) ? '[' : '{';
142
146
  const closingPunctuation = Array.isArray(value) ? ']' : '}';
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { BaseSyntheticEvent, PropsWithChildren, ReactNode, JSX } from 'react';
@@ -34,6 +34,12 @@ export function Panel({
34
34
  }: PropsWithChildren<PanelProps>): JSX.Element {
35
35
  const [isExpanded, setExpanded] = useState(expanded);
36
36
  const [animate, setAnimate] = useState(false);
37
+ const [prevExpanded, setPrevExpanded] = useState(expanded);
38
+
39
+ if (prevExpanded !== expanded) {
40
+ setPrevExpanded(expanded);
41
+ setExpanded(expanded);
42
+ }
37
43
 
38
44
  const toggle = ({ target }: BaseSyntheticEvent): void => {
39
45
  if (target instanceof HTMLAnchorElement || !isExpandable) return;
@@ -42,8 +48,6 @@ export function Panel({
42
48
  onToggle?.(!isExpanded);
43
49
  };
44
50
 
45
- useEffect(() => setExpanded(expanded), [expanded]);
46
-
47
51
  return (
48
52
  <PanelWrapper
49
53
  className={className}
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { SearchFacet, SearchFacetCount, SearchFilterItem } from '@redocly/theme/core/types';
@@ -27,15 +27,8 @@ export function SearchFilterFieldSelect({
27
27
  const { useFacetQuery, useTranslate } = useThemeHooks();
28
28
  const { translate } = useTranslate();
29
29
  const { searchFacet, setSearchFacet, setSearchFacetQuery } = useFacetQuery(facet.field);
30
- /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
31
- const [selectOptions, setSelectOptions] = useState<any[]>([]);
32
30
  const isMultiple = facet.type === 'multi-select';
33
31
 
34
- useEffect(() => {
35
- setSelectOptions(getSelectOptions());
36
- // eslint-disable-next-line react-hooks/exhaustive-deps
37
- }, [searchFacet, facet]);
38
-
39
32
  const onSearch = (value: string | null) => {
40
33
  if (value === null) {
41
34
  setSearchFacet(null);
@@ -49,38 +42,10 @@ export function SearchFilterFieldSelect({
49
42
  }
50
43
  };
51
44
 
52
- const getSelectOptions = () => {
53
- return searchFacet
54
- ? searchFacet.values.map(facetValueToSelectOption)
55
- : facet.values.map(facetValueToSelectOption);
56
- };
57
-
58
- const facetValueToSelectOption = (facetValue?: SearchFacetCount | string) => {
59
- if (!facetValue) {
60
- return { element: null, value: '' };
61
- }
62
- if (typeof facetValue === 'string') {
63
- return {
64
- element: (
65
- <FilterSelectOptionWrapper>
66
- <FilterSelectOptionText>{facetValue}</FilterSelectOptionText>
67
- </FilterSelectOptionWrapper>
68
- ),
69
- value: facetValue,
70
- };
71
- } else {
72
- const { value, count, isCounterVisible } = facetValue as SearchFacetCount;
73
- return {
74
- element: (
75
- <FilterSelectOptionWrapper>
76
- <FilterSelectOptionText>{value}</FilterSelectOptionText>
77
- {isCounterVisible && <CounterTag>{count}</CounterTag>}
78
- </FilterSelectOptionWrapper>
79
- ),
80
- value: value,
81
- };
82
- }
83
- };
45
+ const selectOptions = useMemo(
46
+ () => (searchFacet ? searchFacet.values : facet.values).map(facetValueToSelectOption),
47
+ [searchFacet, facet],
48
+ );
84
49
 
85
50
  return (
86
51
  <Select
@@ -113,6 +78,33 @@ export function SearchFilterFieldSelect({
113
78
  );
114
79
  }
115
80
 
81
+ function facetValueToSelectOption(facetValue?: SearchFacetCount | string) {
82
+ if (!facetValue) {
83
+ return { element: null, value: '' };
84
+ }
85
+ if (typeof facetValue === 'string') {
86
+ return {
87
+ element: (
88
+ <FilterSelectOptionWrapper>
89
+ <FilterSelectOptionText>{facetValue}</FilterSelectOptionText>
90
+ </FilterSelectOptionWrapper>
91
+ ),
92
+ value: facetValue,
93
+ };
94
+ } else {
95
+ const { value, count, isCounterVisible } = facetValue as SearchFacetCount;
96
+ return {
97
+ element: (
98
+ <FilterSelectOptionWrapper>
99
+ <FilterSelectOptionText>{value}</FilterSelectOptionText>
100
+ {isCounterVisible && <CounterTag>{count}</CounterTag>}
101
+ </FilterSelectOptionWrapper>
102
+ ),
103
+ value: value,
104
+ };
105
+ }
106
+ }
107
+
116
108
  const FilterSelectOptionWrapper = styled.div`
117
109
  width: 100%;
118
110
  display: flex;
@@ -114,7 +114,12 @@ export function SearchItem({
114
114
  <div>
115
115
  {searchHighlight(parameter.place)}
116
116
  {` → `}
117
- {parameter.path?.length ? searchHighlight(parameter.path?.join(' → ')) + ' → ' : ''}
117
+ {parameter.path?.length ? (
118
+ <>
119
+ {searchHighlight(parameter.path.join(' → '))}
120
+ {` → `}
121
+ </>
122
+ ) : null}
118
123
  {searchHighlight(parameter.name)}
119
124
  </div>
120
125
  <div>{searchHighlight(parameter.description)}</div>
@@ -97,6 +97,7 @@ export const search = css`
97
97
  --search-message-line-height: var(--line-height-lg); // @presenter LineHeight
98
98
  --search-message-text-color: var(--text-color-secondary); // @presenter Color
99
99
  --search-message-gap: var(--spacing-md);
100
+ --search-message-padding: var(--spacing-md);
100
101
 
101
102
  --search-header-buttons-gap: var(--spacing-sm);
102
103
  --search-header-buttons-padding-left: var(--spacing-sm);
@@ -1,4 +1,4 @@
1
- import React, { Fragment, useEffect, useId, useRef, useState } from 'react';
1
+ import React, { Fragment, useId, useRef, useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
@@ -68,14 +68,20 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
68
68
  setDropdownActive(false);
69
69
  });
70
70
 
71
- useEffect(() => {
71
+ const [prevOptions, setPrevOptions] = useState(options);
72
+ const [prevValue, setPrevValue] = useState(value);
73
+ const [prevMultiple, setPrevMultiple] = useState(multiple);
74
+
75
+ if (prevOptions !== options) {
76
+ setPrevOptions(options);
72
77
  setFilteredOptions(options);
73
- }, [options]);
78
+ }
74
79
 
75
- useEffect(() => {
80
+ if (prevValue !== value || prevMultiple !== multiple) {
81
+ setPrevValue(value);
82
+ setPrevMultiple(multiple);
76
83
  setSelectedOptions(getSelectedOptionsFromPropsValue());
77
- // eslint-disable-next-line react-hooks/exhaustive-deps
78
- }, [multiple, value]);
84
+ }
79
85
 
80
86
  const changeSearchValue = (newSearchValue: string | null) => {
81
87
  if (newSearchValue === searchValue) {
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from 'react';
1
+ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
@@ -36,11 +36,7 @@ export function Sidebar({
36
36
  menuItemsAnimation,
37
37
  }: SidebarProps): JSX.Element | null {
38
38
  const { isOpen } = useMobileMenu();
39
- const [mappedCollapsed, setMappedCollapsed] = React.useState(collapsed);
40
-
41
- useEffect(() => {
42
- setMappedCollapsed(collapsed && !isOpen);
43
- }, [isOpen, collapsed, setMappedCollapsed]);
39
+ const mappedCollapsed = collapsed && !isOpen;
44
40
 
45
41
  const { sidebar } = useThemeConfig();
46
42
 
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useMemo, useRef } from 'react';
1
+ import { useState, useEffect, useMemo } from 'react';
2
2
  import { useLocation, useNavigate } from 'react-router';
3
3
 
4
4
  import type {
@@ -50,10 +50,6 @@ export function useCodeWalkthroughControls(
50
50
  const navigate = useNavigate();
51
51
  const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
52
52
 
53
- const filtersRef = useRef(filters);
54
- const inputsRef = useRef(inputs);
55
- const togglesRef = useRef(toggles);
56
-
57
53
  const getInitialState = () => {
58
54
  const state: CodeWalkthroughControlsState = {};
59
55
 
@@ -90,20 +86,11 @@ export function useCodeWalkthroughControls(
90
86
 
91
87
  const [controlsState, setControlsState] = useState(getInitialState);
92
88
 
93
- useEffect(() => {
94
- const sameProps = [
95
- JSON.stringify(filters) === JSON.stringify(filtersRef.current),
96
- JSON.stringify(inputs) === JSON.stringify(inputsRef.current),
97
- JSON.stringify(toggles) === JSON.stringify(togglesRef.current),
98
- ];
99
-
100
- if (sameProps.every(Boolean)) {
101
- return;
102
- }
89
+ const controlsKey = JSON.stringify([filters, inputs, toggles]);
90
+ const [prevControlsKey, setPrevControlsKey] = useState(controlsKey);
103
91
 
104
- filtersRef.current = filters;
105
- inputsRef.current = inputs;
106
- togglesRef.current = toggles;
92
+ if (prevControlsKey !== controlsKey) {
93
+ setPrevControlsKey(controlsKey);
107
94
 
108
95
  const newState = getInitialState();
109
96
 
@@ -120,8 +107,7 @@ export function useCodeWalkthroughControls(
120
107
  });
121
108
 
122
109
  setControlsState(newState);
123
- // eslint-disable-next-line react-hooks/exhaustive-deps
124
- }, [filters, inputs, toggles, enableDeepLink]);
110
+ }
125
111
 
126
112
  const changeControlState = (id: string, value: string | boolean) => {
127
113
  setControlsState((prev) => {
@@ -252,6 +252,7 @@ export function useCodeWalkthroughSteps({
252
252
 
253
253
  const rootTopOffset = root.current?.offsetTop ?? 0;
254
254
  if (!activeStep && rootTopOffset <= options.navbarHeight) {
255
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- seeded from root.current.offsetTop, a post-layout measurement
255
256
  setActiveStep(visibleSteps[0]?.id || null);
256
257
  }
257
258
 
@@ -16,6 +16,7 @@ export function useMenuItemExpanded(item: ItemState): [boolean, Dispatch<SetStat
16
16
  const [controlledValue, setControlledValue] = useState(isItemExpanded(item));
17
17
 
18
18
  useEffect(() => {
19
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- deferred a commit on purpose so CSS transitions still run
19
20
  setIsExpanded(controlledValue);
20
21
  }, [controlledValue]);
21
22
 
@@ -21,6 +21,7 @@ export function useLocalState<T>(key: string, initialValue: T): [T, (value: T) =
21
21
  // then client loads the actual stored value without hydration mismatch
22
22
  useEffect(() => {
23
23
  if (!isBrowser()) return;
24
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- localStorage is read after mount so SSR and client hydrate from the same value
24
25
  setValue(getStoredValue(key, initialValue));
25
26
  // eslint-disable-next-line react-hooks/exhaustive-deps
26
27
  }, [key]);
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useState } from 'react';
2
2
  import { useLocation } from 'react-router';
3
3
 
4
4
  import { useModalScrollLock } from './use-modal-scroll-lock';
@@ -6,11 +6,19 @@ import { useModalScrollLock } from './use-modal-scroll-lock';
6
6
  export function useMobileMenu(initialState = false) {
7
7
  const location = useLocation();
8
8
  const [isOpen, setIsOpen] = useState(initialState);
9
+ const locationKey = `${location.pathname}${location.hash}`;
10
+ const [prevLocationKey, setPrevLocationKey] = useState(locationKey);
9
11
 
10
- useEffect(() => setIsOpen(false), [location.pathname, location.hash]);
12
+ if (isOpen && prevLocationKey !== locationKey) {
13
+ setPrevLocationKey(locationKey);
14
+ setIsOpen(false);
15
+ }
11
16
  useModalScrollLock(isOpen);
12
17
 
13
- const openMobileMenu = () => setIsOpen(true);
18
+ const openMobileMenu = () => {
19
+ setPrevLocationKey(locationKey);
20
+ setIsOpen(true);
21
+ };
14
22
  const closeMobileMenu = () => setIsOpen(false);
15
23
 
16
24
  return { isOpen, openMobileMenu, closeMobileMenu };