Skip to content

Commit

Permalink
Tab UI Improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
valentinpalkovic committed Jan 26, 2023
1 parent bbc8f9b commit 04b0a59
Show file tree
Hide file tree
Showing 23 changed files with 491 additions and 158 deletions.
2 changes: 1 addition & 1 deletion code/addons/a11y/src/components/VisionSimulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export const VisionSimulator = () => {
});
return <TooltipLinkList links={colorList} />;
}}
closeOnClick
closeOnOutsideClick
onDoubleClick={() => setFilter(null)}
>
<IconButton key="filter" active={!!filter} title="Vision simulator">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const BackgroundSelector: FC = memo(function BackgroundSelector() {
<WithTooltip
placement="top"
trigger="click"
closeOnClick
closeOnOutsideClick
tooltip={({ onHide }) => {
return (
<TooltipLinkList
Expand Down
2 changes: 1 addition & 1 deletion code/addons/toolbars/src/components/ToolbarMenuList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const ToolbarMenuList: FC<ToolbarMenuListProps> = withKeyboardCycle(
});
return <TooltipLinkList links={links} />;
}}
closeOnClick
closeOnOutsideClick
>
<ToolbarMenuButton
active={hasGlobalValue}
Expand Down
2 changes: 1 addition & 1 deletion code/addons/viewport/src/Tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const ViewportTool: FC = memo(
tooltip={({ onHide }) => (
<TooltipLinkList links={toLinks(list, item, setState, state, onHide)} />
)}
closeOnClick
closeOnOutsideClick
>
<IconButtonWithLabel
key="viewport"
Expand Down
6 changes: 3 additions & 3 deletions code/ui/blocks/src/components/ArgsTable/ArgValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,11 @@ const ArgSummary: FC<ArgSummaryProps> = ({ value, initialExpandedArgs }) => {

return (
<WithTooltipPure
closeOnClick
closeOnOutsideClick
trigger="click"
placement="bottom"
tooltipShown={isOpen}
onVisibilityChange={(isVisible) => {
visible={isOpen}
onVisibleChange={(isVisible) => {
setIsOpen(isVisible);
}}
tooltip={
Expand Down
4 changes: 2 additions & 2 deletions code/ui/blocks/src/controls/Color.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,8 @@ export const ColorControl: FC<ColorControlProps> = ({
<PickerTooltip
trigger="click"
startOpen={startOpen}
closeOnClick
onVisibilityChange={() => addPreset(color)}
closeOnOutsideClick
onVisibleChange={() => addPreset(color)}
tooltip={
<TooltipContent>
<Picker
Expand Down
2 changes: 1 addition & 1 deletion code/ui/components/src/bar/bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface SideProps {
right?: boolean;
}

const Side = styled.div<SideProps>(
export const Side = styled.div<SideProps>(
{
display: 'flex',
whiteSpace: 'nowrap',
Expand Down
21 changes: 17 additions & 4 deletions code/ui/components/src/bar/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,30 @@ interface BarButtonProps
}
interface BarLinkProps
extends DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
disabled?: void;
href: string;
}

const ButtonOrLink = ({ children, ...restProps }: BarButtonProps | BarLinkProps) =>
restProps.href != null ? (
<a {...(restProps as BarLinkProps)}>{children}</a>
const ButtonOrLink = React.forwardRef<
HTMLAnchorElement | HTMLButtonElement,
BarLinkProps | BarButtonProps
>(({ children, ...restProps }, ref) => {
return restProps.href != null ? (
<a ref={ref as React.ForwardedRef<HTMLAnchorElement>} {...(restProps as BarLinkProps)}>
{children}
</a>
) : (
<button type="button" {...(restProps as BarButtonProps)}>
<button
ref={ref as React.ForwardedRef<HTMLButtonElement>}
type="button"
{...(restProps as BarButtonProps)}
>
{children}
</button>
);
});

ButtonOrLink.displayName = 'ButtonOrLink';

export interface TabButtonProps {
active?: boolean;
Expand Down
9 changes: 9 additions & 0 deletions code/ui/components/src/hooks/useOnWindowResize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useEffect } from 'react';

export function useOnWindowResize(cb: (ev: UIEvent) => void) {
useEffect(() => {
window.addEventListener('resize', cb);

return () => window.removeEventListener('resize', cb);
}, [cb]);
}
34 changes: 34 additions & 0 deletions code/ui/components/src/tabs/tabs.helpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { styled } from '@storybook/theming';
import type { ReactElement } from 'react';
import React, { Children } from 'react';

export interface VisuallyHiddenProps {
active?: boolean;
}

export const VisuallyHidden = styled.div<VisuallyHiddenProps>(({ active }) =>
active ? { display: 'block' } : { display: 'none' }
);

export 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) => (
<VisuallyHidden key={key} active={active} role="tabpanel">
{content}
</VisuallyHidden>
),
};
}
);

export type ChildrenList = ReturnType<typeof childrenToList>;
174 changes: 174 additions & 0 deletions code/ui/components/src/tabs/tabs.hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { sanitize } from '@storybook/csf';
import { styled } from '@storybook/theming';
import { TabButton } from '../bar/button';
import { useOnWindowResize } from '../hooks/useOnWindowResize';
import { TooltipLinkList } from '../tooltip/TooltipLinkList';
import { WithTooltip } from '../tooltip/WithTooltip';
import type { ChildrenList } from './tabs.helpers';
import type { Link } from '../tooltip/TooltipLinkList';

const CollapseIcon = styled.span<{ isActive: boolean }>(({ theme, isActive }) => ({
display: 'inline-block',
width: 0,
height: 0,
marginLeft: 8,
color: isActive ? theme.color.secondary : theme.color.mediumdark,
borderRight: '3px solid transparent',
borderLeft: `3px solid transparent`,
borderTop: '3px solid',
transition: 'transform .1s ease-out',
}));

const AddonButton = styled(TabButton)<{ preActive: boolean }>(({ active, theme, preActive }) => {
return `
color: ${preActive || active ? theme.color.secondary : theme.color.mediumdark};
&:hover {
color: ${theme.color.secondary};
.addon-collapsible-icon {
color: ${theme.color.secondary};
}
}
`;
});

export function useList(list: ChildrenList) {
const tabBarRef = useRef<HTMLDivElement>();
const addonsRef = useRef<HTMLButtonElement>();
const tabRefs = useRef(new Map<string, HTMLButtonElement>());

const [visibleList, setVisibleList] = useState(list);
const [invisibleList, setInvisibleList] = useState<ChildrenList>([]);
const previousList = useRef<ChildrenList>(list);

const AddonTab = useCallback(
({
menuName,
actions,
}: {
menuName: string;
actions?: {
onSelect: (id: string) => void;
} & Record<string, any>;
}) => {
const isAddonsActive = invisibleList.some(({ active }) => active);
const [isTooltipVisible, setTooltipVisible] = useState(false);
return (
<>
<WithTooltip
interactive
withArrows={false}
visible={isTooltipVisible}
onVisibleChange={setTooltipVisible}
delayHide={100}
tooltip={
<TooltipLinkList
links={invisibleList.map(({ title, id, color, active }) => {
const tabTitle = typeof title === 'function' ? title() : title;
return {
id,
title: tabTitle,
color,
active,
onClick: (e) => {
e.preventDefault();
actions.onSelect(id);
},
} as Link;
})}
/>
}
>
<AddonButton
ref={addonsRef}
active={isAddonsActive}
preActive={isTooltipVisible}
style={{ visibility: invisibleList.length ? 'visible' : 'hidden' }}
className="tabbutton"
type="button"
role="tab"
>
{menuName}
<CollapseIcon
className="addon-collapsible-icon"
isActive={isAddonsActive || isTooltipVisible}
/>
</AddonButton>
</WithTooltip>
{invisibleList.map(({ title, id, color }) => {
const tabTitle = typeof title === 'function' ? title() : title;
return (
<TabButton
id={`tabbutton-${sanitize(tabTitle)}`}
style={{ visibility: 'hidden' }}
tabIndex={-1}
ref={(ref: HTMLButtonElement) => {
tabRefs.current.set(tabTitle, ref);
}}
className="tabbutton"
type="button"
key={id}
textColor={color}
role="tab"
>
{tabTitle}
</TabButton>
);
})}
</>
);
},
[invisibleList]
);

const setTabLists = useCallback(() => {
// get x and width from tabBarRef div
const { x, width } = tabBarRef.current.getBoundingClientRect();
const { width: widthAddonsTab } = addonsRef.current.getBoundingClientRect();
const rightBorder = invisibleList.length ? x + width - widthAddonsTab : x + width;

const newVisibleList: ChildrenList = [];

let widthSum = 0;

const newInvisibleList = list.filter((item) => {
const { title } = item;
const tabTitle = typeof title === 'function' ? title() : title;
const tabButton = tabRefs.current.get(tabTitle);

if (!tabButton) {
return false;
}
const { width: tabWidth } = tabButton.getBoundingClientRect();

const crossBorder = x + widthSum + tabWidth > rightBorder;

if (!crossBorder) {
newVisibleList.push(item);
}

widthSum += tabWidth;

return crossBorder;
});

if (newVisibleList.length !== visibleList.length || previousList.current !== list) {
setVisibleList(newVisibleList);
setInvisibleList(newInvisibleList);
previousList.current = list;
}
}, [invisibleList.length, list, visibleList]);

useOnWindowResize(setTabLists);

useLayoutEffect(setTabLists, [setTabLists]);

return {
tabRefs,
addonsRef,
tabBarRef,
visibleList,
invisibleList,
AddonTab,
};
}
Loading

0 comments on commit 04b0a59

Please sign in to comment.