Skip to content
This repository has been archived by the owner on Jan 20, 2022. It is now read-only.

AR-2148 Fix repeated rendering of global styles for tooltip-like components #330

Merged
merged 3 commits into from
Mar 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 37 additions & 29 deletions src/AbstractTooltip/abstractTooltip/TippyStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,46 @@ import React from "react";
import { base } from "../../typography";
import { colors } from "../../colors";
import { Global, css } from "@emotion/core";
import { SingletonComponent } from "../../shared/components/SingletonComponent";

export const TippyStyles: React.FC = () => (
<Global
styles={css({
".tippy-box": {
"&[data-theme=space-kit]": {
...base.small,
backgroundColor: colors.black.base,
opacity: 0.95,
<SingletonComponent identity="src/AbstractTooltip/abstractTooltip/TippyStyles.tsx">
{React.useMemo(
() => (
<Global
styles={css({
".tippy-box": {
"&[data-theme=space-kit]": {
...base.small,
backgroundColor: colors.black.base,
opacity: 0.95,

'&[data-placement^="top"] > .tippy-arrow': {
borderTopColor: colors.black.base,
},
'&[data-placement^="bottom"] > .tippy-arrow': {
borderBottomColor: colors.black.base,
},
'&[data-placement^="right"] > .tippy-arrow': {
borderRightColor: colors.black.base,
},
'&[data-placement^="left"] > .tippy-arrow': {
borderLeftColor: colors.black.base,
},
'&[data-placement^="top"] > .tippy-arrow': {
borderTopColor: colors.black.base,
},
'&[data-placement^="bottom"] > .tippy-arrow': {
borderBottomColor: colors.black.base,
},
'&[data-placement^="right"] > .tippy-arrow': {
borderRightColor: colors.black.base,
},
'&[data-placement^="left"] > .tippy-arrow': {
borderLeftColor: colors.black.base,
},

".tippy-content": {
padding: "4px 8px",
},
".tippy-content": {
padding: "4px 8px",
},

"&.space-kit-relaxed .tippy-content": {
padding: "8px 12px",
},
},
},
})}
/>
"&.space-kit-relaxed .tippy-content": {
padding: "8px 12px",
},
},
},
})}
/>
),
[],
)}
</SingletonComponent>
);
50 changes: 29 additions & 21 deletions src/Popover/popover/TippyPopoverStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,36 @@ import React from "react";
import { base } from "../../typography";
import { colors } from "../../colors";
import { Global, css } from "@emotion/core";
import { SingletonComponent } from "../../shared/components/SingletonComponent";

export const TippyPopoverStyles: React.FC = () => (
<Global
styles={css({
"*[data-tippy-root]": {},
".tippy-box": {
"&[data-theme=space-kit-list]": {
...base.small,
boxShadow:
"0 3px 4px 0 rgba(18, 21, 26, 0.04), 0 4px 8px 0 rgba(18, 21, 26, 0.08), 0 0 0 1px rgba(18, 21, 26, 0.08)",
backgroundColor: colors.white,
borderRadius: 4,
color: colors.black.base,
padding: 6,
margin: 2,
minWidth: 190,
<SingletonComponent identity="src/Popover/popover/TippyPopoverStyles.tsx">
{React.useMemo(
() => (
<Global
styles={css({
"*[data-tippy-root]": {},
".tippy-box": {
"&[data-theme=space-kit-list]": {
...base.small,
boxShadow:
"0 3px 4px 0 rgba(18, 21, 26, 0.04), 0 4px 8px 0 rgba(18, 21, 26, 0.08), 0 0 0 1px rgba(18, 21, 26, 0.08)",
backgroundColor: colors.white,
borderRadius: 4,
color: colors.black.base,
padding: 6,
margin: 2,
minWidth: 190,

".tippy-content": {
padding: "0",
},
},
},
})}
/>
".tippy-content": {
padding: "0",
},
},
},
})}
/>
),
[],
)}
</SingletonComponent>
);
4 changes: 2 additions & 2 deletions src/Select/SelectTests.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ storiesOf("Tests/Select", module)
</div>
),
{
chromatic: { delay: 200 },
chromatic: { delay: 2000 },
},
)
.add(
Expand Down Expand Up @@ -82,6 +82,6 @@ storiesOf("Tests/Select", module)
</div>
),
{
chromatic: { delay: 200 },
chromatic: { delay: 2000 },
},
);
106 changes: 106 additions & 0 deletions src/SpaceKitProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ interface State {
*/
disableAnimations: boolean;

singletonComponents: Record<
string,
{
element: ReturnType<React.FC>;
instanceCount: React.MutableRefObject<number>;
}
>;

theme: "light" | "dark";
}

const defaultState: State = {
disableAnimations: false,
singletonComponents: {},
theme: "light",
};

Expand Down Expand Up @@ -48,12 +57,25 @@ export const SpaceKitProvider: React.FC<Partial<State>> = ({
return (
<SpaceKitStateContext.Provider value={state}>
<SpaceKitSetContext.Provider value={setState}>
{Object.entries(state?.singletonComponents ?? {}).map(
([identity, { element }]) => {
return <React.Fragment key={identity}>{element}</React.Fragment>;
},
)}
{children}
</SpaceKitSetContext.Provider>
</SpaceKitStateContext.Provider>
);
};

/**
* Hook to indicate if the current component is being rendered inside of a
* `SpaceKitProvider`
*/
export function useHasSpaceKitProvider(): boolean {
return !!React.useContext(SpaceKitStateContext);
}

/**
* Hook to get the values from the Space Kit Provider with sensible defaults for
* all the values.
Expand All @@ -71,3 +93,87 @@ export function useSpaceKitProvider(): Readonly<State> {

return context;
}

/**
* Hook intended to be used internally to communicate with `SpaceKitProvider`
* indicating singleton components being mounted and unmounted.
*
* Use `show` to track when you show a component.
*
* Use `hide` to track when you remove a component.
*/
export function useSingletonComponent(): {
hide: ({ identity }: { identity: string }) => void;
show: ({
identity,
element,
}: {
identity: string;
element: ReturnType<React.FC>;
}) => void;
} {
const setSpaceKitContext = React.useContext(SpaceKitSetContext);

const hide = React.useCallback(
({ identity }: { identity: string }) => {
setSpaceKitContext?.((state = defaultState) => {
if (!state.singletonComponents[identity]) {
// This should never happen; we should never be trying to decrement
// something that isn't rendered.
return state;
}

if (state.singletonComponents[identity].instanceCount.current === 1) {
// This is the last instance; delete it from the component and return
// a new state to trigger a re-render.
const singletonComponentsCopy = { ...state.singletonComponents };
delete singletonComponentsCopy[identity];

return {
...state,
singletonComponents: singletonComponentsCopy,
};
}

// Decrement the instance count and return the original `state` so this
// won't trigger a re-render.
state.singletonComponents[identity].instanceCount.current -= 1;
return state;
});
},
[setSpaceKitContext],
);

const show = React.useCallback(
({
identity,
element,
}: {
identity: string;
element: ReturnType<React.FC>;
}) => {
setSpaceKitContext?.((previousState = defaultState) => {
if (!previousState.singletonComponents[identity]) {
// This is the first time ths identity is being rendered. Create the
// entry in `singletonComponents` and modify `state` to trigger a
// re-render.
return {
...previousState,
singletonComponents: {
...previousState.singletonComponents,
[identity]: { element, instanceCount: { current: 1 } },
},
};
}

// This identity exists. Increment the instanceCount and return the
// original state to avoid a re-render.
previousState.singletonComponents[identity].instanceCount.current += 1;
return previousState;
});
},
[setSpaceKitContext],
);

return { hide, show };
}
Loading