Skip to content

Commit

Permalink
WIP/feat(toolbar): handle toolbar overflow
Browse files Browse the repository at this point in the history
  • Loading branch information
alirezamirian committed Dec 28, 2023
1 parent 9e9e0da commit b88d592
Show file tree
Hide file tree
Showing 16 changed files with 913 additions and 173 deletions.
30 changes: 28 additions & 2 deletions packages/jui/src/Icon/useSvgIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ export function useSvgIcon(
if (svg) {
if (!unmounted && ref?.current) {
if (ref) {
// potential SSR issues here?
ref.current?.querySelector("svg")?.remove();
const svgElement = document.createElement("svg");
ref.current?.appendChild(svgElement);
svgElement.outerHTML = svg;
svgElement.outerHTML = makeIdsUnique(svg); // UNSAFE! Would require sanitization, or icon sources must be trusted.
delete ref.current?.dataset.loadingIcon;
}
}
Expand All @@ -53,3 +52,30 @@ export function useSvgIcon(
};
}, [path, selected]);
}

/**
* If multiple instance of the same icon is rendered at the same time, and the SVG includes
* url(#...) references to locally defined ids, in some cases the icon is not rendered properly.
* because of ids colliding. We make sure the ids are unique in each rendered icon.
*/
function makeIdsUnique(svg: string): string {
const randomPostfix = (Math.random() * 1000).toFixed(0);
const idMatches = svg.matchAll(/id="(.*?)"/g);
return [...idMatches].reduce((modifiedSvg, [_, id]) => {
const newId = `${id}-${randomPostfix}`;
return replaceAll(
`id="${id}"`,
`id="${newId}"`,
replaceAll(`url(#${id})`, `url(#${newId})`, modifiedSvg)
);
}, svg);
}

function replaceAll(theOld: string, theNew: string, str: string): string {
const replaced = str.replace(theOld, theNew);
const replacedAgain = replaced.replace(theOld, theNew);
if (replaced === replacedAgain) {
return replaced;
}
return replaceAll(theOld, theNew, replacedAgain);
}
3 changes: 2 additions & 1 deletion packages/jui/src/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { PopupContext, PopupControllerContext } from "./PopupContext";
import { PopupLayout } from "./PopupLayout";
import { StyledPopupHint } from "./StyledPopupHint";

const StyledPopupContainer = styled.div`
export const StyledPopupContainer = styled.div`
position: fixed;
box-sizing: border-box;
// not checked if there should be a better substitute for * in the following colors. Maybe "Component"?
Expand Down Expand Up @@ -175,6 +175,7 @@ export const _Popup = (
return (
<Overlay>
<OverlayInteractionHandler {...overlayInteractionHandlerProps}>
{/* TODO: FocusScope is redundant. Test focus restoration without it (in status bar progress), and remove it if unnecessary */}
<FocusScope restoreFocus>
<StyledPopupContainer
ref={ref}
Expand Down
2 changes: 1 addition & 1 deletion packages/jui/src/StyledSeparator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const DarculaSeparatorUI: SeparatorUI = {
const defaultSize =
2 * DarculaSeparatorUI.STRIPE_INDENT + DarculaSeparatorUI.STRIPE_WIDTH;

const StyledSeparator = styled.hr(({ theme }) => ({
export const StyledSeparator = styled.hr(({ theme }) => ({
backgroundColor: theme.color(
"Separator.separatorColor",
theme.dark ? "#cdcdcd" : "#515151"
Expand Down
57 changes: 49 additions & 8 deletions packages/jui/src/Tabs/2-Tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta, StoryObj } from "@storybook/react";
import { MenuItemLayout, PlatformIcon } from "@intellij-platform/core";
import React from "react";
import React, { useEffect, useState } from "react";
import { TabContentLayout, TabItem, TabsProps } from ".";
import { Tabs } from "./Tabs";
import { DOMProps } from "@react-types/shared";
Expand Down Expand Up @@ -53,20 +53,22 @@ export const DynamicItems: StoryObj<StoryProps> = {

export const Overflow: StoryObj<StoryProps & { maxWidth: number }> = {
render: ({ maxWidth = 800, ...props }) => {
const tabs = Array(10)
.fill(null)
.map((_, index) => ({
title: `Big tab title #${index}`,
icon: "nodes/folder",
}));
const [tabs, setTabs] = useState(
Array(10)
.fill(null)
.map((_, index) => ({
title: `Big tab title #${index}`,
icon: "nodes/folder",
}))
);
return (
<div style={{ maxWidth }}>
<Tabs {...props} items={tabs}>
{(tab) => {
const icon = <PlatformIcon icon={tab.icon} />;
return (
<TabItem
key={tab.title}
key={tabs.indexOf(tab)}
textValue={tab.title}
inOverflowMenu={
<MenuItemLayout content={tab.title} icon={icon} />
Expand All @@ -77,6 +79,45 @@ export const Overflow: StoryObj<StoryProps & { maxWidth: number }> = {
);
}}
</Tabs>
<div style={{ marginTop: "1rem" }}>
<button
onClick={() => {
setTabs((tabs) => [
...tabs,
{
title: `Big tab title #${tabs.length}`,
icon: "nodes/folder",
},
]);
}}
>
Add tab
</button>
<button
onClick={() => {
setTabs((tabs) =>
tabs.map((tab, index) => {
if (index === 4) {
const title = tab.title;
return {
...tab,
title:
title.replace(/ \(.*\)/, "") +
` ( edited - ${
title.includes("short")
? "long long long long"
: "short"
})`,
};
}
return tab;
})
);
}}
>
Change title of tab #4
</button>
</div>
</div>
);
},
Expand Down
36 changes: 8 additions & 28 deletions packages/jui/src/Tabs/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { useTab } from "@react-aria/tabs";
import { TabListState } from "@react-stately/tabs";
import { Node } from "@react-types/shared";
import { StyledDefaultTab } from "./StyledDefaultTab";
import React, { useEffect } from "react";
import React, { ForwardedRef, forwardRef, RefObject, useEffect } from "react";
import useForwardedRef from "@intellij-platform/core/utils/useForwardedRef";

type TabProps<T extends object> = {
state: TabListState<object>;
item: Node<T>;
intersectionObserver: IntersectionObserver | null;
/**
* {@see TabsProps#focusable}
*/
Expand All @@ -19,16 +19,12 @@ type TabProps<T extends object> = {
Component?: typeof StyledDefaultTab;
};

export const Tab = <T extends object>({
state,
item,
focusable,
active,
Component = StyledDefaultTab,
intersectionObserver,
}: TabProps<T>): React.ReactElement => {
export const Tab = forwardRef(function Tab<T extends object>(
{ state, item, focusable, active, Component = StyledDefaultTab }: TabProps<T>,
forwardedRef: ForwardedRef<HTMLDivElement>
): React.ReactElement {
const { key, rendered } = item;
const ref = React.useRef(null);
const ref = useForwardedRef(forwardedRef);
const {
tabProps: {
/**
Expand All @@ -41,7 +37,6 @@ export const Tab = <T extends object>({
} = useTab({ key }, state, ref);
const isSelected = state.selectedKey === key;
const isDisabled = state.disabledKeys.has(key);
useIntersectionObserver(ref, intersectionObserver);

return (
<Component
Expand All @@ -55,19 +50,4 @@ export const Tab = <T extends object>({
{rendered}
</Component>
);
};

function useIntersectionObserver(
ref: React.MutableRefObject<null>,
intersectionObserver: IntersectionObserver | null
) {
useEffect(() => {
const element = ref.current;
if (element) {
intersectionObserver?.observe(element);
return () => {
intersectionObserver?.unobserve(element);
};
}
}, [intersectionObserver]);
}
});
16 changes: 11 additions & 5 deletions packages/jui/src/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { useTabListState } from "@react-stately/tabs";
import { AriaTabListProps } from "@react-types/tabs";
import { StyledHorizontalOverflowShadows } from "./StyledHorizontalOverflowShadows";
import { TabsOverflowMenu } from "./TabsOverflowMenu";
import { useCollectionOverflowObserver } from "./useCollectionOverflowObserver";
import { useOverflowObserver } from "../utils/overflow-utils/useOverflowObserver";
import { useIsScrolled } from "./useIsScrolled";
import { styled, css } from "@intellij-platform/core/styled";
import { css, styled } from "@intellij-platform/core/styled";
import { StyledDefaultTab } from "./StyledDefaultTab";
import { StyledDefaultTabs } from "./StyledDefaultTabs";
import { Tab } from "./Tab";
import { notNull } from "@intellij-platform/core/utils/array-utils";

export type TabsProps<T> = Omit<
AriaTabListProps<T>,
Expand Down Expand Up @@ -120,8 +121,14 @@ export const Tabs = <T extends object>({
const { tabListProps } = useTabList(props, state, ref);

const { scrolledIndicatorProps, isScrolled } = useIsScrolled({ ref });
const { overflowedKeys, intersectionObserver } =
useCollectionOverflowObserver(ref);
const { overflowedElements } = useOverflowObserver(ref);
const overflowedKeys = new Set(
overflowedElements
.map((element) =>
element instanceof HTMLElement ? element.dataset["key"] : null
)
.filter(notNull)
);

useEffect(() => {
if (!noScroll) {
Expand Down Expand Up @@ -162,7 +169,6 @@ export const Tabs = <T extends object>({
focusable={focusable}
active={active}
Component={TabComponent}
intersectionObserver={intersectionObserver}
/>
))}
</StyledTabList>
Expand Down
69 changes: 0 additions & 69 deletions packages/jui/src/Tabs/useCollectionOverflowObserver.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions packages/jui/src/ToolWindows/StyledToolWindowStripe.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
STRIPE_BUTTON_CROSS_PADDING,
STRIPE_BUTTON_LINE_HEIGHT,
StyledToolWindowStripeButton,
} from "./StyledToolWindowStripeButton";
import { Anchor, isHorizontalToolWindow, theOtherSide } from "./utils";
import { css } from "styled-components";
Expand All @@ -21,11 +22,17 @@ const anchorStyles = ({
flex-direction: row;
width: 100%;
min-height: ${preventCollapse ? minHeight : "fit-content"};
${StyledToolWindowStripeButton} {
height: 1.25rem;
}
`
: css`
flex-direction: column;
height: 100%;
min-width: ${preventCollapse ? minHeight : "fit-content"};
${StyledToolWindowStripeButton} {
width: 1.25rem;
}
`;
const borderStyle = ({ anchor, theme }: { anchor: Anchor; theme: Theme }) =>
css`border-${theOtherSide(anchor)}: 1px solid ${
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const FakeExecutionToolbar = ({
execution: Execution;
toggle: (executionId: string) => void;
}) => (
<Toolbar hasBorder>
<Toolbar border="bottom">
<IconButton onPress={() => toggle(id)}>
<PlatformIcon icon={isRunning ? "actions/restart" : "actions/execute"} />
</IconButton>
Expand Down
Loading

0 comments on commit b88d592

Please sign in to comment.