@redocly/theme 0.59.0-next.0 → 0.59.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/lib/components/Accordion/Accordion.d.ts +12 -0
  2. package/lib/components/Accordion/Accordion.js +85 -0
  3. package/lib/components/Accordion/AccordionBody.d.ts +8 -0
  4. package/lib/components/Accordion/AccordionBody.js +73 -0
  5. package/lib/components/Accordion/AccordionHeader.d.ts +10 -0
  6. package/lib/components/Accordion/AccordionHeader.js +37 -0
  7. package/lib/components/Accordion/AccordionTitle.d.ts +6 -0
  8. package/lib/components/Accordion/AccordionTitle.js +20 -0
  9. package/lib/components/Accordion/variables.d.ts +1 -0
  10. package/lib/components/Accordion/variables.js +59 -0
  11. package/lib/components/Admonition/Admonition.js +17 -7
  12. package/lib/components/Badge/Badge.js +17 -7
  13. package/lib/components/Breadcrumbs/Breadcrumb.js +17 -7
  14. package/lib/components/Breadcrumbs/BreadcrumbDropdown.js +17 -7
  15. package/lib/components/Button/Button.js +17 -7
  16. package/lib/components/Buttons/AIAssistantButton.d.ts +2 -0
  17. package/lib/components/Buttons/AIAssistantButton.js +135 -0
  18. package/lib/components/Buttons/CopyButton.js +17 -7
  19. package/lib/components/Buttons/variables.d.ts +1 -0
  20. package/lib/components/Buttons/variables.dark.d.ts +1 -0
  21. package/lib/components/Buttons/variables.dark.js +10 -0
  22. package/lib/components/Buttons/variables.js +51 -0
  23. package/lib/components/Catalog/Catalog.d.ts +6 -0
  24. package/lib/components/Catalog/Catalog.js +9 -8
  25. package/lib/components/Catalog/CatalogEntities.js +17 -7
  26. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.js +17 -7
  27. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.js +17 -7
  28. package/lib/components/Catalog/CatalogEntity/CatalogEntityMetadata.js +17 -7
  29. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.js +1 -1
  30. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.js +17 -7
  31. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.js +1 -1
  32. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +17 -7
  33. package/lib/components/Catalog/CatalogFilter/CatalogFilter.d.ts +6 -0
  34. package/lib/components/Catalog/CatalogFilter/CatalogFilter.js +35 -0
  35. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.d.ts +6 -0
  36. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +152 -0
  37. package/lib/components/Catalog/CatalogFilter/CatalogFilterContent.d.ts +13 -0
  38. package/lib/components/Catalog/CatalogFilter/CatalogFilterContent.js +102 -0
  39. package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.d.ts +6 -0
  40. package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.js +121 -0
  41. package/lib/components/Catalog/CatalogFilter/CatalogFilterSelect.d.ts +6 -0
  42. package/lib/components/Catalog/CatalogFilter/CatalogFilterSelect.js +126 -0
  43. package/lib/components/Catalog/CatalogSelector.js +0 -1
  44. package/lib/components/Catalog/CatalogSortButton.js +17 -7
  45. package/lib/components/Catalog/CatalogTableView/CatalogTableHeaderCell.js +17 -7
  46. package/lib/components/Catalog/CatalogViewModeToggle.js +17 -7
  47. package/lib/components/Catalog/variables.js +0 -1
  48. package/lib/components/CatalogClassic/CatalogClassicActions.js +17 -7
  49. package/lib/components/CatalogClassic/CatalogClassicCard.js +17 -7
  50. package/lib/components/CatalogClassic/CatalogClassicHighlight.js +17 -7
  51. package/lib/components/CatalogClassic/CatalogClassicVirtualizedGroups.js +17 -7
  52. package/lib/components/CodeBlock/CodeBlock.js +17 -7
  53. package/lib/components/CodeBlock/CodeBlockContainer.js +17 -7
  54. package/lib/components/CodeBlock/CodeBlockTabs.js +17 -7
  55. package/lib/components/Dropdown/Dropdown.d.ts +16 -2
  56. package/lib/components/Dropdown/Dropdown.js +22 -12
  57. package/lib/components/Dropdown/DropdownMenuItem.js +17 -7
  58. package/lib/components/Feedback/Comment.js +17 -7
  59. package/lib/components/Feedback/Feedback.js +17 -7
  60. package/lib/components/Feedback/Mood.js +17 -7
  61. package/lib/components/Feedback/Rating.js +17 -7
  62. package/lib/components/Feedback/Reasons.js +17 -7
  63. package/lib/components/Feedback/Scale.js +17 -7
  64. package/lib/components/Feedback/Sentiment.js +17 -7
  65. package/lib/components/Feedback/Stars.js +17 -7
  66. package/lib/components/Filter/FilterContent.js +17 -7
  67. package/lib/components/Filter/FilterInput.d.ts +1 -0
  68. package/lib/components/Filter/FilterInput.js +19 -9
  69. package/lib/components/Filter/FilterOptions.js +2 -0
  70. package/lib/components/Filter/variables.js +7 -4
  71. package/lib/components/Image/Image.js +17 -7
  72. package/lib/components/JsonViewer/JsonViewer.js +17 -7
  73. package/lib/components/JsonViewer/helpers.js +17 -7
  74. package/lib/components/LastUpdated/LastUpdated.js +17 -7
  75. package/lib/components/Link/Link.js +17 -7
  76. package/lib/components/Markdown/Markdown.js +17 -7
  77. package/lib/components/Marker/Marker.js +17 -7
  78. package/lib/components/Menu/MenuContainer.js +17 -7
  79. package/lib/components/Menu/MenuItem.js +18 -8
  80. package/lib/components/Menu/MenuMobile.js +17 -7
  81. package/lib/components/PageActions/PageActions.js +17 -7
  82. package/lib/components/PageNavigation/NextButton.js +17 -7
  83. package/lib/components/Panel/Panel.js +17 -7
  84. package/lib/components/Panel/PanelBody.js +17 -7
  85. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +17 -7
  86. package/lib/components/Search/SearchAiConversationInput.d.ts +2 -1
  87. package/lib/components/Search/SearchAiConversationInput.js +28 -10
  88. package/lib/components/Search/SearchAiDialog.js +19 -10
  89. package/lib/components/Search/SearchAiResponse.js +2 -3
  90. package/lib/components/Search/SearchDialog.d.ts +2 -1
  91. package/lib/components/Search/SearchDialog.js +25 -12
  92. package/lib/components/Search/SearchFilter.js +17 -7
  93. package/lib/components/Search/SearchGroups.js +17 -7
  94. package/lib/components/Search/SearchHighlight.js +17 -7
  95. package/lib/components/Search/SearchItem.js +17 -7
  96. package/lib/components/Search/SearchRecent.js +17 -7
  97. package/lib/components/Search/SearchShortcut.js +17 -7
  98. package/lib/components/Search/SearchSuggestedPages.js +17 -7
  99. package/lib/components/Search/SearchTrigger.js +17 -7
  100. package/lib/components/Search/variables.js +5 -1
  101. package/lib/components/Segmented/Segmented.js +17 -7
  102. package/lib/components/Select/Select.js +17 -7
  103. package/lib/components/Select/SelectInput.js +17 -7
  104. package/lib/components/Sidebar/Sidebar.js +17 -7
  105. package/lib/components/SidebarActions/styled.js +17 -7
  106. package/lib/components/SkipContent/SkipContent.js +17 -7
  107. package/lib/components/Switch/Switch.js +17 -7
  108. package/lib/components/TableOfContent/TableOfContent.js +17 -7
  109. package/lib/components/Tag/variables.dark.js +2 -2
  110. package/lib/components/Tooltip/Tooltip.js +17 -7
  111. package/lib/components/VersionPicker/VersionPicker.js +17 -7
  112. package/lib/core/constants/search.d.ts +5 -4
  113. package/lib/core/constants/search.js +4 -5
  114. package/lib/core/contexts/CodeSnippetContext.js +17 -7
  115. package/lib/core/hooks/use-tabs.d.ts +3 -2
  116. package/lib/core/hooks/use-tabs.js +115 -57
  117. package/lib/core/styles/dark.js +29 -26
  118. package/lib/core/styles/global.js +64 -59
  119. package/lib/core/templates/Markdown.js +17 -7
  120. package/lib/core/types/hooks.d.ts +6 -3
  121. package/lib/core/types/l10n.d.ts +1 -1
  122. package/lib/core/utils/download-code-walkthrough.js +17 -7
  123. package/lib/core/utils/get-file-icon.js +17 -7
  124. package/lib/icons/AiStarsIcon/AiStarsIcon.js +11 -2
  125. package/lib/icons/GenericIcon/GenericIcon.js +17 -7
  126. package/lib/icons/RedoclyIcon/RedoclyIcon.d.ts +9 -0
  127. package/lib/icons/RedoclyIcon/RedoclyIcon.js +24 -0
  128. package/lib/icons/Spinner/Spinner.js +17 -7
  129. package/lib/index.d.ts +2 -0
  130. package/lib/index.js +19 -7
  131. package/lib/layouts/OIDCForbidden.js +17 -7
  132. package/lib/layouts/RootLayout.js +6 -1
  133. package/lib/layouts/ThreePanelLayout.js +17 -7
  134. package/lib/markdoc/components/Cards/Cards.js +17 -7
  135. package/lib/markdoc/components/CodeGroup/CodeGroup.js +17 -7
  136. package/lib/markdoc/components/CodeWalkthrough/CodeContainer.js +17 -7
  137. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +17 -7
  138. package/lib/markdoc/components/CodeWalkthrough/CodePanelHeader.js +17 -7
  139. package/lib/markdoc/components/CodeWalkthrough/CodePanelPreview.js +17 -7
  140. package/lib/markdoc/components/CodeWalkthrough/CodePanelToolbar.js +17 -7
  141. package/lib/markdoc/components/CodeWalkthrough/CodeStep.js +17 -7
  142. package/lib/markdoc/components/CodeWalkthrough/CodeToggle.js +17 -7
  143. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +17 -7
  144. package/lib/markdoc/components/CodeWalkthrough/Input.js +17 -7
  145. package/lib/markdoc/components/Heading/Heading.js +17 -7
  146. package/lib/markdoc/components/HtmlBlock/HtmlBlock.js +17 -7
  147. package/lib/markdoc/components/InlineSvg/InlineSvg.js +17 -7
  148. package/lib/markdoc/components/MarkdocExample/MarkdocExample.js +17 -7
  149. package/lib/markdoc/components/Tabs/TabList.d.ts +3 -1
  150. package/lib/markdoc/components/Tabs/TabList.js +214 -54
  151. package/lib/markdoc/components/Tabs/Tabs.d.ts +2 -1
  152. package/lib/markdoc/components/Tabs/Tabs.js +74 -19
  153. package/lib/markdoc/default.d.ts +104 -1
  154. package/lib/markdoc/default.js +17 -7
  155. package/package.json +5 -5
  156. package/src/components/Accordion/Accordion.tsx +100 -0
  157. package/src/components/Accordion/AccordionBody.tsx +65 -0
  158. package/src/components/Accordion/AccordionHeader.tsx +68 -0
  159. package/src/components/Accordion/AccordionTitle.tsx +26 -0
  160. package/src/components/Accordion/variables.ts +56 -0
  161. package/src/components/Buttons/AIAssistantButton.tsx +141 -0
  162. package/src/components/Buttons/variables.dark.ts +7 -0
  163. package/src/components/Buttons/variables.ts +48 -0
  164. package/src/components/Catalog/Catalog.tsx +18 -6
  165. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.tsx +1 -1
  166. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.tsx +1 -1
  167. package/src/components/Catalog/CatalogFilter/CatalogFilter.tsx +56 -0
  168. package/src/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.tsx +169 -0
  169. package/src/components/Catalog/CatalogFilter/CatalogFilterContent.tsx +121 -0
  170. package/src/components/Catalog/CatalogFilter/CatalogFilterDateRange.tsx +147 -0
  171. package/src/components/Catalog/CatalogFilter/CatalogFilterSelect.tsx +136 -0
  172. package/src/components/Catalog/CatalogSelector.tsx +0 -1
  173. package/src/components/Catalog/variables.ts +0 -1
  174. package/src/components/Dropdown/Dropdown.tsx +84 -79
  175. package/src/components/Filter/FilterInput.tsx +3 -2
  176. package/src/components/Filter/FilterOptions.tsx +2 -0
  177. package/src/components/Filter/variables.ts +7 -4
  178. package/src/components/Menu/MenuItem.tsx +1 -0
  179. package/src/components/Search/SearchAiConversationInput.tsx +12 -2
  180. package/src/components/Search/SearchAiDialog.tsx +2 -2
  181. package/src/components/Search/SearchAiResponse.tsx +2 -2
  182. package/src/components/Search/SearchDialog.tsx +13 -5
  183. package/src/components/Search/variables.ts +5 -1
  184. package/src/components/Tag/variables.dark.ts +2 -2
  185. package/src/core/constants/search.ts +8 -4
  186. package/src/core/hooks/use-tabs.ts +168 -86
  187. package/src/core/styles/dark.ts +11 -8
  188. package/src/core/styles/global.ts +7 -2
  189. package/src/core/types/hooks.ts +6 -1
  190. package/src/core/types/l10n.ts +2 -0
  191. package/src/icons/AiStarsIcon/AiStarsIcon.tsx +11 -2
  192. package/src/icons/RedoclyIcon/RedoclyIcon.tsx +26 -0
  193. package/src/index.ts +2 -0
  194. package/src/layouts/RootLayout.tsx +6 -0
  195. package/src/markdoc/components/Tabs/TabList.tsx +312 -105
  196. package/src/markdoc/components/Tabs/Tabs.tsx +136 -11
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef } from 'react';
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
@@ -17,6 +17,140 @@ type TabListProps = {
17
17
  size: TabsSize;
18
18
  activeTab: string;
19
19
  onTabChange: (tab: string) => void;
20
+ containerRef: React.RefObject<HTMLUListElement | null>;
21
+ onReadyChange?: (isReady: boolean) => void;
22
+ };
23
+
24
+ type UseHighlightBarAnimationProps = {
25
+ childrenArray: React.ReactElement<TabItemProps>[];
26
+ activeTab: string;
27
+ tabsContainerRef: React.RefObject<HTMLElement | null>;
28
+ visibleTabs: number[];
29
+ overflowTabs: number[];
30
+ };
31
+
32
+ /**
33
+ * Calculates optimal dropdown position relative to viewport to ensure visibility.
34
+ * Positions below the button by default, but moves above if insufficient space.
35
+ * Adjusts horizontal position to prevent overflow off screen edges.
36
+ */
37
+ const calculateDropdownPosition = (
38
+ buttonRect: DOMRect,
39
+ dropdownRect: DOMRect,
40
+ ): { top: number; left: number } => {
41
+ const gap = 4;
42
+ const margin = 16;
43
+ const spaceBelow = window.innerHeight - buttonRect.bottom;
44
+ const spaceAbove = buttonRect.top;
45
+
46
+ // Position below button, or above if dropdown doesn't fit below
47
+ const top =
48
+ spaceBelow < dropdownRect.height + gap && spaceAbove > spaceBelow
49
+ ? buttonRect.top - gap
50
+ : buttonRect.bottom + gap;
51
+
52
+ // Align with button left edge, adjust if overflows screen
53
+ const idealLeft = buttonRect.left;
54
+ const rightEdge = idealLeft + dropdownRect.width;
55
+ const overflowsRight = rightEdge > window.innerWidth - margin;
56
+
57
+ const left = overflowsRight
58
+ ? window.innerWidth - dropdownRect.width - margin
59
+ : Math.max(margin, idealLeft);
60
+
61
+ return { top, left };
62
+ };
63
+
64
+ /**
65
+ * Manages dropdown positioning and updates on scroll/resize events for TabList.
66
+ */
67
+ const useDropdownPosition = (
68
+ hasOverflow: boolean,
69
+ dropdownRef: React.RefObject<HTMLDivElement | null>,
70
+ ) => {
71
+ const [dropdownPosition, setDropdownPosition] = useState<{ top?: number; left?: number }>({});
72
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
73
+
74
+ const updateDropdownPosition = useCallback(() => {
75
+ if (!dropdownRef.current) return;
76
+
77
+ const button = dropdownRef.current.querySelector('button');
78
+ const dropdownMenu = dropdownRef.current.querySelector('div:last-child');
79
+ if (!button || !dropdownMenu) return;
80
+
81
+ const buttonRect = button.getBoundingClientRect();
82
+ const dropdownRect = (dropdownMenu as HTMLElement).getBoundingClientRect();
83
+
84
+ const position = calculateDropdownPosition(buttonRect, dropdownRect);
85
+ setDropdownPosition(position);
86
+ }, [dropdownRef]);
87
+
88
+ // Track when dropdown menu appears and recalculate position
89
+ useEffect(() => {
90
+ if (!hasOverflow || !isDropdownOpen || !dropdownRef.current) return;
91
+
92
+ const dropdownMenu = dropdownRef.current.querySelector('div:last-child') as HTMLElement;
93
+ if (!dropdownMenu) return;
94
+
95
+ // ResizeObserver tracks both initial render and size changes
96
+ const resizeObserver = new ResizeObserver(() => {
97
+ updateDropdownPosition();
98
+ });
99
+
100
+ resizeObserver.observe(dropdownMenu);
101
+
102
+ return () => resizeObserver.disconnect();
103
+ }, [hasOverflow, isDropdownOpen, dropdownRef, updateDropdownPosition]);
104
+
105
+ // Update position on scroll/resize
106
+ useEffect(() => {
107
+ if (!hasOverflow || !isDropdownOpen) return;
108
+
109
+ window.addEventListener('scroll', updateDropdownPosition, true);
110
+ window.addEventListener('resize', updateDropdownPosition);
111
+
112
+ return () => {
113
+ window.removeEventListener('scroll', updateDropdownPosition, true);
114
+ window.removeEventListener('resize', updateDropdownPosition);
115
+ };
116
+ }, [hasOverflow, isDropdownOpen, updateDropdownPosition]);
117
+
118
+ return {
119
+ dropdownPosition,
120
+ isDropdownOpen,
121
+ setIsDropdownOpen,
122
+ setDropdownPosition,
123
+ updateDropdownPosition,
124
+ };
125
+ };
126
+
127
+ const renderTab = (
128
+ child: React.ReactElement<TabItemProps>,
129
+ index: number,
130
+ size: TabsSize,
131
+ setTabRef: (element: HTMLButtonElement | null, index: number) => void,
132
+ handleKeyboard: (event: React.KeyboardEvent, index: number) => void,
133
+ onTabClick: (labelOrIndex: string | number) => void,
134
+ ) => {
135
+ const { label, icon } = child.props;
136
+ const tabId = getTabId(label, index);
137
+
138
+ return (
139
+ <Tab
140
+ key={`key-${tabId}`}
141
+ tabId={tabId}
142
+ label={label}
143
+ icon={icon}
144
+ size={size}
145
+ disabled={child.props.disable}
146
+ setRef={(el: HTMLButtonElement | null) => setTabRef(el, index)}
147
+ onKeyDown={(event) => handleKeyboard(event, index)}
148
+ onClick={() => {
149
+ child.props.onClick?.();
150
+ onTabClick(label);
151
+ }}
152
+ />
153
+ );
20
154
  };
21
155
 
22
156
  export function TabList({
@@ -24,106 +158,119 @@ export function TabList({
24
158
  size,
25
159
  activeTab,
26
160
  onTabChange,
161
+ containerRef,
162
+ onReadyChange,
27
163
  }: TabListProps): JSX.Element {
28
- const tabsContainerRef = useRef<HTMLUListElement>(null);
29
-
30
- const { allTabsHidden, overflowTabs, visibleTabs, handleKeyboard, onTabClick, setTabRef } =
31
- useTabs({
32
- activeTab,
33
- onTabChange,
34
- containerRef: tabsContainerRef,
35
- totalTabs: childrenArray.length,
36
- });
164
+ const dropdownRef = useRef<HTMLDivElement>(null);
165
+ const totalTabs = childrenArray.length;
166
+
167
+ const { overflowTabs, visibleTabs, handleKeyboard, onTabClick, setTabRef, isReady } = useTabs({
168
+ activeTab,
169
+ onTabChange,
170
+ containerRef,
171
+ totalTabs,
172
+ });
173
+
174
+ useEffect(() => {
175
+ onReadyChange?.(isReady);
176
+ }, [isReady, onReadyChange]);
37
177
 
38
178
  const { highlightStyle } = useHighlightBarAnimation({
39
179
  activeTab,
40
180
  childrenArray,
41
181
  overflowTabs,
42
- tabsContainerRef,
182
+ tabsContainerRef: containerRef,
43
183
  visibleTabs,
44
184
  });
45
185
 
186
+ const hasOverflow = overflowTabs.length > 0;
187
+ const isMoreActive =
188
+ hasOverflow &&
189
+ overflowTabs.some((i) => childrenArray[i] && activeTab === childrenArray[i].props.label);
190
+
191
+ // Show as selector when no visible tabs (all tabs in dropdown)
192
+ const showAsSelector = visibleTabs.length === 0 && hasOverflow;
193
+
194
+ const { dropdownPosition, setIsDropdownOpen, setDropdownPosition } = useDropdownPosition(
195
+ hasOverflow,
196
+ dropdownRef,
197
+ );
198
+
46
199
  return (
47
- <TabListContainer role="tablist" ref={tabsContainerRef}>
200
+ <TabListContainer role="tablist" ref={containerRef}>
48
201
  <HighlightBar size={size} style={highlightStyle}>
49
202
  <div />
50
203
  </HighlightBar>
204
+
51
205
  {childrenArray.map((child, index) => {
52
- if (!visibleTabs.includes(index)) return null;
53
- const { label, icon } = child.props;
54
- const tabId = getTabId(label, index);
55
- return (
56
- <Tab
57
- key={`key-${tabId}`}
58
- tabId={tabId}
59
- label={label}
60
- icon={icon}
61
- size={size}
62
- disabled={child.props.disable}
63
- setRef={(el: HTMLButtonElement | null) => setTabRef(el, index)}
64
- onKeyDown={(event) => handleKeyboard(event, index)}
65
- onClick={() => {
66
- child.props.onClick?.();
67
- onTabClick(label);
68
- }}
69
- />
70
- );
206
+ // Show all tabs before ready (for measurement), then only visible ones
207
+ const shouldRender = !isReady || visibleTabs.includes(index);
208
+ if (!shouldRender) return null;
209
+ return renderTab(child, index, size, setTabRef, handleKeyboard, onTabClick);
71
210
  })}
72
- <TabItem
73
- size={size}
74
- active={overflowTabs.some((index) => activeTab === childrenArray[index].props.label)}
75
- tabIndex={0}
76
- >
77
- {overflowTabs.length > 0 && (
78
- <Dropdown
79
- trigger={
80
- <TabButtonLink
81
- size={size}
82
- className={
83
- overflowTabs.some((index) => activeTab === childrenArray[index].props.label)
84
- ? 'active'
85
- : undefined
86
- }
87
- >
88
- {allTabsHidden ? activeTab : 'More'}
89
- </TabButtonLink>
90
- }
91
- alignment="start"
92
- withArrow={true}
211
+
212
+ {hasOverflow && (
213
+ <TabItem
214
+ size={size}
215
+ active={isMoreActive || showAsSelector}
216
+ tabIndex={0}
217
+ className="dropdown-tab"
218
+ >
219
+ <DropdownWrapper
220
+ $top={dropdownPosition.top}
221
+ $left={dropdownPosition.left}
222
+ onClickCapture={() => {
223
+ setIsDropdownOpen(true);
224
+ }}
93
225
  >
94
- <DropdownMenu>
95
- {overflowTabs.map((index) => {
96
- const { label } = childrenArray[index].props;
97
- const tabId = getTabId(label, index);
98
- return (
99
- <DropdownMenuItem
100
- key={`more-${tabId}`}
101
- active={activeTab === label}
102
- onAction={() => {
103
- childrenArray[index].props.onClick?.();
104
- onTabClick(index);
105
- }}
106
- disabled={childrenArray[index].props.disable}
107
- >
108
- {label}
109
- </DropdownMenuItem>
110
- );
111
- })}
112
- </DropdownMenu>
113
- </Dropdown>
114
- )}
115
- </TabItem>
226
+ <FixedPositionDropdown
227
+ ref={dropdownRef}
228
+ trigger={
229
+ <TabButtonLink
230
+ size={size}
231
+ className={isMoreActive || showAsSelector ? 'active' : undefined}
232
+ >
233
+ {showAsSelector ? <TabButtonText>{activeTab}</TabButtonText> : 'More'}
234
+ </TabButtonLink>
235
+ }
236
+ alignment="start"
237
+ withArrow
238
+ onClose={() => {
239
+ setIsDropdownOpen(false);
240
+ setDropdownPosition({});
241
+ }}
242
+ >
243
+ <DropdownMenu>
244
+ {overflowTabs.map((index) => {
245
+ const child = childrenArray[index];
246
+ if (!child) return null;
247
+
248
+ const { label } = child.props;
249
+ const tabId = getTabId(label, index);
250
+
251
+ return (
252
+ <DropdownMenuItem
253
+ key={`more-${tabId}`}
254
+ active={activeTab === label}
255
+ onAction={() => {
256
+ child.props.onClick?.();
257
+ onTabClick(index);
258
+ }}
259
+ disabled={child.props.disable}
260
+ >
261
+ {label}
262
+ </DropdownMenuItem>
263
+ );
264
+ })}
265
+ </DropdownMenu>
266
+ </FixedPositionDropdown>
267
+ </DropdownWrapper>
268
+ </TabItem>
269
+ )}
116
270
  </TabListContainer>
117
271
  );
118
272
  }
119
273
 
120
- type UseHighlightBarAnimationProps = {
121
- childrenArray: React.ReactElement<TabItemProps>[];
122
- activeTab: string;
123
- tabsContainerRef: React.RefObject<HTMLElement | null>;
124
- visibleTabs: number[];
125
- overflowTabs: number[];
126
- };
127
274
  const useHighlightBarAnimation = (props: UseHighlightBarAnimationProps) => {
128
275
  const { childrenArray, activeTab, tabsContainerRef, visibleTabs, overflowTabs } = props;
129
276
 
@@ -141,35 +288,39 @@ const useHighlightBarAnimation = (props: UseHighlightBarAnimationProps) => {
141
288
  return;
142
289
  }
143
290
 
144
- const activeTabElement: HTMLElement | null = container.querySelector(
145
- `[data-label="${activeTab}"]`,
146
- );
147
- if (!activeTabElement) return;
148
-
291
+ // Remove active class from all tabs first
149
292
  container.querySelectorAll('[data-label]').forEach((el) => {
150
293
  el.classList.remove('active');
151
294
  });
152
295
 
153
- const { offsetLeft, offsetWidth } = activeTabElement;
154
-
155
- if (visibleTabs.includes(activeIndex)) {
156
- activeTabElement.classList.add('active');
157
- setHighlightStyle({ left: offsetLeft, width: offsetWidth });
158
- return;
159
- }
160
-
296
+ // Check if active tab is in overflow first
161
297
  if (overflowTabs.includes(activeIndex)) {
162
298
  const moreButton = container.querySelector('button');
163
299
  if (!moreButton) return;
164
300
 
165
301
  const moreButtonRect = moreButton.getBoundingClientRect();
166
302
  const containerRect = container.getBoundingClientRect();
303
+
167
304
  setHighlightStyle({
168
305
  left: moreButtonRect.left - containerRect.left,
169
306
  width: moreButtonRect.width,
170
307
  });
171
308
  return;
172
309
  }
310
+
311
+ // Active tab is visible, find its element
312
+ const activeTabElement: HTMLElement | null = container.querySelector(
313
+ `[data-label="${activeTab}"]`,
314
+ );
315
+ if (!activeTabElement) return;
316
+
317
+ const { offsetLeft, offsetWidth } = activeTabElement;
318
+
319
+ if (visibleTabs.includes(activeIndex)) {
320
+ activeTabElement.classList.add('active');
321
+ setHighlightStyle({ left: offsetLeft, width: offsetWidth });
322
+ return;
323
+ }
173
324
  }, [activeTab, childrenArray, visibleTabs, overflowTabs, tabsContainerRef]);
174
325
 
175
326
  return { highlightStyle };
@@ -181,15 +332,11 @@ export const TabListContainer = styled.ul`
181
332
  gap: var(--md-tabs-gap);
182
333
  width: 100%;
183
334
  min-width: 0;
184
- position: relative;
185
335
 
186
336
  &::before {
187
337
  content: '';
188
338
  position: absolute;
189
- top: 0px;
190
- left: 0px;
191
- right: 0px;
192
- bottom: 0px;
339
+ inset: 0;
193
340
  border: var(--md-tabs-border);
194
341
  border-width: var(--md-tabs-border-width);
195
342
  pointer-events: none;
@@ -197,11 +344,17 @@ export const TabListContainer = styled.ul`
197
344
 
198
345
  && {
199
346
  padding: var(--md-tabs-padding);
200
- margin-block-end: 0;
201
347
  margin: 0;
202
348
 
203
349
  & > li {
204
- margin-bottom: 0px;
350
+ margin-bottom: 0;
351
+ flex-shrink: 0;
352
+
353
+ &.dropdown-tab {
354
+ flex-shrink: 1;
355
+ min-width: 0;
356
+ max-width: 100%;
357
+ }
205
358
  }
206
359
  }
207
360
  `;
@@ -212,7 +365,7 @@ export const TabItem = styled.li<{ active?: boolean; size: TabsSize; tabIndex?:
212
365
  cursor: pointer;
213
366
  align-items: center;
214
367
  padding: var(--md-tabs-tab-wrapper-padding);
215
- z-index: 1;
368
+ z-index: var(--z-index-surface);
216
369
 
217
370
  ${({ active, size }) =>
218
371
  active
@@ -251,6 +404,51 @@ export const TabItem = styled.li<{ active?: boolean; size: TabsSize; tabIndex?:
251
404
  }
252
405
  `;
253
406
 
407
+ const DropdownWrapper = styled.div.attrs<{ $top?: number; $left?: number }>((props) => ({
408
+ style: {
409
+ ...(props.$top !== undefined && { '--dropdown-top': `${props.$top}px` }),
410
+ ...(props.$left !== undefined && { '--dropdown-left': `${props.$left}px` }),
411
+ },
412
+ }))<{ $top?: number; $left?: number }>`
413
+ position: static;
414
+ z-index: var(--z-index-raised);
415
+ width: 100%;
416
+ min-width: 0;
417
+ `;
418
+
419
+ const FixedPositionDropdown = styled(Dropdown)`
420
+ position: static;
421
+ width: 100%;
422
+ min-width: 0;
423
+
424
+ > div:first-child {
425
+ width: 100%;
426
+ min-width: 0;
427
+ }
428
+
429
+ > div:last-child {
430
+ position: fixed;
431
+ top: var(--dropdown-top, 0);
432
+ left: var(--dropdown-left, 0);
433
+ right: auto;
434
+ bottom: auto;
435
+ transform: none;
436
+ padding-top: 0;
437
+ max-width: min(400px, calc(100vw - 32px));
438
+ max-height: calc(100vh - var(--dropdown-top, 0) - 32px);
439
+ overflow-y: auto;
440
+ z-index: var(--z-index-raised);
441
+
442
+ ul {
443
+ li {
444
+ overflow: hidden;
445
+ text-overflow: ellipsis;
446
+ white-space: nowrap;
447
+ }
448
+ }
449
+ }
450
+ `;
451
+
254
452
  const HighlightBar = styled.div<{ size: TabsSize }>`
255
453
  position: absolute;
256
454
  top: 0;
@@ -271,11 +469,20 @@ const HighlightBar = styled.div<{ size: TabsSize }>`
271
469
  }
272
470
  `;
273
471
 
472
+ const TabButtonText = styled.span`
473
+ overflow: hidden;
474
+ text-overflow: ellipsis;
475
+ white-space: nowrap;
476
+ flex: 1;
477
+ min-width: 0;
478
+ `;
479
+
274
480
  export const TabButtonLink = styled(Button)`
275
481
  color: var(--md-tabs-tab-text-color);
276
482
  font-family: var(--md-tabs-tab-font-family);
277
483
  font-style: var(--md-tabs-tab-font-style);
278
484
  background-color: var(--md-tabs-tab-bg-color);
485
+ width: 100%;
279
486
 
280
487
  transition:
281
488
  background-color 300ms ease-in-out,
@@ -295,9 +502,9 @@ export const TabButtonLink = styled(Button)`
295
502
 
296
503
  &.active {
297
504
  color: var(--md-tabs-active-tab-text-color);
298
- font-size: var(--md-tabs-${({ size }) => size}-active-tab-font-size);
299
505
  font-family: var(--md-tabs-active-tab-font-family);
300
506
  font-style: var(--md-tabs-active-tab-font-style);
507
+ font-size: var(--md-tabs-${({ size }) => size}-active-tab-font-size);
301
508
  font-weight: var(--md-tabs-${({ size }) => size}-active-tab-font-weight);
302
509
  line-height: var(--md-tabs-${({ size }) => size}-active-tab-line-height);
303
510
  background-color: var(--md-tabs-active-tab-bg-color);
@@ -307,12 +514,12 @@ export const TabButtonLink = styled(Button)`
307
514
 
308
515
  &:hover {
309
516
  color: var(--md-tabs-hover-tab-text-color);
310
- font-size: var(--md-tabs-${({ size }) => size}-hover-tab-font-size);
311
517
  font-family: var(--md-tabs-hover-tab-font-family);
312
518
  font-style: var(--md-tabs-hover-tab-font-style);
519
+ font-size: var(--md-tabs-${({ size }) => size}-hover-tab-font-size);
313
520
  font-weight: var(--md-tabs-${({ size }) => size}-hover-tab-font-weight);
314
- background-color: var(--md-tabs-hover-tab-bg-color);
315
521
  line-height: var(--md-tabs-${({ size }) => size}-hover-tab-line-height);
522
+ background-color: var(--md-tabs-hover-tab-bg-color);
316
523
  border-radius: var(--md-tabs-${({ size }) => size}-hover-tab-border-radius);
317
524
  padding: var(--md-tabs-${({ size }) => size}-hover-tab-padding);
318
525
  }