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; - } }