Skip to content

Commit

Permalink
feat(RadioButton): redesign to increase contrast in component (#1742)
Browse files Browse the repository at this point in the history
  • Loading branch information
benax-se authored Nov 1, 2024
1 parent c454554 commit 43b9d63
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 103 deletions.
147 changes: 102 additions & 45 deletions src/components/RadioButton/RadioButton.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,94 @@
$block: '.#{variables.$ns}radio-button';

#{$block} {
--_--border-width: 1px;
--_--transition-time: 0.15s;

box-sizing: border-box;
display: inline-flex;
flex-direction: row;
font-family: var(--g-text-body-font-family);
font-weight: var(--g-text-body-font-weight);
border-radius: var(--_--border-radius);
background-color: var(--g-color-base-generic);
position: relative;

--_--border-radius-inner: calc(var(--_--border-radius) - 3px);

&__plate {
position: absolute;
inset-block: 0;
transition:
left 0.2s,
width 0.2s;

&[hidden] {
display: none;
}
}

&__option {
position: relative;
flex: 1 1 auto;
user-select: none;
font-size: var(--g-text-body-1-font-size);
text-align: center;
border-radius: var(--_--border-radius-inner);

cursor: pointer;
transform: scale(1);
transition: color 0.15s linear;
transition: color var(--_--transition-time) linear;

&-outline {
&::before {
position: absolute;
inset-inline-start: 0;
inset-block: var(--_--border-width);
content: '';
width: var(--_--border-width);
background-color: var(--g-color-line-generic);
}

&::after {
content: '';
position: absolute;
z-index: -1;
inset: 3px;
border-radius: var(--_--border-radius-inner);
inset: 0;
border: var(--_--border-width) solid var(--g-color-line-generic);
border-radius: 0;

transition:
background-color var(--_--transition-time) linear,
border-color var(--_--transition-time) linear;
}

&:not(:first-child):not(&_checked)::after {
border-inline-start-width: 0;
}

&:not(:last-child):not(&_checked):after {
border-inline-end-width: 0;
}

&:first-child {
border-start-start-radius: var(--_--border-radius);
border-end-start-radius: var(--_--border-radius);

&::before {
display: none;
}

&::after {
border-start-start-radius: var(--_--border-radius);
border-end-start-radius: var(--_--border-radius);
}
}

&:last-child {
border-start-end-radius: var(--_--border-radius);
border-end-end-radius: var(--_--border-radius);

&::after {
border-start-end-radius: var(--_--border-radius);
border-end-end-radius: var(--_--border-radius);
}
}

&:not(&_checked):not(&_disabled):hover {
&::after {
background-color: var(--g-color-base-simple-hover);
}

#{$block}__option-text {
color: var(--g-color-text-primary);
}
}

&:has(#{&}-control:focus-visible) {
outline: 2px solid var(--g-color-line-misc);
outline-offset: calc(-1 * var(--_--border-width));
}

&-control {
Expand All @@ -56,16 +105,16 @@ $block: '.#{variables.$ns}radio-button';
outline: none;
opacity: 0;
cursor: inherit;

&:focus-visible + #{$block}__option-outline {
outline: 2px solid var(--g-color-line-focus);
}
}

&-text {
display: inline-block;
display: inline-flex;
justify-content: center;
align-items: center;
gap: 8px;
white-space: nowrap;
color: var(--g-color-text-complementary);
overflow: hidden;

&_icon {
height: 100%;
Expand All @@ -74,45 +123,53 @@ $block: '.#{variables.$ns}radio-button';
}
}

&:hover,
&_checked {
cursor: default;
border-color: var(--g-color-line-brand);

#{$block}__option-text {
color: var(--g-color-text-primary);
color: var(--g-color-text-brand-heavy);
}
}

&_checked {
cursor: default;
&::after {
background-color: var(--g-color-base-selection);
border-color: var(--g-color-line-brand);
}

&::before,
& + #{$block}__option::before {
background-color: transparent;
}
}

&_disabled {
cursor: default;
pointer-events: none;

&::after {
background-color: var(--g-color-base-generic);
}

#{$block}__option-text {
color: var(--g-color-text-hint);
}
}
}

&__plate::before,
&__option::before {
position: absolute;
inset: 3px;
border-radius: var(--_--border-radius-inner);
&_disabled#{&}_checked {
&::after {
background-color: var(--g-color-base-generic-accent);
border-color: var(--g-color-line-generic-accent);
}

#{$block}__option-text {
color: var(--g-color-text-secondary);
}
}
}

&__option::before {
z-index: -1;
}

&__plate::before,
&__plate[hidden] ~ &__option_checked::before {
content: '';

background-color: var(--g-color-base-background);
}

&_size {
&_s {
#{$block}__option {
Expand Down
61 changes: 6 additions & 55 deletions src/components/RadioButton/RadioButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,53 +43,14 @@ export const RadioButton = React.forwardRef(function RadioButton<T extends strin
if (!options) {
options = (
React.Children.toArray(children) as React.ReactElement<ControlGroupOption<T>>[]
).map(({props}) => ({
value: props.value,
content: props.content || props.children,
disabled: props.disabled,
title: props.title,
).map(({props: optionProps}) => ({
value: optionProps.value,
content: optionProps.content || optionProps.children,
disabled: optionProps.disabled,
title: optionProps.title,
}));
}

const plateRef = React.useRef<HTMLDivElement>(null);
const optionRef = React.useRef<HTMLLabelElement>();

const handleCheckedOptionMount: React.Ref<HTMLLabelElement> = React.useCallback(
(checkedOptionNode: HTMLLabelElement | null) => {
if (!checkedOptionNode) {
return;
}

const plateNode = plateRef.current;

if (!plateNode) {
return;
}

const uncheckedOptionNode = optionRef.current;

if (uncheckedOptionNode && uncheckedOptionNode !== checkedOptionNode) {
const setPlateStyle = (node: HTMLElement) => {
plateNode.style.left = `${node.offsetLeft}px`;
plateNode.style.width = `${node.offsetWidth}px`;
};

setPlateStyle(uncheckedOptionNode);

plateNode.hidden = false;

setPlateStyle(checkedOptionNode);
}

optionRef.current = checkedOptionNode;
},
[],
);

const handlePlateTransitionEnd: React.TransitionEventHandler<HTMLDivElement> = (event) => {
event.currentTarget.hidden = true;
};

const {containerProps, optionsProps} = useRadioGroup({...props, options});

return (
Expand All @@ -100,18 +61,8 @@ export const RadioButton = React.forwardRef(function RadioButton<T extends strin
className={b({size, width}, className)}
data-qa={qa}
>
<div
ref={plateRef}
className={b('plate')}
onTransitionEnd={handlePlateTransitionEnd}
hidden
/>
{optionsProps.map((optionProps) => (
<Option
{...optionProps}
key={optionProps.value}
ref={optionProps.checked ? handleCheckedOptionMount : undefined}
/>
<Option {...optionProps} key={optionProps.value} />
))}
</div>
);
Expand Down
3 changes: 1 addition & 2 deletions src/components/RadioButton/RadioButtonOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface RadioButtonOptionProps<ValueType extends string> extends Contro
}

type RadioButtonOptionComponentType = <T extends string>(
props: RadioButtonOptionProps<T> & {ref?: React.ForwardedRef<HTMLLabelElement>},
props: RadioButtonOptionProps<T>,
) => React.JSX.Element;

export const RadioButtonOption = React.forwardRef(function RadioButtonOption<T extends string>(
Expand All @@ -39,7 +39,6 @@ export const RadioButtonOption = React.forwardRef(function RadioButtonOption<T e
title={title}
>
<input {...inputProps} className={b('option-control')} />
<span className={b('option-outline')} />
{inner && <span className={b('option-text', {icon})}>{inner}</span>}
</label>
);
Expand Down
11 changes: 10 additions & 1 deletion src/components/RadioButton/__stories__/RadioButtonShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ export function RadioButtonShowcase() {
];

const iconOptions: RadioButtonOption[] = [
{value: 'Value 1', content: <Icon data={TriangleExclamationFill} />, title: 'Warning'},
{
value: 'Value 1',
content: (
<React.Fragment>
<Icon data={TriangleExclamationFill} />
<span>Warning</span>
</React.Fragment>
),
title: 'Warning',
},
{value: 'Value 2', content: <Icon data={CircleInfoFill} />, title: 'Info'},
];

Expand Down

0 comments on commit 43b9d63

Please sign in to comment.