Radio improvements

- Drop `base` part (rel #207)
- Add `hint` slot and attribute (it was the only form control that did not support that)
- Refactor `@watch` calls to `updated()` per @claviska's plan for them
This commit is contained in:
Lea Verou
2025-02-07 14:11:44 -05:00
parent d25f3748c4
commit 13b5385633
5 changed files with 74 additions and 43 deletions

View File

@@ -74,3 +74,15 @@ Add the `size` attribute to the [Radio Group](/docs/components/radio-group) to c
<wa-radio value="3">Large 3</wa-radio>
</wa-radio-group>
```
### Hint
Add descriptive hint to a switch with the `hint` attribute. For hints that contain HTML, use the `hint` slot instead.
```html {.example}
<wa-radio-group label="Select an option" name="a" value="1">
<wa-radio value="1" hint="What should the user know about radio 1?">Option 1</wa-radio>
<wa-radio value="2" hint="What should the user know about radio 2?">Option 2</wa-radio>
<wa-radio value="3" hint="What should the user know about radio 3?">Option 3</wa-radio>
</wa-radio-group>
```

View File

@@ -127,7 +127,7 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
@property({ type: Boolean, reflect: true }) required = false;
/** The checkbox's hint. If you need to display HTML, use the `hint` slot instead. */
@property({ attribute: 'hint' }) hint = '';
@property() hint = '';
private handleClick() {
this.hasInteracted = true;

View File

@@ -1,15 +1,15 @@
:host(:focus-visible) {
outline: none;
}
.radio {
display: flex;
:host {
display: grid;
grid-template-columns: auto 1fr;
align-items: top;
font: inherit;
vertical-align: middle;
cursor: pointer;
}
:host(:focus-visible) {
outline: none;
}
.checked-icon {
display: flex;
fill: currentColor;
@@ -19,6 +19,11 @@
}
/* When the control isn't checked, hide the circle for Windows High Contrast mode a11y */
.radio:not(.radio--checked) svg circle {
:host(:not(:state(checked))) svg circle {
opacity: 0;
}
[part~='hint'] {
grid-column: 2;
margin-block-start: var(--wa-space-3xs);
}

View File

@@ -1,9 +1,11 @@
import type { PropertyValues } from 'lit';
import { html, isServer } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { watch } from '../../internal/watch.js';
import { HasSlotController } from '../../internal/slot.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import nativeStyles from '../../styles/native/radio.css';
import formControlStyles from '../../styles/shadow/form-control.css';
import sizeStyles from '../../styles/utilities/size.css';
import '../icon/icon.js';
import styles from './radio.css';
@@ -17,14 +19,15 @@ import styles from './radio.css';
* @dependency wa-icon
*
* @slot - The radio's label.
* @slot hint - Text that describes how to use the checkbox. Alternatively, you can use the `hint` attribute.
*
* @event blur - Emitted when the control loses focus.
* @event focus - Emitted when the control gains focus.
*
* @csspart base - The component's base wrapper.
* @csspart control - The circular container that wraps the radio's checked state.
* @csspart checked-icon - The checked icon.
* @csspart label - The container that wraps the radio's label.
* @csspart hint - The hint's wrapper.
*
* @cssproperty --background-color - The radio's background color.
* @cssproperty --background-color-checked - The radio's background color when checked.
@@ -42,7 +45,7 @@ import styles from './radio.css';
*/
@customElement('wa-radio')
export default class WaRadio extends WebAwesomeFormAssociatedElement {
static shadowStyle = [sizeStyles, nativeStyles, styles];
static shadowStyle = [formControlStyles, sizeStyles, nativeStyles, styles];
@state() checked = false;
@@ -63,6 +66,11 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
/** Disables the radio. */
@property({ type: Boolean }) disabled = false;
/** The radio's hint. If you need to display HTML, use the `hint` slot instead. */
@property() hint = '';
private readonly hasSlotController = new HasSlotController(this, 'hint');
constructor() {
super();
if (!isServer) {
@@ -81,11 +89,19 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
@watch('checked')
handleCheckedChange() {
this.toggleCustomState('checked', this.checked);
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
this.tabIndex = this.checked ? 0 : -1;
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (changedProperties.has('checked')) {
this.toggleCustomState('checked', this.checked);
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
this.tabIndex = this.checked ? 0 : -1;
}
if (changedProperties.has('disabled')) {
this.toggleCustomState('disabled', this.disabled);
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
}
/**
@@ -95,12 +111,6 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
// We override `setValue` because we don't want to set form values from here. We want to do that in "RadioGroup" itself.
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
this.toggleCustomState('disabled', this.disabled);
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
private handleClick = () => {
if (!this.disabled) {
this.checked = true;
@@ -108,26 +118,30 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
};
render() {
return html`
<span
part="base"
class=${classMap({
radio: true,
'radio--checked': this.checked,
})}
>
<span part="control" class="control">
${this.checked
? html`
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" part="checked-icon" class="checked-icon">
<circle cx="8" cy="8" r="8" />
</svg>
`
: ''}
</span>
const hasHintSlot = isServer ? true : this.hasSlotController.test('hint');
const hasHint = this.hint ? true : !!hasHintSlot;
<slot part="label" class="label"></slot>
return html`
<span part="control" class="control">
${this.checked
? html`
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" part="checked-icon" class="checked-icon">
<circle cx="8" cy="8" r="8" />
</svg>
`
: ''}
</span>
<slot part="label" class="label"></slot>
<slot
name="hint"
aria-hidden=${hasHint ? 'false' : 'true'}
class="${classMap({ 'has-slotted': hasHint })}"
id="hint"
part="hint"
>${this.hint}</slot
>
`;
}
}

View File

@@ -71,13 +71,13 @@ input[type='radio'],
input[type='radio'],
label:has(input[type='radio']),
input[type='radio'] + label,
:host [part~='base'] {
:host {
cursor: pointer;
}
/* Checked */
input[type='radio']:checked,
:host .radio--checked .control {
:host(:state(checked)) .control {
color: var(--checked-icon-color);
border-color: var(--border-color-checked);
background-color: var(--background-color-checked);