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
5 changes: 0 additions & 5 deletions src/components/select/select-multi.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
// Lots of rules disabled because we're using DS code that is plain JS, not TS
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Multiselect } from '@cfpb/cfpb-design-system/src/components/cfpb-forms';
import { JSX, useEffect, useRef, useState } from 'react';
import { noOp } from '../../utils/no-op';
Expand Down
48 changes: 39 additions & 9 deletions src/components/tabs/tab.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,64 @@

.tablist {
display: flex;
border-bottom: 1px solid var(--gray-40);

// margin-bottom: -1px;
// position: relative;
// z-index: 10;
// Inset shadow sits in the background layer so an opaque active tab can cover
// it. A real `border-bottom` paints above overlapping children and leaves a
// hairline under the active tab (especially with subpixel heights / retina).
box-shadow: inset 0 -1px 0 0 var(--gray-40);

button.tab {
@include heading-4($has-margin-bottom: false);
position: relative;
z-index: 1;
padding: math.div(math.div($grid-gutter-width, 3), $base-font-size-px) + rem
math.div($grid-gutter-width, $base-font-size-px) + rem;
margin-bottom: -1px;
border: 1px solid transparent;
// `heading-4` still sets margin-bottom at the xs breakpoint even when
// `$has-margin-bottom` is false. That gap leaves the tablist rule hanging
// below the tab fills (especially obvious on phone widths).
margin-bottom: 0;

@include respond-to-max($bp-xs-max) {
margin-bottom: 0;
}

&:focus:not(:focus-visible) {
outline: none;
}

// Keep the focus ring outside the tab border so overlapping neighbor
// borders (same issue as footer links at mobile width) don’t clip it.
&:focus-visible {
outline-offset: -1px;
outline-offset: 2px;
}

&--active {
color: var(--black);
background: var(--gray-5);
// Beat `.a-btn--link { background-color: transparent !important }` so the
// active fill can cover the tablist rule.
background-color: var(--gray-5) !important;
background-image: none !important;
text-decoration: none;
pointer-events: none;
border-color: var(--gray-40);
border-bottom-color: transparent;
border-bottom-color: var(--gray-5);
}
}

// Like default tabs, but with no inactive fills. Active tab uses an opaque
// page-colored fill so the tablist rule cannot show through underneath.
// Use background-color + !important so DS `.a-btn` / default `.tab--active`
// fills cannot win on equal specificity in app bundles.
&--inverted {
button.tab {
background-color: transparent !important;
background-image: none !important;

&--active {
background-color: var(--white) !important;
background-image: none !important;
border-bottom-color: var(--white);
}
}
}
}
Expand Down
85 changes: 46 additions & 39 deletions src/components/tabs/tab.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,52 @@ export default meta;

type Story = StoryObj<typeof meta>;

const TabsDemo = ({ isInverted = false }: { isInverted?: boolean }) => {
const [activeTab, setActiveTab] = useState('one');
const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setActiveTab(event.currentTarget.value);
};
return (
<>
<TabList isInverted={isInverted}>
<Tab
id='one'
value='one'
isActive={activeTab === 'one'}
iconLeft='list'
label='Tab one'
onClick={onClick}
/>
<Tab
id='two'
value='two'
isActive={activeTab === 'two'}
iconLeft='chart'
label='Tab two'
onClick={onClick}
/>
<Tab
id='three'
value='three'
isActive={activeTab === 'three'}
iconLeft='map'
label='Tab three'
onClick={onClick}
/>
</TabList>
<TabPanel id={activeTab} style={{ padding: '30px' }}>
<Heading type='4'>Panel {activeTab}</Heading>
</TabPanel>
</>
);
};

export const Default: Story = {
name: 'Tabs',
render: () => {
const [activeTab, setActiveTab] = useState('one');
const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setActiveTab(event.currentTarget.value);
};
return (
<>
<TabList>
<Tab
id='one'
value='one'
isActive={activeTab === 'one'}
iconLeft='list'
label='Tab one'
onClick={onClick}
/>
<Tab
id='two'
value='two'
isActive={activeTab === 'two'}
iconLeft='chart'
label='Tab two'
onClick={onClick}
/>
<Tab
id='three'
value='three'
isActive={activeTab === 'three'}
iconLeft='map'
label='Tab three'
onClick={onClick}
/>
</TabList>
<TabPanel id={activeTab} style={{ padding: '30px' }}>
<Heading type='4'>Panel {activeTab}</Heading>
</TabPanel>
</>
);
},
render: () => <TabsDemo />,
};

export const Inverted: Story = {
name: 'Tabs (inverted backgrounds)',
render: () => <TabsDemo isInverted />,
};
13 changes: 13 additions & 0 deletions src/components/tabs/tab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,17 @@ describe('<Tabs />', () => {
const tabs = screen.getByRole('tablist');
expect(tabs).toBeInTheDocument();
});

it('applies inverted background class on TabList', () => {
render(
<TabList isInverted>
<Tab id='one' isActive>
One tab
</Tab>
<Tab id='two'>Second tab</Tab>
</TabList>,
);

expect(screen.getByRole('tablist')).toHaveClass('tablist--inverted');
});
});
10 changes: 9 additions & 1 deletion src/components/tabs/tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,22 @@ export const Tab = ({
export interface TabListProperties extends HTMLAttributes<HTMLDivElement> {
className?: string;
children?: ReactNode;
/**
* Render tabs without fills. Same chrome as default tabs (active border,
* inactive link styles), but transparent backgrounds on every tab.
*/
isInverted?: boolean;
}

export const TabList = ({
className,
children,
isInverted = false,
...properties
}: TabListProperties): JSXElement => {
const cname = classnames('tablist', className);
const cname = classnames('tablist', className, {
'tablist--inverted': isInverted,
});

return (
<div role='tablist' className={cname} {...properties}>
Expand Down
Loading