diff --git a/src/components/select/select-multi.tsx b/src/components/select/select-multi.tsx index c8e3cd0d15..54c0103539 100644 --- a/src/components/select/select-multi.tsx +++ b/src/components/select/select-multi.tsx @@ -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'; diff --git a/src/components/tabs/tab.scss b/src/components/tabs/tab.scss index 95de049710..3fbdbb0683 100644 --- a/src/components/tabs/tab.scss +++ b/src/components/tabs/tab.scss @@ -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); + } } } } diff --git a/src/components/tabs/tab.stories.tsx b/src/components/tabs/tab.stories.tsx index 1438030e04..05bcca8b93 100644 --- a/src/components/tabs/tab.stories.tsx +++ b/src/components/tabs/tab.stories.tsx @@ -14,45 +14,52 @@ export default meta; type Story = StoryObj; +const TabsDemo = ({ isInverted = false }: { isInverted?: boolean }) => { + const [activeTab, setActiveTab] = useState('one'); + const onClick = (event: React.MouseEvent) => { + setActiveTab(event.currentTarget.value); + }; + return ( + <> + + + + + + + Panel {activeTab} + + + ); +}; + export const Default: Story = { name: 'Tabs', - render: () => { - const [activeTab, setActiveTab] = useState('one'); - const onClick = (event: React.MouseEvent) => { - setActiveTab(event.currentTarget.value); - }; - return ( - <> - - - - - - - Panel {activeTab} - - - ); - }, + render: () => , +}; + +export const Inverted: Story = { + name: 'Tabs (inverted backgrounds)', + render: () => , }; diff --git a/src/components/tabs/tab.test.tsx b/src/components/tabs/tab.test.tsx index ec6f357c9c..eee9a367fa 100644 --- a/src/components/tabs/tab.test.tsx +++ b/src/components/tabs/tab.test.tsx @@ -14,4 +14,17 @@ describe('', () => { const tabs = screen.getByRole('tablist'); expect(tabs).toBeInTheDocument(); }); + + it('applies inverted background class on TabList', () => { + render( + + + One tab + + Second tab + , + ); + + expect(screen.getByRole('tablist')).toHaveClass('tablist--inverted'); + }); }); diff --git a/src/components/tabs/tab.tsx b/src/components/tabs/tab.tsx index c6cb23af5d..78cc3a85b9 100644 --- a/src/components/tabs/tab.tsx +++ b/src/components/tabs/tab.tsx @@ -61,14 +61,22 @@ export const Tab = ({ export interface TabListProperties extends HTMLAttributes { 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 (