@redocly/theme 0.68.0-next.4 → 0.68.0-next.6

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 (66) hide show
  1. package/lib/components/Button/ShowMoreToggleButton.d.ts +11 -0
  2. package/lib/components/Button/ShowMoreToggleButton.js +21 -0
  3. package/lib/components/CodeBlock/CodeBlock.d.ts +7 -1
  4. package/lib/components/CodeBlock/CodeBlock.js +30 -7
  5. package/lib/components/Dropdown/Dropdown.d.ts +2 -0
  6. package/lib/components/Dropdown/Dropdown.js +59 -14
  7. package/lib/components/Dropdown/variables.js +3 -1
  8. package/lib/components/Feedback/Feedback.js +1 -3
  9. package/lib/components/Feedback/Stars.js +1 -1
  10. package/lib/components/Footer/FooterItem.js +1 -1
  11. package/lib/components/JsonViewer/Helpers.d.ts +1 -0
  12. package/lib/components/JsonViewer/Helpers.js +20 -7
  13. package/lib/components/JsonViewer/JsonViewer.d.ts +2 -0
  14. package/lib/components/JsonViewer/JsonViewer.js +2 -2
  15. package/lib/components/LanguagePicker/LanguagePicker.js +3 -2
  16. package/lib/components/Markdown/Markdown.js +13 -6
  17. package/lib/components/Markdown/styles/links.d.ts +1 -0
  18. package/lib/components/Markdown/styles/links.js +19 -16
  19. package/lib/components/Menu/MenuItem.d.ts +7 -1
  20. package/lib/components/Menu/MenuItem.js +43 -2
  21. package/lib/components/Navbar/NavbarItem.js +8 -3
  22. package/lib/components/PageActions/PageActions.js +1 -4
  23. package/lib/components/Panel/variables.js +1 -0
  24. package/lib/components/ReplayGate/ReplayGate.d.ts +12 -0
  25. package/lib/components/ReplayGate/ReplayGate.js +11 -0
  26. package/lib/components/ReplayTopBarActions/ReplayTopBarActions.d.ts +13 -0
  27. package/lib/components/ReplayTopBarActions/ReplayTopBarActions.js +7 -0
  28. package/lib/components/Search/SearchDialog.js +4 -4
  29. package/lib/core/hooks/use-active-section-id.d.ts +2 -0
  30. package/lib/core/hooks/use-active-section-id.js +11 -1
  31. package/lib/core/hooks/use-page-actions.js +10 -9
  32. package/lib/core/hooks/use-theme-hooks.js +1 -0
  33. package/lib/core/openapi/index.d.ts +1 -1
  34. package/lib/core/openapi/index.js +2 -1
  35. package/lib/core/styles/global.js +8 -0
  36. package/lib/core/types/hooks.d.ts +1 -0
  37. package/lib/core/types/l10n.d.ts +1 -1
  38. package/lib/layouts/ThreePanelLayout.d.ts +5 -2
  39. package/package.json +3 -3
  40. package/src/components/Button/ShowMoreToggleButton.tsx +33 -0
  41. package/src/components/CodeBlock/CodeBlock.tsx +56 -12
  42. package/src/components/Dropdown/Dropdown.tsx +93 -21
  43. package/src/components/Dropdown/variables.ts +3 -1
  44. package/src/components/Feedback/Feedback.tsx +1 -3
  45. package/src/components/Feedback/Stars.tsx +1 -0
  46. package/src/components/Footer/FooterItem.tsx +3 -0
  47. package/src/components/JsonViewer/Helpers.tsx +41 -3
  48. package/src/components/JsonViewer/JsonViewer.tsx +4 -0
  49. package/src/components/LanguagePicker/LanguagePicker.tsx +3 -1
  50. package/src/components/Markdown/Markdown.tsx +13 -6
  51. package/src/components/Markdown/styles/links.ts +19 -15
  52. package/src/components/Menu/MenuItem.tsx +47 -2
  53. package/src/components/Navbar/NavbarItem.tsx +10 -4
  54. package/src/components/PageActions/PageActions.tsx +3 -6
  55. package/src/components/Panel/variables.ts +1 -0
  56. package/src/components/ReplayGate/ReplayGate.tsx +18 -0
  57. package/src/components/ReplayTopBarActions/ReplayTopBarActions.tsx +14 -0
  58. package/src/components/Search/SearchDialog.tsx +4 -4
  59. package/src/core/hooks/use-active-section-id.ts +13 -1
  60. package/src/core/hooks/use-page-actions.ts +13 -11
  61. package/src/core/hooks/use-theme-hooks.ts +1 -0
  62. package/src/core/openapi/index.ts +1 -1
  63. package/src/core/styles/global.ts +8 -0
  64. package/src/core/types/hooks.ts +1 -0
  65. package/src/core/types/l10n.ts +39 -1
  66. package/src/layouts/ThreePanelLayout.tsx +1 -0
@@ -1,12 +1,23 @@
1
- import React, { cloneElement, useRef, forwardRef } from 'react';
2
- import styled from 'styled-components';
3
-
4
- import type { PropsWithChildren, ReactElement } from 'react';
1
+ import React, {
2
+ cloneElement,
3
+ useRef,
4
+ forwardRef,
5
+ useCallback,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useState,
9
+ } from 'react';
10
+ import { createPortal } from 'react-dom';
11
+ import styled, { css } from 'styled-components';
12
+
13
+ import type { CSSProperties, PropsWithChildren, ReactElement } from 'react';
5
14
 
6
15
  import { useOutsideClick, useControlledState } from '@redocly/theme/core/hooks';
7
16
  import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
8
17
  import { ChevronUpIcon } from '@redocly/theme/icons/ChevronUpIcon/ChevronUpIcon';
9
18
 
19
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
20
+
10
21
  interface TriggerProps {
11
22
  onClick?: (event: React.UIEvent) => void;
12
23
  onKeyDown?: (event: React.KeyboardEvent) => void;
@@ -27,6 +38,7 @@ export type DropdownProps = PropsWithChildren<{
27
38
  dataAttributes?: Record<string, string>;
28
39
  className?: string;
29
40
  withArrow?: boolean;
41
+ portalled?: boolean;
30
42
 
31
43
  onClick?: (event: React.UIEvent) => void;
32
44
  onClose?: () => void;
@@ -46,6 +58,7 @@ export const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(
46
58
  dataAttributes,
47
59
  placement,
48
60
  alignment,
61
+ portalled = false,
49
62
  onClick,
50
63
  onClose,
51
64
  onOpen,
@@ -53,7 +66,11 @@ export const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(
53
66
  ref,
54
67
  ) => {
55
68
  const dropdownRef = useRef<HTMLDivElement | null>(null);
69
+ const menuRef = useRef<HTMLDivElement | null>(null);
56
70
  const [isOpen, setIsOpen] = useControlledState<boolean>(false, active);
71
+ const [menuStyle, setMenuStyle] = useState<CSSProperties>({});
72
+
73
+ const wrapperRef = (ref as React.RefObject<HTMLDivElement | null>) || dropdownRef;
57
74
 
58
75
  const handleOpen = () => {
59
76
  setIsOpen(true);
@@ -85,7 +102,40 @@ export const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(
85
102
  }
86
103
  };
87
104
 
88
- useOutsideClick((ref as React.RefObject<HTMLElement | null>) || dropdownRef, handleClose);
105
+ const updateMenuPosition = useCallback(() => {
106
+ const wrapper = wrapperRef.current;
107
+ if (!wrapper) return;
108
+ const rect = wrapper.getBoundingClientRect();
109
+ const style: CSSProperties = { minWidth: rect.width };
110
+ if (placement === 'top') {
111
+ style.bottom = window.innerHeight - rect.top;
112
+ } else {
113
+ style.top = rect.bottom;
114
+ }
115
+ if (alignment === 'end') {
116
+ style.right = window.innerWidth - rect.right;
117
+ } else {
118
+ style.left = rect.left;
119
+ }
120
+ const zIndex = getComputedStyle(wrapper).getPropertyValue('--dropdown-menu-z-index').trim();
121
+ if (zIndex) {
122
+ style.zIndex = zIndex;
123
+ }
124
+ setMenuStyle(style);
125
+ }, [placement, alignment, wrapperRef]);
126
+
127
+ useIsomorphicLayoutEffect(() => {
128
+ if (!portalled || !isOpen) return;
129
+ updateMenuPosition();
130
+ window.addEventListener('scroll', updateMenuPosition, true);
131
+ window.addEventListener('resize', updateMenuPosition);
132
+ return () => {
133
+ window.removeEventListener('scroll', updateMenuPosition, true);
134
+ window.removeEventListener('resize', updateMenuPosition);
135
+ };
136
+ }, [portalled, isOpen, updateMenuPosition]);
137
+
138
+ useOutsideClick(portalled ? [wrapperRef, menuRef] : wrapperRef, handleClose);
89
139
 
90
140
  const triggerChild = React.Children.only(trigger) as ReactElement<TriggerProps>;
91
141
 
@@ -97,27 +147,37 @@ export const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(
97
147
  onKeyDown: triggerEvent === 'click' ? handleKeyDown : undefined,
98
148
  });
99
149
 
150
+ const menu = (
151
+ <ChildrenWrapper
152
+ ref={menuRef}
153
+ $placement={placement}
154
+ $alignment={alignment}
155
+ $isOpen={isOpen}
156
+ $portalled={portalled}
157
+ style={portalled ? menuStyle : undefined}
158
+ onClick={closeOnClick ? handleChildClick : undefined}
159
+ >
160
+ {children}
161
+ </ChildrenWrapper>
162
+ );
163
+
164
+ const portalledMenu =
165
+ isOpen && typeof document !== 'undefined' ? createPortal(menu, document.body) : null;
166
+
100
167
  return (
101
168
  <DropdownWrapper
102
169
  data-component-name="Dropdown/Dropdown"
103
170
  data-testid="dropdown"
104
171
  {...dataAttributes}
105
172
  className={className}
106
- ref={ref || dropdownRef}
173
+ ref={wrapperRef}
107
174
  onPointerEnter={triggerEvent === 'hover' ? handleOpen : undefined}
108
175
  onPointerLeave={triggerEvent === 'hover' ? handleClose : undefined}
109
176
  onClick={onClick}
110
177
  >
111
178
  {dropdownTrigger}
112
179
 
113
- <ChildrenWrapper
114
- $placement={placement}
115
- $alignment={alignment}
116
- $isOpen={isOpen}
117
- onClick={closeOnClick ? handleChildClick : undefined}
118
- >
119
- {children}
120
- </ChildrenWrapper>
180
+ {portalled ? portalledMenu : menu}
121
181
  </DropdownWrapper>
122
182
  );
123
183
  },
@@ -140,14 +200,26 @@ const DropdownWrapper = styled.div`
140
200
  text-decoration: none;
141
201
  `;
142
202
 
143
- const ChildrenWrapper = styled.div<{ $placement?: string; $alignment?: string; $isOpen?: boolean }>`
203
+ const ChildrenWrapper = styled.div<{
204
+ $placement?: string;
205
+ $alignment?: string;
206
+ $isOpen?: boolean;
207
+ $portalled?: boolean;
208
+ }>`
144
209
  padding-top: var(--dropdown-menu-padding-top);
145
- position: absolute;
146
- top: ${({ $placement }) => ($placement === 'top' ? 'auto' : '100%')};
147
- bottom: ${({ $placement }) => ($placement === 'top' ? '100%' : 'auto')};
148
- left: ${({ $alignment }) => ($alignment === 'start' ? '0' : 'auto')};
149
- right: ${({ $alignment }) => ($alignment === 'end' ? '0' : 'auto')};
150
210
  display: ${({ $isOpen }) => ($isOpen ? 'block' : 'none')};
151
-
152
211
  z-index: var(--dropdown-menu-z-index);
212
+
213
+ ${({ $portalled, $placement, $alignment }) =>
214
+ $portalled
215
+ ? css`
216
+ position: fixed;
217
+ `
218
+ : css`
219
+ position: absolute;
220
+ top: ${$placement === 'top' ? 'auto' : '100%'};
221
+ bottom: ${$placement === 'top' ? '100%' : 'auto'};
222
+ left: ${$alignment === 'start' ? '0' : 'auto'};
223
+ right: ${$alignment === 'end' ? '0' : 'auto'};
224
+ `}
153
225
  `;
@@ -8,7 +8,9 @@ export const dropdown = css`
8
8
  --dropdown-menu-font-weight: var(--font-weight-regular); // @presenter FontWeight
9
9
  --dropdown-menu-line-height: var(--line-height-base); // @presenter LineHeight
10
10
  --dropdown-menu-text-color: var(--text-color-secondary); // @presenter Color
11
- --dropdown-menu-z-index: var(--z-index-raised); // @presenter ZIndex
11
+ /* One below --z-index-raised (the sticky Navbar) so a portaled menu, which is
12
+ appended to <body> after the navbar, never paints over it on a z-index tie. */
13
+ --dropdown-menu-z-index: calc(var(--z-index-raised) - 1); // @presenter ZIndex
12
14
 
13
15
  --dropdown-menu-padding-top: var(--spacing-xxs);
14
16
  --dropdown-menu-min-width: 100px;
@@ -1,5 +1,4 @@
1
1
  import * as React from 'react';
2
- import { useLocation } from 'react-router-dom';
3
2
  import styled from 'styled-components';
4
3
 
5
4
  import type { FeedbackProps } from '@redocly/theme/core/types';
@@ -28,7 +27,6 @@ export function Feedback(props: FeedbackProps & { path?: string }) {
28
27
  const { useSubmitFeedback, useTelemetry } = useThemeHooks();
29
28
  const { submitFeedback } = useSubmitFeedback();
30
29
  const telemetry = useTelemetry();
31
- const { pathname } = useLocation();
32
30
  const { feedback: themeFeedbackConf } = useThemeConfig();
33
31
  const feedbackConf = {
34
32
  ...themeFeedbackConf,
@@ -75,7 +73,7 @@ export function Feedback(props: FeedbackProps & { path?: string }) {
75
73
  );
76
74
  };
77
75
 
78
- return <React.Fragment key={pathname}>{renderFeedbackComponent()}</React.Fragment>;
76
+ return <React.Fragment key={feedbackConf.path}>{renderFeedbackComponent()}</React.Fragment>;
79
77
  }
80
78
 
81
79
  const FeedbackWrapper = styled.div`
@@ -22,6 +22,7 @@ export function Stars({ max = 5, onChange, value }: StarsProps): JSX.Element {
22
22
  <Star
23
23
  data-testid={`star-${index}`}
24
24
  key={index}
25
+ aria-label={`${index} ${index === 1 ? 'star' : 'stars'}`}
25
26
  onClick={() => onChange(index)}
26
27
  onMouseOver={() => setHovered(index)}
27
28
  onMouseLeave={() => !value && setHovered(0)}
@@ -48,6 +48,9 @@ export function FooterItem({ item, iconsOnly, className }: FooterItemProps): JSX
48
48
  telemetry.sendFooterItemClickedMessage([getBaseDataAttributes('footerItem', 'footer')])
49
49
  }
50
50
  data-translation-key={item.labelTranslationKey}
51
+ aria-label={
52
+ iconWithoutLabel ? translate(item.labelTranslationKey, item.label) : undefined
53
+ }
51
54
  >
52
55
  {hasIcon ? (
53
56
  <FooterLinkIcon $iconsOnly={iconsOnly}>
@@ -4,6 +4,7 @@ import styled from 'styled-components';
4
4
  import type { JSX } from 'react';
5
5
 
6
6
  import { Button } from '@redocly/theme/components/Button/Button';
7
+ import { ShowMoreToggleButton } from '@redocly/theme/components/Button/ShowMoreToggleButton';
7
8
  import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
8
9
 
9
10
  export function JsonValue(props: {
@@ -13,8 +14,9 @@ export function JsonValue(props: {
13
14
  standalone?: boolean; // if true, will render the opening and closing punctuation (when it's an item inside array or top-level)
14
15
  defaultExpandLevel: number;
15
16
  expandAllSignal: boolean | undefined;
17
+ maxStringLength?: number;
16
18
  }): React.ReactNode {
17
- const { standalone, defaultExpandLevel, level, value, expandAllSignal } = props;
19
+ const { standalone, defaultExpandLevel, level, value, expandAllSignal, maxStringLength } = props;
18
20
  const indent = ' '.repeat(level * 2);
19
21
 
20
22
  const [isExpandedState, setExpanded] = React.useState(
@@ -37,10 +39,16 @@ export function JsonValue(props: {
37
39
  const isPrimitive = (valueType !== 'object' && value !== null) || keys.length === 0;
38
40
 
39
41
  if (isPrimitive) {
42
+ const isOversizedString =
43
+ typeof value === 'string' && maxStringLength !== undefined && value.length > maxStringLength;
40
44
  return (
41
45
  <>
42
46
  {standalone ? indent : null}
43
- <span className={'token ' + valueType}>{JSON.stringify(value)}</span>
47
+ {isOversizedString ? (
48
+ <TruncatedStringValue value={value} maxStringLength={maxStringLength} />
49
+ ) : (
50
+ <span className={'token ' + valueType}>{JSON.stringify(value)}</span>
51
+ )}
44
52
  </>
45
53
  );
46
54
  }
@@ -57,6 +65,7 @@ export function JsonValue(props: {
57
65
  standalone={true}
58
66
  defaultExpandLevel={defaultExpandLevel}
59
67
  expandAllSignal={expandAllSignal}
68
+ maxStringLength={maxStringLength}
60
69
  />
61
70
  {index < value.length - 1 ? ',\n' : null}
62
71
  </React.Fragment>
@@ -69,6 +78,7 @@ export function JsonValue(props: {
69
78
  level={level + 1}
70
79
  defaultExpandLevel={defaultExpandLevel}
71
80
  expandAllSignal={expandAllSignal}
81
+ maxStringLength={maxStringLength}
72
82
  />
73
83
  {index < keys.length - 1 ? ',\n' : null}
74
84
  </React.Fragment>
@@ -109,8 +119,9 @@ function JsonKeyValue(props: {
109
119
  level: number;
110
120
  defaultExpandLevel: number;
111
121
  expandAllSignal: boolean | undefined;
122
+ maxStringLength?: number;
112
123
  }): React.ReactNode {
113
- const { defaultExpandLevel, level, name, value, expandAllSignal } = props;
124
+ const { defaultExpandLevel, level, name, value, expandAllSignal, maxStringLength } = props;
114
125
  const indent = ' '.repeat(level * 2);
115
126
  const keys: string[] =
116
127
  (value && Object.keys(value).filter((key) => value[key] !== undefined)) || []; // filter out undefined properties
@@ -141,6 +152,7 @@ function JsonKeyValue(props: {
141
152
  level={level}
142
153
  defaultExpandLevel={defaultExpandLevel}
143
154
  expandAllSignal={expandAllSignal}
155
+ maxStringLength={maxStringLength}
144
156
  />
145
157
  {'\n'}
146
158
  </>
@@ -156,6 +168,7 @@ function JsonKeyValue(props: {
156
168
  level={level}
157
169
  defaultExpandLevel={defaultExpandLevel}
158
170
  expandAllSignal={expandAllSignal}
171
+ maxStringLength={maxStringLength}
159
172
  />
160
173
  );
161
174
 
@@ -170,6 +183,27 @@ function JsonKeyValue(props: {
170
183
  );
171
184
  }
172
185
 
186
+ function TruncatedStringValue({
187
+ value,
188
+ maxStringLength,
189
+ }: {
190
+ value: string;
191
+ maxStringLength: number;
192
+ }): JSX.Element {
193
+ const [isExpanded, setExpanded] = React.useState(false);
194
+ const shownValue = React.useMemo(
195
+ () => JSON.stringify(isExpanded ? value : `${value.slice(0, maxStringLength)}…`),
196
+ [isExpanded, value, maxStringLength],
197
+ );
198
+
199
+ return (
200
+ <>
201
+ <span className="token string">{shownValue}</span>{' '}
202
+ <ShowMoreButton isExpanded={isExpanded} onToggle={() => setExpanded((v) => !v)} />
203
+ </>
204
+ );
205
+ }
206
+
173
207
  function ExpandIcon({
174
208
  isExpanded,
175
209
  onClick,
@@ -189,6 +223,10 @@ function ExpandIcon({
189
223
  );
190
224
  }
191
225
 
226
+ const ShowMoreButton = styled(ShowMoreToggleButton)`
227
+ user-select: none;
228
+ `;
229
+
192
230
  const ExpandButton = styled(Button)`
193
231
  position: absolute;
194
232
  left: -10px;
@@ -17,6 +17,8 @@ export type JsonProps = {
17
17
  className?: string;
18
18
  expandLevel: number;
19
19
  startLineNumber?: number;
20
+ /** Truncate string values longer than this at render time (full value stays available for copy). */
21
+ maxStringLength?: number;
20
22
  onCopyClick?: () => void;
21
23
  onPanelToggle?: (isExpanded: boolean, panelType?: PanelType) => void;
22
24
  };
@@ -29,6 +31,7 @@ function JsonComponent({
29
31
  onPanelToggle,
30
32
  title,
31
33
  controls = {},
34
+ maxStringLength,
32
35
  }: JsonProps): JSX.Element {
33
36
  const showFoldingButtons =
34
37
  data && Object.values(data).some((value) => typeof value === 'object' && value !== null);
@@ -89,6 +92,7 @@ function JsonComponent({
89
92
  standalone={true}
90
93
  expandAllSignal={expandAllSignal}
91
94
  defaultExpandLevel={Math.max(1, expandLevel)}
95
+ maxStringLength={maxStringLength}
92
96
  />
93
97
  </FoldingWrap>
94
98
  </CodeBlock>
@@ -20,8 +20,9 @@ export type LanguagePickerProps = {
20
20
 
21
21
  export function LanguagePicker(props: LanguagePickerProps): JSX.Element | null {
22
22
  const { currentLocale, locales, getLocaleUrl } = useLanguagePicker();
23
- const { useTelemetry } = useThemeHooks();
23
+ const { useTelemetry, useTranslate } = useThemeHooks();
24
24
  const telemetry = useTelemetry();
25
+ const { translate } = useTranslate();
25
26
 
26
27
  if (locales.length < 2 || !currentLocale) {
27
28
  return null;
@@ -32,6 +33,7 @@ export function LanguagePicker(props: LanguagePickerProps): JSX.Element | null {
32
33
  icon={<GlobalOutlinedIcon color="--button-content-color" />}
33
34
  variant="secondary"
34
35
  size="medium"
36
+ aria-label={translate('languagePicker.label', 'Select language')}
35
37
  />
36
38
  );
37
39
 
@@ -38,6 +38,7 @@ export const Markdown = styled.main
38
38
  line-height: var(--md-content-line-height);
39
39
  color: var(--md-content-text-color);
40
40
  padding: 0;
41
+ overflow-wrap: break-word;
41
42
 
42
43
  ${({ compact }) =>
43
44
  compact &&
@@ -85,7 +86,12 @@ export const Markdown = styled.main
85
86
  scroll-margin-top: calc(var(--navbar-height) + var(--banner-height));
86
87
  ${headingAnchorCss(ANCHOR_CLASS)};
87
88
 
88
- h1, h2, h3, h4, h5, h6 {
89
+ h1,
90
+ h2,
91
+ h3,
92
+ h4,
93
+ h5,
94
+ h6 {
89
95
  display: flex;
90
96
  align-items: flex-start;
91
97
  gap: var(--spacing-sm);
@@ -294,8 +300,10 @@ export const Markdown = styled.main
294
300
  }
295
301
 
296
302
  blockquote {
297
- margin: var(--md-blockquote-margin-vertical) var(--md-blockquote-margin-horizontal);
298
- padding: var(--md-blockquote-padding-vertical) var(--md-blockquote-padding-horizontal);
303
+ margin: var(--md-blockquote-margin-vertical)
304
+ var(--md-blockquote-margin-horizontal);
305
+ padding: var(--md-blockquote-padding-vertical)
306
+ var(--md-blockquote-padding-horizontal);
299
307
  border-left: var(--md-blockquote-border-left);
300
308
  background-color: var(--md-blockquote-bg-color);
301
309
  color: var(--md-blockquote-text-color);
@@ -323,8 +331,7 @@ export const Markdown = styled.main
323
331
  }
324
332
  }
325
333
 
326
- ul.${MARKDOWN_CLASS_NAME},
327
- ol.${MARKDOWN_CLASS_NAME} {
334
+ ul.${MARKDOWN_CLASS_NAME}, ol.${MARKDOWN_CLASS_NAME} {
328
335
  padding-left: var(--md-list-left-padding);
329
336
  margin: var(--md-list-margin);
330
337
 
@@ -347,7 +354,7 @@ export const Markdown = styled.main
347
354
  }
348
355
  }
349
356
 
350
- ul > li:has(input[type='checkbox']) {
357
+ ul > li:has(input[type="checkbox"]) {
351
358
  list-style: var(--md-checkbox-list-item-style);
352
359
  }
353
360
 
@@ -1,21 +1,25 @@
1
1
  import { css } from 'styled-components';
2
2
 
3
- export const markdownLinksCss = css`
4
- a:not([role='button']) {
5
- text-decoration: var(--link-decoration);
6
- text-underline-offset: var(--link-underline-offset);
7
- color: var(--link-color-primary);
8
- font-weight: var(--link-font-weight);
3
+ export const linkCss = css`
4
+ text-decoration: var(--link-decoration);
5
+ text-underline-offset: var(--link-underline-offset);
6
+ color: var(--link-color-primary);
7
+ font-weight: var(--link-font-weight);
8
+
9
+ &:visited {
10
+ color: var(--link-color-visited);
11
+ text-decoration: var(--link-decoration-visited);
12
+ }
9
13
 
10
- &:visited {
11
- color: var(--link-color-visited);
12
- text-decoration: var(--link-decoration-visited);
13
- }
14
+ &:hover,
15
+ &:focus {
16
+ text-decoration: var(--link-decoration-hover);
17
+ color: var(--link-color-primary-hover);
18
+ }
19
+ `;
14
20
 
15
- &:hover,
16
- &:focus {
17
- text-decoration: var(--link-decoration-hover);
18
- color: var(--link-color-primary-hover);
19
- }
21
+ export const markdownLinksCss = css`
22
+ a:not([role='button']) {
23
+ ${linkCss}
20
24
  }
21
25
  `;
@@ -1,4 +1,4 @@
1
- import React, { useRef } from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
 
4
4
  import type { ItemState, MenuItemProps } from '@redocly/theme/core/types';
@@ -20,7 +20,20 @@ import { ArrowRightIcon } from '@redocly/theme/icons/ArrowRightIcon/ArrowRightIc
20
20
  import { GenericIcon } from '@redocly/theme/icons/GenericIcon/GenericIcon';
21
21
  import { Tag, ContentWrapper } from '@redocly/theme/components/Tag/Tag';
22
22
 
23
- export function MenuItem(props: React.PropsWithChildren<MenuItemProps>): JSX.Element {
23
+ /** Nearest scrollable ancestor, stopping before <body> so only the sidebar scrolls — never the page. */
24
+ function getScrollParent(el: HTMLElement): HTMLElement | null {
25
+ let node = el.parentElement;
26
+ while (node && node !== document.body) {
27
+ const { overflowY } = getComputedStyle(node);
28
+ if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight) {
29
+ return node;
30
+ }
31
+ node = node.parentElement;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ function MenuItemImpl(props: React.PropsWithChildren<MenuItemProps>): JSX.Element {
24
37
  const { item, depth, className, onClick } = props;
25
38
  const { useTranslate, useTelemetry } = useThemeHooks();
26
39
  const { translate } = useTranslate();
@@ -40,6 +53,25 @@ export function MenuItem(props: React.PropsWithChildren<MenuItemProps>): JSX.Ele
40
53
  const hasChevron = isNested && !isDrilldown;
41
54
  const hasHttpTag = !!item.httpVerb || type === MenuItemType.Operation;
42
55
 
56
+ // When this item becomes the active one (via navigation or scroll-spy), keep
57
+ // it visible by scrolling only the sidebar's own scroll container.
58
+ useEffect(() => {
59
+ const el = labelRef.current;
60
+ if (!item.active || !el) return;
61
+ const scroller = getScrollParent(el);
62
+ if (!scroller) return;
63
+
64
+ const scrollerRect = scroller.getBoundingClientRect();
65
+ const elRect = el.getBoundingClientRect();
66
+ const margin = elRect.height;
67
+
68
+ if (elRect.top < scrollerRect.top + margin) {
69
+ scroller.scrollTop -= scrollerRect.top + margin - elRect.top;
70
+ } else if (elRect.bottom > scrollerRect.bottom - margin) {
71
+ scroller.scrollTop += elRect.bottom - (scrollerRect.bottom - margin);
72
+ }
73
+ }, [item.active]);
74
+
43
75
  const handleOnClick = () => {
44
76
  telemetry.sendSidebarItemClickedMessage([
45
77
  {
@@ -448,3 +480,16 @@ const BadgeIcon = styled(GenericIcon)`
448
480
  margin-right: var(--spacing-xxs);
449
481
  flex-shrink: 0;
450
482
  `;
483
+
484
+ /** Compares only the props that actually affect this row. `item` is expected
485
+ * to be reference-stable from the producer (e.g. `useMenuItems`) when the
486
+ * row's state hasn't changed; `onClick` and `children` are fresh JSX from
487
+ * the parent's `.map()` every render and would defeat the memo. */
488
+ export const MenuItem = React.memo(MenuItemImpl, (prev, next) => {
489
+ return (
490
+ prev.item === next.item &&
491
+ prev.depth === next.depth &&
492
+ prev.isMobile === next.isMobile &&
493
+ prev.className === next.className
494
+ );
495
+ });
@@ -92,15 +92,15 @@ export function NavbarItem({ navItem, className }: NavbarItemProps): JSX.Element
92
92
  });
93
93
 
94
94
  return (
95
- <div data-component-name="Navbar/NavbarItem">
95
+ <NavbarListItem data-component-name="Navbar/NavbarItem">
96
96
  <NavbarMenuItemDropdown trigger={itemContent} triggerEvent="hover">
97
97
  <DropdownMenu>{groupItemsComponents}</DropdownMenu>
98
98
  </NavbarMenuItemDropdown>
99
- </div>
99
+ </NavbarListItem>
100
100
  );
101
101
  }
102
102
 
103
- return <div data-component-name="Navbar/NavbarItem">{itemContent}</div>;
103
+ return <NavbarListItem data-component-name="Navbar/NavbarItem">{itemContent}</NavbarListItem>;
104
104
  }
105
105
 
106
106
  const NavbarMenuItemDropdown = styled(Dropdown)`
@@ -109,7 +109,13 @@ const NavbarMenuItemDropdown = styled(Dropdown)`
109
109
  --dropdown-menu-item-separator-line-height: var(--navbar-separator-line-height);
110
110
  `;
111
111
 
112
- const NavbarMenuItem = styled.li<LinkProps & { $active: boolean }>`
112
+ const NavbarListItem = styled.li`
113
+ list-style: none;
114
+ display: inline-flex;
115
+ align-items: center;
116
+ `;
117
+
118
+ const NavbarMenuItem = styled.div<LinkProps & { $active: boolean }>`
113
119
  display: inline-block;
114
120
  padding: var(--navbar-item-padding);
115
121
  text-align: center;
@@ -83,14 +83,15 @@ export function PageActions(props: PageActionProps): JSX.Element | null {
83
83
  {buttonAction.buttonText}
84
84
  </Button>
85
85
  {actions.length > 1 ? (
86
- <StyledDropdown
86
+ <Dropdown
87
87
  withArrow
88
+ portalled
88
89
  trigger={<Button aria-label="More actions" />}
89
90
  placement="bottom"
90
91
  alignment="end"
91
92
  >
92
93
  <StyledDropdownMenu>{menuItems}</StyledDropdownMenu>
93
- </StyledDropdown>
94
+ </Dropdown>
94
95
  ) : null}
95
96
  </ButtonGroup>
96
97
  </PageActionsWrapper>
@@ -124,10 +125,6 @@ const LinkMenuItem = styled(Link)`
124
125
  --link-decoration-hover: none;
125
126
  `;
126
127
 
127
- const StyledDropdown = styled(Dropdown)`
128
- --dropdown-menu-z-index: calc(var(--z-index-raised) - 1);
129
- `;
130
-
131
128
  const StyledDropdownMenu = styled(DropdownMenu)`
132
129
  --dropdown-menu-max-height: var(--page-actions-dropdown-max-height);
133
130
  `;
@@ -326,6 +326,7 @@ export const apiReferencePanels = css`
326
326
  * @tokens Panel try-it, request-samples, response-samples, callback samples
327
327
  */
328
328
 
329
+ .panel-api-docs,
329
330
  .panel-language-list,
330
331
  .panel-overview,
331
332
  .panel-download,
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+
3
+ export type ReplayGateProps = {
4
+ setEnvironmentValues: (
5
+ envName: string,
6
+ values: Record<string, string | { value: string; isSecret?: boolean }>,
7
+ ) => void;
8
+ setSelectedEnvironment: (envName: string) => void;
9
+ selectedEnvironment?: string;
10
+ // The current OpenAPI description's id (from x-metadata), when known. Lets a custom
11
+ // ReplayGate vary behavior per API in a multi-API catalog.
12
+ apiId?: string;
13
+ children: React.ReactNode;
14
+ };
15
+
16
+ export function ReplayGate({ children }: ReplayGateProps) {
17
+ return <>{children}</>;
18
+ }
@@ -0,0 +1,14 @@
1
+ export type ReplayTopBarActionsProps = {
2
+ environments: ReadonlyArray<{ name: string }>;
3
+ activeEnvironment: string;
4
+ setEnvironmentValues: (
5
+ envName: string,
6
+ values: Record<string, string | { value: string; isSecret?: boolean }>,
7
+ ) => void;
8
+ setSelectedEnvironment: (envName: string) => void;
9
+ apiId?: string;
10
+ };
11
+
12
+ export function ReplayTopBarActions(_props: ReplayTopBarActionsProps) {
13
+ return null;
14
+ }