/**
* AgnosticUI v2 SelectionButton - Core Implementation
*
* A button-styled selection control that can function as either a radio or checkbox.
* Must be used within an ag-selection-button-group.
*
* @element ag-selection-button
* @slot - Default slot for button text/content
* @csspart container - The outer clickable label
* @csspart control - The hidden input element
* @csspart indicator - The selection indicator (inline, end side)
* @csspart content - The slotted content wrapper
*/
import { LitElement, html, css, nothing } from 'lit';
import { property } from 'lit/decorators.js';
export type SelectionButtonTheme = 'success' | 'info' | 'error' | 'warning' | 'monochrome' | '';
export type SelectionButtonSize = 'sm' | 'md' | 'lg';
export type SelectionButtonShape = '' | 'rounded' | 'capsule';
export interface SelectionButtonProps {
/** Unique value for this button (required) */
value: string;
/** Accessible label for this button (required) */
label: string;
/** Whether this button is selected */
checked?: boolean;
/** Whether this button is disabled */
disabled?: boolean;
}
// Internal props set by parent group
interface SelectionButtonInternalProps {
/** Input type (set by parent group) */
_type?: 'radio' | 'checkbox';
/** Input name (set by parent group) */
_name?: string;
/** Theme (set by parent group) */
_theme?: SelectionButtonTheme;
/** Size (set by parent group) */
_size?: SelectionButtonSize;
/** Shape (set by parent group) */
_shape?: SelectionButtonShape;
}
export class AgSelectionButton extends LitElement implements SelectionButtonProps, SelectionButtonInternalProps {
static override styles = css`
:host {
display: inline-flex;
}
.selection-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--ag-space-2);
margin: 0;
font: inherit;
cursor: pointer;
transition:
background-color var(--ag-motion-fast) var(--ag-fx-ease-ease-out),
border-color var(--ag-motion-fast) var(--ag-fx-ease-ease-out),
color var(--ag-motion-fast) var(--ag-fx-ease-ease-out);
}
/* ========================================
SIZE VARIANTS
======================================== */
/* Small */
.selection-button--sm {
height: var(--ag-space-9);
min-height: var(--ag-space-9);
font-size: var(--ag-font-size-xs);
padding-inline: calc(var(--ag-space-3) + var(--ag-space-3));
--_indicator-size: 0.75rem;
--_indicator-offset: var(--ag-space-1);
}
/* Medium (default) */
.selection-button--md {
height: var(--ag-space-10);
min-height: var(--ag-space-10);
font-size: var(--ag-font-size-sm);
padding-inline: calc(var(--ag-space-4) + var(--ag-space-4));
--_indicator-size: 0.875rem;
--_indicator-offset: var(--ag-space-1);
}
/* Large */
.selection-button--lg {
height: var(--ag-space-12);
min-height: var(--ag-space-12);
font-size: var(--ag-font-size-base);
padding-inline: calc(var(--ag-space-5) + var(--ag-space-5));
--_indicator-size: 1rem;
--_indicator-offset: var(--ag-space-1);
}
/* ========================================
SHAPE VARIANTS
======================================== */
.selection-button--shape-default {
border-radius: 0;
}
.selection-button--shape-rounded {
border-radius: var(--ag-radius-md);
}
.selection-button--shape-capsule {
border-radius: var(--ag-radius-full);
/* 0.375rem is between --ag-space-1 (0.25rem) and --ag-space-2 (0.5rem) */
--_indicator-offset: 0.375rem;
}
/* ========================================
THEME VARIANTS - UNCHECKED STATE
Border/text uses theme color, transparent background
======================================== */
/* Default (primary) - unchecked */
.selection-button--default {
background: transparent;
border: 1px solid var(--ag-primary);
color: var(--ag-primary-text);
--_indicator-bg: var(--ag-primary);
}
/* Success - unchecked */
.selection-button--success {
background: transparent;
border: 1px solid var(--ag-success);
color: var(--ag-success-text);
--_indicator-bg: var(--ag-success);
}
/* Info - unchecked */
.selection-button--info {
background: transparent;
border: 1px solid var(--ag-info);
color: var(--ag-info-text);
--_indicator-bg: var(--ag-info);
}
/* Warning - unchecked */
.selection-button--warning {
background: transparent;
border: 1px solid var(--ag-warning);
color: var(--ag-warning-text);
--_indicator-bg: var(--ag-warning);
}
/* Error - unchecked */
.selection-button--error {
background: transparent;
border: 1px solid var(--ag-danger);
color: var(--ag-danger-text);
--_indicator-bg: var(--ag-danger);
}
/* Monochrome - unchecked */
.selection-button--monochrome {
background: transparent;
border: 1px solid var(--ag-text-primary);
color: var(--ag-text-primary);
--_indicator-bg: var(--ag-text-primary);
}
/* ========================================
THEME VARIANTS - CHECKED STATE
Filled background with white text
======================================== */
/* Default (primary) - checked */
.selection-button--default.selection-button--checked {
background: var(--ag-primary);
border-color: var(--ag-primary);
color: var(--ag-white);
}
/* Success - checked */
.selection-button--success.selection-button--checked {
background: var(--ag-success);
border-color: var(--ag-success);
color: var(--ag-white);
}
/* Info - checked */
.selection-button--info.selection-button--checked {
background: var(--ag-info);
border-color: var(--ag-info);
color: var(--ag-white);
}
/* Warning - checked */
.selection-button--warning.selection-button--checked {
background: var(--ag-warning);
border-color: var(--ag-warning);
color: var(--ag-neutral-900);
}
/* Error - checked */
.selection-button--error.selection-button--checked {
background: var(--ag-danger);
border-color: var(--ag-danger);
color: var(--ag-white);
}
/* Monochrome - checked */
.selection-button--monochrome.selection-button--checked {
background: var(--ag-background-primary-inverted);
border-color: var(--ag-background-primary-inverted);
color: var(--ag-text-primary-inverted);
}
/* ========================================
HOVER STATES
======================================== */
/* Default - unchecked hover (fill with theme color) */
.selection-button--default:hover:not(.selection-button--disabled):not(.selection-button--checked) {
background: var(--ag-primary);
color: var(--ag-white);
}
/* Default - checked hover (darker shade) */
.selection-button--default.selection-button--checked:hover:not(.selection-button--disabled) {
background: var(--ag-primary-dark);
border-color: var(--ag-primary-dark);
}
/* Success - unchecked hover */
.selection-button--success:hover:not(.selection-button--disabled):not(.selection-button--checked) {
background: var(--ag-success);
color: var(--ag-white);
}
/* Success - checked hover */
.selection-button--success.selection-button--checked:hover:not(.selection-button--disabled) {
background: var(--ag-success-dark);
border-color: var(--ag-success-dark);
}
/* Info - unchecked hover */
.selection-button--info:hover:not(.selection-button--disabled):not(.selection-button--checked) {
background: var(--ag-info);
color: var(--ag-white);
}
/* Info - checked hover */
.selection-button--info.selection-button--checked:hover:not(.selection-button--disabled) {
background: var(--ag-info-dark);
border-color: var(--ag-info-dark);
}
/* Warning - unchecked hover */
.selection-button--warning:hover:not(.selection-button--disabled):not(.selection-button--checked) {
background: var(--ag-warning);
color: var(--ag-neutral-900);
}
/* Warning - checked hover */
.selection-button--warning.selection-button--checked:hover:not(.selection-button--disabled) {
background: var(--ag-warning-dark);
border-color: var(--ag-warning-dark);
}
/* Error - unchecked hover */
.selection-button--error:hover:not(.selection-button--disabled):not(.selection-button--checked) {
background: var(--ag-danger);
color: var(--ag-white);
}
/* Error - checked hover */
.selection-button--error.selection-button--checked:hover:not(.selection-button--disabled) {
background: var(--ag-danger-dark);
border-color: var(--ag-danger-dark);
}
/* Monochrome - unchecked hover */
.selection-button--monochrome:hover:not(.selection-button--disabled):not(.selection-button--checked) {
background: var(--ag-background-primary-inverted);
color: var(--ag-text-primary-inverted);
}
/* Monochrome - checked hover */
.selection-button--monochrome.selection-button--checked:hover:not(.selection-button--disabled) {
background: var(--ag-background-secondary-inverted);
border-color: var(--ag-background-secondary-inverted);
}
/* ========================================
FOCUS STATE
======================================== */
.selection-button:focus-within:not(.selection-button--disabled) {
outline: var(--ag-focus-width) solid var(--ag-focus-ring-color, rgba(var(--ag-focus), 0.5));
outline-offset: var(--ag-focus-offset);
}
/* ========================================
DISABLED STATE
======================================== */
.selection-button--disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* ========================================
HIDDEN INPUT (screen reader accessible)
======================================== */
.selection-button__input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ========================================
INDICATOR (corner badge, top-right)
======================================== */
.selection-button__indicator {
position: absolute;
top: var(--_indicator-offset);
inset-inline-end: var(--_indicator-offset);
display: flex;
align-items: center;
justify-content: center;
width: var(--_indicator-size);
height: var(--_indicator-size);
opacity: 0;
transform: scale(0.6);
transition:
opacity var(--ag-motion-fast) ease-in-out,
transform var(--ag-motion-fast) var(--ag-fx-ease-ease-out);
}
.selection-button--checked .selection-button__indicator {
opacity: 1;
transform: scale(1);
}
.selection-button__indicator-svg {
width: 100%;
height: 100%;
}
/* Content wrapper */
.selection-button__content {
display: inline-flex;
align-items: center;
}
/* Visually hidden label for screen readers */
.selection-button__label-sr {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
/* ========================================
REDUCED MOTION
======================================== */
@media (prefers-reduced-motion: reduce) {
.selection-button,
.selection-button__indicator {
transition: none;
}
}
/* ========================================
HIGH CONTRAST MODE
======================================== */
@media (forced-colors: active) {
.selection-button--checked {
outline: 2px solid CanvasText;
}
.selection-button__indicator-svg circle {
fill: CanvasText;
}
.selection-button__indicator-svg path,
.selection-button__indicator-svg circle:last-child {
stroke: Canvas;
fill: Canvas;
}
}
`;
@property({ type: String, reflect: true })
declare value: string;
@property({ type: String })
declare label: string;
@property({ type: Boolean, reflect: true })
declare checked: boolean;
@property({ type: Boolean, reflect: true })
declare disabled: boolean;
// Internal props set by parent group
@property({ type: String, attribute: false })
declare _type: 'radio' | 'checkbox';
@property({ type: String, attribute: false })
declare _name: string;
@property({ type: String, attribute: false })
declare _theme: SelectionButtonTheme;
@property({ type: String, attribute: false })
declare _size: SelectionButtonSize;
@property({ type: String, attribute: false })
declare _shape: SelectionButtonShape;
constructor() {
super();
this.value = '';
this.label = '';
this.checked = false;
this.disabled = false;
this._type = 'radio';
this._name = '';
this._theme = '';
this._size = 'md';
this._shape = '';
}
override focus() {
const label = this.shadowRoot?.querySelector('.selection-button') as HTMLElement | null;
label?.focus();
}
private _handleClick(e: Event) {
// Prevent label from forwarding click to input (we handle selection ourselves)
e.preventDefault();
if (this.disabled) {
return;
}
// For radio, always select; for checkbox, toggle
const newChecked = this._type === 'radio' ? true : !this.checked;
// Dispatch event to parent group
this.dispatchEvent(new CustomEvent('selection-button-change', {
detail: {
value: this.value,
checked: newChecked,
},
bubbles: true,
composed: true,
}));
}
private _handleKeyDown(e: KeyboardEvent) {
if (this.disabled) return;
// Space or Enter to select/toggle
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._handleClick(e);
}
}
private _renderIndicator() {
if (this._type === 'checkbox') {
// Circle with checkmark for checkbox
return html`
`;
}
// Circle with dot for radio
return html`
`;
}
override render() {
// Map empty theme to 'default' for CSS class
const themeClass = this._theme || 'default';
const sizeClass = this._size || 'md';
const shapeClass = this._shape || 'default';
const containerClasses = [
'selection-button',
`selection-button--${themeClass}`,
`selection-button--${sizeClass}`,
`selection-button--shape-${shapeClass}`,
this.checked ? 'selection-button--checked' : '',
this.disabled ? 'selection-button--disabled' : '',
].filter(Boolean).join(' ');
return html`
`;
}
}