@@ -15,7 +15,7 @@ import {property, query, queryAsync, state} from 'lit/decorators.js';
1515import { classMap } from 'lit/directives/class-map.js' ;
1616import { when } from 'lit/directives/when.js' ;
1717
18- import { dispatchActivationClick , isActivationClick } from '../../controller/events.js' ;
18+ import { dispatchActivationClick , isActivationClick , redispatchEvent } from '../../controller/events.js' ;
1919import { FormController , getFormValue } from '../../controller/form-controller.js' ;
2020import { ariaProperty } from '../../decorators/aria-property.js' ;
2121import { pointerPress , shouldShowStrongFocus } from '../../focus/strong-focus.js' ;
@@ -24,6 +24,8 @@ import {MdRipple} from '../../ripple/ripple.js';
2424
2525import { SingleSelectionController } from './single-selection-controller.js' ;
2626
27+ const CHECKED = Symbol ( 'checked' ) ;
28+
2729/**
2830 * @fires checked
2931 * @soyCompatible
@@ -35,45 +37,21 @@ export class Radio extends LitElement {
3537 static formAssociated = true ;
3638
3739 @property ( { type : Boolean , reflect : true } )
38- get checked ( ) : boolean {
39- return this . _checked ;
40+ get checked ( ) {
41+ return this [ CHECKED ] ;
4042 }
41-
42- /**
43- * We define our own getter/setter for `checked` because we need to track
44- * changes to it synchronously.
45- *
46- * The order in which the `checked` property is set across radio buttons
47- * within the same group is very important. However, we can't rely on
48- * UpdatingElement's `updated` callback to observe these changes (which is
49- * also what the `@observer` decorator uses), because it batches changes to
50- * all properties.
51- *
52- * Consider:
53- *
54- * radio1.disabled = true;
55- * radio2.checked = true;
56- * radio1.checked = true;
57- *
58- * In this case we'd first see all changes for radio1, and then for radio2,
59- * and we couldn't tell that radio1 was the most recently checked.
60- */
61- set checked ( isChecked : boolean ) {
62- const oldValue = this . _checked ;
63- if ( isChecked === oldValue ) {
43+ set checked ( checked : boolean ) {
44+ const wasChecked = this . checked ;
45+ if ( wasChecked === checked ) {
6446 return ;
6547 }
66- this . _checked = isChecked ;
67- this . selectionController ?. update ( this ) ;
68-
69- this . requestUpdate ( 'checked' , oldValue ) ;
7048
71- // useful when unchecks self and wrapping element needs to synchronize
72- // TODO(b/168543810): Remove triggering event on programmatic API call.
73- this . dispatchEvent ( new Event ( 'checked' , { bubbles : true , composed : true } ) ) ;
49+ this [ CHECKED ] = checked ;
50+ this . requestUpdate ( 'checked' , wasChecked ) ;
51+ this . selectionController . handleCheckedChange ( ) ;
7452 }
7553
76- private _checked = false ; // tslint:disable-line:enforce-name-casing
54+ [ CHECKED ] = false ;
7755
7856 @property ( { type : Boolean } ) disabled = false ;
7957
@@ -94,12 +72,6 @@ export class Radio extends LitElement {
9472 */
9573 @property ( { type : Boolean } ) reducedTouchTarget = false ;
9674
97- /**
98- * input's tabindex is updated based on checked status.
99- * Tab navigation will be removed from unchecked radios.
100- */
101- @property ( { type : Number } ) formElementTabIndex = 0 ;
102-
10375 @ariaProperty // tslint:disable-line:no-new-decorators
10476 @property ( { attribute : 'data-aria-label' , noAccessor : true } )
10577 override ariaLabel ! : string ;
@@ -114,13 +86,14 @@ export class Radio extends LitElement {
11486 @state ( ) private focused = false ;
11587 @query ( 'input' ) private readonly input ! : HTMLInputElement | null ;
11688 @queryAsync ( 'md-ripple' ) private readonly ripple ! : Promise < MdRipple | null > ;
117- private selectionController ?: SingleSelectionController ;
89+ private readonly selectionController = new SingleSelectionController ( this ) ;
11890 @state ( ) private showFocusRing = false ;
11991 @state ( ) private showRipple = false ;
12092
12193 constructor ( ) {
12294 super ( ) ;
12395 this . addController ( new FormController ( this ) ) ;
96+ this . addController ( this . selectionController ) ;
12497 this . addEventListener ( 'click' , ( event : Event ) => {
12598 if ( ! isActivationClick ( event ) ) {
12699 return ;
@@ -138,38 +111,6 @@ export class Radio extends LitElement {
138111 this . input ?. focus ( ) ;
139112 }
140113
141- override connectedCallback ( ) {
142- super . connectedCallback ( ) ;
143- // Note that we must defer creating the selection controller until the
144- // element has connected, because selection controllers are keyed by the
145- // radio's shadow root. For example, if we're stamping in a lit map
146- // or repeat, then we'll be constructed before we're added to a root node.
147- //
148- // Also note if we aren't using native shadow DOM, we still need a
149- // SelectionController, because we should update checked status of other
150- // radios in the group when selection changes. It also simplifies
151- // implementation and testing to use one in all cases.
152- //
153- // eslint-disable-next-line @typescript-eslint/no-use-before-define
154- this . selectionController = SingleSelectionController . getController ( this ) ;
155- this . selectionController . register ( this ) ;
156-
157- // Radios maybe checked before connected, update selection as soon it is
158- // connected to DOM. Last checked radio button in the DOM will be selected.
159- //
160- // NOTE: If we update selection only after firstUpdate() we might mistakenly
161- // update checked status before other radios are rendered.
162- this . selectionController . update ( this ) ;
163- }
164-
165- override disconnectedCallback ( ) {
166- // The controller is initialized in connectedCallback, so if we are in
167- // disconnectedCallback then it must be initialized.
168- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
169- this . selectionController ! . unregister ( this ) ;
170- this . selectionController = undefined ;
171- }
172-
173114 override updated ( changedProperties : PropertyValues ) {
174115 if ( changedProperties . has ( 'checked' ) && this . input ) {
175116 this . input . checked = this . checked ;
@@ -198,7 +139,6 @@ export class Radio extends LitElement {
198139 < div class ="md3-radio ${ classMap ( classes ) } ">
199140 ${ this . renderFocusRing ( ) }
200141 < input
201- tabindex ="${ this . formElementTabIndex } "
202142 class ="md3-radio__native-control "
203143 type ="radio "
204144 name ="${ this . name } "
@@ -232,17 +172,14 @@ export class Radio extends LitElement {
232172 this . showFocusRing = shouldShowStrongFocus ( ) ;
233173 }
234174
235- private handleChange ( ) {
175+ private handleChange ( event : Event ) {
236176 if ( this . disabled ) {
237177 return ;
238178 }
239179
240180 // Per spec, the change event on a radio input always represents checked.
241181 this . checked = true ;
242- this . dispatchEvent ( new Event ( 'change' , {
243- bubbles : true ,
244- composed : true ,
245- } ) ) ;
182+ redispatchEvent ( this , event ) ;
246183 }
247184
248185 private handlePointerDown ( event : PointerEvent ) {
0 commit comments