Update shipping and payment radio controls to use borders on selected items (#46150)

This commit is contained in:
Thomas Roberts 2024-04-10 10:54:05 +01:00 committed by GitHub
parent b8df34659c
commit 255a45911c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 203 additions and 23 deletions

View File

@ -6,6 +6,8 @@ import {
RadioControlOptionType,
} from '@woocommerce/blocks-components';
import { CartShippingPackageShippingRate } from '@woocommerce/types';
import { CART_STORE_KEY } from '@woocommerce/block-data';
import { useSelect } from '@wordpress/data';
interface LocalPickupSelectProps {
title?: string | undefined;
@ -31,9 +33,14 @@ export const LocalPickupSelect = ( {
renderPickupLocation,
packageCount,
}: LocalPickupSelectProps ) => {
const internalPackageCount = useSelect(
( select ) =>
select( CART_STORE_KEY )?.getCartData()?.shippingRates?.length
);
// Hacky way to check if there are multiple packages, this way is borrowed from `assets/js/base/components/cart-checkout/shipping-rates-control-package/index.tsx`
// We have no built-in way of checking if other extensions have added packages.
const multiplePackages =
internalPackageCount > 1 ||
document.querySelectorAll(
'.wc-block-components-local-pickup-select .wc-block-components-radio-control'
).length > 1;
@ -45,6 +52,7 @@ export const LocalPickupSelect = ( {
setSelectedOption( value );
onSelectRate( value );
} }
highlightChecked={ true }
selected={ selectedOption }
options={ pickupLocations.map( ( location ) =>
renderPickupLocation( location, packageCount )

View File

@ -5,10 +5,12 @@ import classNames from 'classnames';
import { _n, sprintf } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { Label, Panel } from '@woocommerce/blocks-components';
import { useCallback } from '@wordpress/element';
import { useCallback, useMemo } from '@wordpress/element';
import { useShippingData } from '@woocommerce/base-context/hooks';
import { sanitizeHTML } from '@woocommerce/utils';
import type { ReactElement } from 'react';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
@ -25,9 +27,18 @@ export const ShippingRatesControlPackage = ( {
packageData,
collapsible,
showItems,
highlightChecked = false,
}: PackageProps ): ReactElement => {
const { selectShippingRate, isSelectingRate } = useShippingData();
const internalPackageCount = useSelect(
( select ) =>
select( CART_STORE_KEY )?.getCartData()?.shippingRates?.length
);
// We have no built-in way of checking if other extensions have added packages e.g. if subscriptions has added them.
const multiplePackages =
internalPackageCount > 1 ||
document.querySelectorAll(
'.wc-block-components-shipping-rates-control__package'
).length > 1;
@ -102,8 +113,15 @@ export const ShippingRatesControlPackage = ( {
),
renderOption,
disabled: isSelectingRate,
highlightChecked,
};
const selectedOptionNumber = useMemo( () => {
return packageData?.shipping_rates?.findIndex(
( rate ) => rate?.selected
);
}, [ packageData?.shipping_rates ] );
if ( shouldBeCollapsible ) {
return (
<Panel
@ -135,6 +153,12 @@ export const ShippingRatesControlPackage = ( {
{
'wc-block-components-shipping-rates-control__package--disabled':
isSelectingRate,
'wc-block-components-shipping-rates-control__package--first-selected':
! isSelectingRate && selectedOptionNumber === 0,
'wc-block-components-shipping-rates-control__package--last-selected':
! isSelectingRate &&
selectedOptionNumber ===
packageData?.shipping_rates?.length - 1,
}
) }
>

View File

@ -13,7 +13,7 @@ import { usePrevious } from '@woocommerce/base-hooks';
* Internal dependencies
*/
import { renderPackageRateOption } from './render-package-rate-option';
import type { PackageRateRenderOption } from '../shipping-rates-control-package';
import type { PackageRateRenderOption } from '../shipping-rates-control-package/types';
interface PackageRates {
onSelectRate: ( selectedRateId: string ) => void;
@ -23,6 +23,8 @@ interface PackageRates {
noResultsMessage: JSX.Element;
selectedRate: CartShippingPackageShippingRate | undefined;
disabled?: boolean;
// Should the selected rate be highlighted.
highlightChecked?: boolean;
}
const PackageRates = ( {
@ -33,6 +35,7 @@ const PackageRates = ( {
renderOption = renderPackageRateOption,
selectedRate,
disabled = false,
highlightChecked = false,
}: PackageRates ): JSX.Element => {
const selectedRateId = selectedRate?.rate_id || '';
const previousSelectedRateId = usePrevious( selectedRateId );
@ -76,6 +79,7 @@ const PackageRates = ( {
setSelectedOption( value );
onSelectRate( value );
} }
highlightChecked={ highlightChecked }
disabled={ disabled }
selected={ selectedOption }
options={ rates.map( renderOption ) }

View File

@ -1,6 +1,5 @@
.wc-block-components-shipping-rates-control__package {
margin: 0;
border-bottom: 1px solid $universal-border-light;
&.wc-block-components-panel {
margin-bottom: 0;
@ -14,8 +13,6 @@
}
&:last-child {
border-bottom: 0;
.wc-block-components-panel__button {
padding-bottom: 0;
}
@ -51,7 +48,7 @@
@include font-size(small);
display: block;
list-style: none;
margin: 0;
margin: 0 0 $gap-small 0;
padding: 0;
}

View File

@ -46,4 +46,6 @@ export interface PackageProps {
collapsible?: TernaryFlag;
noResultsMessage: ReactElement;
showItems?: TernaryFlag;
// Should the selected rate be highlighted.
highlightChecked?: boolean;
}

View File

@ -33,6 +33,7 @@ const Packages = ( {
collapsible,
noResultsMessage,
renderOption,
context = '',
}: PackagesProps ): JSX.Element | null => {
// If there are no packages, return nothing.
if ( ! packages.length ) {
@ -42,6 +43,7 @@ const Packages = ( {
<>
{ packages.map( ( { package_id: packageId, ...packageData } ) => (
<ShippingRatesControlPackage
highlightChecked={ context !== 'woocommerce/cart' }
key={ packageId }
packageId={ packageId }
packageData={ packageData }

View File

@ -27,6 +27,9 @@ export interface PackagesProps {
// Function to render a shipping rate
renderOption: PackageRateRenderOption;
// The context that this component is rendered in (Cart/Checkout)
context?: 'woocommerce/cart' | 'woocommerce/checkout' | '';
}
export interface ShippingRatesControlProps {

View File

@ -96,6 +96,7 @@ const PaymentMethodOptions = () => {
} );
return isExpressPaymentMethodActive ? null : (
<RadioControlAccordion
highlightChecked={ true }
id={ 'wc-payment-method-options' }
className={ singleOptionClass }
selected={ activeSavedToken ? null : activePaymentMethod }

View File

@ -170,6 +170,7 @@ const SavedPaymentMethodOptions = () => {
return options.length > 0 ? (
<>
<RadioControl
highlightChecked={ true }
id={ 'wc-payment-method-saved-tokens' }
selected={ activeSavedToken }
options={ options }

View File

@ -174,6 +174,7 @@
.wc-block-checkout__payment-method {
.wc-block-components-radio-control__option {
padding-left: em($gap-huge);
padding-right: em($gap-small);
&::after {
content: none;
@ -197,17 +198,6 @@
font-weight: bold;
}
.wc-block-components-radio-control {
border: 1px solid $universal-border-light;
border-radius: $universal-border-radius;
}
.wc-block-components-radio-control-accordion-option,
.wc-block-components-radio-control__option {
border-top: 1px solid $universal-border-light;
&:first-child {
border-top: 0;
}
}
.wc-block-components-radio-control-accordion-option
.wc-block-components-radio-control__option {
border-width: 0;

View File

@ -2,9 +2,8 @@
.wp-block-woocommerce-checkout-pickup-options-block {
.wc-block-components-local-pickup-rates-control {
.wc-block-components-radio-control__option {
border-bottom: 1px solid $universal-border-light;
margin: 0;
padding: em($gap-small) 0 em($gap-small) em($gap-huge);
padding: em($gap-small) em($gap-small) em($gap-small) em($gap-huge);
}
.wc-block-components-shipping-rates-control__no-results-notice {
margin: em($gap-small) 0;

View File

@ -1,8 +1,7 @@
.wc-block-checkout__shipping-option {
.wc-block-components-radio-control__option {
border-bottom: 1px solid $universal-border-light;
margin: 0;
padding: em($gap-small) 0 em($gap-small) em($gap-huge);
padding: em($gap-small) em($gap-small) em($gap-small) em($gap-huge);
}
.wc-block-components-shipping-rates-control__no-results-notice {

View File

@ -3,6 +3,7 @@
*/
import classnames from 'classnames';
import { withInstanceId } from '@wordpress/compose';
import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
@ -22,6 +23,8 @@ export interface RadioControlAccordionProps {
content: JSX.Element;
} >;
selected: string | null;
// Should the selected option be highlighted with a border?
highlightChecked?: boolean;
}
const RadioControlAccordion = ( {
@ -31,9 +34,14 @@ const RadioControlAccordion = ( {
selected,
onChange,
options = [],
highlightChecked = false,
}: RadioControlAccordionProps ): JSX.Element | null => {
const radioControlId = id || instanceId;
const selectedOptionNumber = useMemo( () => {
return options.findIndex( ( option ) => option.value === selected );
}, [ options, selected ] );
if ( ! options.length ) {
return null;
}
@ -41,6 +49,15 @@ const RadioControlAccordion = ( {
<div
className={ classnames(
'wc-block-components-radio-control',
{
'wc-block-components-radio-control--highlight-checked':
highlightChecked,
'wc-block-components-radio-control--highlight-checked--first-selected':
highlightChecked && selectedOptionNumber === 0,
'wc-block-components-radio-control--highlight-checked--last-selected':
highlightChecked &&
selectedOptionNumber === options.length - 1,
},
className
) }
>
@ -50,7 +67,13 @@ const RadioControlAccordion = ( {
const checked = option.value === selected;
return (
<div
className="wc-block-components-radio-control-accordion-option"
className={ classnames(
'wc-block-components-radio-control-accordion-option',
{
'wc-block-components-radio-control-accordion-option--checked-option-highlighted':
checked && highlightChecked,
}
) }
key={ option.value }
>
<RadioControlOption

View File

@ -3,6 +3,8 @@
*/
import classnames from 'classnames';
import { useInstanceId } from '@wordpress/compose';
import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
@ -17,10 +19,15 @@ const RadioControl = ( {
onChange,
options = [],
disabled = false,
highlightChecked = false,
}: RadioControlProps ): JSX.Element | null => {
const instanceId = useInstanceId( RadioControl );
const radioControlId = id || instanceId;
const selectedOptionNumber = useMemo( () => {
return options.findIndex( ( option ) => option.value === selected );
}, [ options, selected ] );
if ( ! options.length ) {
return null;
}
@ -29,11 +36,21 @@ const RadioControl = ( {
<div
className={ classnames(
'wc-block-components-radio-control',
{
'wc-block-components-radio-control--highlight-checked--first-selected':
highlightChecked && selectedOptionNumber === 0,
'wc-block-components-radio-control--highlight-checked--last-selected':
highlightChecked &&
selectedOptionNumber === options.length - 1,
'wc-block-components-radio-control--highlight-checked':
highlightChecked,
},
className
) }
>
{ options.map( ( option ) => (
<RadioControlOption
highlightChecked={ highlightChecked }
key={ `${ radioControlId }-${ option.value }` }
name={ `radio-control-${ radioControlId }` }
checked={ option.value === selected }

View File

@ -15,6 +15,7 @@ const Option = ( {
onChange,
option,
disabled = false,
highlightChecked = false,
}: RadioControlOptionProps ): JSX.Element => {
const { value, label, description, secondaryLabel, secondaryDescription } =
option;
@ -29,6 +30,8 @@ const Option = ( {
{
'wc-block-components-radio-control__option-checked':
checked,
'wc-block-components-radio-control__option--checked-option-highlighted':
checked && highlightChecked,
}
) }
htmlFor={ `${ name }-${ value }` }

View File

@ -1,3 +1,103 @@
.wc-block-components-radio-control--highlight-checked {
position: relative;
div.wc-block-components-radio-control-accordion-option {
position: relative;
// This ::after element is to fake a transparent border-top on each option.
// We can't just use border-top on the option itself because of the border around the entire accordion.
// Both borders have transparency so there's an overlap where the border is darker (due to adding two
// transparent colours together). Doing it with an ::after lets us bring the "border" in by one pixel on each
// side to avoid the overlap.
&::after {
content: "";
background: $universal-border-light;
height: 1px;
right: 1px;
left: 1px;
top: 0;
position: absolute;
}
// The first child doesn't need a fake border-top because it's handled by its parent's border-top. This stops
// a double border.
&:first-child::after {
display: none;
}
// This rule removes the fake border-top from the selected element to prevent a double border.
&.wc-block-components-radio-control-accordion-option--checked-option-highlighted + div.wc-block-components-radio-control-accordion-option::after {
display: none;
}
}
// Adds a "border" around the selected option. This is done with a box-shadow to prevent a double border on the left
// and right of the selected element, and top and bottom of the first/last elements.
label.wc-block-components-radio-control__option--checked-option-highlighted,
.wc-block-components-radio-control-accordion-option--checked-option-highlighted {
box-shadow: 0 0 0 1.5px currentColor inset;
border-radius: 4px;
}
// Defines a border around the radio control. Cannot be done with normal CSS borders or outlines because when
// selecting an item we get a double border on the left and right. It's not possible to remove the outer border just
// for the selected element, but using a pseudo element gives us more control.
&::after {
content: "";
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
position: absolute;
border: 1px solid $universal-border-light;
border-radius: 4px;
width: 100%;
box-sizing: border-box;
}
// Remove the top border when the first element is selected, this is so we don't get a double border with the
// box-shadow.
&.wc-block-components-radio-control--highlight-checked--first-selected::after {
border-top: 0;
margin-top: 2px;
}
// Remove the bottom border when the last element is selected, this is so we don't get a double border with the
// box-shadow.
&.wc-block-components-radio-control--highlight-checked--last-selected::after {
margin-bottom: 2px;
border-bottom: 0;
}
// Remove the fake border-top from the item after the selected element, this is to prevent a double border with the
// selected element's box-shadow.
.wc-block-components-radio-control__option--checked-option-highlighted + .wc-block-components-radio-control__option::after {
display: none;
}
.wc-block-components-radio-control__option {
// Add a fake border to the top of each radio option. This is because using CSS borders would result in an
// overlap and two transparent borders combining to make a darker pixel. This fake border allows us to bring the
// border in by one pixel on each side to avoid the overlap.
&::after {
content: "";
background: $universal-border-light;
height: 1px;
right: 1px;
left: 1px;
top: 0;
position: absolute;
}
// The first child doesn't need a fake border-top because it's handled by its parent's border-top.
&:first-child::after {
display: none;
}
}
}
.wc-block-components-radio-control__option {
@include reset-color();
@include reset-typography();
@ -5,7 +105,6 @@
margin: em($gap) 0;
margin-top: 0;
padding: 0 0 0 em($gap-larger);
position: relative;
cursor: pointer;

View File

@ -16,6 +16,8 @@ export interface RadioControlProps {
options: RadioControlOption[];
// Is the control disabled.
disabled?: boolean;
// Should the selected option be highlighted with a border?
highlightChecked?: boolean;
}
export interface RadioControlOptionProps {
@ -24,6 +26,8 @@ export interface RadioControlOptionProps {
onChange: ( value: string ) => void;
option: RadioControlOption;
disabled?: boolean;
// Should the selected option be highlighted with a border?
highlightChecked?: boolean;
}
interface RadioControlOptionContent {

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Change styling for shipping, payment, and local pickup radio buttons in the Checkout block