{v.render}
@@ -193,16 +223,48 @@ export const StatefulDynamic = {
))}
),
-};
+} satisfies Story;
+
+export const StatefulDynamicWithSelectedAddon = {
+ parameters: {
+ viewport: {
+ defaultViewport: 'mobile2',
+ },
+ chromatic: { viewports: [414] },
+ },
+ play: async (context) => {
+ await StatefulDynamicWithOpenTooltip.play(context);
+
+ const popperContainer = screen.getByTestId('tooltip');
+ const tab4 = getByText(popperContainer, 'Tab title #4', {});
+ fireEvent(tab4, new MouseEvent('click', { bubbles: true }));
+ await waitFor(() => screen.getByText('CONTENT 4'));
+
+ // reopen the tooltip
+ await StatefulDynamicWithOpenTooltip.play(context);
+ },
+ render: (args) => (
+
+ {Object.entries(panels).map(([k, v]) => (
+
+ {v.render}
+
+ ))}
+
+ ),
+} satisfies Story;
+
export const StatefulNoInitial = {
- render: (args: ComponentProps
) => {content},
-};
+ render: (args) => {content},
+} satisfies Story;
+
export const StatelessBordered = {
- render: (args: ComponentProps) => (
+ render: (args) => (
),
-};
+} satisfies Story;
+
export const StatelessWithTools = {
- render: (args: ComponentProps) => (
+ render: (args) => (
),
-};
+} satisfies Story;
+
export const StatelessAbsolute = {
- render: (args: ComponentProps) => (
+ render: (args) => (
),
-};
+} satisfies Story;
+
export const StatelessAbsoluteBordered = {
- render: (args: ComponentProps) => (
+ render: (args) => (
),
-};
+} satisfies Story;
+
export const StatelessEmpty = {
- render: (args: ComponentProps) => (
+ render: (args) => (
),
-};
+} satisfies Story;
diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx
index 276be1cb737b..284725a759fd 100644
--- a/code/ui/components/src/tabs/tabs.tsx
+++ b/code/ui/components/src/tabs/tabs.tsx
@@ -1,11 +1,14 @@
-import type { FC, MouseEvent, ReactElement, ReactNode } from 'react';
-import React, { Children, Component, Fragment, memo } from 'react';
+import type { FC, MouseEvent, ReactNode } from 'react';
+import React, { useMemo, Component, Fragment, memo } from 'react';
import { styled } from '@storybook/theming';
import { sanitize } from '@storybook/csf';
import { Placeholder } from '../placeholder/placeholder';
-import { FlexBar } from '../bar/bar';
import { TabButton } from '../bar/button';
+import { FlexBar } from '../bar/bar';
+import type { ChildrenList } from './tabs.helpers';
+import { childrenToList, VisuallyHidden } from './tabs.helpers';
+import { useList } from './tabs.hooks';
export interface WrapperProps {
bordered?: boolean;
@@ -43,8 +46,13 @@ export const TabBar = styled.div({
'&:first-of-type': {
marginLeft: -3,
},
+
+ whiteSpace: 'nowrap',
+ flexGrow: 1,
});
+TabBar.displayName = 'TabBar';
+
export interface ContentProps {
absolute?: boolean;
bordered?: boolean;
@@ -89,14 +97,6 @@ const Content = styled.div(
: {}
);
-export interface VisuallyHiddenProps {
- active?: boolean;
-}
-
-const VisuallyHidden = styled.div(({ active }) =>
- active ? { display: 'block' } : { display: 'none' }
-);
-
export interface TabWrapperProps {
active: boolean;
render?: () => JSX.Element;
@@ -109,27 +109,6 @@ export const TabWrapper: FC = ({ active, render, children }) =>
export const panelProps = {};
-const childrenToList = (children: any, selected: string) =>
- Children.toArray(children).map(
- ({ props: { title, id, color, children: childrenOfChild } }: ReactElement, index) => {
- const content = Array.isArray(childrenOfChild) ? childrenOfChild[0] : childrenOfChild;
- return {
- active: selected ? id === selected : index === 0,
- title,
- id,
- color,
- render:
- typeof content === 'function'
- ? content
- : ({ active, key }: any) => (
-
- {content}
-
- ),
- };
- }
- );
-
export interface TabsProps {
children?: FuncChildren[] | ReactNode;
id?: string;
@@ -141,21 +120,39 @@ export interface TabsProps {
backgroundColor?: string;
absolute?: boolean;
bordered?: boolean;
+ menuName?: string;
}
export const Tabs: FC = memo(
- ({ children, selected, actions, absolute, bordered, tools, backgroundColor, id: htmlId }) => {
- const list = childrenToList(children, selected);
+ ({
+ children,
+ selected,
+ actions,
+ absolute,
+ bordered,
+ tools,
+ backgroundColor,
+ id: htmlId,
+ menuName,
+ }) => {
+ const list = useMemo(
+ () => childrenToList(children, selected),
+ [children, selected]
+ );
+
+ const { visibleList, tabBarRef, tabRefs, AddonTab } = useList(list);
return list.length ? (
-
-
- {list.map(({ title, id, active, color }) => {
- const tabTitle = typeof title === 'function' ? title() : title;
+
+
+ {visibleList.map(({ title, id, active, color }) => {
return (
{
+ tabRefs.current.set(id, ref);
+ }}
className={`tabbutton ${active ? 'tabbutton-active' : ''}`}
type="button"
key={id}
@@ -167,12 +164,13 @@ export const Tabs: FC = memo(
}}
role="tab"
>
- {tabTitle}
+ {title}
);
})}
+
- {tools ? {tools} : null}
+ {tools}
{list.map(({ id, active, render }) => render({ key: id, active }))}
@@ -186,13 +184,14 @@ export const Tabs: FC = memo(
}
);
Tabs.displayName = 'Tabs';
-(Tabs as any).defaultProps = {
+Tabs.defaultProps = {
id: null,
children: null,
tools: null,
selected: null,
absolute: false,
bordered: false,
+ menuName: 'Tabs',
};
type FuncChildren = ({ active }: { active: boolean }) => JSX.Element;
@@ -203,6 +202,7 @@ export interface TabsStateProps {
absolute: boolean;
bordered: boolean;
backgroundColor: string;
+ menuName: string;
}
export interface TabsStateState {
@@ -216,6 +216,7 @@ export class TabsState extends Component {
absolute: false,
bordered: false,
backgroundColor: '',
+ menuName: undefined,
};
constructor(props: TabsStateProps) {
@@ -231,7 +232,7 @@ export class TabsState extends Component {
};
render() {
- const { bordered = false, absolute = false, children, backgroundColor } = this.props;
+ const { bordered = false, absolute = false, children, backgroundColor, menuName } = this.props;
const { selected } = this.state;
return (
{
absolute={absolute}
selected={selected}
backgroundColor={backgroundColor}
+ menuName={menuName}
actions={this.handlers}
>
{children}
diff --git a/code/ui/components/src/tooltip/ListItem.tsx b/code/ui/components/src/tooltip/ListItem.tsx
index cbb45bce396a..c6d1a442154a 100644
--- a/code/ui/components/src/tooltip/ListItem.tsx
+++ b/code/ui/components/src/tooltip/ListItem.tsx
@@ -23,7 +23,7 @@ const Title = styled(({ active, loading, disabled, ...rest }: TitleProps) =>
active
? {
- color: theme.color.primary,
+ color: theme.color.secondary,
fontWeight: theme.typography.weight.bold,
}
: {},
@@ -68,7 +68,7 @@ const Right = styled.span(
opacity: 1,
},
'& path': {
- fill: theme.color.primary,
+ fill: theme.color.secondary,
},
}
: {}
@@ -97,7 +97,7 @@ const CenterText = styled.span(
({ active, theme }) =>
active
? {
- color: theme.color.primary,
+ color: theme.color.secondary,
}
: {},
({ theme, disabled }) =>
diff --git a/code/ui/components/src/tooltip/Tooltip.tsx b/code/ui/components/src/tooltip/Tooltip.tsx
index 942550e88202..98e8dce76a20 100644
--- a/code/ui/components/src/tooltip/Tooltip.tsx
+++ b/code/ui/components/src/tooltip/Tooltip.tsx
@@ -121,13 +121,17 @@ export interface TooltipProps {
arrowProps?: any;
placement?: string;
color?: keyof Color;
+ withArrows?: boolean;
}
export const Tooltip = React.forwardRef(
- ({ placement, hasChrome, children, arrowProps, tooltipRef, color, ...props }, ref) => {
+ (
+ { placement, hasChrome, children, arrowProps, tooltipRef, color, withArrows = true, ...props },
+ ref
+ ) => {
return (
-
- {hasChrome && }
+
+ {hasChrome && withArrows && }
{children}
);
diff --git a/code/ui/components/src/tooltip/TooltipLinkList.tsx b/code/ui/components/src/tooltip/TooltipLinkList.tsx
index 947656b785c4..b3bc5e697409 100644
--- a/code/ui/components/src/tooltip/TooltipLinkList.tsx
+++ b/code/ui/components/src/tooltip/TooltipLinkList.tsx
@@ -29,7 +29,7 @@ export interface TooltipLinkListProps {
}
const Item: FC = (props) => {
- const { LinkWrapper, onClick: onClickFromProps, ...rest } = props;
+ const { LinkWrapper, onClick: onClickFromProps, id, ...rest } = props;
const { title, href, active } = rest;
const onClick = useCallback(
(event: SyntheticEvent) => {
@@ -45,6 +45,7 @@ const Item: FC = (props) => {
title={title}
active={active}
href={href}
+ id={`list-item-${id}`}
LinkWrapper={LinkWrapper}
{...rest}
{...(hasOnClick ? { onClick } : {})}
diff --git a/code/ui/components/src/tooltip/WithTooltip.stories.tsx b/code/ui/components/src/tooltip/WithTooltip.stories.tsx
index 6792be11b086..f2f7c9e0f6da 100644
--- a/code/ui/components/src/tooltip/WithTooltip.stories.tsx
+++ b/code/ui/components/src/tooltip/WithTooltip.stories.tsx
@@ -112,7 +112,7 @@ export const SimpleClickCloseOnClick: StoryObj (
} {...args}>
diff --git a/code/ui/components/src/tooltip/WithTooltip.tsx b/code/ui/components/src/tooltip/WithTooltip.tsx
index b29801488162..b60443a76124 100644
--- a/code/ui/components/src/tooltip/WithTooltip.tsx
+++ b/code/ui/components/src/tooltip/WithTooltip.tsx
@@ -4,90 +4,135 @@ import ReactDOM from 'react-dom';
import { styled } from '@storybook/theming';
import { global } from '@storybook/global';
-import type { TriggerType } from 'react-popper-tooltip';
+import type { Config as ReactPopperTooltipConfig, PopperOptions } from 'react-popper-tooltip';
import { usePopperTooltip } from 'react-popper-tooltip';
-import type { Modifier, Placement } from '@popperjs/core';
import { Tooltip } from './Tooltip';
const { document } = global;
// A target that doesn't speak popper
-const TargetContainer = styled.div<{ mode: string }>`
+const TargetContainer = styled.div<{ trigger: ReactPopperTooltipConfig['trigger'] }>`
display: inline-block;
- cursor: ${(props) => (props.mode === 'hover' ? 'default' : 'pointer')};
+ cursor: ${(props) =>
+ props.trigger === 'hover' || props.trigger.includes('hover') ? 'default' : 'pointer'};
`;
-const TargetSvgContainer = styled.g<{ mode: string }>`
- cursor: ${(props) => (props.mode === 'hover' ? 'default' : 'pointer')};
+const TargetSvgContainer = styled.g<{ trigger: ReactPopperTooltipConfig['trigger'] }>`
+ cursor: ${(props) =>
+ props.trigger === 'hover' || props.trigger.includes('hover') ? 'default' : 'pointer'};
`;
interface WithHideFn {
onHide: () => void;
}
-export interface WithTooltipPureProps {
+export interface WithTooltipPureProps
+ extends Omit,
+ PopperOptions {
svg?: boolean;
- trigger?: TriggerType;
- closeOnClick?: boolean;
- placement?: Placement;
- modifiers?: Array>>;
+ withArrows?: boolean;
hasChrome?: boolean;
tooltip: ReactNode | ((p: WithHideFn) => ReactNode);
children: ReactNode;
+ onDoubleClick?: () => void;
+ /**
+ * @deprecated use `defaultVisible` property instead. This property will be removed in SB 8.0
+ */
tooltipShown?: boolean;
+ /**
+ * @deprecated use `closeOnOutsideClick` property instead. This property will be removed in SB 8.0
+ */
+ closeOnClick?: boolean;
+ /**
+ * @deprecated use `onVisibleChange` property instead. This property will be removed in SB 8.0
+ */
onVisibilityChange?: (visibility: boolean) => void | boolean;
- onDoubleClick?: () => void;
+ /**
+ * If `true`, a click outside the trigger element closes the tooltip
+ * @default false
+ */
+ closeOnOutsideClick?: boolean;
}
// Pure, does not bind to the body
const WithTooltipPure: FC = ({
svg,
trigger,
- closeOnClick,
+ closeOnOutsideClick,
placement,
- modifiers,
hasChrome,
+ withArrows,
+ offset,
tooltip,
children,
+ closeOnTriggerHidden,
+ mutationObserverOptions,
+ closeOnClick,
tooltipShown,
onVisibilityChange,
+ defaultVisible,
+ delayHide,
+ visible,
+ interactive,
+ delayShow,
+ modifiers,
+ strategy,
+ followCursor,
+ onVisibleChange,
...props
}) => {
const Container = svg ? TargetSvgContainer : TargetContainer;
- const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible, state } =
- usePopperTooltip(
- {
- trigger,
- placement,
- defaultVisible: tooltipShown,
- closeOnOutsideClick: closeOnClick,
- onVisibleChange: onVisibilityChange,
+ const {
+ getArrowProps,
+ getTooltipProps,
+ setTooltipRef,
+ setTriggerRef,
+ visible: isVisible,
+ state,
+ } = usePopperTooltip(
+ {
+ trigger,
+ placement,
+ defaultVisible: defaultVisible ?? tooltipShown,
+ delayHide,
+ interactive,
+ closeOnOutsideClick: closeOnOutsideClick ?? closeOnClick,
+ closeOnTriggerHidden,
+ onVisibleChange: (_isVisible) => {
+ onVisibilityChange?.(_isVisible);
+ onVisibleChange?.(_isVisible);
},
- {
- modifiers,
- }
- );
+ delayShow,
+ followCursor,
+ mutationObserverOptions,
+ visible,
+ offset,
+ },
+ {
+ modifiers,
+ strategy,
+ }
+ );
+
+ const tooltipComponent = (
+
+ {typeof tooltip === 'function' ? tooltip({ onHide: () => onVisibleChange(false) }) : tooltip}
+
+ );
return (
<>
-
+
{children}
- {visible &&
- ReactDOM.createPortal(
-
- {typeof tooltip === 'function'
- ? tooltip({ onHide: () => onVisibilityChange(false) })
- : tooltip}
- ,
- document.body
- )}
+ {isVisible && ReactDOM.createPortal(tooltipComponent, document.body)}
>
);
};
@@ -95,7 +140,7 @@ const WithTooltipPure: FC = ({
WithTooltipPure.defaultProps = {
svg: false,
trigger: 'hover',
- closeOnClick: false,
+ closeOnOutsideClick: false,
placement: 'top',
modifiers: [
{
@@ -118,17 +163,18 @@ WithTooltipPure.defaultProps = {
},
],
hasChrome: true,
- tooltipShown: false,
+ defaultVisible: false,
};
const WithToolTipState: FC<
- WithTooltipPureProps & {
+ Omit & {
startOpen?: boolean;
+ onVisibleChange?: (visible: boolean) => void | boolean;
}
-> = ({ startOpen = false, onVisibilityChange: onChange, ...rest }) => {
+> = ({ startOpen = false, onVisibleChange: onChange, ...rest }) => {
const [tooltipShown, setTooltipShown] = useState(startOpen);
- const onVisibilityChange: (visibility: boolean) => void = useCallback(
- (visibility) => {
+ const onVisibilityChange = useCallback(
+ (visibility: boolean) => {
if (onChange && onChange(visibility) === false) return;
setTooltipShown(visibility);
},
@@ -176,11 +222,7 @@ const WithToolTipState: FC<
});
return (
-
+
);
};
diff --git a/code/ui/components/tsconfig.json b/code/ui/components/tsconfig.json
index 65743dc000ea..e279989f36ce 100644
--- a/code/ui/components/tsconfig.json
+++ b/code/ui/components/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
- "types": ["react-syntax-highlighter", "jest"]
+ "types": ["react-syntax-highlighter", "jest", "testing-library__jest-dom"]
},
"include": ["src/**/*"]
}
diff --git a/code/ui/manager/src/components/hooks/useMedia.tsx b/code/ui/manager/src/components/hooks/useMedia.tsx
new file mode 100644
index 000000000000..240629e79cfe
--- /dev/null
+++ b/code/ui/manager/src/components/hooks/useMedia.tsx
@@ -0,0 +1,19 @@
+import { useState, useEffect } from 'react';
+
+const useMediaQuery = (query: string) => {
+ const [matches, setMatches] = useState(false);
+
+ useEffect(() => {
+ const media = window.matchMedia(query);
+ if (media.matches !== matches) {
+ setMatches(media.matches);
+ }
+ const listener = () => setMatches(media.matches);
+ window.addEventListener('resize', listener);
+ return () => window.removeEventListener('resize', listener);
+ }, [matches, query]);
+
+ return matches;
+};
+
+export default useMediaQuery;
diff --git a/code/ui/manager/src/components/notifications/NotificationItem.tsx b/code/ui/manager/src/components/notifications/NotificationItem.tsx
index a595012de53e..48bb989b985d 100644
--- a/code/ui/manager/src/components/notifications/NotificationItem.tsx
+++ b/code/ui/manager/src/components/notifications/NotificationItem.tsx
@@ -106,7 +106,6 @@ const DismissButtonWrapper = styled(IconButton)(({ theme }) => ({
const DismissNotificationItem: FC<{
onDismiss: () => void;
}> = ({ onDismiss }) => (
- // @ts-expect-error (we need to improve the types of IconButton)
{
diff --git a/code/ui/manager/src/components/panel/panel.stories.tsx b/code/ui/manager/src/components/panel/panel.stories.tsx
index 388305193d74..75a66c60942d 100644
--- a/code/ui/manager/src/components/panel/panel.stories.tsx
+++ b/code/ui/manager/src/components/panel/panel.stories.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import Panel from './panel';
import { panels, shortcuts } from '../layout/app.mockdata';
@@ -12,15 +12,18 @@ export default {
component: Panel,
};
-export const Default = () => (
-
-);
+export const Default = () => {
+ const [selectedPanel, setSelectedPanel] = useState('test2');
+ return (
+
+ );
+};
export const NoPanels = () => (
string) | string;
@@ -63,38 +56,46 @@ const AddonPanel = React.memo<{
selectedPanel = null,
panelPosition = 'right',
absolute = true,
- }) => (
-
-
-
-
-
-
-
-
- }
- id="storybook-panel-root"
- >
- {Object.entries(panels).map(([k, v]) => (
-
- {v.render}
-
- ))}
-
- )
+ }) => {
+ const isTablet = useMediaQuery('(min-width: 599px)');
+ return (
+
+
+
+
+
+
+
+
+ ) : undefined
+ }
+ id="storybook-panel-root"
+ >
+ {Object.entries(panels).map(([k, v]) => (
+
+ {v.render}
+
+ ))}
+
+ );
+ }
);
AddonPanel.displayName = 'AddonPanel';
diff --git a/code/ui/manager/src/components/sidebar/Menu.tsx b/code/ui/manager/src/components/sidebar/Menu.tsx
index c1e72f64dd8d..64c0508e4c67 100644
--- a/code/ui/manager/src/components/sidebar/Menu.tsx
+++ b/code/ui/manager/src/components/sidebar/Menu.tsx
@@ -104,7 +104,7 @@ export const SidebarMenu: FC<{
}
>
@@ -121,7 +121,7 @@ export const ToolbarMenu: FC<{
= ({ error }) => (
diff --git a/code/yarn.lock b/code/yarn.lock
index 19bfea729063..83e05b0f685c 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -3076,6 +3076,13 @@ __metadata:
languageName: node
linkType: hard
+"@juggle/resize-observer@npm:^3.3.1":
+ version: 3.4.0
+ resolution: "@juggle/resize-observer@npm:3.4.0"
+ checksum: 12930242357298c6f2ad5d4ec7cf631dfb344ca7c8c830ab7f64e6ac11eb1aae486901d8d880fd08fb1b257800c160a0da3aee1e7ed9adac0ccbb9b7c5d93347
+ languageName: node
+ linkType: hard
+
"@leichtgewicht/ip-codec@npm:^2.0.1":
version: 2.0.4
resolution: "@leichtgewicht/ip-codec@npm:2.0.4"
@@ -6018,6 +6025,7 @@ __metadata:
react-textarea-autosize: ^8.3.0
ts-dedent: ^2.0.0
typescript: ~4.9.3
+ use-resize-observer: ^9.1.0
util-deprecate: ^1.0.2
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -27845,6 +27853,18 @@ __metadata:
languageName: node
linkType: hard
+"use-resize-observer@npm:^9.1.0":
+ version: 9.1.0
+ resolution: "use-resize-observer@npm:9.1.0"
+ dependencies:
+ "@juggle/resize-observer": ^3.3.1
+ peerDependencies:
+ react: 16.8.0 - 18
+ react-dom: 16.8.0 - 18
+ checksum: 6ccdeb09fe20566ec182b1635a22f189e13d46226b74610432590e69b31ef5d05d069badc3306ebd0d2bb608743b17981fb535763a1d7dc2c8ae462ee8e5999c
+ languageName: node
+ linkType: hard
+
"use@npm:^3.1.0":
version: 3.1.1
resolution: "use@npm:3.1.1"