import { html, css, TemplateResult, nothing, PropertyValueMap } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { OmniElement } from '../core/OmniElement.js';
import '../label/Label.js';
/**
* Control to group radio components for single selection
*
* @import
* ```js
* import '@capitec/omni-components/radio';
* ```
*
* @example
* ```html
*
*
*
*
* ```
*
* @element omni-radio-group
*
* @slot - Content to manage in the radio group, typically <input type="radio" /> and/or <omni-radio></omni-radio>.
*
* @fires {CustomEvent} radio-change - Dispatched when a radio selection is changed.
*
* @csspart radios - Container element for slotted radio elements
*
* @cssprop --omni-radio-group-label-font-size - Label font size.
* @cssprop --omni-radio-group-label-font-weight - Label font weight.
* @cssprop --omni-radio-group-label-margin-bottom - Label bottom margin.
* @cssprop --omni-radio-group-vertical-margin - Margin in between radio elements when arranged vertically.
* @cssprop --omni-radio-group-horizontal-margin - Margin in between radio elements when arranged horizontally.
*
*/
@customElement('omni-radio-group')
export class RadioGroup extends OmniElement {
/**
* Text label.
* @attr
*/
@property({ type: String, reflect: true }) label?: string;
/**
* Allow deselection of radio elements.
* @attr [allow-deselect]
*/
@property({ type: Boolean, attribute: 'allow-deselect', reflect: true }) allowDeselect?: boolean;
/**
* Arrange radio elements horizontally.
* @attr
*/
@property({ type: Boolean, reflect: true }) horizontal?: boolean;
/**
* Data associated with the component.
* @attr
*/
@property({ type: Object, reflect: true }) data?: object;
private _selected: number = NaN;
private radios: CheckableElement[] = [];
/**
* Selected index of radio elements
* @no_attribute
* @ignore
*/
set selected(idx: number) {
if (this.selected === idx) {
return;
}
if (isFinite(this.selected)) {
const previousSelected = this.radios[this.selected];
if (previousSelected) {
this._uncheckElement(previousSelected);
}
}
const newSelected = this.radios[idx];
if (newSelected) {
this._checkElement(newSelected);
}
this.setAttribute('selected', idx.toString());
this._selected = idx;
}
/**
* Selected index of radio elements
* @no_attribute
* @ignore
*/
get selected() {
return this._selected;
}
private _uncheckElement(previousSelected: CheckableElement) {
previousSelected.removeAttribute('aria-checked');
if ('checked' in previousSelected) {
previousSelected.checked = false;
}
previousSelected.removeAttribute('checked');
}
private _checkElement(newSelected: CheckableElement) {
newSelected.focus();
newSelected.setAttribute('aria-checked', 'true');
if ('checked' in newSelected) {
newSelected.checked = true;
}
newSelected.setAttribute('checked', 'true');
}
override connectedCallback(): void {
this.setAttribute('role', 'radioGroup');
this.addEventListener('click', this._handleClick.bind(this));
super.connectedCallback();
}
protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void {
super.firstUpdated(_changedProperties);
const slot = this.renderRoot.querySelector('slot');
if (slot) {
slot.addEventListener('slotchange', () => {
this._loadRadios();
});
}
this._loadRadios();
}
_loadRadios() {
const slot = this.renderRoot.querySelector('slot');
this.radios = Array.from(slot?.assignedElements() ?? []) as CheckableElement[];
if (this.radios.length > 0) {
// Setup initial state
const selectedRadio = this.radios.find((r) => r.hasAttribute('checked'));
if (selectedRadio) {
selectedRadio.setAttribute('aria-checked', 'true');
this._selected = this.radios.indexOf(selectedRadio);
} else {
this._selected = -1;
}
}
}
_handleClick(e: MouseEvent) {
const target = e.target as CheckableElement;
let idx = this.radios.indexOf(target);
if (idx === -1 || target.hasAttribute('disabled')) {
return;
}
const previousSelected = this.radios[this.selected];
const isDeselect = this.selected === idx;
if (isDeselect && this.allowDeselect) {
idx = -1;
}
this.selected = idx;
const newSelected = this.radios[this.selected];
if (!this.allowDeselect && !target.hasAttribute('checked')) {
this._checkElement(target);
}
if (isDeselect && this.allowDeselect && target.hasAttribute('checked')) {
this._uncheckElement(target);
}
this.dispatchEvent(
new CustomEvent('radio-change', {
bubbles: true,
composed: true,
detail: {
current: newSelected,
previous: previousSelected
}
})
);
this.requestUpdate();
}
static override get styles() {
return [
super.styles,
css`
:host {
flex-shrink: 0;
display: flex;
flex-direction: column;
}
:host > .wrapper {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.label {
--omni-label-default-font-size: var(--omni-radio-group-label-font-size, 14px);
--omni-label-default-font-weight: var(--omni-radio-group-label-font-weight, 400);
margin-bottom: var(--omni-radio-group-label-margin-bottom, 6px);
}
.radios:not([data-horizontal]) ::slotted(:not(:last-child)) {
margin-bottom: var(--omni-radio-group-vertical-margin, 10px) !important;
}
.radios[data-horizontal] {
display: flex;
flex-direction: row;
align-items: center;
}
.radios[data-horizontal] ::slotted(:not(:last-child)) {
margin-right: var(--omni-radio-group-horizontal-margin, 10px) !important;
}
`
];
}
override render(): TemplateResult {
return html`
${this.label ? html`` : nothing}
`;
}
}
export type CheckableElement = HTMLElement & { checked: boolean | undefined };
export type RadioChangeEventDetail = { current?: CheckableElement; previous?: CheckableElement };
declare global {
interface HTMLElementTagNameMap {
'omni-radio-group': RadioGroup;
}
}