Update shipping and payment radio controls to use borders on selected items (#46150)
This commit is contained in:
parent
b8df34659c
commit
255a45911c
|
@ -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 )
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
) }
|
||||
>
|
||||
|
|
|
@ -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 ) }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,4 +46,6 @@ export interface PackageProps {
|
|||
collapsible?: TernaryFlag;
|
||||
noResultsMessage: ReactElement;
|
||||
showItems?: TernaryFlag;
|
||||
// Should the selected rate be highlighted.
|
||||
highlightChecked?: boolean;
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -96,6 +96,7 @@ const PaymentMethodOptions = () => {
|
|||
} );
|
||||
return isExpressPaymentMethodActive ? null : (
|
||||
<RadioControlAccordion
|
||||
highlightChecked={ true }
|
||||
id={ 'wc-payment-method-options' }
|
||||
className={ singleOptionClass }
|
||||
selected={ activeSavedToken ? null : activePaymentMethod }
|
||||
|
|
|
@ -170,6 +170,7 @@ const SavedPaymentMethodOptions = () => {
|
|||
return options.length > 0 ? (
|
||||
<>
|
||||
<RadioControl
|
||||
highlightChecked={ true }
|
||||
id={ 'wc-payment-method-saved-tokens' }
|
||||
selected={ activeSavedToken }
|
||||
options={ options }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }` }
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Change styling for shipping, payment, and local pickup radio buttons in the Checkout block
|
Loading…
Reference in New Issue