Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: New dual brand switching mechanism #3017

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
}
}

/* TODO: When is this class is this ever set? */
.adyen-checkout__card-input__form {
transition: opacity 0.25s ease-out;
}
Expand Down Expand Up @@ -230,3 +231,15 @@
margin-left: token(spacer-070);
}
}

.adyen-checkout__card-input .adyen-checkout__fieldset--dual-brand-switcher {
margin-top: token(spacer-050);

.adyen-checkout__radio_group{
margin-top: token(spacer-050);
}

.adyen-checkout__fieldset__title {
padding-bottom: 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import Address from '../../../../internal/Address';
import CardHolderName from './CardHolderName';
import Installments from './Installments';
import DisclaimerMessage from '../../../../internal/DisclaimerMessage';
import RadioGroupExtended from '../../../../internal/FormFields/RadioGroupExtended';
import Field from '../../../../internal/FormFields/Field';
import { getCardImageUrl, getFullBrandName } from '../utils';
import useImage from '../../../../../core/Context/useImage';
import Fieldset from '../../../../internal/FormFields/Fieldset';
import { useCoreContext } from '../../../../../core/Context/CoreProvider';
// import SegmentedControl from '../../../../internal/SegmentedControl';

export const CardFieldsWrapper = ({
// vars created in CardInput:
Expand Down Expand Up @@ -71,6 +78,8 @@ export const CardFieldsWrapper = ({
onFieldFocusAnalytics,
onFieldBlurAnalytics
}) => {
const { i18n } = useCoreContext();

const cardHolderField = (
<CardHolderName
required={holderNameRequired}
Expand Down Expand Up @@ -110,6 +119,54 @@ export const CardFieldsWrapper = ({

{hasHolderName && !positionHolderNameOnTop && cardHolderField}

{dualBrandSelectElements.length > 0 && dualBrandSelectElements && (
<Fieldset classNameModifiers={['dual-brand-switcher']} label={i18n.get('brand.selector.title')}>
<Field
classNameModifiers={['dualBrandSwitcher', 'no-borders']}
name={'dualBrandSwitcher'}
label={i18n.get('brand.selector.message')}
>
<RadioGroupExtended
name={'dualBrandSwitcher'}
value={selectedBrandValue} // Set which button is in a selected (checked) state
items={dualBrandSelectElements.map(item => {
const brand = item.id;
const getImage = useImage();
const imageName = brand === 'card' ? 'nocard' : brand;
const imageURL = brandsConfiguration[brand]?.icon ?? getCardImageUrl(imageName, getImage);

// TODO - check below if we have to still generate altName through the mapping function or whether it just
// corresponds to item.brandObject.localeBrand
return { id: item.id, name: item.brandObject.localeBrand, imageURL, altName: getFullBrandName(brand) };
})}
onChange={extensions.handleDualBrandSelection}
required={true}
style={'button'}
showRadioIcon={false}
/>

{/* <SegmentedControl
name={'dualBrandSwitcher'}
selectedValue={selectedBrandValue} // Set which button is in a selected (checked) state
options={dualBrandSelectElements.map(item => {
const brand = item.id;
const getImage = useImage();
const imageName = brand === 'card' ? 'nocard' : brand;
const imageURL = brandsConfiguration[brand]?.icon ?? getCardImageUrl(imageName, getImage);

// TODO - check below if we have to still generate altName through the mapping function or whether it just
// corresponds to item.brandObject.localeBrand
return { value: item.id, label: item.brandObject.localeBrand, imageURL, altName: getFullBrandName(brand) };
})}
onChange={extensions.handleDualBrandSelection}
required={true}
style={'button'}
showRadioIcon={false}
/>*/}
</Field>
</Fieldset>
)}

{showKCP && (
<KCPAuthentication
onFocusField={setFocusOn}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import './Fieldset.scss';

interface FieldsetProps {
children: ComponentChildren;
classNameModifiers: string[];
classNameModifiers?: string[];
label?: string;
readonly?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@import 'styles/variable-generator';

.adyen-checkout__input-icon {
border-radius: token(border-radius-s);
height: 18px;
width: 27px;
margin-right: token(spacer-060);
}

.adyen-checkout__input-icon--hidden {
display: none;
}

.adyen-checkout__input-icon--no-radio-icon {
margin-left: calc(token(spacer-090) * -1);
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { h } from 'preact';
import { useState } from 'preact/hooks';
import classNames from 'classnames';
import { RadioButtonIconProps } from './types';
import './RadioButtonIcon.scss';

const RadioButtonIcon = ({ dataValue, imageURL, altName, hasRadioIcon }: RadioButtonIconProps) => {
const [hasLoaded, setHasLoaded] = useState(false);

const handleError = () => {
setHasLoaded(false);
};

const handleLoad = () => {
setHasLoaded(true);
};

const fieldClassnames = classNames({
'adyen-checkout__input-icon': true,
'adyen-checkout__input-icon--hidden': !hasLoaded,
'adyen-checkout__input-icon--no-radio-icon': !hasRadioIcon
});

return <img className={fieldClassnames} onError={handleError} onLoad={handleLoad} alt={altName} src={imageURL} data-value={dataValue} />;
};

export default RadioButtonIcon;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@import 'styles/variable-generator';

.adyen-checkout__radio_group__label--no-radio {
margin:0;
padding-bottom: 0;
padding-left: token(spacer-090);
position: relative;
display: block;
color: inherit;
font-size: token(text-body-font-size);
font-weight: normal;
line-height: token(text-body-line-height);
overflow: visible;
}

.adyen-checkout__radio_group__label--no-radio:hover {
border-color: token(color-label-primary);
cursor: pointer;
}

.adyen-checkout__label--focused .adyen-checkout__radio_group__label--no-radio {
color: inherit;
}

.adyen-checkout__radio_group__label--no-radio.adyen-checkout__radio_group__label--no-radio--invalid {
color: token(color-outline-critical);
}

.adyen-checkout__radio_group--button {
.adyen-checkout__radio_group__label--no-radio {
padding: token(spacer-060);
padding-left: calc(token(spacer-060) + token(spacer-090));
width: 100%;
border: token(border-width-s) solid token(color-outline-primary);
border-radius: token(border-radius-m);
background-color: token(color-background-primary);
}

.adyen-checkout__radio_group__input:checked + .adyen-checkout__radio_group__label--no-radio {
box-shadow: 0 0 0 0.5px token(color-outline-primary-active);
border-color: token(color-outline-primary-active);
}

.adyen-checkout__radio_group__input + .adyen-checkout__radio_group__label--no-radio:hover {
border-color: token(color-outline-primary-active);
}

.adyen-checkout__radio_group__input:checked:focus + .adyen-checkout__radio_group__label--no-radio,
.adyen-checkout__radio_group__input:checked:active + .adyen-checkout__radio_group__label--no-radio {
box-shadow: 0 0 0 0.5px token(color-outline-primary-active);
border-color: token(color-outline-primary-active);
}

.adyen-checkout__radio_group-extended__label-wrapper{
display: flex;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { h } from 'preact';
import cx from 'classnames';
import './RadioGroupExtended.scss';
import { RadioGroupProps } from './types';
import { getUniqueId } from '../../../../utils/idGenerator';
import { useCoreContext } from '../../../../core/Context/CoreProvider';
import RadioButtonIcon from './RadioButtonIcon';

export default function RadioGroupExtended(props: RadioGroupProps) {
const { items, name, onChange, value, isInvalid, uniqueId, ariaLabel, showRadioIcon = true, style = 'classic' } = props;

const { i18n } = useCoreContext();
const uniqueIdBase = uniqueId?.replace(/[0-9]/g, '').substring(0, uniqueId.lastIndexOf('-'));

let invalidClassName = '';
if (isInvalid) {
invalidClassName = showRadioIcon ? 'adyen-checkout__radio_group__label--invalid' : 'adyen-checkout__radio_group__label--no-radio--invalid';
}

// {...(showRadioIcon ? { type: 'radio' } : { role: 'radio' })}

const fieldClassnames = cx([
'adyen-checkout__label__text',
showRadioIcon ? 'adyen-checkout__radio_group__label' : 'adyen-checkout__radio_group__label--no-radio',
props.className,
invalidClassName
]);

return (
<div
className={cx(['adyen-checkout__radio_group', `adyen-checkout__radio_group--${style}`])}
role={'radiogroup'}
{...(ariaLabel && { ['aria-label']: ariaLabel })}
>
{items.map(item => {
const uniqueId = getUniqueId(uniqueIdBase);
return (
<div key={item.id} className="adyen-checkout__radio_group__input-wrapper">
<input
id={uniqueId}
type={'radio'}
checked={value === item.id}
className="adyen-checkout__radio_group__input"
name={name}
onChange={onChange}
value={item.id}
/>
{/*eslint-disable-next-line jsx-a11y/label-has-associated-control*/}
<label className={fieldClassnames} htmlFor={uniqueId}>
<div className={'adyen-checkout__radio_group-extended__label-wrapper'}>
<RadioButtonIcon
key={item.id}
imageURL={item.imageURL}
altName={item.altName}
dataValue={item.id}
hasRadioIcon={showRadioIcon}
/>
<span>{i18n.get(item.name)}</span>
</div>
</label>
</div>
);
})}
</div>
);
}

RadioGroupExtended.defaultProps = {
onChange: () => {},
items: []
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './RadioGroupExtended';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { InputBaseProps } from '../InputBase';

interface RadioGroupItem {
name: string;
id: string;
imageURL: string;
altName: string;
}

export interface RadioGroupProps extends InputBaseProps {
className?: string;
isInvalid?: boolean;
items: RadioGroupItem[];
name?: string;
onChange: (e) => void;
value?: string;
uniqueId?: string;
ariaLabel?: string;
style?: 'classic' | 'button';
showRadioIcon?: boolean;
}

export interface RadioButtonIconProps {
onClick?: any;
dataValue?: string;
notSelected?: boolean; // TODO - needed??
imageURL?: string;
altName?: string;
hasRadioIcon?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function createCardVariantSwitcher(brandObjArr: BrandObject[]) {
{ id: leadBrand.brand, brandObject: leadBrand },
{ id: subBrand.brand, brandObject: subBrand }
] as DualBrandSelectElement[],
selectedBrandValue: '', // set to leadBrand.brand if an initial selection is to be made
selectedBrandValue: leadBrand.brand, // set to leadBrand.brand if an initial selection is to be made; else set to empty string
leadBrand
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,9 @@ export default function extensions(props, refs, states, hasPanLengthRef: Partial
*/
handleDualBrandSelection: (e: Event | string): void => {
let value: Event | string = e;

if (e instanceof Event) {
const target = e.target as HTMLLIElement;
value = target.getAttribute('data-value') || target.getAttribute('alt');
value = target.getAttribute('data-value') || target.getAttribute('value');
}

// Check if we have a value and whether that value corresponds to a brandObject we can propagate
Expand Down
Loading