Skip to content

Commit

Permalink
feat!: Use inline SVG icon for Radio button (#1460)
Browse files Browse the repository at this point in the history
Co-authored-by: Vincent Smedinga <[email protected]>
  • Loading branch information
alimpens and VincentSmedinga authored Sep 25, 2024
1 parent 715f4d6 commit c19055b
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 128 deletions.
211 changes: 122 additions & 89 deletions packages/css/src/components/radio/radio.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,7 @@
@include input-label-focus;
}

.ams-radio__circle {
align-items: center;
block-size: calc(var(--ams-radio-font-size) * var(--ams-radio-line-height));
display: flex;
flex-shrink: 0;
inline-size: 1.5rem;

&::after {
background-position: center;
background-repeat: no-repeat;
background-size: 1rem;
block-size: 1.5rem;
border-color: var(--ams-radio-circle-border-color);
border-radius: 100%;
border-style: solid;
border-width: var(--ams-radio-circle-border-width);
box-sizing: border-box;
content: "";
inline-size: 100%;
}
}

// Default
.ams-radio__label {
color: var(--ams-radio-color);
cursor: pointer;
Expand All @@ -44,111 +23,165 @@
gap: var(--ams-radio-gap);
line-height: var(--ams-radio-line-height);
outline-offset: var(--ams-radio-outline-offset);
text-decoration-thickness: var(--ams-radio-text-decoration-thickness);
text-underline-offset: var(--ams-radio-text-underline-offset);

@include text-rendering;
}

&:hover {
color: var(--ams-radio-hover-color);
text-decoration-line: underline;
text-decoration-thickness: var(--ams-radio-hover-text-decoration-thickness);
text-underline-offset: 0.375rem;
.ams-radio__icon-container {
block-size: var(--ams-radio-icon-container-block-size);
display: flex;
flex: none;
inline-size: var(--ams-radio-icon-container-inline-size);
}

.ams-radio__circle::after {
border-color: var(--ams-radio-circle-hover-border-color);
}
.ams-radio__circle {
fill: none;
stroke: var(--ams-radio-circle-stroke);
stroke-width: 0.125rem;
}

.ams-radio__checked-indicator {
display: none;
fill: var(--ams-radio-checked-indicator-fill);
}

// Default hover
.ams-radio__label:hover {
color: var(--ams-radio-hover-color);
text-decoration-line: var(--ams-radio-hover-text-decoration-line);

.ams-radio__circle {
stroke: var(--ams-radio-circle-hover-stroke);
}

.ams-radio__checked-indicator {
fill: var(--ams-radio-checked-indicator-hover-fill);
}
}

// Default checked
.ams-radio__input:checked {
+ .ams-radio__label .ams-radio__circle::after {
background-image: var(--ams-radio-circle-checked-background-image);
// Invalid
.ams-radio__input[aria-invalid="true"] + .ams-radio__label {
.ams-radio__circle {
stroke: var(--ams-radio-circle-invalid-stroke);
}

.ams-radio__checked-indicator {
fill: var(--ams-radio-checked-indicator-invalid-fill);
}
}

// Invalid unchecked
.ams-radio__input[aria-invalid="true"] {
+ .ams-radio__label .ams-radio__circle::after {
border-color: var(--ams-radio-circle-invalid-border-color);
// Checked
.ams-radio__input:checked + .ams-radio__label {
.ams-radio__checked-indicator {
display: block;
}
}

// Disabled unchecked
.ams-radio__input:disabled {
+ .ams-radio__label {
color: var(--ams-radio-disabled-color);
cursor: not-allowed;
// Disabled
.ams-radio__input:disabled + .ams-radio__label {
color: var(--ams-radio-disabled-color);
cursor: not-allowed;

.ams-radio__circle::after {
border-color: var(--ams-radio-circle-disabled-border-color);
border-width: var(--ams-radio-circle-disabled-border-width);
}
.ams-radio__circle {
stroke: var(--ams-radio-circle-disabled-stroke);
}

.ams-radio__checked-indicator {
fill: var(--ams-radio-checked-indicator-disabled-fill);
}
}

// Invalid checked
.ams-radio__input[aria-invalid="true"]:checked {
+ .ams-radio__label .ams-radio__circle::after {
background-image: var(--ams-radio-circle-invalid-checked-background-image);
// Disabled invalid
.ams-radio__input[aria-invalid="true"]:disabled + .ams-radio__label {
.ams-radio__circle {
// TODO: currently disabled invalid gets the same styling as disabled. This should get its own styling.
stroke: var(--ams-radio-circle-disabled-invalid-stroke);
}

.ams-radio__checked-indicator {
// TODO: currently disabled invalid gets the same styling as disabled. This should get its own styling.
fill: var(--ams-radio-checked-indicator-disabled-invalid-fill);
}
}

// Disabled label
// HOVER

// Disabled label hover
.ams-radio__input:disabled + .ams-radio__label:hover {
text-decoration: none;
}

// Disabled checked
.ams-radio__input:disabled:checked {
+ .ams-radio__label .ams-radio__circle::after {
background-image: var(--ams-radio-circle-disabled-checked-background-image);
// Invalid hover
.ams-radio__input[aria-invalid="true"] + .ams-radio__label:hover {
.ams-radio__circle {
// TODO: this should be the (currently non-existent) dark red hover color
stroke: var(--ams-radio-circle-invalid-hover-stroke);
}
}

// Disabled invalid unchecked
.ams-radio__input[aria-invalid="true"]:disabled {
+ .ams-radio__label .ams-radio__circle::after {
// TODO: currently disabled invalid gets the same styling as disabled. This should get its own styling.
border-color: var(--ams-radio-circle-disabled-border-color);
.ams-radio__checked-indicator {
// TODO: this should be the (currently non-existent) dark red hover color
fill: var(--ams-radio-checked-indicator-invalid-hover-fill);
}
}

// HOVER STATES
// Disabled invalid hover
.ams-radio__input[aria-invalid="true"]:disabled + .ams-radio__label:hover {
.ams-radio__circle {
// TODO: currently disabled invalid gets the same styling as disabled. This should get its own styling.
stroke: var(--ams-radio-circle-disabled-invalid-hover-stroke);
}

// Invalid unchecked hover
.ams-radio__input[aria-invalid="true"] + .ams-radio__label:hover .ams-radio__circle::after {
// TODO: this should be the (currently non-existent) dark red hover color
border-color: var(--ams-radio-circle-invalid-hover-border-color);
.ams-radio__checked-indicator {
// TODO: currently disabled invalid gets the same styling as disabled. This should get its own styling.
fill: var(--ams-radio-checked-indicator-disabled-invalid-hover-fill);
}
}

// Default checked hover
.ams-radio__input:checked + .ams-radio__label:hover .ams-radio__circle::after {
background-image: var(--ams-radio-circle-checked-hover-background-image);
}
// FORCED COLORS

// Invalid checked hover
.ams-radio__input[aria-invalid="true"]:checked + .ams-radio__label:hover .ams-radio__circle::after {
// TODO: this should be the (currently non-existent) dark red hover color
background-image: var(--ams-radio-circle-invalid-checked-hover-background-image);
}
// Default
@media (forced-colors: active) {
.ams-radio__label,
.ams-radio__label:hover,
.ams-radio__input[aria-invalid="true"] + .ams-radio__label,
.ams-radio__input[aria-invalid="true"] + .ams-radio__label:hover {
.ams-radio__circle {
stroke: FieldText;
}

// Disabled checked hover
.ams-radio__input:disabled:checked + .ams-radio__label:hover .ams-radio__circle::after {
background-image: var(--ams-radio-circle-disabled-checked-hover-background-image);
.ams-radio__checked-indicator {
fill: FieldText;
}
}
}

// Disabled invalid unchecked hover
.ams-radio__input[aria-invalid="true"]:disabled + .ams-radio__label:hover .ams-radio__circle::after {
// TODO: currently disabled invalid gets the same styling as disabled. This should get its own styling.
border-color: var(--ams-radio-circle-disabled-border-color);
// Checked
@media (forced-colors: active) {
.ams-radio__input:checked + .ams-radio__label,
.ams-radio__input[aria-invalid="true"]:checked + .ams-radio__label:hover {
.ams-radio__circle {
stroke: ActiveText;
}

.ams-radio__checked-indicator {
fill: ActiveText;
}
}
}

// DISABLED INVALID STATES
// Disabled
@media (forced-colors: active) {
.ams-radio__input:disabled + .ams-radio__label,
.ams-radio__input[aria-invalid="true"]:disabled + .ams-radio__label,
.ams-radio__input[aria-invalid="true"]:disabled + .ams-radio__label:hover {
.ams-radio__circle {
stroke: GrayText;
}

// Disabled invalid checked
.ams-radio__input[aria-invalid="true"]:disabled:checked {
+ .ams-radio__label .ams-radio__circle::after {
// TODO: currently disabled invalid gets the same styling as disabled. This should get its own styling.
background-image: var(--ams-radio-circle-disabled-checked-background-image);
.ams-radio__checked-indicator {
fill: GrayText;
}
}
}
15 changes: 14 additions & 1 deletion packages/react/src/Radio/Radio.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FavouriteIcon } from '@amsterdam/design-system-react-icons'
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { Radio } from './Radio'
Expand All @@ -19,16 +20,20 @@ describe('Radio', () => {
expect(label).toBeVisible()
})

it('renders a design system BEM class name', () => {
it('renders design system BEM class names', () => {
const { container } = render(<Radio />)

const wrapper = container.querySelector(':only-child')
const input = screen.getByRole('radio')
const label = container.querySelector('label')
const circle = container.querySelector('.ams-radio__circle')
const indicator = container.querySelector('.ams-radio__checked-indicator')

expect(wrapper).toHaveClass('ams-radio')
expect(input).toHaveClass('ams-radio__input')
expect(label).toHaveClass('ams-radio__label')
expect(circle).toBeInTheDocument()
expect(indicator).toBeInTheDocument()
})

it('renders an additional class name', () => {
Expand Down Expand Up @@ -150,6 +155,14 @@ describe('Radio', () => {
expect(handleChange).toHaveBeenCalled()
})

it('shows a custom icon', () => {
const { container } = render(<Radio icon={<FavouriteIcon className="test-class" />} />)

const icon = container.querySelector('svg')

expect(icon).toHaveClass('test-class')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLInputElement>()

Expand Down
9 changes: 6 additions & 3 deletions packages/react/src/Radio/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
* Copyright Gemeente Amsterdam
*/

import { RadioIcon } from '@amsterdam/design-system-react-icons'
import clsx from 'clsx'
import { forwardRef, useId } from 'react'
import type { ForwardedRef, InputHTMLAttributes, PropsWithChildren } from 'react'
import type { ForwardedRef, InputHTMLAttributes, PropsWithChildren, ReactNode } from 'react'

export type RadioProps = {
/** An icon to display instead of the default icon. */
icon?: ReactNode
/** Whether the value fails a validation rule. */
invalid?: boolean
} & PropsWithChildren<Omit<InputHTMLAttributes<HTMLInputElement>, 'aria-invalid' | 'type'>>

export const Radio = forwardRef(
({ children, className, invalid, ...restProps }: RadioProps, ref: ForwardedRef<HTMLInputElement>) => {
({ children, className, icon, invalid, ...restProps }: RadioProps, ref: ForwardedRef<HTMLInputElement>) => {
const id = useId()

return (
Expand All @@ -29,7 +32,7 @@ export const Radio = forwardRef(
type="radio"
/>
<label className="ams-radio__label" htmlFor={id}>
<span className="ams-radio__circle" />
<span className="ams-radio__icon-container">{icon ?? <RadioIcon />}</span>
{children}
</label>
</div>
Expand Down
1 change: 1 addition & 0 deletions proprietary/assets/icons/Radio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions proprietary/react-icons/.svgrrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ module.exports = {
focusable: 'false',
},
typescript: true,
svgoConfig: './svgo.config.mjs',
}
15 changes: 15 additions & 0 deletions proprietary/react-icons/svgo.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
prefixIds: {
// Do not change class names defined in the SVGs
prefixClassNames: false,
},
},
},
},
],
}
Loading

0 comments on commit c19055b

Please sign in to comment.