Skip to content
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
11 changes: 10 additions & 1 deletion packages/tabs/src/elements/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ export const Tab = React.forwardRef<HTMLDivElement, ITabProps>(
const tabsPropGetters = useTabsContext();

if (disabled || !tabsPropGetters) {
return <StyledTab role="tab" aria-disabled={disabled} ref={ref} {...otherProps} />;
return (
<StyledTab
role="tab"
aria-disabled={disabled}
ref={ref}
isVertical={tabsPropGetters?.isVertical}
{...otherProps}
/>
);
}

const { ref: tabRef, ...tabProps } = tabsPropGetters.getTabProps<HTMLDivElement>({
Expand All @@ -30,6 +38,7 @@ export const Tab = React.forwardRef<HTMLDivElement, ITabProps>(
return (
<StyledTab
isSelected={item === tabsPropGetters.selectedValue}
isVertical={tabsPropGetters.isVertical}
{...tabProps}
{...otherProps}
ref={mergeRefs([tabRef, ref])}
Expand Down
9 changes: 8 additions & 1 deletion packages/tabs/src/elements/TabList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ export const TabList = React.forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEl
const tabListProps =
tabsPropGetters.getTabListProps<HTMLDivElement>() as HTMLAttributes<HTMLDivElement>;

return <StyledTabList {...tabListProps} {...props} ref={ref} />;
return (
<StyledTabList
isVertical={tabsPropGetters.isVertical}
{...tabListProps}
{...props}
ref={ref}
/>
);
}
);

Expand Down
1 change: 1 addition & 0 deletions packages/tabs/src/elements/TabPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const TabPanel = React.forwardRef<HTMLDivElement, ITabPanelProps>(
return (
<StyledTabPanel
aria-hidden={tabsPropGetters.selectedValue !== item}
isVertical={tabsPropGetters.isVertical}
{...tabPanelProps}
{...otherProps}
ref={ref}
Expand Down
7 changes: 6 additions & 1 deletion packages/tabs/src/elements/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,13 @@ export const Tabs = forwardRef<HTMLDivElement, ITabsProps>(
}
});

const contextValue = useMemo(
() => ({ isVertical, ...tabsContextValue }),
[isVertical, tabsContextValue]
);

return (
<TabsContext.Provider value={tabsContextValue}>
<TabsContext.Provider value={contextValue}>
<StyledTabs isVertical={isVertical} {...otherProps} ref={ref}>
{children}
</StyledTabs>
Expand Down
83 changes: 58 additions & 25 deletions packages/tabs/src/styled/StyledTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@ const COMPONENT_ID = 'tabs.tab';

interface IStyledTabProps {
isSelected?: boolean;
isVertical?: boolean;
}

/**
* 1. A high specificity is needed to apply the border-color in vertical orientations
*/
const colorStyles = ({ theme, isSelected }: IStyledTabProps & ThemeProps<DefaultTheme>) => {
const colorStyles = ({
theme,
isSelected,
isVertical
}: IStyledTabProps & ThemeProps<DefaultTheme>) => {
const borderColor = isSelected ? 'currentcolor' : 'transparent';
const selectedColor = getColorV8('primaryHue', 600, theme);

return css`
border-color: ${isSelected && 'currentcolor !important'}; /* [1] */
border-bottom-color: ${isVertical ? undefined : borderColor};
border-${theme.rtl ? 'right' : 'left'}-color: ${isVertical ? borderColor : undefined};
color: ${isSelected ? selectedColor : 'inherit'};

&:hover {
Expand Down Expand Up @@ -55,43 +59,73 @@ const colorStyles = ({ theme, isSelected }: IStyledTabProps & ThemeProps<Default
`;
};

const sizeStyles = ({ theme }: ThemeProps<DefaultTheme>) => {
const paddingTop = theme.space.base * 2.5;
const paddingHorizontal = theme.space.base * 7;
const paddingBottom =
paddingTop -
(stripUnit(theme.borderWidths.md) as number) -
(stripUnit(theme.borderWidths.sm) as number);
const sizeStyles = ({ theme, isVertical }: IStyledTabProps & ThemeProps<DefaultTheme>) => {
const borderWidth = theme.borderWidths.md;
const focusHeight = `${theme.space.base * 5}px`;
let marginBottom;
let padding;

if (isVertical) {
marginBottom = `${theme.space.base * 5}px`;
padding = `${theme.space.base}px ${theme.space.base * 2}px`;
} else {
const paddingTop = theme.space.base * 2.5;
const paddingHorizontal = theme.space.base * 7;
const paddingBottom =
paddingTop -
(stripUnit(theme.borderWidths.md) as number) -
(stripUnit(theme.borderWidths.sm) as number);

padding = `${paddingTop}px ${paddingHorizontal}px ${paddingBottom}px`;
}

return css`
padding: ${paddingTop}px ${paddingHorizontal}px ${paddingBottom}px;
margin-bottom: ${marginBottom};
border-width: ${borderWidth};
padding: ${padding};

&:focus-visible::before,
&[data-garden-focus-visible]::before {
height: ${focusHeight};
}

&:last-of-type {
margin-bottom: 0;
}
`;
};

/**
/*
* 1. Text truncation (requires `max-width`).
* 2. Overflow compensation.
* 3. Override default anchor styling
*/
export const StyledTab = styled.div.attrs<IStyledTabProps>({
export const StyledTab = styled.div.attrs({
'data-garden-id': COMPONENT_ID,
'data-garden-version': PACKAGE_VERSION
})<IStyledTabProps>`
display: inline-block;
display: ${props => (props.isVertical ? 'block' : 'inline-block')};
position: relative;
transition: color 0.25s ease-in-out;
border-bottom: ${props => props.theme.borderStyles.solid} transparent;
border-width: ${props => props.theme.borderWidths.md};
border-bottom: ${props => (props.isVertical ? undefined : props.theme.borderStyles.solid)};
border-${props => (props.theme.rtl ? 'right' : 'left')}: ${props => (props.isVertical ? props.theme.borderStyles.solid : undefined)};
cursor: pointer;
overflow: hidden; /* [1] */
vertical-align: top; /* [2] */
user-select: none;
text-align: center;
text-align: ${props => {
if (props.isVertical) {
return props.theme.rtl ? 'right' : 'left';
}

return 'center';
}};
text-decoration: none; /* [3] */
text-overflow: ellipsis; /* [1] */

${sizeStyles}
${colorStyles}
${sizeStyles};

${colorStyles};

&:focus {
text-decoration: none;
Expand All @@ -105,11 +139,10 @@ export const StyledTab = styled.div.attrs<IStyledTabProps>({
&:focus-visible::before,
&[data-garden-focus-visible]::before {
position: absolute;
top: ${props => props.theme.space.base * 2.5}px;
right: ${props => props.theme.space.base * 6}px;
left: ${props => props.theme.space.base * 6}px;
top: ${props => props.theme.space.base * (props.isVertical ? 1 : 2.5)}px;
right: ${props => props.theme.space.base * (props.isVertical ? 1 : 6)}px;
left: ${props => props.theme.space.base * (props.isVertical ? 1 : 6)}px;
border-radius: ${props => props.theme.borderRadii.md};
height: ${props => props.theme.space.base * 5}px;
pointer-events: none;
}

Expand Down
59 changes: 46 additions & 13 deletions packages/tabs/src/styled/StyledTabList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,61 @@
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import styled from 'styled-components';
import { retrieveComponentStyles, getColorV8, DEFAULT_THEME } from '@zendeskgarden/react-theming';
import styled, { DefaultTheme, ThemeProps, css } from 'styled-components';
import {
retrieveComponentStyles,
getColorV8,
DEFAULT_THEME,
getLineHeight
} from '@zendeskgarden/react-theming';

const COMPONENT_ID = 'tabs.tablist';

/**
interface IStyledTabListProps {
isVertical?: boolean;
}

const colorStyles = ({ theme }: ThemeProps<DefaultTheme>) => {
const borderColor = getColorV8('neutralHue', 300, theme);
const foregroundColor = getColorV8('neutralHue', 600, theme);

return css`
border-bottom-color: ${borderColor};
color: ${foregroundColor};
`;
};

/*
* 1. List element reset.
*/
const sizeStyles = ({ theme, isVertical }: IStyledTabListProps & ThemeProps<DefaultTheme>) => {
const marginBottom = isVertical ? 0 : `${theme.space.base * 5}px`;
const borderBottom = isVertical ? undefined : theme.borderWidths.sm;
const fontSize = theme.fontSizes.md;
const lineHeight = getLineHeight(theme.space.base * 5, fontSize);

return css`
margin-top: 0; /* [1] */
margin-bottom: ${marginBottom};
border-bottom-width: ${borderBottom};
padding: 0; /* [1] */
line-height: ${lineHeight};
font-size: ${fontSize};
`;
};

export const StyledTabList = styled.div.attrs({
'data-garden-id': COMPONENT_ID,
'data-garden-version': PACKAGE_VERSION
})`
display: block;
margin-top: 0; /* [1] */
margin-bottom: ${props => props.theme.space.base * 5}px;
border-bottom: ${props => props.theme.borderWidths.sm} ${props => props.theme.borderStyles.solid}
${props => getColorV8('neutralHue', 300, props.theme)};
padding: 0; /* [1] */
line-height: ${props => props.theme.space.base * 5}px;
})<IStyledTabListProps>`
display: ${props => (props.isVertical ? 'table-cell' : 'block')};
border-bottom: ${props => (props.isVertical ? 'none' : props.theme.borderStyles.solid)};
vertical-align: ${props => (props.isVertical ? 'top' : undefined)};
white-space: nowrap;
color: ${props => getColorV8('neutralHue', 600, props.theme)};
font-size: ${props => props.theme.fontSizes.md};

${sizeStyles};

${colorStyles};

${props => retrieveComponentStyles(COMPONENT_ID, props)};
`;
Expand Down
22 changes: 17 additions & 5 deletions packages/tabs/src/styled/StyledTabPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,31 @@
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import styled from 'styled-components';
import styled, { DefaultTheme, ThemeProps, css } from 'styled-components';
import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming';

const COMPONENT_ID = 'tabs.tabpanel';

/**
* Accepts all `<div>` props
*/
interface IStyledTabPanelProps {
isVertical?: boolean;
}

const sizeStyles = ({ theme, isVertical }: IStyledTabPanelProps & ThemeProps<DefaultTheme>) => {
const margin = isVertical ? `${theme.space.base * 8}px` : undefined;

return css`
margin-${theme.rtl ? 'right' : 'left'}: ${margin};
`;
};

export const StyledTabPanel = styled.div.attrs({
'data-garden-id': COMPONENT_ID,
'data-garden-version': PACKAGE_VERSION
})`
})<IStyledTabPanelProps>`
display: block;
vertical-align: ${props => props.isVertical && 'top'};

${sizeStyles};

&[aria-hidden='true'] {
display: none;
Expand Down
56 changes: 2 additions & 54 deletions packages/tabs/src/styled/StyledTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,78 +5,26 @@
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import styled, { css, ThemeProps, DefaultTheme } from 'styled-components';
import styled from 'styled-components';
import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming';
import { StyledTab } from './StyledTab';
import { StyledTabPanel } from './StyledTabPanel';
import { StyledTabList } from './StyledTabList';

const COMPONENT_ID = 'tabs.tabs';

interface IStyledTabsProps {
/**
* Displays vertical TabList styling
*/
isVertical?: boolean;
}

const verticalStyling = ({ theme }: ThemeProps<DefaultTheme>) => {
return css`
display: table;
${StyledTabList} {
display: table-cell;
margin-bottom: 0;
border-bottom: none;
vertical-align: top;
}
${StyledTab} {
display: block;
margin-bottom: ${theme.space.base * 5}px;
margin-left: ${theme.rtl && '0'};
border-left: ${theme.rtl && '0'};
border-bottom-style: none;
/* stylelint-disable property-case, property-no-unknown */
border-${theme.rtl ? 'right' : 'left'}-style: ${theme.borderStyles.solid};
border-${theme.rtl ? 'right' : 'left'}-color: transparent;
/* stylelint-enable property-case, property-no-unknown */
padding: ${theme.space.base}px ${theme.space.base * 2}px;
text-align: ${theme.rtl ? 'right' : 'left'};
&:last-of-type {
margin-bottom: 0;
}
&:focus-visible::before,
&[data-garden-focus-visible]::before {
top: ${theme.space.base}px;
right: ${theme.space.base}px;
left: ${theme.space.base}px;
}
}
${StyledTabPanel} {
/* stylelint-disable-next-line property-no-unknown */
margin-${theme.rtl ? 'right' : 'left'}: ${theme.space.base * 8}px;
vertical-align: top;
}
`;
};

/**
* Accepts all `<div>` props
*/
export const StyledTabs = styled.div.attrs<IStyledTabsProps>({
'data-garden-id': COMPONENT_ID,
'data-garden-version': PACKAGE_VERSION
})<IStyledTabsProps>`
display: block;
display: ${props => (props.isVertical ? 'table' : 'block')};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondeirng if using flex might be preferable for handling flow. πŸ€” display: table can impact the element's role for some screen readers (source). Granted, it's a small subset of SRs, so maybe not a huge deal?

Copy link
Contributor

@geotrev geotrev Jun 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also confirmed using a fake table demo in the link above that Chrome + JAWS treats CSS tables as actual tables (so long as they have more than one cell). 😬

Chrome + JAWS is kinda weird with CSS tables. maybe it's not a huge deal for us. The container isn't announced as a table so maybe it's OK.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@geotrev all valid concerns for sure. For this PR I want to retain exact parity with the previous styling, since that's already been through a11y audit.

trying to use arrow keys backward through tabs requires one extra arrow key press to loop from the first to last tab

Looks like that might be related to the inclusion of a disabled tab in the Storybook example. Again, let's plan to address under a separate effort.

overflow: hidden;
direction: ${props => props.theme.rtl && 'rtl'};
${props => props.isVertical && verticalStyling(props)};
${props => retrieveComponentStyles(COMPONENT_ID, props)};
`;

Expand Down
Loading