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
71 changes: 71 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Forms: modernize responses header with tabs, compact number formatting, and improved mobile layout.
1 change: 1 addition & 0 deletions projects/packages/forms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,30 @@
*/
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';
/**
* 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 }
<span className="jp-forms__inbox-status-count">{ formatNumberCompact( count || 0 ) }</span>
</>
);
}

type InboxStatusToggleProps = {
Expand Down Expand Up @@ -76,24 +76,14 @@ export default function InboxStatusToggle( { onChange }: InboxStatusToggleProps
);

return (
<ToggleGroupControl
__next40pxDefaultSize
__nextHasNoMarginBottom
hideLabelFromVision
isAdaptiveWidth={ true }
isBlock
key={ `${ totalItemsInbox ?? 0 }-${ totalItemsSpam ?? 0 }-${ totalItemsTrash ?? 0 }` }
label={ __( 'Form responses type', 'jetpack-forms' ) }
onChange={ handleChange }
value={ status }
>
{ statusTabs.map( option => (
<ToggleGroupControlOption
key={ option.value }
value={ option.value }
label={ option.label }
/>
) ) }
</ToggleGroupControl>
<Tabs.Root value={ status } onValueChange={ handleChange }>
<Tabs.List density="compact">
{ statusTabs.map( option => (
<Tabs.Tab key={ option.value } value={ option.value }>
{ option.label }
</Tabs.Tab>
) ) }
</Tabs.List>
</Tabs.Root>
);
}
Original file line number Diff line number Diff line change
@@ -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 };
137 changes: 137 additions & 0 deletions projects/packages/forms/src/dashboard/components/tabs/list.tsx
Original file line number Diff line number Diff line change
@@ -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 <div { ...newProps } />;
},
[ render ]
);

return (
<BaseUITabs.List
ref={ mergedListRef }
activateOnFocus={ activateOnFocus }
data-select-on-move={ activateOnFocus ? 'true' : 'false' }
className={ clsx(
'jp-forms-tabs__tablist',
overflow.first && 'jp-forms-tabs__is-overflowing-first',
overflow.last && 'jp-forms-tabs__is-overflowing-last',
`jp-forms-tabs__has-${ density }-density`,
className
) }
render={ renderTabList }
{ ...otherProps }
>
{ children }
<BaseUITabs.Indicator className="jp-forms-tabs__indicator" />
</BaseUITabs.List>
);
} );
Loading
Loading