diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 797ff0246c75d..7576be45cd399 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2500,6 +2500,9 @@ importers:
'@automattic/ui':
specifier: 1.0.2
version: 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@base-ui-components/react':
+ specifier: ^1.0.0-beta.4
+ version: 1.0.0-beta.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@gravatar-com/hovercards':
specifier: 0.15.0
version: 0.15.0
@@ -6686,6 +6689,27 @@ packages:
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
+ '@base-ui-components/react@1.0.0-beta.4':
+ resolution: {integrity: sha512-sPYKj26gbFHD2ZsrMYqQshXnMuomBodzPn+d0dDxWieTj232XCQ9QGt9fU9l5SDGC9hi8s24lDlg9FXPSI7T8A==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@types/react': ^17 || ^18 || ^19
+ react: ^17 || ^18 || ^19
+ react-dom: ^17 || ^18 || ^19
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@base-ui-components/utils@0.1.2':
+ resolution: {integrity: sha512-aEitDGpMsYO2qnSpYOwZNykn9Rzn2ioyEVk2fyDRH7t+TIHVKpp9CeV7SPTq43M9mMSDxQ+7UeZJVkrj2dCVIQ==}
+ peerDependencies:
+ '@types/react': ^17 || ^18 || ^19
+ react: ^17 || ^18 || ^19
+ react-dom: ^17 || ^18 || ^19
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
@@ -7166,6 +7190,12 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
+ '@floating-ui/react-dom@2.1.6':
+ resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
@@ -15445,6 +15475,9 @@ packages:
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+ reselect@5.1.1:
+ resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
+
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
@@ -16366,6 +16399,9 @@ packages:
tabbable@5.3.3:
resolution: {integrity: sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==}
+ tabbable@6.3.0:
+ resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==}
+
table@6.9.0:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'}
@@ -18448,6 +18484,31 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
+ '@base-ui-components/react@1.0.0-beta.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@babel/runtime': 7.28.4
+ '@base-ui-components/utils': 0.1.2(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@floating-ui/utils': 0.2.10
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ reselect: 5.1.1
+ tabbable: 6.3.0
+ use-sync-external-store: 1.6.0(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@base-ui-components/utils@0.1.2(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@babel/runtime': 7.28.4
+ '@floating-ui/utils': 0.2.10
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ reselect: 5.1.1
+ use-sync-external-store: 1.6.0(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+
'@bcoe/v8-coverage@0.2.3': {}
'@bufbuild/protobuf@2.9.0': {}
@@ -19074,6 +19135,12 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
+ '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@floating-ui/dom': 1.7.4
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
'@floating-ui/utils@0.2.10': {}
'@formatjs/ecma402-abstract@2.3.6':
@@ -31454,6 +31521,8 @@ snapshots:
requires-port@1.0.0: {}
+ reselect@5.1.1: {}
+
resize-observer-polyfill@1.5.1: {}
resolve-cwd@3.0.0:
@@ -32530,6 +32599,8 @@ snapshots:
tabbable@5.3.3: {}
+ tabbable@6.3.0: {}
+
table@6.9.0:
dependencies:
ajv: 8.17.1
diff --git a/projects/packages/forms/changelog/update-forms-responses-header-modernization b/projects/packages/forms/changelog/update-forms-responses-header-modernization
new file mode 100644
index 0000000000000..96f42a5a32b37
--- /dev/null
+++ b/projects/packages/forms/changelog/update-forms-responses-header-modernization
@@ -0,0 +1,4 @@
+Significance: minor
+Type: changed
+
+Forms: modernize responses header with tabs, compact number formatting, and improved mobile layout.
diff --git a/projects/packages/forms/package.json b/projects/packages/forms/package.json
index 03c7214b8875a..65cb8c4c65c19 100644
--- a/projects/packages/forms/package.json
+++ b/projects/packages/forms/package.json
@@ -42,6 +42,7 @@
"@automattic/number-formatters": "workspace:*",
"@automattic/request-external-access": "1.0.1",
"@automattic/ui": "1.0.2",
+ "@base-ui-components/react": "^1.0.0-beta.4",
"@gravatar-com/hovercards": "0.15.0",
"@wordpress/admin-ui": "1.1.0",
"@wordpress/base-styles": "6.7.0",
diff --git a/projects/packages/forms/src/dashboard/components/inbox-status-toggle/index.tsx b/projects/packages/forms/src/dashboard/components/inbox-status-toggle/index.tsx
index 6bb8571dc67c0..0967dce0f0c38 100644
--- a/projects/packages/forms/src/dashboard/components/inbox-status-toggle/index.tsx
+++ b/projects/packages/forms/src/dashboard/components/inbox-status-toggle/index.tsx
@@ -3,13 +3,7 @@
*/
import jetpackAnalytics from '@automattic/jetpack-analytics';
import { useBreakpointMatch } from '@automattic/jetpack-components';
-import { formatNumber } from '@automattic/number-formatters';
-import {
- // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
- __experimentalToggleGroupControl as ToggleGroupControl,
- // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
- __experimentalToggleGroupControlOption as ToggleGroupControlOption,
-} from '@wordpress/components';
+import { formatNumberCompact } from '@automattic/number-formatters';
import { __, _x } from '@wordpress/i18n';
import { useCallback } from 'react';
import { useSearchParams } from 'react-router';
@@ -17,16 +11,22 @@ import { useSearchParams } from 'react-router';
* Internal dependencies
*/
import useInboxData from '../../hooks/use-inbox-data';
+import * as Tabs from '../tabs';
/**
- * Returns a formatted tab label with count.
+ * Returns a formatted tab label with count badge.
*
* @param {string} label - The label for the tab.
* @param {number} count - The count to display.
- * @return {string} The formatted label.
+ * @return {JSX.Element} The formatted label with count badge.
*/
-function getTabLabel( label: string, count: number ): string {
- return `${ label } (${ formatNumber( count || 0 ) })`;
+function getTabLabel( label: string, count: number ): JSX.Element {
+ return (
+ <>
+ { label }
+ { formatNumberCompact( count || 0 ) }
+ >
+ );
}
type InboxStatusToggleProps = {
@@ -76,24 +76,14 @@ export default function InboxStatusToggle( { onChange }: InboxStatusToggleProps
);
return (
-
- { statusTabs.map( option => (
-
- ) ) }
-
+
+
+ { statusTabs.map( option => (
+
+ { option.label }
+
+ ) ) }
+
+
);
}
diff --git a/projects/packages/forms/src/dashboard/components/tabs/index.ts b/projects/packages/forms/src/dashboard/components/tabs/index.ts
new file mode 100644
index 0000000000000..6aa75f30d52c5
--- /dev/null
+++ b/projects/packages/forms/src/dashboard/components/tabs/index.ts
@@ -0,0 +1,9 @@
+/**
+ * Internal dependencies
+ */
+import { List } from './list.tsx';
+import { Panel } from './panel.tsx';
+import { Root } from './root.tsx';
+import { Tab } from './tab.tsx';
+
+export { Root, List, Panel, Tab };
diff --git a/projects/packages/forms/src/dashboard/components/tabs/list.tsx b/projects/packages/forms/src/dashboard/components/tabs/list.tsx
new file mode 100644
index 0000000000000..fa67b40b3c36c
--- /dev/null
+++ b/projects/packages/forms/src/dashboard/components/tabs/list.tsx
@@ -0,0 +1,137 @@
+/**
+ * External dependencies
+ */
+import { Tabs as BaseUITabs } from '@base-ui-components/react/tabs';
+/**
+ * WordPress dependencies
+ */
+import { useMergeRefs } from '@wordpress/compose';
+import clsx from 'clsx';
+import { forwardRef, useState, useEffect, useCallback, isValidElement, cloneElement } from 'react';
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import type { TabListProps } from './types.ts';
+
+const DEFAULT_SCROLL_MARGIN = 0;
+
+/**
+ * Groups the individual tab buttons.
+ *
+ * `Tabs` is a collection of React components that combine to render
+ * an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
+ */
+export const List = forwardRef< HTMLDivElement, TabListProps >( function TabList(
+ { children, density = 'default', className, activateOnFocus, render, ...otherProps },
+ forwardedRef
+) {
+ const [ listEl, setListEl ] = useState< HTMLDivElement | null >( null );
+ const [ overflow, setOverflow ] = useState< {
+ first: boolean;
+ last: boolean;
+ } >( {
+ first: false,
+ last: false,
+ } );
+
+ /**
+ * Checks if list is overflowing when it scrolls or resizes.
+ */
+ useEffect( () => {
+ if ( ! listEl ) {
+ return;
+ }
+
+ // Grab a local reference to the list element to ensure it remains stable
+ // during the effect and the event listeners.
+ const localListEl = listEl;
+
+ /**
+ * Measures if the tab list is overflowing horizontally.
+ */
+ function measureOverflow() {
+ if ( ! localListEl ) {
+ setOverflow( {
+ first: false,
+ last: false,
+ } );
+ return;
+ }
+
+ const { scrollWidth, clientWidth, scrollLeft } = localListEl;
+
+ setOverflow( {
+ first: scrollLeft > DEFAULT_SCROLL_MARGIN,
+ last: scrollLeft + clientWidth < scrollWidth - DEFAULT_SCROLL_MARGIN,
+ } );
+ }
+
+ const resizeObserver = new ResizeObserver( measureOverflow );
+ resizeObserver.observe( localListEl );
+ let scrollTick = false;
+ /**
+ * Throttles overflow measurement on scroll using requestAnimationFrame.
+ */
+ function throttleMeasureOverflowOnScroll() {
+ if ( ! scrollTick ) {
+ requestAnimationFrame( () => {
+ measureOverflow();
+ scrollTick = false;
+ } );
+ scrollTick = true;
+ }
+ }
+ localListEl.addEventListener( 'scroll', throttleMeasureOverflowOnScroll, { passive: true } );
+
+ // Initial check.
+ measureOverflow();
+
+ return () => {
+ localListEl.removeEventListener( 'scroll', throttleMeasureOverflowOnScroll );
+ resizeObserver.disconnect();
+ };
+ }, [ listEl ] );
+
+ const setListElRef = useCallback( el => setListEl( el ), [] );
+ const mergedListRef = useMergeRefs( [ forwardedRef, setListElRef ] );
+
+ const renderTabList = useCallback(
+ ( props, state ) => {
+ // Fallback to -1 to prevent browsers from making the tablist
+ // tabbable when it is a scrolling container.
+ const newProps = {
+ ...props,
+ tabIndex: props.tabIndex ?? -1,
+ };
+
+ if ( isValidElement( render ) ) {
+ return cloneElement( render, newProps );
+ } else if ( typeof render === 'function' ) {
+ return render( newProps, state );
+ }
+ return
;
+ },
+ [ render ]
+ );
+
+ return (
+
+ { children }
+
+
+ );
+} );
diff --git a/projects/packages/forms/src/dashboard/components/tabs/panel.tsx b/projects/packages/forms/src/dashboard/components/tabs/panel.tsx
new file mode 100644
index 0000000000000..36fa52b6240d8
--- /dev/null
+++ b/projects/packages/forms/src/dashboard/components/tabs/panel.tsx
@@ -0,0 +1,31 @@
+/**
+ * External dependencies
+ */
+import { Tabs as BaseUITabs } from '@base-ui-components/react/tabs';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import type { TabPanelProps } from './types.ts';
+
+/**
+ * A panel displayed when the corresponding tab is active.
+ *
+ * `Tabs` is a collection of React components that combine to render
+ * an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
+ */
+export const Panel = forwardRef< HTMLDivElement, TabPanelProps >( function TabPanel(
+ { className, focusable = true, tabIndex, ...otherProps },
+ forwardedRef
+) {
+ return (
+
+ );
+} );
diff --git a/projects/packages/forms/src/dashboard/components/tabs/root.tsx b/projects/packages/forms/src/dashboard/components/tabs/root.tsx
new file mode 100644
index 0000000000000..91e3d9a51d535
--- /dev/null
+++ b/projects/packages/forms/src/dashboard/components/tabs/root.tsx
@@ -0,0 +1,22 @@
+/**
+ * External dependencies
+ */
+import { Tabs as BaseUITabs } from '@base-ui-components/react/tabs';
+import { forwardRef } from 'react';
+/**
+ * Internal dependencies
+ */
+import type { TabRootProps } from './types.ts';
+
+/**
+ * Groups the tabs and the corresponding panels.
+ *
+ * `Tabs` is a collection of React components that combine to render
+ * an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
+ */
+export const Root = forwardRef< HTMLDivElement, TabRootProps >( function TabsRoot(
+ { ...otherProps },
+ forwardedRef
+) {
+ return ;
+} );
diff --git a/projects/packages/forms/src/dashboard/components/tabs/style.scss b/projects/packages/forms/src/dashboard/components/tabs/style.scss
new file mode 100644
index 0000000000000..876afb7a01567
--- /dev/null
+++ b/projects/packages/forms/src/dashboard/components/tabs/style.scss
@@ -0,0 +1,291 @@
+/* Map design tokens to WordPress/Jetpack equivalents */
+:root {
+ --wpds-border-width-focus: 1.5px;
+ --wpds-color-stroke-interactive-neutral-strong: #1e1e1e;
+ --wpds-color-fg-interactive-neutral: #50575e;
+ --wpds-color-fg-interactive-neutral-active: #1e1e1e;
+ --wpds-color-fg-interactive-neutral-disabled: #ccc;
+ --wpds-color-stroke-focus-brand: #2271b1;
+ --wpds-color-bg-interactive-neutral-weak-active: #f0f0f1;
+ --wpds-border-radius-small: 2px;
+ --wpds-spacing-05: 4px;
+ --wpds-spacing-10: 8px;
+ --wpds-spacing-15: 12px;
+ --wpds-spacing-20: 16px;
+ --wpds-spacing-50: 40px;
+ --wpds-spacing-60: 48px;
+}
+
+.jp-forms-tabs__tablist {
+ display: flex;
+ align-items: stretch;
+ overflow-x: auto;
+
+ --direction-factor: 1;
+ --direction-start: left;
+ --direction-end: right;
+
+ &:dir(rtl) {
+ --direction-factor: -1;
+ --direction-start: right;
+ --direction-end: left;
+ }
+
+ position: relative;
+
+ &[data-orientation="horizontal"] {
+ width: fit-content;
+ --fade-width: 4rem;
+ --fade-gradient-base: transparent 0%, #000 var(--fade-width);
+ --fade-gradient-composed:
+ var(--fade-gradient-base),
+ #000 60%,
+ transparent 50%;
+
+ &.jp-forms-tabs__is-overflowing-first {
+ mask-image: linear-gradient(to var(--direction-end), var(--fade-gradient-base));
+ }
+
+ &.jp-forms-tabs__is-overflowing-last {
+ mask-image: linear-gradient(to var(--direction-start), var(--fade-gradient-base));
+ }
+
+ &.jp-forms-tabs__is-overflowing-first.jp-forms-tabs__is-overflowing-last {
+ mask-image:
+ linear-gradient(to right, var(--fade-gradient-composed)),
+ linear-gradient(to left, var(--fade-gradient-composed));
+ }
+
+ &.jp-forms-tabs__has-compact-density {
+ gap: 1rem;
+ }
+ }
+
+ &[data-orientation="vertical"] {
+ flex-direction: column;
+ }
+}
+
+.jp-forms-tabs__indicator {
+
+ @media not ( prefers-reduced-motion ) {
+ transition-property:
+ translate,
+ width,
+ height,
+ border-radius,
+ border-block;
+ transition-duration: 0.2s;
+ transition-timing-function: ease-out;
+ }
+
+ position: absolute;
+ pointer-events: none;
+
+ /* Windows high contrast mode. */
+ outline: 2px solid transparent;
+ outline-offset: -1px;
+
+ &[data-orientation="horizontal"] {
+ z-index: 1;
+ left: 0;
+ bottom: 0;
+ height: var(--wpds-border-width-focus);
+
+ width: var(--active-tab-width);
+ translate: var(--active-tab-left) 0;
+ background-color: var(--wpds-color-stroke-interactive-neutral-strong);
+ }
+
+ &[data-orientation="vertical"] {
+ z-index: 0;
+ border-radius: var(--wpds-border-radius-small);
+ top: 0;
+ left: 50%;
+ width: 100%;
+ height: var(--active-tab-height);
+ translate: -50% var(--active-tab-top);
+ background-color: var(--wpds-color-bg-interactive-neutral-weak-active);
+ }
+
+ .jp-forms-tabs__tablist[data-select-on-move="true"]:has(:focus-visible)
+ &[data-orientation="vertical"] {
+ box-sizing: border-box;
+ border:
+ var(--wpds-border-width-focus) solid
+ var(--wpds-color-stroke-focus-brand);
+ }
+}
+
+.jp-forms-tabs__tab {
+
+ /* Resets */
+ border-radius: 0;
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ outline: none; /* Focus ring applied to the ::after pseudo-element */
+ padding: 0;
+
+ /* Positioning */
+ z-index: 1;
+ flex: 1 0 auto;
+ position: relative;
+ display: flex;
+ align-items: center;
+
+ /* Appearance */
+ cursor: pointer;
+
+ /* Typography */
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+ font-size: 13px;
+ white-space: nowrap;
+
+ /*
+ * Characters in some languages (e.g. Japanese)
+ * may have a native higher line-height.
+ */
+
+ line-height: 1.2;
+ font-weight: 400;
+ color: var(--wpds-color-fg-interactive-neutral);
+
+ &[data-disabled] {
+ cursor: default;
+ color: var(--wpds-color-fg-interactive-neutral-disabled);
+
+ @media ( forced-colors: active ) {
+ color: GrayText;
+ }
+ }
+
+ &:not([data-disabled]):is(:hover, :focus-visible) {
+ color: var(--wpds-color-fg-interactive-neutral-active);
+ }
+
+ /* Focus indicator. */
+ &::after {
+ position: absolute;
+ z-index: -1;
+ pointer-events: none;
+
+ /* Outline works for Windows high contrast mode as well. */
+ outline:
+ var(--wpds-border-width-focus) solid
+ var(--wpds-color-stroke-focus-brand);
+ border-radius: var(--wpds-border-radius-small);
+
+ /* Animation */
+ opacity: 0;
+
+ @media not ( prefers-reduced-motion ) {
+ transition: opacity 0.1s linear;
+ }
+ }
+
+ &:focus-visible::after {
+ opacity: 1;
+ }
+
+ [data-orientation="horizontal"] & {
+ padding-inline: var(--wpds-spacing-20);
+ height: var(--wpds-spacing-60);
+ scroll-margin: 24px;
+
+ &::after {
+ content: "";
+ inset: var(--wpds-spacing-15);
+ }
+ }
+
+ .jp-forms-tabs__has-compact-density[data-orientation="horizontal"] & {
+ padding-inline: 0;
+
+ &::after {
+
+ /*
+ * Add enough inset to prevent the focus ring (which is 1.5px thick)
+ * from being visually clipped by the tablist.
+ */
+ inset-inline: 2px;
+ }
+ }
+
+ [data-orientation="vertical"] & {
+ padding: var(--wpds-spacing-10) var(--wpds-spacing-15);
+ min-height: var(--wpds-spacing-50);
+ }
+
+ [data-orientation="vertical"][data-select-on-move="false"] &::after {
+ content: "";
+ inset: var(--wpds-border-width-focus);
+ }
+}
+
+.jp-forms-tabs__tab__children {
+ flex-grow: 1;
+
+ display: flex;
+ align-items: center;
+
+ [data-orientation="horizontal"] & {
+ justify-content: center;
+ }
+
+ [data-orientation="vertical"] & {
+ justify-content: start;
+ }
+}
+
+.jp-forms-tabs__tab__chevron {
+ fill: currentColor;
+ height: 24px;
+ flex-shrink: 0;
+ margin-inline-end: calc(var(--wpds-spacing-05) * -1);
+
+ [data-orientation="horizontal"] & {
+ display: none;
+ }
+ opacity: 0;
+
+ [role="tab"]:is([aria-selected="true"], :focus-visible, :hover) & {
+ opacity: 1;
+ }
+
+ /*
+ * The chevron is transitioned into existence when selectOnMove is enabled,
+ * because otherwise it looks jarring, as it shows up outside of the focus
+ * indicator that's being animated at the same time.
+ */
+ @media not ( prefers-reduced-motion ) {
+
+ [data-select-on-move="true"]
+ [role="tab"]:is([aria-selected="true"])
+ & {
+ transition: opacity 0.15s 0.15s linear;
+ }
+ }
+
+ &:dir(rtl) {
+ rotate: 180deg;
+ }
+}
+
+.jp-forms-tabs__tabpanel {
+
+ &:focus {
+ box-shadow: none;
+ outline: none;
+ }
+
+ &:focus-visible {
+ box-shadow:
+ 0 0 0 var(--wpds-border-width-focus)
+ var(--wpds-color-stroke-focus-brand);
+
+ /* Windows high contrast mode. */
+ outline: 2px solid transparent;
+ outline-offset: 0;
+ }
+}
diff --git a/projects/packages/forms/src/dashboard/components/tabs/tab.tsx b/projects/packages/forms/src/dashboard/components/tabs/tab.tsx
new file mode 100644
index 0000000000000..c418f6ed3bdcb
--- /dev/null
+++ b/projects/packages/forms/src/dashboard/components/tabs/tab.tsx
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+import { Tabs as BaseUITabs } from '@base-ui-components/react/tabs';
+/**
+ * WordPress dependencies
+ */
+import { chevronRight } from '@wordpress/icons';
+import clsx from 'clsx';
+import { forwardRef, cloneElement } from 'react';
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import type { TabProps } from './types.ts';
+
+const ChevronRight = ( props: React.SVGProps< SVGSVGElement > ) => {
+ return cloneElement( chevronRight, props );
+};
+
+/**
+ * An individual interactive tab button that toggles the corresponding panel.
+ *
+ * `Tabs` is a collection of React components that combine to render
+ * an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
+ */
+export const Tab = forwardRef< HTMLButtonElement, TabProps >( function Tab(
+ { className, children, ...otherProps },
+ forwardedRef
+) {
+ return (
+
+ { children }
+
+
+ );
+} );
diff --git a/projects/packages/forms/src/dashboard/components/tabs/types.ts b/projects/packages/forms/src/dashboard/components/tabs/types.ts
new file mode 100644
index 0000000000000..28b11d4c505e6
--- /dev/null
+++ b/projects/packages/forms/src/dashboard/components/tabs/types.ts
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import type { Tabs } from '@base-ui-components/react/tabs';
+
+export type TabRootProps = Omit< Tabs.Root.Props, 'className' > & {
+ /**
+ * The CSS class to apply.
+ */
+ className?: Tabs.Root.Props[ 'className' ];
+};
+
+export type TabListProps = Omit< Tabs.List.Props, 'className' > & {
+ /**
+ * The CSS class to apply.
+ */
+ className?: Tabs.List.Props[ 'className' ];
+ /**
+ * The visual density of the tab list.
+ * @default "default"
+ */
+ density?: 'compact' | 'default';
+};
+
+export type TabProps = Omit< Tabs.Tab.Props, 'className' > & {
+ /**
+ * The CSS class to apply.
+ */
+ className?: Tabs.Tab.Props[ 'className' ];
+};
+
+export type TabPanelProps = Omit< Tabs.Panel.Props, 'className' > & {
+ /**
+ * The CSS class to apply.
+ */
+ className?: Tabs.Panel.Props[ 'className' ];
+ /**
+ * Whether the tab panel should be included in the tab order.
+ * @default true
+ */
+ focusable?: boolean;
+};
diff --git a/projects/packages/forms/src/dashboard/inbox/dataviews/index.js b/projects/packages/forms/src/dashboard/inbox/dataviews/index.js
index e7b7fd638a834..43568469698e8 100644
--- a/projects/packages/forms/src/dashboard/inbox/dataviews/index.js
+++ b/projects/packages/forms/src/dashboard/inbox/dataviews/index.js
@@ -458,7 +458,6 @@ export default function InboxView() {
onChangeSelection={ onChangeSelection }
getItemId={ getItemId }
defaultLayouts={ defaultLayouts }
- header={ }
empty={
}
- />
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{ isResponseModalOpen && (
div {
+ width: 100%;
+ }
+ }
+
+ > div:not(:empty) {
+ min-height: 48px;
+ }
+
+ // Tab styling
+ button[role="tab"] {
+ padding: 12px 0;
+ margin: 0;
+ color: var(--wpds-color-fg-interactive-neutral);
+
+ &[data-active="true"] {
+ color: var(--wpds-color-fg-interactive-neutral-active);
+ }
+
+ &:hover:not([data-active="true"]) {
+ color: var(--wpds-color-fg-interactive-neutral-active);
+ }
+
+ .jp-forms__inbox-status-count {
+ padding: 2px 6px;
+ margin-left: 4px;
+ background-color: var(--jp-gray-5);
+ border-radius: 2px;
+ }
+ }
+}
+
+.jp-forms__inbox__filters-container:not(:empty) {
+ padding-inline: 48px; // Match header padding
+ padding-block: 12px;
+
+ @media (max-width: 782px) {
+ padding-inline: 24px;
}
}
@@ -304,29 +365,6 @@
@media (min-width: 782px) {
border-right: 1px solid var(--jp-gray-5);
}
-
- .dataviews__view-actions {
- align-items: center;
-
- @media (max-width: 782px) {
- flex-direction: column-reverse;
- }
-
- > div {
-
- @media (max-width: 782px) {
- width: 100%;
- justify-content: space-between;
- }
-
- &:nth-of-type(2) {
-
- @media (max-width: 782px) {
- justify-content: flex-end;
- }
- }
- }
- }
}
.dataviews-footer {
@@ -428,10 +466,6 @@
font-size: 1.2rem;
font-weight: 600;
}
-
- .jp-forms__inbox__response-mobile__header-actions {
- width: auto;
- }
}