import { FocusMonitor , FocusOrigin } from '@angular/cdk/a11y' ; import { BooleanInput , coerceBooleanProperty , coerceNumberProperty } from '@angular/cdk/coercion' ; import { UniqueSelectionDispatcher } from '@angular/cdk/collections' ; import { AfterContentInit , AfterViewInit , Attribute , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChildren , Directive , ElementRef , EventEmitter , forwardRef , HostBinding , Inject , InjectionToken , Input , OnDestroy , OnInit , Optional , Output , QueryList , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ; import { HasTabIndex , mixinTabIndex , TypeRef } from '@sbb-esta/angular/core' ; let nextUniqueId = 0 ; docs-private export const SBB_RADIO_GROUP_CONTROL_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => SbbRadioGroup ) , multi : true , } ; /** Change event object emitted by SbbRadio and SbbRadioGroup. */ export class SbbRadioChange { constructor ( /** The SbbRadioButton that emits the change event. */ public source , /** The value of the SbbRadioButton. */ public value ) { } } /**
 * Injection token that can be used to inject instances of `SbbRadioGroup`. It serves as
 * alternative token to the actual `SbbRadioGroup` class which could cause unnecessary
 * retention of the class and its component metadata.
 */ export const SBB_RADIO_GROUP = new InjectionToken < _SbbRadioGroupBase < _SbbRadioButtonBase > > ( 'SbbRadioGroup' ) ; /**
 * Injection token that can be used to inject instances of `_SbbRadioButtonBase`
 * implementations. It serves as alternative token to the actual `_SbbRadioButtonBase`
 * class which could cause unnecessary retention of the class and its component metadata.
 */ export const SBB_RADIO_BUTTON = new InjectionToken < _SbbRadioButtonBase > ( 'SbbRadioButton' ) ; @ Directive ( ) export abstract class _SbbRadioGroupBase < TRadio extends _SbbRadioButtonBase > implements AfterContentInit , ControlValueAccessor { /** Name of the radio button group. All radio buttons inside this group will use this name. */ @ Input ( ) get name ( ) : string { return this . _name ; } set name ( value ) { this . _name = value ; this . _updateRadioButtonNames ( ) ; } /**
     * Value for the radio-group. Should equal the value of the selected radio button if there is
     * a corresponding radio button with a matching value. If there is not such a corresponding
     * radio button, this value persists to be applied in case a new radio button is added with a
     * matching value.
     */ @ Input ( ) get value ( ) : any { return this . _value ; } set value ( newValue ) { if ( this . _value !== newValue ) { this . _value = newValue ; this . _updateSelectedRadioFromValue ( ) ; this . _checkSelectedRadioButton ( ) ; } } /**
     * The currently selected radio button. If set to a new radio button, the radio group value
     * will be updated to match the new selected button.
     */ @ Input ( ) get selected ( ) : TRadio | null { return this . _selected ; } set selected ( selected ) { this . _selected = selected ; this . value = selected ? selected . value : null ; this . _checkSelectedRadioButton ( ) ; } /** Whether the radio group is disabled */ @ Input ( ) get disabled ( ) : boolean { return this . _disabled ; } set disabled ( value ) { this . _disabled = coerceBooleanProperty ( value ) ; this . _markRadiosForCheck ( ) ; } /** Whether the radio group is required */ @ Input ( ) get required ( ) : boolean { return this . _required ; } set required ( value ) { this . _required = coerceBooleanProperty ( value ) ; this . _markRadiosForCheck ( ) ; } /**
     * Event emitted when the group value changes.
     * Change events are only emitted when the value changes due to user interaction with
     * a radio button (the same behavior as `<input type-"radio">`).
     */ @ Output ( ) readonly change : EventEmitter < SbbRadioChange > = new EventEmitter < SbbRadioChange > ( ) ; /** Child radio buttons. */ @ ContentChildren ( forwardRef ( ( ) => SBB_RADIO_BUTTON ) , { descendants : true } ) _radios : QueryList < TRadio > ; /** Selected value for the radio group. */ private _value : any = null ; /** The HTML name attribute applied to radio buttons in this group. */ private _name = `sbb-radio-group-${ nextUniqueId ++ }` ; /** The currently selected radio button. Should match value. */ private _selected : TRadio | null = null ; /** Whether the `value` has been set to its initial value. */ private _isInitialized = false ; /** Whether the radio group is disabled. */ private _disabled = false ; /** Whether the radio group is required. */ private _required = false ; /** `View -> model callback called when value changes` */ _controlValueAccessorChangeFn : ( value : any ) => void = ( ) => { } ; /** `View -> model callback called when radio group has been touched` */ _onTouched : ( ) => any = ( ) => { } ; constructor ( protected _changeDetector ) { } _checkSelectedRadioButton ( ) { if ( this . _selected && ! this . _selected . checked ) { this . _selected . checked = true ; } } /**
     * Initialize properties once content children are available.
     * This allows us to propagate relevant attributes to associated buttons.
     */ ngAfterContentInit ( ) { this . _isInitialized = true ; } /**
     * Mark this group as being "touched" (for ngModel). Meant to be called by the contained
     * radio buttons upon their blur.
     */ _touch ( ) { if ( this . _onTouched ) { this . _onTouched ( ) ; } } /** Dispatch change event with current selection and group value. */ _emitChangeEvent ( ) { if ( this . _isInitialized ) { this . change . emit ( new SbbRadioChange ( this . _selected ! , this . _value ) ) ; } } _markRadiosForCheck ( ) { if ( this . _radios ) { this . _radios . forEach ( ( radio ) => radio . _markForCheck ( ) ) ; } } /** Sets the model value. Implemented as part of ControlValueAccessor. */ writeValue ( value ) { this . value = value ; this . _changeDetector . markForCheck ( ) ; } param fn registerOnChange ( fn ) { this . _controlValueAccessorChangeFn = fn ; } param fn registerOnTouched ( fn ) { this . _onTouched = fn ; } param isDisabled setDisabledState ( isDisabled ) { this . disabled = isDisabled ; this . _changeDetector . markForCheck ( ) ; } private _updateRadioButtonNames ( ) { if ( this . _radios ) { this . _radios . forEach ( ( radio ) => { radio . name = this . name ; radio . _markForCheck ( ) ; } ) ; } } /** Updates the `selected` radio button from the internal _value state. */ private _updateSelectedRadioFromValue ( ) { const isAlreadySelected = this . _selected !== null && this . _selected . value === this . _value ; if ( this . _radios && ! isAlreadySelected ) { this . _selected = null ; this . _radios . forEach ( ( radio ) => { radio . checked = this . value === radio . value ; if ( radio . checked ) { this . _selected = radio ; } } ) ; } } } @ Directive ( { selector : 'sbb-radio-group' , exportAs : 'sbbRadioGroup' , providers : [ SBB_RADIO_GROUP_CONTROL_VALUE_ACCESSOR , { provide : SBB_RADIO_GROUP , useExisting : SbbRadioGroup } , ] , host : { class : 'sbb-radio-group' , role : 'radiogroup' , } , } ) export class SbbRadioGroup < TRadio extends _SbbRadioButtonBase = SbbRadioButton > extends _SbbRadioGroupBase < TRadio > { @ ContentChildren ( forwardRef ( ( ) => SBB_RADIO_BUTTON ) , { descendants : true } ) override _radios : QueryList < TRadio > ; } docs-private abstract class RadioButtonBase { abstract disabled : boolean ; } const _RadioButtonMixinBase = mixinTabIndex ( RadioButtonBase ) ; let nextId = 0 ; @ Directive ( ) export class _SbbRadioButtonBase extends _RadioButtonMixinBase implements OnInit , AfterViewInit , OnDestroy , HasTabIndex { private _uniqueId = `sbb-radio-button-${ ++ nextId }` ; /** The id of this component. */ @ Input ( ) @ HostBinding ( 'attr.id' ) id : string = this . _uniqueId ; /** Analog to HTML 'name' attribute used to group radios for unique selection. */ @ Input ( ) name : string ; /** Used to set the 'aria-label' attribute on the underlying input element. */ @ Input ( 'aria-label' ) ariaLabel : string ; /** The 'aria-labelledby' attribute takes precedence as the element's text alternative. */ @ Input ( 'aria-labelledby' ) ariaLabelledby : string ; /** The 'aria-describedby' attribute is read after the element's label and field type. */ @ Input ( 'aria-describedby' ) ariaDescribedby : string ; /** Whether this radio button is checked. */ @ Input ( ) get checked ( ) : boolean { return this . _checked ; } set checked ( value ) { const newCheckedState = coerceBooleanProperty ( value ) ; if ( this . _checked !== newCheckedState ) { this . _checked = newCheckedState ; if ( newCheckedState && this . radioGroup && this . radioGroup . value !== this . value ) { this . radioGroup . selected = this ; } else if ( ! newCheckedState && this . radioGroup && this . radioGroup . value === this . value ) { this . radioGroup . selected = null ; } if ( newCheckedState ) { this . _radioDispatcher . notify ( this . id , this . name ) ; } this . _changeDetector . markForCheck ( ) ; } } /** The value of this radio button. */ @ Input ( ) get value ( ) : any { return this . _value ; } set value ( value ) { if ( this . _value !== value ) { this . _value = value ; if ( this . radioGroup !== null ) { if ( ! this . checked ) { this . checked = this . radioGroup . value === value ; } if ( this . checked ) { this . radioGroup . selected = this ; } } } } /** Whether the radio button is disabled. */ @ Input ( ) get disabled ( ) : boolean { return this . _disabled || ( this . radioGroup !== null && this . radioGroup . disabled ) ; } set disabled ( value ) { this . _setDisabled ( coerceBooleanProperty ( value ) ) ; } /** Whether the radio button is required. */ @ Input ( ) get required ( ) : boolean { return this . _required || ( this . radioGroup && this . radioGroup . required ) ; } set required ( value ) { this . _required = coerceBooleanProperty ( value ) ; } /**
     * Event emitted when the checked state of this radio button changes.
     * Change events are only emitted when the value changes due to user interaction with
     * the radio button (the same behavior as `<input type-"radio">`).
     */ @ Output ( ) readonly change : EventEmitter < SbbRadioChange > = new EventEmitter < SbbRadioChange > ( ) ; /** The parent radio group. May or may not be present. */ radioGroup : _SbbRadioGroupBase < _SbbRadioButtonBase > ; /** Id for the inner input field. */ get inputId ( ) { return `${ this . id || this . _uniqueId }-input` ; } /** Whether this radio is checked. */ private _checked : boolean = false ; /** Whether this radio is disabled. */ private _disabled : boolean ; /** Whether this radio is required. */ private _required : boolean ; /** Value assigned to this radio. */ private _value : any = null ; /** Unregister function for _radioDispatcher */ private _removeUniqueSelectionListener : ( ) => void = ( ) => { } ; /** The native `<input type=radio>` element */ @ ViewChild ( 'input' ) _inputElement : ElementRef < HTMLInputElement > ; constructor ( radioGroup , private _elementRef , protected readonly _changeDetector , private _focusMonitor , private _radioDispatcher , tabIndex ? ) { super ( ) ; this . radioGroup = radioGroup ; if ( tabIndex ) { this . tabIndex = coerceNumberProperty ( tabIndex , 0 ) ; } this . _removeUniqueSelectionListener = _radioDispatcher . listen ( ( id , name ) => { if ( id !== this . id && name === this . name ) { this . checked = false ; } } ) ; } /** Focuses the radio button. */ focus ( options ? , origin ? ) { if ( origin ) { this . _focusMonitor . focusVia ( this . _inputElement , origin , options ) ; } else { this . _inputElement . nativeElement . focus ( options ) ; } } docs-private _markForCheck ( ) { this . _changeDetector . markForCheck ( ) ; } ngOnInit ( ) { if ( this . radioGroup ) { this . checked = this . radioGroup . value === this . _value ; if ( this . checked ) { this . radioGroup . selected = this ; } this . name = this . radioGroup . name ; } } ngAfterViewInit ( ) { this . _focusMonitor . monitor ( this . _elementRef , true ) . subscribe ( ( focusOrigin ) => { if ( ! focusOrigin && this . radioGroup ) { this . radioGroup . _touch ( ) ; } } ) ; } ngOnDestroy ( ) { this . _focusMonitor . stopMonitoring ( this . _elementRef ) ; this . _removeUniqueSelectionListener ( ) ; } /** Dispatch change event with current value. */ private _emitChangeEvent ( ) { this . change . emit ( new SbbRadioChange ( this , this . _value ) ) ; } docs-private _onInputClick ( event ) { event . stopPropagation ( ) ; } docs-private _onInputChange ( event ? ) { event ?. stopPropagation ( ) ; const groupValueChanged = this . radioGroup && this . value !== this . radioGroup . value ; this . checked = true ; this . _emitChangeEvent ( ) ; if ( this . radioGroup ) { this . radioGroup . _controlValueAccessorChangeFn ( this . value ) ; if ( groupValueChanged ) { this . radioGroup . _emitChangeEvent ( ) ; } } } /** Sets the disabled state and marks for check if a change occurred. */ protected _setDisabled ( value ) { if ( this . _disabled !== value ) { this . _disabled = value ; this . _changeDetector . markForCheck ( ) ; } } } @ Component ( { selector : 'sbb-radio-button' , templateUrl : './radio-button.html' , inputs : [ 'tabIndex' ] , encapsulation : ViewEncapsulation . None , exportAs : 'sbbRadioButton' , host : { class : 'sbb-radio-button sbb-selection-item' , '[class.sbb-selection-checked]' : 'checked' , '[class.sbb-selection-disabled]' : 'disabled' , '[attr.tabindex]' : 'null' , '[attr.id]' : 'id' , '[attr.aria-label]' : 'null' , '[attr.aria-labelledby]' : 'null' , '[attr.aria-describedby]' : 'null' , '(focus)' : '_inputElement.nativeElement.focus()' , } , changeDetection : ChangeDetectionStrategy . OnPush , providers : [ { provide : SBB_RADIO_BUTTON , useExisting : SbbRadioButton } ] , } ) export class SbbRadioButton extends _SbbRadioButtonBase { constructor ( @ Optional ( ) @ Inject ( SBB_RADIO_GROUP ) radioGroup , elementRef , changeDetector , focusMonitor , radioDispatcher , @ Attribute ( 'tabindex' ) tabIndex ? ) { super ( radioGroup , elementRef , changeDetector , focusMonitor , radioDispatcher , tabIndex ) ; } }