Skip to content

Commit

Permalink
Tabs: VR implementation changes (#3763)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertCarreras authored Sep 18, 2024
1 parent 8885faa commit 45ee62b
Show file tree
Hide file tree
Showing 9 changed files with 407 additions and 210 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,30 @@
"value": "{sema.color.pressed.background.default.value}"
}
}
},
"tabs": {
"default": {
"base": {
"value": "{sema.color.background.default.value}"
},
"hover": {
"value": "{sema.color.hover.background.default.value}"
},
"active": {
"value": "{sema.color.pressed.background.default.value}"
}
},
"transparent": {
"base": {
"value": "{sema.color.background.default.value}"
},
"hover": {
"value": "{sema.color.hover.background.default.value}"
},
"active": {
"value": "{sema.color.pressed.background.default.value}"
}
}
}
},
"border": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,30 @@
"value": "{sema.color.pressed.background.default.value}"
}
}
},
"tabs": {
"default": {
"base": {
"value": "{base.color.transparent.value}"
},
"hover": {
"value": "{sema.color.hover.background.default.value}"
},
"active": {
"value": "{sema.color.pressed.background.default.value}"
}
},
"transparent": {
"base": {
"value": "{base.color.transparent.value}"
},
"hover": {
"value": "{sema.color.hover.background.default.value}"
},
"active": {
"value": "{sema.color.pressed.background.default.value}"
}
}
}
},
"border": {
Expand Down
9 changes: 9 additions & 0 deletions packages/gestalt/src/Tabs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.paddingY {
padding-left: var(--space-300);
padding-right: var(--space-300);
}

.focused {
border-radius: var(--sema-rounding-400);
outline: 2px solid var(--sema-color-border-focus-outer-default);
}
228 changes: 34 additions & 194 deletions packages/gestalt/src/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,193 +1,7 @@
import { forwardRef, ReactNode, useState } from 'react';
import {
TOKEN_COLOR_BACKGROUND_TABS_DEFAULT_ACTIVE,
TOKEN_COLOR_BACKGROUND_TABS_DEFAULT_BASE,
TOKEN_COLOR_BACKGROUND_TABS_DEFAULT_HOVER,
TOKEN_COLOR_BACKGROUND_TABS_TRANSPARENT_ACTIVE,
TOKEN_COLOR_BACKGROUND_TABS_TRANSPARENT_BASE,
TOKEN_COLOR_BACKGROUND_TABS_TRANSPARENT_HOVER,
} from 'gestalt-design-tokens';
import Box from './Box';
import { ReactNode } from 'react';
import Flex from './Flex';
import TapAreaLink from './TapAreaLink';
import Text from './Text';

type OnChangeHandler = (arg1: {
event: React.MouseEvent<HTMLAnchorElement> | React.KeyboardEvent<HTMLAnchorElement>;
readonly activeTabIndex: number;
dangerouslyDisableOnNavigation: () => void;
}) => void;

function Dot() {
return (
<Box
color="brand"
dangerouslySetInlineStyle={{ __style: { marginTop: '1px' } }}
height={6}
rounding="circle"
width={6}
/>
);
}

const UNDERLINE_HEIGHT = 3;

function Underline() {
return (
<Box
color="selected"
dangerouslySetInlineStyle={{
__style: {
borderRadius: 1.5,
},
}}
height={UNDERLINE_HEIGHT}
width="100%"
/>
);
}

const COUNT_HEIGHT_PX = 16;

function Count({ count }: { count: number }) {
const displayCount = count < 100 ? `${count}` : '99+';

return (
<Box
color="brand"
dangerouslySetInlineStyle={{
__style: {
padding: `0 ${displayCount.length > 1 ? 3 : 0}px`,
},
}}
height={COUNT_HEIGHT_PX}
minWidth={COUNT_HEIGHT_PX}
rounding="pill"
>
<Box
dangerouslySetInlineStyle={{
__style: { padding: '0 0 1px 1px' },
}}
>
<Text align="center" color="light" size="100" weight="bold">
{displayCount}
</Text>
</Box>
</Box>
);
}

type TabType = {
href: string;
id?: string;
indicator?: 'dot' | number;
text: ReactNode;
};
type BgColor = 'default' | 'transparent';

type TabProps = TabType & {
bgColor: BgColor;
index: number;
isActive: boolean;
onChange: OnChangeHandler;
};

const TAB_ROUNDING = 2;
const TAB_INNER_PADDING = 2;

const COLORS = {
default: {
base: TOKEN_COLOR_BACKGROUND_TABS_DEFAULT_BASE,
active: TOKEN_COLOR_BACKGROUND_TABS_DEFAULT_ACTIVE,
hover: TOKEN_COLOR_BACKGROUND_TABS_DEFAULT_HOVER,
},
transparent: {
base: TOKEN_COLOR_BACKGROUND_TABS_TRANSPARENT_BASE,
active: TOKEN_COLOR_BACKGROUND_TABS_TRANSPARENT_ACTIVE,
hover: TOKEN_COLOR_BACKGROUND_TABS_TRANSPARENT_HOVER,
},
} as const;

const TabWithForwardRef = forwardRef<HTMLElement, TabProps>(function Tab(
{ bgColor, href, indicator, id, index, isActive, onChange, text }: TabProps,
ref,
) {
const [hovered, setHovered] = useState(false);
const [focused, setFocused] = useState(false);
const [pressed, setPressed] = useState(false);

let color = COLORS[bgColor].base;
if (!isActive) {
if (pressed) {
// @ts-expect-error - TS2322 - Type '"var(--color-background-tabs-default-active)" | "var(--color-background-tabs-transparent-active)"' is not assignable to type '"var(--color-background-tabs-default-base)" | "var(--color-background-tabs-transparent-base)"'.
color = COLORS[bgColor].active;
} else if (hovered || focused) {
// @ts-expect-error - TS2322 - Type '"var(--color-background-tabs-default-hover)" | "var(--color-background-tabs-transparent-hover)"' is not assignable to type '"var(--color-background-tabs-default-base)" | "var(--color-background-tabs-transparent-base)"'.
color = COLORS[bgColor].hover;
}
}

return (
<Box ref={ref} id={id} paddingY={3}>
<TapAreaLink
accessibilityCurrent={isActive ? 'page' : undefined}
href={href}
onBlur={() => setFocused(false)}
onFocus={() => setFocused(true)}
onMouseDown={() => setPressed(true)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onMouseUp={() => setPressed(false)}
onTap={({ event, dangerouslyDisableOnNavigation }) => {
onChange({
activeTabIndex: index,
event,
dangerouslyDisableOnNavigation,
});
}}
rounding={TAB_ROUNDING}
tapStyle={isActive ? 'none' : 'compress'}
>
<Flex alignItems="center" direction="column">
<Box
dangerouslySetInlineStyle={{ __style: { backgroundColor: color } }}
padding={TAB_INNER_PADDING}
position="relative"
rounding={TAB_ROUNDING}
userSelect="none"
>
<Flex alignItems="center" gap={{ row: 2, column: 0 }} justifyContent="center">
<Text color="default" overflow="noWrap" weight="bold">
{text}
</Text>

{indicator === 'dot' && <Dot />}
{/* Flow is dumb and doesn't realize Number.isFinite will return false for a string or undefined */}
{typeof indicator === 'number' && Number.isFinite(indicator) && (
<Count count={indicator} />
)}
</Flex>

{isActive && (
<Box
dangerouslySetInlineStyle={{
__style: { bottom: -UNDERLINE_HEIGHT },
}}
position="absolute"
// 4px/boint, padding on left and right
width={`calc(100% - ${TAB_INNER_PADDING * 4 * 2}px)`}
>
<Underline />
</Box>
)}
</Box>
</Flex>
</TapAreaLink>
</Box>
);
});

TabWithForwardRef.displayName = 'Tab';
import Tab from './Tabs/Tab';
import useInExperiment from './useInExperiment';

type Props = {
/**
Expand All @@ -197,11 +11,19 @@ type Props = {
/**
* If Tabs is displayed in a container with a colored background, use this prop to remove the white tab background. See the [background color example](https://gestalt.pinterest.systems/web/tabs#Background-color) to learn more.
*/
bgColor?: BgColor;
bgColor?: 'default' | 'transparent';
/**
* Available for testing purposes, if needed. Consider [better queries](https://testing-library.com/docs/queries/about/#priority) before using this prop.
*/
dataTestId?: string;
/**
* If your app requires client navigation, be sure to use [GlobalEventsHandlerProvider](https://gestalt.pinterest.systems/web/utilities/globaleventshandlerprovider#Link-handlers) and/or `onChange` to navigate instead of getting a full page refresh just using `href`.
*/
onChange: OnChangeHandler;
onChange: (arg1: {
event: React.MouseEvent<HTMLAnchorElement> | React.KeyboardEvent<HTMLAnchorElement>;
readonly activeTabIndex: number;
dangerouslyDisableOnNavigation: () => void;
}) => void;
/**
* The array of tabs to be displayed. The active tab (as indicated by `activeTabIndex`) will be underlined. Use the optional `indicator` field to show a notification of new items on the tab — see the [indicator variant](https://gestalt.pinterest.systems/web/tabs#Indicator) to learn more. Though `text` currently accepts a React.Node, this is deprecated and will be replaced by a simple `string` type soon.
*/
Expand All @@ -227,15 +49,33 @@ type Props = {
* ![Tabs dark mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/Tabs-dark.spec.ts-snapshots/Tabs-dark-chromium-darwin.png)
*
*/
export default function Tabs({ activeTabIndex, bgColor = 'default', onChange, tabs, wrap }: Props) {
export default function Tabs({
activeTabIndex,
bgColor = 'default',
onChange,
tabs,
wrap,
dataTestId,
}: Props) {
const isInVRExperiment = useInExperiment({
webExperimentName: 'web_gestalt_visualRefresh',
mwebExperimentName: 'web_gestalt_visualRefresh',
});

return (
<Flex alignItems="center" gap={{ row: 4, column: 0 }} justifyContent="start" wrap={wrap}>
<Flex
alignItems="center"
gap={isInVRExperiment ? 1 : { row: 4, column: 0 }}
justifyContent="start"
wrap={wrap}
>
{tabs.map(({ href, id, indicator, ref, text }, index) => (
<TabWithForwardRef
<Tab
key={id || `${href}_${index}`}
// @ts-expect-error - TS2322 - Type '{ current: HTMLElement | null | undefined; } | undefined' is not assignable to type 'LegacyRef<HTMLElement> | undefined'.
ref={ref}
bgColor={bgColor}
dataTestId={dataTestId}
href={href}
id={id}
index={index}
Expand Down
Loading

0 comments on commit 45ee62b

Please sign in to comment.