diff --git a/packages/css/src/components/radio/radio.scss b/packages/css/src/components/radio/radio.scss
index 1aa6c4efe9..ddfe99957d 100644
--- a/packages/css/src/components/radio/radio.scss
+++ b/packages/css/src/components/radio/radio.scss
@@ -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;
@@ -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;
+ }
}
}
diff --git a/packages/react/src/Radio/Radio.test.tsx b/packages/react/src/Radio/Radio.test.tsx
index 74b3168694..3100d2fa82 100644
--- a/packages/react/src/Radio/Radio.test.tsx
+++ b/packages/react/src/Radio/Radio.test.tsx
@@ -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'
@@ -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()
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', () => {
@@ -150,6 +155,14 @@ describe('Radio', () => {
expect(handleChange).toHaveBeenCalled()
})
+ it('shows a custom icon', () => {
+ const { container } = render(} />)
+
+ const icon = container.querySelector('svg')
+
+ expect(icon).toHaveClass('test-class')
+ })
+
it('supports ForwardRef in React', () => {
const ref = createRef()
diff --git a/packages/react/src/Radio/Radio.tsx b/packages/react/src/Radio/Radio.tsx
index 67893ff72d..fcb1ca03bb 100644
--- a/packages/react/src/Radio/Radio.tsx
+++ b/packages/react/src/Radio/Radio.tsx
@@ -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, 'aria-invalid' | 'type'>>
export const Radio = forwardRef(
- ({ children, className, invalid, ...restProps }: RadioProps, ref: ForwardedRef) => {
+ ({ children, className, icon, invalid, ...restProps }: RadioProps, ref: ForwardedRef) => {
const id = useId()
return (
@@ -29,7 +32,7 @@ export const Radio = forwardRef(
type="radio"
/>
diff --git a/proprietary/assets/icons/Radio.svg b/proprietary/assets/icons/Radio.svg
new file mode 100644
index 0000000000..2df6ab053e
--- /dev/null
+++ b/proprietary/assets/icons/Radio.svg
@@ -0,0 +1 @@
+
diff --git a/proprietary/react-icons/.svgrrc.js b/proprietary/react-icons/.svgrrc.js
index 6a26334fbf..60fa2dc525 100644
--- a/proprietary/react-icons/.svgrrc.js
+++ b/proprietary/react-icons/.svgrrc.js
@@ -7,4 +7,5 @@ module.exports = {
focusable: 'false',
},
typescript: true,
+ svgoConfig: './svgo.config.mjs',
}
diff --git a/proprietary/react-icons/svgo.config.mjs b/proprietary/react-icons/svgo.config.mjs
new file mode 100644
index 0000000000..ffa2678e86
--- /dev/null
+++ b/proprietary/react-icons/svgo.config.mjs
@@ -0,0 +1,15 @@
+export default {
+ plugins: [
+ {
+ name: 'preset-default',
+ params: {
+ overrides: {
+ prefixIds: {
+ // Do not change class names defined in the SVGs
+ prefixClassNames: false,
+ },
+ },
+ },
+ },
+ ],
+}
diff --git a/proprietary/tokens/src/components/ams/radio.tokens.json b/proprietary/tokens/src/components/ams/radio.tokens.json
index f704b39a77..3fe1016855 100644
--- a/proprietary/tokens/src/components/ams/radio.tokens.json
+++ b/proprietary/tokens/src/components/ams/radio.tokens.json
@@ -8,50 +8,47 @@
"gap": { "value": "{ams.space.sm}" },
"line-height": { "value": "{ams.text.level.5.line-height}" },
"outline-offset": { "value": "{ams.focus.outline-offset}" },
- "circle": {
- "border-color": { "value": "{ams.color.primary-blue}" },
- "border-width": { "value": "{ams.border.width.md}" },
- "checked": {
- "background-image": {
- "value": "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' fill='%23004699'%3E%3Ccircle cx='16' cy='16' r='16' /%3E%3C/svg%3E\")"
- },
+ "text-decoration-thickness": { "value": "{ams.link-appearance.text-decoration-thickness}" },
+ "text-underline-offset": { "value": "{ams.link-appearance.text-underline-offset}" },
+ "checked-indicator": {
+ "fill": { "value": "{ams.color.primary-blue}" },
+ "disabled": {
+ "fill": { "value": "{ams.color.neutral-grey3}" }
+ },
+ "disabled-invalid": {
+ "fill": { "value": "{ams.color.neutral-grey3}" },
"hover": {
- "background-image": {
- "value": "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' fill='%23102E62'%3E%3Ccircle cx='16' cy='16' r='16' /%3E%3C/svg%3E\")"
- }
+ "fill": { "value": "{ams.color.neutral-grey3}" }
}
},
+ "hover": {
+ "fill": { "value": "{ams.color.dark-blue}" }
+ },
+ "invalid": {
+ "fill": { "value": "{ams.color.primary-red}" },
+ "hover": {
+ "fill": { "value": "{ams.color.primary-red}" }
+ }
+ }
+ },
+ "circle": {
+ "stroke": { "value": "{ams.color.primary-blue}" },
"disabled": {
- "border-color": { "value": "{ams.color.neutral-grey3}" },
- "border-width": { "value": "{ams.border.width.md}" },
- "checked": {
- "background-image": {
- "value": "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' fill='%23767676'%3E%3Ccircle cx='16' cy='16' r='16' /%3E%3C/svg%3E\")"
- },
- "hover": {
- "background-image": {
- "value": "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' fill='%23767676'%3E%3Ccircle cx='16' cy='16' r='16' /%3E%3C/svg%3E\")"
- }
- }
+ "stroke": { "value": "{ams.color.neutral-grey3}" }
+ },
+ "disabled-invalid": {
+ "stroke": { "value": "{ams.color.neutral-grey3}" },
+ "hover": {
+ "stroke": { "value": "{ams.color.neutral-grey3}" }
}
},
"hover": {
- "border-color": { "value": "{ams.color.dark-blue}" }
+ "stroke": { "value": "{ams.color.dark-blue}" }
},
"invalid": {
- "border-color": { "value": "{ams.color.primary-red}" },
- "checked": {
- "background-image": {
- "value": "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' fill='%23EC0000'%3E%3Ccircle cx='16' cy='16' r='16' /%3E%3C/svg%3E\")"
- },
- "hover": {
- "background-image": {
- "value": "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' fill='%23EC0000'%3E%3Ccircle cx='16' cy='16' r='16' /%3E%3C/svg%3E\")"
- }
- }
- },
+ "stroke": { "value": "{ams.color.primary-red}" },
"hover": {
- "border-color": { "value": "{ams.color.primary-red}" }
+ "stroke": { "value": "{ams.color.primary-red}" }
}
}
},
@@ -60,7 +57,11 @@
},
"hover": {
"color": { "value": "{ams.color.dark-blue}" },
- "text-decoration-thickness": { "value": "{ams.border.width.md}" }
+ "text-decoration-line": { "value": "{ams.link-appearance.subtle.hover.text-decoration-line}" }
+ },
+ "icon-container": {
+ "block-size": { "value": "calc({ams.radio.font-size} * {ams.radio.line-height})" },
+ "inline-size": { "value": "1.5rem" }
}
}
}
diff --git a/storybook/src/components/Radio/CustomIcon.tsx b/storybook/src/components/Radio/CustomIcon.tsx
new file mode 100644
index 0000000000..14b725e54b
--- /dev/null
+++ b/storybook/src/components/Radio/CustomIcon.tsx
@@ -0,0 +1,14 @@
+import type { SVGProps } from 'react'
+const CustomIcon = (props: SVGProps) => (
+
+)
+export default CustomIcon
diff --git a/storybook/src/components/Radio/Radio.docs.mdx b/storybook/src/components/Radio/Radio.docs.mdx
index 75ee926d65..d48f68ae60 100644
--- a/storybook/src/components/Radio/Radio.docs.mdx
+++ b/storybook/src/components/Radio/Radio.docs.mdx
@@ -35,3 +35,12 @@ If the Radio can become invalid, add an Error Message component and its `id` to
Check [the Field Set docs](/docs/components-forms-field-set--docs) for more information on configuring it.
+
+### Custom icons
+
+It is possible to change the default Radio icon.
+This is intended for users who do not work within the City of Amsterdam corporate identity.
+If you work for the City of Amsterdam, it is unlikely that you need this.
+If you would like to use a custom icon, please contact designsystem@amsterdam.nl.
+
+
diff --git a/storybook/src/components/Radio/Radio.stories.tsx b/storybook/src/components/Radio/Radio.stories.tsx
index 4c1174ce06..f50abb9a39 100644
--- a/storybook/src/components/Radio/Radio.stories.tsx
+++ b/storybook/src/components/Radio/Radio.stories.tsx
@@ -7,6 +7,7 @@ import { Column, ErrorMessage, FieldSet, Paragraph } from '@amsterdam/design-sys
import { Radio } from '@amsterdam/design-system-react/src'
import { useArgs } from '@storybook/preview-api'
import { Meta, StoryObj } from '@storybook/react'
+import CustomIcon from './CustomIcon'
const meta = {
title: 'Components/Forms/Radio',
@@ -28,6 +29,9 @@ const meta = {
disabled: {
description: 'Prevents interaction. Avoid if possible.',
},
+ icon: {
+ table: { disable: true },
+ },
invalid: {
description: 'Whether the value fails a validation rule.',
},
@@ -145,3 +149,9 @@ export const InAFieldSetWithValidation: Story = {
),
}
+
+export const CustomIcons: Story = {
+ args: {
+ icon: ,
+ },
+}