Replace @wordpress/components Button, Radio, RadioGroup with Ariakit Button (#45974)

This commit is contained in:
Sam Seay 2024-05-03 11:28:39 +08:00 committed by GitHub
parent 24348366b4
commit 3a5721c0d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 204 additions and 69 deletions

View File

@ -1,9 +1,12 @@
/**
* External dependencies
*/
import { Button as WPButton } from 'wordpress-components';
import type { Button as WPButtonType } from '@wordpress/components';
import { Button as AriakitButton } from '@ariakit/react';
import { forwardRef } from '@wordpress/element';
import classNames from 'classnames';
import type { ForwardedRef } from 'react';
import type { ButtonProps as AriakitButtonProps } from '@ariakit/react';
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
@ -11,60 +14,114 @@ import classNames from 'classnames';
import './style.scss';
import Spinner from '../../../../../packages/components/spinner';
export interface ButtonProps
extends Omit< WPButtonType.ButtonProps, 'variant' | 'href' > {
type WCButtonProps = AriakitButtonProps & { children?: React.ReactNode };
export interface ButtonProps extends WCButtonProps {
/**
* Show spinner
* Deprecated: Show a spinner. Preferably,
* render a spinner in the button children
* instead.
*
* @default false
*/
showSpinner?: boolean | undefined;
/**
* Button variant
*
* @default 'contained'
*/
variant?: 'text' | 'contained' | 'outlined';
/**
* The URL the button should link to.
* By default we render a wrapper around the button children,
* but you can opt in to removing it by setting removeTextWrap
* to true.
*
* @default false
*/
href?: string | undefined;
removeTextWrap?: boolean;
}
export interface AnchorProps extends Omit< ButtonProps, 'href' > {
interface LinkProps extends ButtonProps {
/**
* Button href
*/
href?: string | undefined;
href: string;
}
/**
* Component that visually renders a button but semantically might be `<button>` or `<a>` depending
* on the props.
*/
const Button = ( {
className,
showSpinner = false,
children,
variant = 'contained',
...props
}: ButtonProps ): JSX.Element => {
const buttonClassName = classNames(
'wc-block-components-button',
'wp-element-button',
className,
variant,
{
'wc-block-components-button--loading': showSpinner,
const Button = forwardRef< HTMLButtonElement, ButtonProps | LinkProps >(
( props, ref ) => {
if ( 'showSpinner' in props ) {
deprecated( 'showSpinner prop', {
version: '8.9.0',
alternative: 'Render a spinner in the button children instead.',
plugin: 'WooCommerce',
} );
}
);
return (
<WPButton className={ buttonClassName } { ...props }>
{ showSpinner && <Spinner /> }
const {
className,
showSpinner = false,
children,
variant = 'contained',
// To maintain backward compat we render a wrapper for button text by default,
// but you can opt in to removing it by setting removeTextWrap to true.
removeTextWrap = false,
...rest
} = props;
const buttonClassName = classNames(
'wc-block-components-button',
'wp-element-button',
className,
variant,
{
'wc-block-components-button--loading': showSpinner,
}
);
if ( 'href' in props ) {
return (
<AriakitButton
render={
<a
ref={ ref as ForwardedRef< HTMLAnchorElement > }
href={ props.href }
>
{ showSpinner && <Spinner /> }
<span className="wc-block-components-button__text">
{ children }
</span>
</a>
}
className={ buttonClassName }
{ ...rest }
/>
);
}
const buttonChildren = removeTextWrap ? (
props.children
) : (
<span className="wc-block-components-button__text">
{ children }
{ props.children }
</span>
</WPButton>
);
};
);
return (
<AriakitButton
ref={ ref }
className={ buttonClassName }
{ ...rest }
>
{ showSpinner && <Spinner /> }
{ buttonChildren }
</AriakitButton>
);
}
);
export default Button;

View File

@ -14,6 +14,7 @@ import { TotalsCoupon } from '..';
describe( 'TotalsCoupon', () => {
it( "Shows a validation error when one is in the wc/store/validation data store and doesn't show one when there isn't", () => {
const { rerender } = render( <TotalsCoupon instanceId={ 'coupon' } /> );
const openCouponFormButton = screen.getByText( 'Add a coupon' );
expect( openCouponFormButton ).toBeInTheDocument();
userEvent.click( openCouponFormButton );
@ -31,6 +32,9 @@ describe( 'TotalsCoupon', () => {
} );
} );
rerender( <TotalsCoupon instanceId={ 'coupon' } /> );
// TODO: Fix a recent deprecation of showSpinner prop of Button called in this component.
expect( console ).toHaveWarned();
expect( screen.getByText( 'Invalid coupon code' ) ).toBeInTheDocument();
} );
} );

View File

@ -15,7 +15,7 @@ import {
useRef,
forwardRef,
} from '@wordpress/element';
import { close } from '@wordpress/icons';
import { close, Icon } from '@wordpress/icons';
import {
useFocusReturn,
useFocusOnMount,
@ -56,11 +56,12 @@ const CloseButtonPortal = ( {
? createPortal(
<Button
className="wc-block-components-drawer__close"
icon={ close }
onClick={ onClick }
label={ __( 'Close', 'woocommerce' ) }
showTooltip={ false }
/>,
removeTextWrap
aria-label={ __( 'Close', 'woocommerce' ) }
>
<Icon icon={ close } />
</Button>,
closeButtonWrapper
)
: null;

View File

@ -83,20 +83,25 @@ $drawer-animation-duration: 0.3s;
transform: translateX(min(100%, var(--drawer-width)));
}
.wc-block-components-drawer__screen-overlay--with-slide-out .wc-block-components-drawer {
.wc-block-components-drawer__screen-overlay--with-slide-out
.wc-block-components-drawer {
transition: transform $drawer-animation-duration;
}
.wc-block-components-drawer__screen-overlay--with-slide-in .wc-block-components-drawer {
.wc-block-components-drawer__screen-overlay--with-slide-in
.wc-block-components-drawer {
animation-duration: $drawer-animation-duration;
animation-name: slidein;
}
.rtl .wc-block-components-drawer__screen-overlay--with-slide-in .wc-block-components-drawer {
.rtl
.wc-block-components-drawer__screen-overlay--with-slide-in
.wc-block-components-drawer {
animation-name: rtlslidein;
}
.wc-block-components-drawer__screen-overlay--is-hidden .wc-block-components-drawer {
.wc-block-components-drawer__screen-overlay--is-hidden
.wc-block-components-drawer {
transform: translateX(0);
}
@ -112,7 +117,7 @@ $drawer-animation-duration: 0.3s;
}
// Important rules are needed to reset button styles.
.wc-block-components-drawer__close {
.wc-block-components-button.wc-block-components-drawer__close {
@include reset-box();
background: transparent !important;
color: inherit !important;
@ -140,9 +145,6 @@ $drawer-animation-duration: 0.3s;
outline: none;
}
> span {
@include visually-hidden();
}
svg {
fill: currentColor;
display: block;

View File

@ -74,11 +74,12 @@ const NoticeBanner = ( {
{ !! isDismissible && (
<Button
className="wc-block-components-notice-banner__dismiss"
icon={ close }
label={ __( 'Dismiss this notice', 'woocommerce' ) }
aria-label={ __( 'Dismiss this notice', 'woocommerce' ) }
onClick={ dismiss }
showTooltip={ false }
/>
removeTextWrap
>
<Icon icon={ close } />
</Button>
) }
</div>
);

View File

@ -33,6 +33,9 @@ describe( 'CartEventsProvider', () => {
</div>
</CartEventsProvider>
);
// TODO: Fix a recent deprecation of showSpinner prop of Button called in this component.
expect( console ).toHaveWarned();
expect( screen.getByText( 'Mock observer' ) ).toBeInTheDocument();
const button = screen.getByText( 'Proceed to Checkout' );

View File

@ -23,6 +23,10 @@ describe( 'Proceed to checkout block', () => {
render(
<Block checkoutPageId={ 0 } buttonLabel={ '' } className={ '' } />
);
// TODO: Fix a recent deprecation of showSpinner prop of Button called in this component.
expect( console ).toHaveWarned();
expect( screen.getByText( 'Proceed to step two' ) ).toBeInTheDocument();
} );
it( 'allows the link to be filtered', () => {

View File

@ -92,6 +92,10 @@ describe( 'Testing cart', () => {
it( 'renders cart if there are items in the cart', async () => {
render( <CartBlock /> );
// TODO: Fix a recent deprecation of showSpinner prop of Button called in this component.
expect( console ).toHaveWarned();
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
expect(

View File

@ -3,10 +3,6 @@
*/
import { __ } from '@wordpress/i18n';
import { useShippingData } from '@woocommerce/base-context/hooks';
import {
__experimentalRadio as Radio,
__experimentalRadioGroup as RadioGroup,
} from 'wordpress-components';
import classnames from 'classnames';
import { Icon, store, shipping } from '@wordpress/icons';
import { useEffect } from '@wordpress/element';
@ -22,6 +18,7 @@ import { RatePrice, getLocalPickupPrices, getShippingPrices } from './shared';
import type { minMaxPrices } from './shared';
import { defaultLocalPickupText, defaultShippingText } from './constants';
import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils';
import Button from '../../../../base/components/button';
const SHIPPING_RATE_ERROR = {
hidden: true,
@ -35,6 +32,7 @@ const LocalPickupSelector = ( {
showIcon,
toggleText,
multiple,
onClick,
}: {
checked: string;
rate: minMaxPrices;
@ -42,10 +40,13 @@ const LocalPickupSelector = ( {
showIcon: boolean;
toggleText: string;
multiple: boolean;
onClick: () => void;
} ) => {
return (
<Radio
value="pickup"
<Button
role="radio"
removeTextWrap
onClick={ onClick }
className={ classnames(
'wc-block-checkout__shipping-method-option',
{
@ -71,7 +72,7 @@ const LocalPickupSelector = ( {
maxRate={ rate.max }
/>
) }
</Radio>
</Button>
);
};
@ -81,6 +82,7 @@ const ShippingSelector = ( {
showPrice,
showIcon,
toggleText,
onClick,
shippingCostRequiresAddress = false,
}: {
checked: string;
@ -88,6 +90,7 @@ const ShippingSelector = ( {
showPrice: boolean;
showIcon: boolean;
shippingCostRequiresAddress: boolean;
onClick: () => void;
toggleText: string;
} ) => {
const hasShippableRates = useSelect( ( select ) => {
@ -128,8 +131,10 @@ const ShippingSelector = ( {
);
return (
<Radio
value="shipping"
<Button
role="radio"
onClick={ onClick }
removeTextWrap
className={ classnames(
'wc-block-checkout__shipping-method-option',
{
@ -149,9 +154,10 @@ const ShippingSelector = ( {
{ toggleText }
</span>
{ showPrice === true && Price }
</Radio>
</Button>
);
};
const Block = ( {
checked,
onChange,
@ -178,15 +184,17 @@ const Block = ( {
);
return (
<RadioGroup
<div
id="shipping-method"
className="wc-block-checkout__shipping-method-container"
label="options"
onChange={ onChange }
checked={ checked }
// components-button-group is here for backwards compatibility, in case themes or plugins rely on it.
className="components-button-group wc-block-checkout__shipping-method-container"
role="radiogroup"
>
<ShippingSelector
checked={ checked }
onClick={ () => {
onChange( 'shipping' );
} }
rate={ getShippingPrices( shippingRates[ 0 ]?.shipping_rates ) }
showPrice={ showPrice }
showIcon={ showIcon }
@ -195,6 +203,9 @@ const Block = ( {
/>
<LocalPickupSelector
checked={ checked }
onClick={ () => {
onChange( 'pickup' );
} }
rate={ getLocalPickupPrices(
shippingRates[ 0 ]?.shipping_rates
) }
@ -203,7 +214,7 @@ const Block = ( {
showIcon={ showIcon }
toggleText={ localPickupTextFromSettings }
/>
</RadioGroup>
</div>
);
};

View File

@ -5,8 +5,9 @@
justify-content: space-between;
}
.edit-post-visual-editor .wc-block-checkout__shipping-method-option,
.wc-block-checkout__shipping-method-option {
.edit-post-visual-editor
.wc-block-components-button.wc-block-checkout__shipping-method-option,
.wc-block-components-button.wc-block-checkout__shipping-method-option {
flex-grow: 1;
display: flex;
flex-direction: row;
@ -34,6 +35,11 @@
&.wc-block-checkout__shipping-method-option--selected {
outline: 1px solid $universal-border-strong;
background-color: $universal-background;
&:focus {
outline: 1px solid $universal-border-strong;
background-color: $universal-background;
}
}
}

View File

@ -104,6 +104,10 @@ describe( 'Testing cart', () => {
it( 'Renders checkout if there are items in the cart', async () => {
render( <CheckoutBlock /> );
// TODO: Fix a recent deprecation of showSpinner prop of Button called in this component.
expect( console ).toHaveWarned();
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
expect( screen.getByText( /Place Order/i ) ).toBeInTheDocument();

View File

@ -99,6 +99,8 @@ const getAlias = ( options = {} ) => {
__dirname,
`../assets/js/templates/`
),
'react/jsx-dev-runtime': require.resolve( 'react/jsx-dev-runtime.js' ),
'react/jsx-runtime': require.resolve( 'react/jsx-runtime.js' ),
};
};

View File

@ -280,6 +280,7 @@
"pnpm": "^8.12.1"
},
"dependencies": {
"@ariakit/react": "^0.4.4",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",

View File

@ -173,7 +173,7 @@ test.describe( 'Shopper → Cart block', () => {
// Verify the "Proceed to Checkout" button is disabled during network request
await expect(
page.getByRole( 'button', { name: 'Proceed to Checkout' } )
page.getByRole( 'link', { name: 'Proceed to Checkout' } )
).toBeDisabled();
// Verify the "Proceed to Checkout" button is enabled after network request
@ -195,7 +195,7 @@ test.describe( 'Shopper → Cart block', () => {
.click();
// Verify the "Proceed to Checkout" button is disabled during network request
await expect(
page.getByRole( 'button', { name: 'Proceed to Checkout' } )
page.getByRole( 'link', { name: 'Proceed to Checkout' } )
).toBeDisabled();
// Verify the "Proceed to Checkout" button is enabled after network request
@ -215,7 +215,7 @@ test.describe( 'Shopper → Cart block', () => {
.click();
// Verify the "Proceed to Checkout" button is disabled during network request
await expect(
page.getByRole( 'button', { name: 'Proceed to Checkout' } )
page.getByRole( 'link', { name: 'Proceed to Checkout' } )
).toBeDisabled();
// Verify the "Proceed to Checkout" button is enabled after network request

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
In blocks migrate `@wordpress/components` Button to Ariakit, replace `__experimentalRadio/RadioGroup` with Ariakit Button.

View File

@ -3857,6 +3857,9 @@ importers:
plugins/woocommerce-blocks:
dependencies:
'@ariakit/react':
specifier: ^0.4.4
version: 0.4.5(react-dom@17.0.2)(react@17.0.2)
'@dnd-kit/core':
specifier: ^6.1.0
version: 6.1.0(react-dom@17.0.2)(react@17.0.2)
@ -5069,6 +5072,10 @@ packages:
/@ariakit/core@0.3.8:
resolution: {integrity: sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==}
/@ariakit/core@0.4.5:
resolution: {integrity: sha512-e294+bEcyzt/H/kO4fS5/czLAlkF7PY+Kul3q2z54VY+GGay8NlVs9UezAB7L4jUBlYRAXwp7/1Sq3R7b+MZ7w==}
dev: false
/@ariakit/react-core@0.3.9(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-K1Rcxr6FpF0n3L7Uvo+e5hm+zqoZmXLRcYF/skI+/j+ole+uNbnsnfGhG1avqJlklqH4bmkFkjZzmMdOnUC0Ig==}
peerDependencies:
@ -5081,6 +5088,19 @@ packages:
react-dom: 17.0.2(react@17.0.2)
use-sync-external-store: 1.2.0(react@17.0.2)
/@ariakit/react-core@0.4.5(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-ciTYPwpj/+mdA+EstveEnoygbx5e4PXQJxfkLKy4lkTkDJJUS9GcbYhdnIFJVUta6P1YFvzkIKo+/y9mcbMKJg==}
peerDependencies:
react: ^17.0.2
react-dom: ^17.0.0 || ^18.0.0
dependencies:
'@ariakit/core': 0.4.5
'@floating-ui/dom': 1.5.3
react: 17.0.2
react-dom: 17.0.2(react@17.0.2)
use-sync-external-store: 1.2.0(react@17.0.2)
dev: false
/@ariakit/react@0.3.9(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-gC+gibh2go8wvBqzYXavlHKwAfmee5GUMrPSQ9WBBLIfm9nQElujxcHJydaRx+ULR5FbOnbZVC3vU2ic8hSrNw==}
peerDependencies:
@ -5091,6 +5111,17 @@ packages:
react: 17.0.2
react-dom: 17.0.2(react@17.0.2)
/@ariakit/react@0.4.5(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-GUHxaOY1JZrJUHkuV20IY4NWcgknhqTQM0qCQcVZDCi+pJiWchUjTG+UyIr/Of02hU569qnQ7yovskCf+V3tNg==}
peerDependencies:
react: ^17.0.2
react-dom: ^17.0.0 || ^18.0.0
dependencies:
'@ariakit/react-core': 0.4.5(react-dom@17.0.2)(react@17.0.2)
react: 17.0.2
react-dom: 17.0.2(react@17.0.2)
dev: false
/@automattic/calypso-color-schemes@2.1.1:
resolution: {integrity: sha512-X5gmQEDJVtw8N9NARgZGM/pmalfapV8ZyRzEn2o0sCLmTAXGYg6A28ucLCQdBIn1l9t2rghBDFkY71vyqjyyFQ==}
dev: false