Skip to content
Closed
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
18 changes: 18 additions & 0 deletions libs/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,10 @@ export namespace Components {
* Visually hides the label text for instances where only the radio should be displayed. Label remains accessible to assistive technology such as screen readers.
*/
"hideLabel": boolean;
/**
* Icon name to display when using the contained variant.
*/
"icon": string;
/**
* Determines whether or not the radio is invalid.
* @defaultValue false
Expand All @@ -693,6 +697,11 @@ export namespace Components {
* The value of the radio that is submitted with a form.
*/
"value": string;
/**
* Sets the style variant of the radio.
* @defaultValue 'default'
*/
"variant": 'default' | 'contained';
}
interface PdsRow {
/**
Expand Down Expand Up @@ -2313,6 +2322,10 @@ declare namespace LocalJSX {
* Visually hides the label text for instances where only the radio should be displayed. Label remains accessible to assistive technology such as screen readers.
*/
"hideLabel"?: boolean;
/**
* Icon name to display when using the contained variant.
*/
"icon"?: string;
/**
* Determines whether or not the radio is invalid.
* @defaultValue false
Expand All @@ -2339,6 +2352,11 @@ declare namespace LocalJSX {
* The value of the radio that is submitted with a form.
*/
"value"?: string;
/**
* Sets the style variant of the radio.
* @defaultValue 'default'
*/
"variant"?: 'default' | 'contained';
}
interface PdsRow {
/**
Expand Down
38 changes: 38 additions & 0 deletions libs/core/src/components/pds-radio/docs/pds-radio.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,44 @@ Only one radio in the group can be selected at a time.
</>
</DocCanvas>

### Contained Variant

The contained variant replaces the standard radio circle with an icon while maintaining all radio functionality.

<DocCanvas client:only mdxSource={{
react: `<PdsRadio componentId="contained" label="Favorite item" variant="contained" icon="star" />`,
webComponent: `<pds-radio component-id="contained" label="Favorite item" variant="contained" icon="star"></pds-radio>`
}}>
<pds-radio component-id="contained" label="Favorite item" variant="contained" icon="star"></pds-radio>
</DocCanvas>

### Contained Variant - Checked

<DocCanvas client:only mdxSource={{
react: `<PdsRadio componentId="contained-checked" label="Selected item" variant="contained" icon="heart" checked />`,
webComponent: `<pds-radio component-id="contained-checked" label="Selected item" variant="contained" icon="heart" checked></pds-radio>`
}}>
<pds-radio component-id="contained-checked" label="Selected item" variant="contained" icon="heart" checked></pds-radio>
</DocCanvas>

### Contained Variant - Disabled

<DocCanvas client:only mdxSource={{
react: `<PdsRadio componentId="contained-disabled" label="Disabled option" variant="contained" icon="star" disabled />`,
webComponent: `<pds-radio component-id="contained-disabled" label="Disabled option" variant="contained" icon="star" disabled></pds-radio>`
}}>
<pds-radio component-id="contained-disabled" label="Disabled option" variant="contained" icon="star" disabled></pds-radio>
</DocCanvas>

### Contained Variant - Invalid

<DocCanvas client:only mdxSource={{
react: `<PdsRadio componentId="contained-invalid" label="Invalid option" variant="contained" icon="star" invalid errorMessage="This is a short error message" />`,
webComponent: `<pds-radio component-id="contained-invalid" label="Invalid option" variant="contained" icon="star" invalid error-message="This is a short error message"></pds-radio>`
}}>
<pds-radio component-id="contained-invalid" label="Invalid option" variant="contained" icon="star" invalid error-message="This is a short error message"></pds-radio>
</DocCanvas>

## Additional Resources

[MDN Web Docs: Radio](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio)
50 changes: 50 additions & 0 deletions libs/core/src/components/pds-radio/pds-radio.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,56 @@
.pds-radio__message--error {
color: var(--pine-color-text-message-danger);
}

&.is-contained {
pds-icon {
color: var(--pine-color-text-message-danger);

&:hover {
color: var(--pine-color-text-message-danger);
}
}
}
}

:host(.is-contained) {
input {
display: none;
}

pds-icon {
color: var(--pine-color-text-secondary);
cursor: pointer;
flex: none;
margin-block-start: var(--pine-dimension-025);
transition: color 0.2s ease;

&:hover {
color: var(--pine-color-text-primary);
}
}

&:has(input:checked) pds-icon {
color: var(--pine-color-accent);

&:hover {
color: var(--pine-color-accent-hover);
}
}

&:has(input:disabled) pds-icon {
color: var(--pine-color-text-disabled);
cursor: not-allowed;

&:hover {
color: var(--pine-color-text-disabled);
}
}

&:has(input:focus-visible) pds-icon {
outline: var(--pine-outline-focus);
outline-offset: 2px;
}
}

input {
Expand Down
17 changes: 17 additions & 0 deletions libs/core/src/components/pds-radio/pds-radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ export class PdsRadio {
*/
@Prop() value: string;

/**
* Sets the style variant of the radio.
* @defaultValue 'default'
*/
@Prop() variant: 'default' | 'contained' = 'default';

/**
* Icon name to display when using the contained variant.
*/
@Prop() icon: string;

/**
* Emits a boolean indicating whether the checkbox is currently checked or unchecked.
*/
Expand All @@ -92,6 +103,9 @@ export class PdsRadio {
if (this.disabled) {
classNames.push('is-disabled');
}
if (this.variant === 'contained') {
classNames.push('is-contained');
}

return classNames.join(' ');
}
Expand All @@ -112,6 +126,9 @@ export class PdsRadio {
disabled={this.disabled}
onChange={this.handleRadioChange}
/>
{this.variant === 'contained' && this.icon ? (
<pds-icon icon={this.icon} size="16px" />
) : null}
<span class={this.hideLabel ? 'visually-hidden' : ''}>
{this.label}
</span>
Expand Down
28 changes: 15 additions & 13 deletions libs/core/src/components/pds-radio/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@

## Properties

| Property | Attribute | Description | Type | Default |
| -------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- |
| `checked` | `checked` | Determines whether or not the radio is checked. | `boolean` | `false` |
| `componentId` _(required)_ | `component-id` | A unique identifier used for the underlying component `id` attribute and the label `for` attribute. | `string` | `undefined` |
| `disabled` | `disabled` | Determines whether or not the radio is disabled. | `boolean` | `false` |
| `errorMessage` | `error-message` | Displays error message text describing an invalid state. | `string` | `undefined` |
| `helperMessage` | `helper-message` | Displays helper message text below radio. | `string` | `undefined` |
| `hideLabel` | `hide-label` | Visually hides the label text for instances where only the radio should be displayed. Label remains accessible to assistive technology such as screen readers. | `boolean` | `undefined` |
| `invalid` | `invalid` | Determines whether or not the radio is invalid. | `boolean` | `false` |
| `label` | `label` | String used for label text next to radio. | `string` | `undefined` |
| `name` | `name` | String used for radio `name` attribute. | `string` | `undefined` |
| `required` | `required` | Determines whether or not the radio is required. | `boolean` | `false` |
| `value` | `value` | The value of the radio that is submitted with a form. | `string` | `undefined` |
| Property | Attribute | Description | Type | Default |
| -------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ----------- |
| `checked` | `checked` | Determines whether or not the radio is checked. | `boolean` | `false` |
| `componentId` _(required)_ | `component-id` | A unique identifier used for the underlying component `id` attribute and the label `for` attribute. | `string` | `undefined` |
| `disabled` | `disabled` | Determines whether or not the radio is disabled. | `boolean` | `false` |
| `errorMessage` | `error-message` | Displays error message text describing an invalid state. | `string` | `undefined` |
| `helperMessage` | `helper-message` | Displays helper message text below radio. | `string` | `undefined` |
| `hideLabel` | `hide-label` | Visually hides the label text for instances where only the radio should be displayed. Label remains accessible to assistive technology such as screen readers. | `boolean` | `undefined` |
| `icon` | `icon` | Icon name to display when using the contained variant. | `string` | `undefined` |
| `invalid` | `invalid` | Determines whether or not the radio is invalid. | `boolean` | `false` |
| `label` | `label` | String used for label text next to radio. | `string` | `undefined` |
| `name` | `name` | String used for radio `name` attribute. | `string` | `undefined` |
| `required` | `required` | Determines whether or not the radio is required. | `boolean` | `false` |
| `value` | `value` | The value of the radio that is submitted with a form. | `string` | `undefined` |
| `variant` | `variant` | Sets the style variant of the radio. | `"contained" \| "default"` | `'default'` |


## Events
Expand Down
39 changes: 39 additions & 0 deletions libs/core/src/components/pds-radio/stories/pds-radio.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default {
invalid: false,
hideLabel: false,
required: false,
variant: 'default',
},
argTypes: extractArgTypes('pds-radio'),
component: 'pds-radio',
Expand All @@ -35,6 +36,8 @@ const BaseTemplate = (args) =>
required=${args.required}
value=${args.value}
invalid=${args.invalid}
variant=${args.variant}
icon=${args.icon}
/>`;

export const Default = BaseTemplate.bind();
Expand Down Expand Up @@ -71,3 +74,39 @@ Invalid.args = {
label: 'Label text',
invalid: true,
};

export const Contained = BaseTemplate.bind();
Contained.args = {
componentId: 'contained',
label: 'Label text',
variant: 'contained',
icon: 'star',
};

export const ContainedChecked = BaseTemplate.bind();
ContainedChecked.args = {
componentId: 'contained-checked',
label: 'Label text',
variant: 'contained',
icon: 'star',
checked: true,
};

export const ContainedDisabled = BaseTemplate.bind();
ContainedDisabled.args = {
componentId: 'contained-disabled',
label: 'Label text',
variant: 'contained',
icon: 'star',
disabled: true,
};

export const ContainedInvalid = BaseTemplate.bind();
ContainedInvalid.args = {
componentId: 'contained-invalid',
label: 'Label text',
variant: 'contained',
icon: 'star',
invalid: true,
errorMessage: 'This is a short error message',
};
94 changes: 94 additions & 0 deletions libs/core/src/components/pds-radio/test/pds-radio.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,98 @@ describe('pds-radio', () => {
expect(eventSpy).not.toHaveBeenCalled();
});

it('renders with default variant when variant prop is not set', async () => {
const page = await newSpecPage({
components: [PdsRadio],
html: `<pds-radio component-id="default" label="Label text" />`,
});

expect(page.root).not.toHaveClass('is-contained');
expect(page.root?.querySelector('pds-icon')).toBeNull();
expect(page.root?.querySelector('input[type="radio"]')).toBeTruthy();
});

it('renders with contained variant when variant="contained"', async () => {
const page = await newSpecPage({
components: [PdsRadio],
html: `<pds-radio component-id="contained" label="Label text" variant="contained" icon="star" />`,
});

expect(page.root).toHaveClass('is-contained');
expect(page.root?.querySelector('pds-icon')).toBeTruthy();
expect(page.root?.querySelector('input[type="radio"]')).toBeTruthy();
});

it('renders pds-icon with correct icon prop in contained variant', async () => {
const page = await newSpecPage({
components: [PdsRadio],
html: `<pds-radio component-id="contained" label="Label text" variant="contained" icon="heart" />`,
});

const icon = page.root?.querySelector('pds-icon');
expect(icon).toBeTruthy();
expect(icon?.getAttribute('icon')).toBe('heart');
expect(icon?.getAttribute('size')).toBe('16px');
});

it('does not render pds-icon in contained variant when icon prop is not provided', async () => {
const page = await newSpecPage({
components: [PdsRadio],
html: `<pds-radio component-id="contained" label="Label text" variant="contained" />`,
});

expect(page.root).toHaveClass('is-contained');
expect(page.root?.querySelector('pds-icon')).toBeNull();
});

it('does not render pds-icon in default variant even when icon prop is provided', async () => {
const page = await newSpecPage({
components: [PdsRadio],
html: `<pds-radio component-id="default" label="Label text" variant="default" icon="star" />`,
});

expect(page.root).not.toHaveClass('is-contained');
expect(page.root?.querySelector('pds-icon')).toBeNull();
});

it('maintains radio functionality in contained variant', async () => {
const page = await newSpecPage({
components: [PdsRadio],
html: '<pds-radio component-id="contained" label="Label text" variant="contained" icon="star" />',
});

const radio = page.root?.querySelector('input[type="radio"]');
const eventSpy = jest.fn();

page.root?.addEventListener('pdsRadioChange', eventSpy);
radio?.dispatchEvent(new Event('change'));
await page.waitForChanges();

expect(eventSpy).toHaveBeenCalled();
});

it('renders contained variant in disabled state correctly', async () => {
const page = await newSpecPage({
components: [PdsRadio],
html: `<pds-radio component-id="contained-disabled" label="Label text" variant="contained" icon="star" disabled />`,
});

expect(page.root).toHaveClass('is-contained');
expect(page.root).toHaveClass('is-disabled');
const input = page.root?.querySelector('input');
expect(input?.disabled).toBe(true);
});

it('renders contained variant in invalid state correctly', async () => {
const page = await newSpecPage({
components: [PdsRadio],
html: `<pds-radio component-id="contained-invalid" label="Label text" variant="contained" icon="star" invalid />`,
});

expect(page.root).toHaveClass('is-contained');
expect(page.root).toHaveClass('is-invalid');
const input = page.root?.querySelector('input');
expect(input?.getAttribute('aria-invalid')).toBe('true');
});

});
Loading