@redocly/theme 0.70.0-next.3 → 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.
Files changed (123) hide show
  1. package/lib/components/Catalog/Catalog.js +1 -1
  2. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.d.ts +1 -1
  3. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.js +2 -2
  4. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +1 -1
  5. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +30 -11
  6. package/lib/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanel.js +1 -1
  7. package/lib/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanelOverlay.js +2 -2
  8. package/lib/components/Dropdown/Dropdown.js +3 -1
  9. package/lib/components/Dropdown/DropdownMenu.d.ts +1 -0
  10. package/lib/components/Dropdown/DropdownMenu.js +1 -1
  11. package/lib/components/Feedback/Reasons.js +3 -2
  12. package/lib/components/Filter/FilterCheckboxes.js +30 -11
  13. package/lib/components/Image/Image.js +24 -4
  14. package/lib/components/Markdown/Markdown.js +3 -2
  15. package/lib/components/Menu/Menu.js +4 -0
  16. package/lib/components/Menu/MenuSelect.d.ts +7 -0
  17. package/lib/components/Menu/MenuSelect.js +129 -0
  18. package/lib/components/Menu/variables.js +43 -4
  19. package/lib/components/Navbar/Navbar.js +44 -5
  20. package/lib/components/Navbar/variables.js +3 -0
  21. package/lib/components/NavigationTabs/NavigationTab.d.ts +7 -0
  22. package/lib/components/NavigationTabs/NavigationTab.js +93 -0
  23. package/lib/components/NavigationTabs/NavigationTabContent.d.ts +7 -0
  24. package/lib/components/NavigationTabs/NavigationTabContent.js +50 -0
  25. package/lib/components/NavigationTabs/NavigationTabMenu.d.ts +7 -0
  26. package/lib/components/NavigationTabs/NavigationTabMenu.js +18 -0
  27. package/lib/components/NavigationTabs/NavigationTabs.d.ts +7 -0
  28. package/lib/components/NavigationTabs/NavigationTabs.js +97 -0
  29. package/lib/components/NavigationTabs/variables.d.ts +1 -0
  30. package/lib/components/NavigationTabs/variables.js +37 -0
  31. package/lib/components/Search/SearchTrigger.js +8 -2
  32. package/lib/components/Segmented/Segmented.js +23 -3
  33. package/lib/components/Select/Select.js +10 -3
  34. package/lib/components/Select/SelectInput.d.ts +1 -0
  35. package/lib/components/Select/SelectInput.js +3 -3
  36. package/lib/components/Sidebar/Sidebar.js +2 -2
  37. package/lib/components/TableOfContent/TableOfContent.js +2 -2
  38. package/lib/core/constants/common.d.ts +1 -0
  39. package/lib/core/constants/common.js +1 -0
  40. package/lib/core/hooks/index.d.ts +2 -0
  41. package/lib/core/hooks/index.js +2 -0
  42. package/lib/core/hooks/menu/use-mobile-menu-items.js +75 -51
  43. package/lib/core/hooks/menu/use-mobile-menu-levels.js +12 -7
  44. package/lib/core/hooks/use-scroll-item-into-view.d.ts +2 -0
  45. package/lib/core/hooks/use-scroll-item-into-view.js +16 -0
  46. package/lib/core/hooks/use-tab-list.d.ts +31 -0
  47. package/lib/core/hooks/use-tab-list.js +49 -0
  48. package/lib/core/hooks/use-theme-hooks.js +2 -0
  49. package/lib/core/styles/global.js +12 -2
  50. package/lib/core/types/hooks.d.ts +5 -0
  51. package/lib/core/types/index.d.ts +1 -0
  52. package/lib/core/types/l10n.d.ts +1 -1
  53. package/lib/core/types/navigation-tabs.d.ts +49 -0
  54. package/lib/core/types/navigation-tabs.js +3 -0
  55. package/lib/core/utils/Dynamic.d.ts +8 -1
  56. package/lib/core/utils/Dynamic.js +34 -3
  57. package/lib/core/utils/index.d.ts +1 -0
  58. package/lib/core/utils/index.js +1 -0
  59. package/lib/core/utils/menu.d.ts +4 -0
  60. package/lib/core/utils/menu.js +42 -3
  61. package/lib/core/utils/navigation-tabs.d.ts +8 -0
  62. package/lib/core/utils/navigation-tabs.js +71 -0
  63. package/lib/index.d.ts +5 -0
  64. package/lib/index.js +7 -1
  65. package/lib/layouts/PageLayout.js +1 -1
  66. package/lib/layouts/RootLayout.js +2 -2
  67. package/lib/markdoc/components/CodeWalkthrough/CodeFilters.js +1 -1
  68. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +3 -3
  69. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +1 -1
  70. package/package.json +3 -3
  71. package/src/components/Catalog/Catalog.tsx +1 -1
  72. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.tsx +2 -2
  73. package/src/components/Catalog/CatalogEntity/CatalogEntitySchema.tsx +1 -1
  74. package/src/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.tsx +37 -22
  75. package/src/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanel.tsx +1 -1
  76. package/src/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanelOverlay.tsx +2 -2
  77. package/src/components/Dropdown/Dropdown.tsx +4 -1
  78. package/src/components/Dropdown/DropdownMenu.tsx +7 -1
  79. package/src/components/Feedback/Reasons.tsx +3 -2
  80. package/src/components/Filter/FilterCheckboxes.tsx +33 -18
  81. package/src/components/Image/Image.tsx +26 -5
  82. package/src/components/Markdown/Markdown.tsx +3 -2
  83. package/src/components/Menu/Menu.tsx +5 -0
  84. package/src/components/Menu/MenuSelect.tsx +198 -0
  85. package/src/components/Menu/variables.ts +43 -4
  86. package/src/components/Navbar/Navbar.tsx +22 -5
  87. package/src/components/Navbar/variables.ts +3 -0
  88. package/src/components/NavigationTabs/NavigationTab.tsx +148 -0
  89. package/src/components/NavigationTabs/NavigationTabContent.tsx +66 -0
  90. package/src/components/NavigationTabs/NavigationTabMenu.tsx +40 -0
  91. package/src/components/NavigationTabs/NavigationTabs.tsx +83 -0
  92. package/src/components/NavigationTabs/variables.ts +34 -0
  93. package/src/components/Search/SearchTrigger.tsx +11 -2
  94. package/src/components/Segmented/Segmented.tsx +25 -16
  95. package/src/components/Select/Select.tsx +12 -6
  96. package/src/components/Select/SelectInput.tsx +8 -10
  97. package/src/components/Sidebar/Sidebar.tsx +2 -2
  98. package/src/components/TableOfContent/TableOfContent.tsx +2 -2
  99. package/src/core/constants/common.ts +1 -0
  100. package/src/core/hooks/__mocks__/use-theme-hooks.ts +3 -0
  101. package/src/core/hooks/index.ts +2 -0
  102. package/src/core/hooks/menu/use-mobile-menu-items.ts +91 -56
  103. package/src/core/hooks/menu/use-mobile-menu-levels.ts +14 -6
  104. package/src/core/hooks/use-scroll-item-into-view.ts +18 -0
  105. package/src/core/hooks/use-tab-list.ts +63 -0
  106. package/src/core/hooks/use-theme-hooks.ts +2 -0
  107. package/src/core/styles/global.ts +12 -2
  108. package/src/core/types/hooks.ts +5 -0
  109. package/src/core/types/index.ts +1 -0
  110. package/src/core/types/l10n.ts +3 -0
  111. package/src/core/types/navigation-tabs.ts +54 -0
  112. package/src/core/utils/Dynamic.tsx +67 -35
  113. package/src/core/utils/index.ts +1 -0
  114. package/src/core/utils/menu.ts +49 -3
  115. package/src/core/utils/navigation-tabs.ts +99 -0
  116. package/src/icons/CheckboxIcon/CheckboxIcon.tsx +1 -0
  117. package/src/icons/ChevronSortIcon/ChevronSortIcon.tsx +1 -0
  118. package/src/index.ts +6 -0
  119. package/src/layouts/PageLayout.tsx +1 -1
  120. package/src/layouts/RootLayout.tsx +2 -2
  121. package/src/markdoc/components/CodeWalkthrough/CodeFilters.tsx +1 -1
  122. package/src/markdoc/components/CodeWalkthrough/CodePanel.tsx +3 -3
  123. package/src/markdoc/components/CodeWalkthrough/CodeWalkthrough.tsx +1 -1
@@ -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
- onClick={() => handleImageClick(src)}
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
- onClick={() => handleImageClick(value)}
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
- onClick={() => handleImageClick(galleryImageSrc)}
146
+ {...getLightboxTriggerProps(galleryImageSrc)}
126
147
  />
127
148
  ))}
128
149
  </GalleryRow>
@@ -71,7 +71,7 @@ export const Markdown = styled.main
71
71
  ${markdownLinksCss}
72
72
 
73
73
  a[name], [id] {
74
- scroll-margin-top: calc(var(--navbar-height) + var(--banner-height));
74
+ scroll-margin-top: var(--navbar-offset);
75
75
  }
76
76
 
77
77
  img,
@@ -84,7 +84,7 @@ export const Markdown = styled.main
84
84
  font-family: var(--heading-font-family);
85
85
  position: relative;
86
86
 
87
- scroll-margin-top: calc(var(--navbar-height) + var(--banner-height));
87
+ scroll-margin-top: var(--navbar-offset);
88
88
  ${headingAnchorCss(ANCHOR_CLASS)};
89
89
 
90
90
  h1,
@@ -295,6 +295,7 @@ export const Markdown = styled.main
295
295
  font-size: inherit;
296
296
  font-family: inherit;
297
297
  border: none;
298
+ padding: 0;
298
299
  }
299
300
  }
300
301
 
@@ -5,6 +5,7 @@ import type { JSX } from 'react';
5
5
  import type { DrilldownMenuItemDetails, ItemState } from '@redocly/theme/core/types';
6
6
 
7
7
  import { MenuItem } from '@redocly/theme/components/Menu/MenuItem';
8
+ import { MenuSelect } from '@redocly/theme/components/Menu/MenuSelect';
8
9
  import { MenuItemType } from '@redocly/theme/core/constants';
9
10
  import { getMenuItemType } from '@redocly/theme/core/utils';
10
11
 
@@ -28,6 +29,10 @@ export function Menu({
28
29
  return (
29
30
  <MenuWrapper className={className} data-component-name="Menu/Menu">
30
31
  {items.map((item, index) => {
32
+ if (getMenuItemType(item) === MenuItemType.Select) {
33
+ return <MenuSelect item={item} key={`${item.label}-${index}`} />;
34
+ }
35
+
31
36
  return (
32
37
  <MenuItem
33
38
  item={item}
@@ -0,0 +1,198 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { JSX } from 'react';
5
+ import type { ItemState } from '@redocly/theme/core/types';
6
+
7
+ import { getBaseDataAttributes } from '@redocly/theme/core/utils';
8
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
9
+ import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
10
+ import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
11
+ import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
12
+ import { GenericIcon } from '@redocly/theme/icons/GenericIcon/GenericIcon';
13
+ import { LaunchIcon } from '@redocly/theme/icons/LaunchIcon/LaunchIcon';
14
+ import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
15
+ import { CheckmarkIcon } from '@redocly/theme/icons/CheckmarkIcon/CheckmarkIcon';
16
+
17
+ export type MenuSelectProps = {
18
+ item: ItemState;
19
+ className?: string;
20
+ };
21
+
22
+ export function MenuSelect({ item, className }: MenuSelectProps): JSX.Element {
23
+ const { useTranslate, useTelemetry } = useThemeHooks();
24
+ const { translate } = useTranslate();
25
+ const telemetry = useTelemetry();
26
+
27
+ return (
28
+ <MenuSelectWrapper data-component-name="Menu/MenuSelect" className={className} role="listitem">
29
+ <MenuSelectDropdown
30
+ alignment="start"
31
+ trigger={
32
+ <MenuSelectTrigger type="button">
33
+ <MenuSelectLabel>{translate(item.labelTranslationKey, item.label)}</MenuSelectLabel>
34
+ <MenuSelectChevron
35
+ size="var(--menu-select-icon-size)"
36
+ color="--menu-select-icon-color"
37
+ />
38
+ </MenuSelectTrigger>
39
+ }
40
+ >
41
+ <MenuSelectMenu>
42
+ {item.items.map((option, index) => {
43
+ const key = `${option.link || option.label}-${index}`;
44
+ const checkmark = option.active ? (
45
+ <MenuSelectCheckmark size="var(--menu-select-option-checkmark-size)" />
46
+ ) : null;
47
+ const content = (
48
+ <MenuSelectOptionContent $active={option.active}>
49
+ {option.icon || option.srcSet ? (
50
+ <MenuSelectOptionIcon icon={option.icon} srcSet={option.srcSet} />
51
+ ) : null}
52
+ {translate(option.labelTranslationKey, option.label)}
53
+ {option.external ? (
54
+ <LaunchIcon size="var(--menu-select-option-external-icon-size)" />
55
+ ) : null}
56
+ </MenuSelectOptionContent>
57
+ );
58
+
59
+ if (!option.link) {
60
+ return (
61
+ <MenuSelectOption
62
+ key={key}
63
+ active={option.active}
64
+ prefix={checkmark}
65
+ dataAttributes={{ 'aria-disabled': 'true' }}
66
+ >
67
+ {content}
68
+ </MenuSelectOption>
69
+ );
70
+ }
71
+
72
+ return (
73
+ <MenuSelectOption
74
+ key={key}
75
+ to={option.link}
76
+ external={option.external}
77
+ target={option.target === '_blank' ? '_blank' : undefined}
78
+ active={option.active}
79
+ aria-current={option.active ? 'page' : undefined}
80
+ prefix={checkmark}
81
+ onAction={() =>
82
+ telemetry.sendSidebarItemClickedMessage([
83
+ {
84
+ ...getBaseDataAttributes('sidebarItem', 'sidebar'),
85
+ label: option.label,
86
+ type: 'link',
87
+ },
88
+ ])
89
+ }
90
+ >
91
+ {content}
92
+ </MenuSelectOption>
93
+ );
94
+ })}
95
+ </MenuSelectMenu>
96
+ </MenuSelectDropdown>
97
+ </MenuSelectWrapper>
98
+ );
99
+ }
100
+
101
+ const MenuSelectWrapper = styled.div`
102
+ margin: var(--menu-select-margin);
103
+ `;
104
+
105
+ const MenuSelectDropdown = styled(Dropdown)`
106
+ --dropdown-menu-padding-top: var(--menu-select-menu-offset);
107
+ --dropdown-menu-padding: var(--menu-select-menu-padding);
108
+ --dropdown-menu-border-radius: var(--menu-select-menu-border-radius);
109
+ --dropdown-menu-item-gap: var(--menu-select-option-gap);
110
+ --dropdown-menu-item-border-radius: var(--menu-select-option-border-radius);
111
+ --dropdown-menu-item-bg-color-active: var(--menu-select-option-bg-color-active);
112
+ --dropdown-menu-item-color-active: var(--menu-select-option-text-color-active);
113
+
114
+ width: 100%;
115
+
116
+ > * {
117
+ min-width: 100%;
118
+ }
119
+ `;
120
+
121
+ const MenuSelectChevron = styled(ChevronDownIcon)`
122
+ transition: var(--menu-select-transition);
123
+ `;
124
+
125
+ const MenuSelectTrigger = styled.button`
126
+ display: flex;
127
+ align-items: center;
128
+ gap: var(--menu-select-gap);
129
+ box-sizing: border-box;
130
+ width: 100%;
131
+ height: var(--menu-select-height);
132
+ padding: 0 var(--menu-select-padding-horizontal);
133
+ border: var(--menu-select-border);
134
+ border-radius: var(--menu-select-border-radius);
135
+ background-color: var(--menu-select-bg-color);
136
+ color: var(--menu-select-text-color);
137
+ font-family: inherit;
138
+ font-size: var(--menu-select-font-size);
139
+ line-height: var(--menu-select-line-height);
140
+ text-align: left;
141
+ cursor: pointer;
142
+
143
+ &:focus-visible {
144
+ outline: 1px solid var(--menu-select-border-color-focused);
145
+ }
146
+
147
+ &[aria-expanded='true'] ${MenuSelectChevron} {
148
+ transform: rotate(180deg);
149
+ }
150
+ `;
151
+
152
+ const MenuSelectLabel = styled.span`
153
+ flex: 1;
154
+ min-width: 0;
155
+ overflow: hidden;
156
+ text-overflow: ellipsis;
157
+ white-space: nowrap;
158
+ `;
159
+
160
+ const MenuSelectMenu = styled(DropdownMenu)`
161
+ box-sizing: border-box;
162
+ width: 100%;
163
+ max-width: 100%;
164
+ `;
165
+
166
+ const MenuSelectOption = styled(DropdownMenuItem)`
167
+ position: relative;
168
+ box-sizing: border-box;
169
+ padding: var(--menu-select-option-padding);
170
+
171
+ &[aria-disabled='true'] {
172
+ cursor: default;
173
+ pointer-events: none;
174
+ }
175
+ `;
176
+
177
+ const MenuSelectCheckmark = styled(CheckmarkIcon)`
178
+ position: absolute;
179
+ top: 50%;
180
+ left: var(--menu-select-option-checkmark-offset);
181
+ transform: translateY(-50%);
182
+ `;
183
+
184
+ const MenuSelectOptionContent = styled.span<{ $active: boolean }>`
185
+ display: inline-flex;
186
+ align-items: center;
187
+ gap: var(--menu-select-option-content-gap);
188
+ font-weight: ${({ $active }) =>
189
+ $active
190
+ ? 'var(--menu-select-option-font-weight-active)'
191
+ : 'var(--menu-select-option-font-weight)'};
192
+ `;
193
+
194
+ const MenuSelectOptionIcon = styled(GenericIcon)`
195
+ --icon-width: var(--menu-select-option-icon-size);
196
+ --icon-height: var(--menu-select-option-icon-size);
197
+ flex-shrink: 0;
198
+ `;
@@ -102,6 +102,45 @@ export const menu = css`
102
102
  --menu-back-button-gap: var(--spacing-xs);
103
103
  --menu-back-button-icon: none;
104
104
 
105
+ /**
106
+ * @tokens Menu select
107
+ */
108
+ --menu-select-margin-top: 0;
109
+ --menu-select-margin-bottom: var(--spacing-xs);
110
+ --menu-select-margin-horizontal: var(--menu-item-label-margin-horizontal);
111
+ --menu-select-margin: var(--menu-select-margin-top) var(--menu-select-margin-horizontal) var(--menu-select-margin-bottom); // @presenter Spacing
112
+ --menu-select-height: 32px; // @presenter Spacing
113
+ --menu-select-padding-horizontal: calc(var(--spacing-sm) - var(--border-width)); // @presenter Spacing
114
+ --menu-select-gap: var(--spacing-xxs); // @presenter Spacing
115
+ --menu-select-font-size: var(--font-size-base); // @presenter FontSize
116
+ --menu-select-line-height: var(--line-height-base); // @presenter LineHeight
117
+ --menu-select-text-color: var(--input-content-color-active); // @presenter Color
118
+ --menu-select-bg-color: var(--input-bg-color); // @presenter Color
119
+ --menu-select-border: var(--input-border); // @presenter Border
120
+ --menu-select-border-color-focused: var(--input-border-color-focused); // @presenter Color
121
+ --menu-select-border-radius: var(--input-border-radius); // @presenter BorderRadius
122
+ --menu-select-icon-size: 16px;
123
+ --menu-select-icon-color: var(--icon-color-secondary); // @presenter Color
124
+ --menu-select-transition: transform 150ms ease-in-out;
125
+ --menu-select-menu-offset: var(--spacing-xs); // @presenter Spacing
126
+ --menu-select-menu-padding: calc(var(--dropdown-menu-padding) - var(--border-width)); // @presenter Spacing
127
+ --menu-select-menu-border-radius: var(--border-radius-lg); // @presenter BorderRadius
128
+ --menu-select-option-gap: calc(var(--spacing-unit) / 2); // @presenter Spacing
129
+ --menu-select-option-content-gap: var(--spacing-xs); // @presenter Spacing
130
+ --menu-select-option-padding-vertical: var(--dropdown-menu-item-padding-vertical);
131
+ --menu-select-option-padding-left: var(--spacing-md);
132
+ --menu-select-option-padding-right: var(--spacing-sm);
133
+ --menu-select-option-padding: var(--menu-select-option-padding-vertical) var(--menu-select-option-padding-right) var(--menu-select-option-padding-vertical) var(--menu-select-option-padding-left); // @presenter Spacing
134
+ --menu-select-option-border-radius: var(--border-radius-md); // @presenter BorderRadius
135
+ --menu-select-option-font-weight: var(--font-weight-regular); // @presenter FontWeight
136
+ --menu-select-option-font-weight-active: var(--font-weight-medium); // @presenter FontWeight
137
+ --menu-select-option-text-color-active: var(--text-color-primary); // @presenter Color
138
+ --menu-select-option-bg-color-active: transparent; // @presenter Color
139
+ --menu-select-option-icon-size: var(--menu-item-icon-size);
140
+ --menu-select-option-external-icon-size: var(--menu-item-external-icon-size);
141
+ --menu-select-option-checkmark-size: 14px;
142
+ --menu-select-option-checkmark-offset: calc((var(--menu-select-option-padding-left) - var(--menu-select-option-checkmark-size)) / 2);
143
+
105
144
  /**
106
145
  * @tokens Menu items
107
146
  */
@@ -133,18 +172,18 @@ export const mobileMenu = css`
133
172
  * @tokens Mobile Menu
134
173
  * */
135
174
  /* Fallback for older browsers. dvh accounts for dynamic UI elements like mobile address bars */
136
- --menu-mobile-height: calc(100vh - var(--navbar-height) - var(--banner-height));
137
- --menu-mobile-height: calc(100dvh - var(--navbar-height) - var(--banner-height));
175
+ --menu-mobile-height: calc(100vh - var(--navbar-offset));
176
+ --menu-mobile-height: calc(100dvh - var(--navbar-offset));
138
177
  --menu-mobile-width: 100%;
139
178
  --menu-mobile-z-index: var(--z-index-raised);
140
179
  --menu-mobile-left: 0;
141
- --menu-mobile-top: calc(var(--navbar-height) + var(--banner-height));
180
+ --menu-mobile-top: var(--navbar-offset);
142
181
  --menu-mobile-transition: 0.5s;
143
182
  --menu-mobile-bg: var(--bg-color); // @presenter Color
144
183
  --menu-mobile-margin: var(--menu-mobile-items-margin-top) var(--menu-mobile-margin-horizontal) 0 var(--menu-mobile-margin-horizontal);
145
184
  --menu-mobile-line-height: var(--line-height-base); // @presenter LineHeight
146
185
  --menu-mobile-padding-top: var(--spacing-sm); // @presenter Spacing
147
- --menu-mobile-margin-horizontal: var(--spacing-xs); // @presenter Spacing
186
+ --menu-mobile-margin-horizontal: 0; // @presenter Spacing
148
187
  --menu-mobile-items-margin-top: var(--spacing-xxs); // @presenter Spacing
149
188
 
150
189
  --menu-mobile-profile-bg-color: var(--bg-color); // @presenter Color
@@ -1,13 +1,18 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
5
  import type { ResolvedConfigLinks } from '@redocly/config';
6
6
 
7
- import { breakpoints, getBaseDataAttributes } from '@redocly/theme/core/utils';
7
+ import {
8
+ breakpoints,
9
+ getBaseDataAttributes,
10
+ toNavigationTabsConfig,
11
+ } from '@redocly/theme/core/utils';
8
12
  import { useThemeHooks, useThemeConfig, useMobileMenu } from '@redocly/theme/core/hooks';
9
13
  import { NavbarLogo } from '@redocly/theme/components/Navbar/NavbarLogo';
10
14
  import { NavbarMenu } from '@redocly/theme/components/Navbar/NavbarMenu';
15
+ import { NavigationTabs } from '@redocly/theme/components/NavigationTabs/NavigationTabs';
11
16
  import { MenuMobile } from '@redocly/theme/components/Menu/MenuMobile';
12
17
  import { ColorModeSwitcher } from '@redocly/theme/components/ColorModeSwitcher/ColorModeSwitcher';
13
18
  import { Search } from '@redocly/theme/components/Search/Search';
@@ -26,9 +31,15 @@ export type NavbarProps = {
26
31
  export function Navbar({ className }: NavbarProps): JSX.Element | null {
27
32
  const { isOpen, closeMobileMenu, openMobileMenu } = useMobileMenu(false);
28
33
  const themeConfig = useThemeConfig();
29
- const { useL10n, useTelemetry } = useThemeHooks();
34
+ const { useL10n, useTelemetry, useTranslate, useNavigationTabs } = useThemeHooks();
30
35
  const { changeLanguage } = useL10n();
36
+ const { translate } = useTranslate();
31
37
  const telemetry = useTelemetry();
38
+ const navigationTabs = useNavigationTabs();
39
+ const navigationTabsConfig = useMemo(
40
+ () => toNavigationTabsConfig({ state: navigationTabs }),
41
+ [navigationTabs],
42
+ );
32
43
 
33
44
  const menu = themeConfig.navbar?.items;
34
45
 
@@ -43,7 +54,11 @@ export function Navbar({ className }: NavbarProps): JSX.Element | null {
43
54
  const hideUserMenu = userMenuSettings?.hide;
44
55
 
45
56
  return (
46
- <NavbarWrapper data-component-name="Navbar/Navbar" className={className}>
57
+ <NavbarWrapper
58
+ data-component-name="Navbar/Navbar"
59
+ className={className}
60
+ aria-label={translate('navbar.label', 'Main navigation')}
61
+ >
47
62
  <Banner />
48
63
  {isOpen && <MenuMobile hideUserProfile={!!hideUserMenu} />}
49
64
  <NavbarRow>
@@ -76,13 +91,14 @@ export function Navbar({ className }: NavbarProps): JSX.Element | null {
76
91
  />
77
92
  {hideUserMenu ? null : <UserMenu />}
78
93
  </NavbarRow>
94
+ {navigationTabsConfig && <NavigationTabs config={navigationTabsConfig} />}
79
95
  </NavbarWrapper>
80
96
  );
81
97
  }
82
98
 
83
99
  const NavbarWrapper = styled.nav`
84
100
  --text-color: var(--navbar-text-color);
85
- height: calc(var(--navbar-height) + var(--banner-height));
101
+ height: var(--navbar-offset, calc(var(--navbar-height) + var(--banner-height)));
86
102
  transition: height 0.4s ease-out;
87
103
 
88
104
  position: sticky;
@@ -123,6 +139,7 @@ const NavbarRow = styled.div`
123
139
  box-sizing: border-box;
124
140
  margin-top: var(--banner-height);
125
141
  transition: margin-top 0.5s ease-out;
142
+ border-bottom-color: var(--navbar-row-bottom-border-color, var(--navbar-border-color));
126
143
 
127
144
  @media screen and (min-width: ${breakpoints.max}) {
128
145
  max-width: var(--container-max-width);
@@ -4,9 +4,12 @@ export const navbar = css`
4
4
  --navbar-bg-color: var(--bg-color); // @presenter Color
5
5
  --navbar-border-color: var(--border-color-secondary);
6
6
  --navbar-border: 1px solid var(--navbar-border-color);
7
+ --navbar-row-bottom-border-color: var(--navbar-border-color);
7
8
 
8
9
  --navbar-text-color: var(--text-color-secondary); // @presenter Color
9
10
  --navbar-height: 64px; // @presenter Spacing
11
+ --navbar-stack-height: var(--navbar-height);
12
+ --navbar-offset: calc(var(--navbar-stack-height) + var(--banner-height));
10
13
  --navbar-font-size: var(--font-size-base);
11
14
  --navbar-padding: 0 var(--spacing-xl); // @presenter Spacing
12
15
  --navbar-font-family: var(--font-family-base);
@@ -0,0 +1,148 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { JSX } from 'react';
5
+ import type { LinkProps, NavigationTabRowItem } from '@redocly/theme/core/types';
6
+
7
+ import { getNavbarItemClickedPayload } from '@redocly/theme/core/utils';
8
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
9
+ import { Link } from '@redocly/theme/components/Link/Link';
10
+ import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
11
+ import { NavigationTabMenu } from '@redocly/theme/components/NavigationTabs/NavigationTabMenu';
12
+ import { NavigationTabContent } from '@redocly/theme/components/NavigationTabs/NavigationTabContent';
13
+ import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
14
+
15
+ export type NavigationTabProps = {
16
+ tab: NavigationTabRowItem;
17
+ className?: string;
18
+ };
19
+
20
+ export function NavigationTab({ tab, className }: NavigationTabProps): JSX.Element | null {
21
+ const { useTelemetry } = useThemeHooks();
22
+ const telemetry = useTelemetry();
23
+
24
+ const active = !!tab.active;
25
+ const menuItems = tab.type === 'group' ? tab.items : [];
26
+ const isGroup = tab.type === 'group' && menuItems.length > 0;
27
+ const linkItem = tab.type === 'link' ? tab : undefined;
28
+
29
+ if (!linkItem?.link && !isGroup) {
30
+ return null;
31
+ }
32
+
33
+ const content = (
34
+ <>
35
+ <NavigationTabContent item={tab} />
36
+ {isGroup ? <NavigationTabChevron size="10px" /> : null}
37
+ </>
38
+ );
39
+
40
+ if (isGroup) {
41
+ return (
42
+ <NavigationTabWrapper
43
+ data-component-name="NavigationTabs/NavigationTab"
44
+ className={className}
45
+ >
46
+ <NavigationTabDropdown
47
+ triggerEvent="hover"
48
+ portalled
49
+ trigger={
50
+ <NavigationTabTrigger
51
+ $active={active}
52
+ aria-current={active ? 'true' : undefined}
53
+ onClick={() =>
54
+ telemetry.sendNavbarMenuItemClickedMessage(getNavbarItemClickedPayload(tab.type))
55
+ }
56
+ >
57
+ {content}
58
+ </NavigationTabTrigger>
59
+ }
60
+ >
61
+ <NavigationTabMenu items={menuItems} />
62
+ </NavigationTabDropdown>
63
+ </NavigationTabWrapper>
64
+ );
65
+ }
66
+
67
+ return (
68
+ <NavigationTabWrapper data-component-name="NavigationTabs/NavigationTab" className={className}>
69
+ <NavigationTabLink
70
+ as={Link}
71
+ to={linkItem?.link}
72
+ $active={active}
73
+ external={linkItem?.external}
74
+ target={linkItem?.target}
75
+ aria-current={active ? 'page' : undefined}
76
+ onClick={() =>
77
+ telemetry.sendNavbarMenuItemClickedMessage(getNavbarItemClickedPayload(tab.type))
78
+ }
79
+ >
80
+ {content}
81
+ </NavigationTabLink>
82
+ </NavigationTabWrapper>
83
+ );
84
+ }
85
+
86
+ const NavigationTabChevron = styled(ChevronDownIcon)`
87
+ flex-shrink: 0;
88
+ `;
89
+
90
+ const NavigationTabWrapper = styled.li`
91
+ list-style: none;
92
+ display: inline-flex;
93
+ align-items: stretch;
94
+ flex-shrink: 0;
95
+ `;
96
+
97
+ const NavigationTabDropdown = styled(Dropdown)`
98
+ display: inline-flex;
99
+ align-items: stretch;
100
+ height: 100%;
101
+ `;
102
+
103
+ const NavigationTabLink = styled.div<LinkProps & { $active: boolean }>`
104
+ display: inline-flex;
105
+ align-items: center;
106
+ gap: var(--navigation-tabs-item-content-gap);
107
+ box-sizing: border-box;
108
+ height: 100%;
109
+ padding: var(--navigation-tabs-item-padding);
110
+ font-size: var(--navigation-tabs-item-font-size);
111
+ line-height: var(--navigation-tabs-item-line-height);
112
+ white-space: nowrap;
113
+ text-decoration: none;
114
+ transition: border-color var(--navigation-tabs-item-transition);
115
+
116
+ font-weight: ${({ $active }) =>
117
+ $active
118
+ ? 'var(--navigation-tabs-item-font-weight-active)'
119
+ : 'var(--navigation-tabs-item-font-weight)'};
120
+ color: ${({ $active }) =>
121
+ $active
122
+ ? 'var(--navigation-tabs-item-text-color-active)'
123
+ : 'var(--navigation-tabs-item-text-color)'};
124
+ border-bottom: ${({ $active }) =>
125
+ $active
126
+ ? 'var(--navigation-tabs-item-bottom-border-active)'
127
+ : 'var(--navigation-tabs-item-bottom-border)'};
128
+
129
+ &:hover {
130
+ color: var(--navigation-tabs-item-text-color-hover);
131
+ border-bottom: ${({ $active }) => !$active && 'var(--navigation-tabs-item-bottom-border-hover)'};
132
+ }
133
+
134
+ &:focus-visible {
135
+ outline: 1px solid var(--navigation-tabs-item-border-color-focused);
136
+ outline-offset: 2px;
137
+ border-radius: var(--navigation-tabs-item-border-radius);
138
+ }
139
+ `;
140
+
141
+ const NavigationTabTrigger = styled(NavigationTabLink).attrs({ as: 'button', type: 'button' })`
142
+ background: none;
143
+ border-top: none;
144
+ border-left: none;
145
+ border-right: none;
146
+ font-family: inherit;
147
+ cursor: pointer;
148
+ `;