Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/lovely-flies-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ultraviolet/ui": minor
---

Refactor component `Tabs` to use vanilla extract instead of Emotion
123 changes: 23 additions & 100 deletions packages/ui/src/components/Tabs/Tab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'

import styled from '@emotion/styled'
import type {
ComponentProps,
ElementType,
Expand All @@ -15,96 +14,14 @@ import { Badge } from '../Badge'
import { Stack } from '../Stack'
import { Text } from '../Text'
import { Tooltip } from '../Tooltip'
import {
tabsBadge,
tabsBadgeContainer,
tabsButton,
tabsTextSelected,
} from './styles.css'
import { useTabsContext } from './TabsContext'

const StyledBadge = styled(Badge)`
padding: 0 ${({ theme }) => theme.space['1']};
margin-left: ${({ theme }) => theme.space['1']};
`

const StyledText = styled(Text)``

const StyledTooltip = styled(Tooltip)``

const BadgeContainer = styled.span`
margin-left: ${({ theme }) => theme.space['1']};
display: flex;
`

export const StyledTabButton = styled('button')`
display: flex;
flex-direction: row;
padding: ${({ theme }) => `${theme.space['1']} ${theme.space['2']}`};
cursor: pointer;
justify-content: center;
align-items: baseline;
white-space: nowrap;
color: ${({ theme }) => theme.colors.neutral.text};
text-decoration: none;
user-select: none;
touch-action: manipulation;
transition: color 0.2s;
border: none;
background: none;
border-bottom-width: 2px;
border-bottom-style: solid;
border-bottom-color: ${({ theme }) => theme.colors.neutral.border};
outline: none;

font-size: ${({ theme }) => theme.typography.bodyStrong.fontSize};
font-family: ${({ theme }) => theme.typography.bodyStrong.fontFamily};
font-weight: ${({ theme }) => theme.typography.bodyStrong.weight};
letter-spacing: ${({ theme }) => theme.typography.bodyStrong.letterSpacing};
line-height: ${({ theme }) => theme.typography.bodyStrong.lineHeight};

&:hover,
&:active,
&:focus {
text-decoration: none;
outline: none;
}

&:focus-visible {
outline: auto;
}

&[aria-selected='true'] {
color: ${({ theme }) => theme.colors.primary.text};
border-bottom-color: ${({ theme }) => theme.colors.primary.border};

${StyledText} {
color: ${({ theme }) => theme.colors.primary.text};
}
}

&[aria-disabled='false']:not(:disabled) {
&:hover,
&:focus,
&:active {
outline: none;
color: ${({ theme }) => theme.colors.primary.text};
border-bottom-color: ${({ theme }) => theme.colors.primary.border};

&[data-is-selected='false'] {
${StyledBadge} {
background-color: ${({ theme }) => theme.colors.primary.background};
border-color: ${({ theme }) => theme.colors.primary.background};
color: ${({ theme }) => theme.colors.primary.text};
}
${StyledText} {
color: ${({ theme }) => theme.colors.primary.text};
}
}
}
}

&[aria-disabled='true'],
&:disabled {
cursor: not-allowed;
filter: grayscale(1) opacity(50%);
}
`

type TabProps<T extends ElementType = 'button'> = {
as?: T
badge?: ReactNode
Expand Down Expand Up @@ -150,19 +67,19 @@ export const Tab = forwardRef(
) => {
const { selected, onChange } = useTabsContext()
const computedAs = as ?? 'button'
const ComputedComponent = as ?? 'button'
const isSelected = useMemo(
() => value !== undefined && selected === value,
[value, selected],
)

return (
<StyledTooltip text={tooltip}>
<StyledTabButton
<Tooltip text={tooltip}>
<ComputedComponent
aria-disabled={disabled}
aria-label={value ? `${value}` : undefined}
aria-selected={isSelected}
as={computedAs}
className={className}
className={`${className ? `${className} ` : ''}${tabsButton}`}
data-is-selected={isSelected}
disabled={computedAs === 'button' ? disabled : undefined}
onClick={event => {
Expand All @@ -186,31 +103,37 @@ export const Tab = forwardRef(
<Stack alignItems="center" direction="row">
{children}
{typeof counter === 'number' || typeof counter === 'string' ? (
<StyledBadge
<Badge
className={tabsBadge}
prominence={isSelected ? 'strong' : 'default'}
sentiment={isSelected ? 'primary' : 'neutral'}
size="medium"
>
{counter}
</StyledBadge>
</Badge>
) : null}
{badge ? (
<span className={tabsBadgeContainer}>{badge}</span>
) : null}
{badge ? <BadgeContainer>{badge}</BadgeContainer> : null}
</Stack>
{subtitle ? (
<Stack direction="row">
<StyledText
<Text
as="span"
className={
tabsTextSelected[isSelected ? 'selected' : 'default']
}
prominence="weak"
sentiment="neutral"
variant="bodySmall"
>
{subtitle}
</StyledText>
</Text>
</Stack>
) : null}
</Stack>
</StyledTabButton>
</StyledTooltip>
</ComputedComponent>
</Tooltip>
)
},
)
37 changes: 7 additions & 30 deletions packages/ui/src/components/Tabs/TabMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'

import styled from '@emotion/styled'
import { ArrowDownIcon } from '@ultraviolet/icons'
import type {
ButtonHTMLAttributes,
Expand All @@ -10,29 +9,7 @@ import type {
} from 'react'
import { forwardRef } from 'react'
import { Menu } from '../Menu'
import { StyledTabButton } from './Tab'

const ArrowIcon = styled(ArrowDownIcon)``
const StyledMenu = styled(StyledTabButton)`
${ArrowIcon} {
color: inherit;
margin-left: ${({ theme }) => theme.space['1']};
transition: 300ms transform ease-out;
}

&[aria-expanded='true'] ${ArrowIcon} {
transform: rotate(-180deg);
}
`

// This will wrap and give the positioning to the popup div that is added onto the disclosure
const StyledPositioningWrapper = styled.div`
display: flex;
position: sticky;
top: 0;
bottom: 0;
right: 0;
`
import { tabsArrowIcon, tabsButton, tabsMenuWrapper } from './styles.css'

type TabMenuProps = {
children: ReactNode
Expand All @@ -55,22 +32,22 @@ export const TabMenu = forwardRef(
}: TabMenuProps,
ref: Ref<HTMLButtonElement>,
) => (
<StyledPositioningWrapper>
<div className={tabsMenuWrapper}>
<Menu
disclosure={
<StyledMenu
<button
aria-disabled={disabled ?? 'false'}
aria-haspopup="menu"
aria-selected={ariaSelected}
className={className}
className={`${className ? `${className} ` : ''}${tabsButton}`}
disabled={disabled}
role="tab"
type="button"
{...props}
>
{disclosure}
<ArrowIcon />
</StyledMenu>
<ArrowDownIcon className={tabsArrowIcon} />
</button>
}
id={id}
portalTarget={document.body}
Expand All @@ -79,6 +56,6 @@ export const TabMenu = forwardRef(
>
{children}
</Menu>
</StyledPositioningWrapper>
</div>
),
)
15 changes: 5 additions & 10 deletions packages/ui/src/components/Tabs/TabMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
'use client'

import styled from '@emotion/styled'
import type { ComponentProps } from 'react'
import { useMemo } from 'react'
import { Menu } from '../Menu'
import { tabsTextSelected } from './styles.css'
import { useTabsContext } from './TabsContext'

const StyledMenuItem = styled(Menu.Item)`
&[aria-selected='true'] {
color: ${({ theme }) => theme.colors.primary.text};
}
`

type TabMenuItemProps = {
value?: string | number
} & ComponentProps<typeof StyledMenuItem>
} & ComponentProps<typeof Menu.Item>

export const TabMenuItem = ({
value,
Expand All @@ -30,8 +24,9 @@ export const TabMenuItem = ({
)

return (
<StyledMenuItem
<Menu.Item
aria-selected={isSelected}
className={tabsTextSelected[isSelected ? 'selected' : 'default']}
onClick={event => {
if (value !== undefined) {
onChange(value)
Expand All @@ -41,6 +36,6 @@ export const TabMenuItem = ({
{...props}
>
{children}
</StyledMenuItem>
</Menu.Item>
)
}
Loading
Loading