11import { LitElement , html , type TemplateResult } from 'lit' ;
22import { customElement } from 'lit/decorators/custom-element.js' ;
3- import styles from './pf-radio.css' ;
43import { property } from 'lit/decorators/property.js' ;
4+ import { observes } from '@patternfly/pfe-core/decorators/observes.js' ;
5+ import { state } from 'lit/decorators/state.js' ;
6+
7+ import styles from './pf-radio.css' ;
58
69export class PfRadioChangeEvent extends Event {
710 constructor ( public event : Event , public value : string ) {
@@ -16,46 +19,89 @@ export class PfRadioChangeEvent extends Event {
1619@customElement ( 'pf-radio' )
1720export class PfRadio extends LitElement {
1821 static readonly styles : CSSStyleSheet [ ] = [ styles ] ;
22+
1923 static formAssociated = true ;
24+
2025 static shadowRootOptions : ShadowRootInit = {
2126 ...LitElement . shadowRootOptions ,
2227 delegatesFocus : true ,
2328 } ;
2429
25- @property ( {
26- type : Boolean ,
27- attribute : 'checked' ,
28- converter : {
29- fromAttribute : value => value === 'true' ,
30- } ,
31- reflect : true ,
32- } )
30+ @property ( { type : Boolean , reflect : true } )
3331 checked = false ;
3432
35- @property ( {
36- type : Boolean ,
37- attribute : 'disabled' ,
38- converter : {
39- fromAttribute : value => value === 'true' ,
40- } ,
41- reflect : true ,
42- } )
33+ @property ( { type : Boolean , reflect : true } )
4334 disabled = false ;
4435
45- @property ( { attribute : 'name' , reflect : true } ) name = '' ;
46- @property ( { attribute : 'label' , reflect : true } ) label ?: string ;
47- @property ( { attribute : 'value' , reflect : true } ) value = '' ;
48- @property ( { attribute : 'id' , reflect : true } ) id = '' ;
49- @property ( { attribute : 'tabindex' , reflect : true } ) tabIndex = - 1 ;
36+ @property ( { reflect : true } ) name = '' ;
37+
38+ @property ( { reflect : true } ) label ?: string ;
39+
40+ @property ( { reflect : true } ) value = '' ;
41+
42+ @state ( ) private focusable = false ;
5043
51- constructor ( ) {
52- super ( ) ;
44+ /** Radio groups: instances.get(groupName).forEach(pfRadio => { ... }) */
45+ private static instances = new Map < string , Set < PfRadio > > ( ) ;
46+
47+ private static selected = new Map < string , PfRadio > ;
48+
49+ static {
50+ globalThis . addEventListener ( 'keydown' , e => {
51+ switch ( e . key ) {
52+ case 'Tab' :
53+ this . instances . forEach ( ( radioSet , groupName ) => {
54+ const selected = this . selected . get ( groupName ) ;
55+ [ ...radioSet ] . forEach ( ( radio , i , radios ) => {
56+ // the radio group has a selected element
57+ // it should be the only focusable member of the group
58+ if ( selected ) {
59+ radio . focusable = radio === selected ;
60+ // when Shift-tabbing into a group, only the last member should be selected
61+ } else if ( e . shiftKey ) {
62+ radio . focusable = radio === radios . at ( - 1 ) ;
63+ // otherwise, the first member must be focusable
64+ } else {
65+ radio . focusable = i === 0 ;
66+ }
67+ } ) ;
68+ } ) ;
69+ break ;
70+ }
71+ } ) ;
5372 }
5473
5574 connectedCallback ( ) : void {
5675 super . connectedCallback ( ) ;
5776 this . addEventListener ( 'keydown' , this . #onKeydown) ;
58- document . addEventListener ( 'keydown' , this . #onKeyPress) ;
77+ }
78+
79+ @observes ( 'checked' )
80+ protected checkedChanged ( ) : void {
81+ if ( this . checked ) {
82+ PfRadio . selected . set ( this . name , this ) ;
83+ }
84+ }
85+
86+ @observes ( 'name' )
87+ protected nameChanged ( oldName : string ) : void {
88+ // reset the map of groupname to selected radio button
89+ if ( PfRadio . selected . get ( oldName ) === this ) {
90+ PfRadio . selected . delete ( oldName ) ;
91+ PfRadio . selected . set ( this . name , this ) ;
92+ }
93+ if ( typeof oldName === 'string' ) {
94+ PfRadio . instances . get ( oldName ) ?. delete ( this ) ;
95+ }
96+ if ( ! PfRadio . instances . has ( this . name ) ) {
97+ PfRadio . instances . set ( this . name , new Set ( ) ) ;
98+ }
99+ PfRadio . instances . get ( this . name ) ?. add ( this ) ;
100+ }
101+
102+ disconnectedCallback ( ) : void {
103+ PfRadio . instances . get ( this . name ) ?. delete ( this ) ;
104+ super . disconnectedCallback ( ) ;
59105 }
60106
61107 #onRadioButtonClick( event : Event ) {
@@ -66,44 +112,17 @@ export class PfRadio extends LitElement {
66112 radioGroup = root . querySelectorAll ( 'pf-radio' ) ;
67113 radioGroup . forEach ( ( radio : PfRadio ) => {
68114 const element : HTMLElement = radio as HTMLElement ;
115+ // avoid removeAttribute: set checked property instead
116+ // even better: listen for `change` on the shadow input,
117+ // and recalculate state from there.
69118 element ?. removeAttribute ( 'checked' ) ;
70- element . tabIndex = - 1 ;
71119 } ) ;
72120 this . checked = true ;
73- this . tabIndex = 0 ;
74121 this . dispatchEvent ( new PfRadioChangeEvent ( event , this . value ) ) ;
75122 }
76123 }
77124 }
78125
79- // Function to handle tab key navigation
80- #onKeyPress = ( event : KeyboardEvent ) => {
81- const root : Node = this . getRootNode ( ) ;
82- if ( root instanceof Document || root instanceof ShadowRoot ) {
83- const radioGroup : NodeListOf < PfRadio > = root . querySelectorAll ( 'pf-radio' ) ;
84- const isRadioChecked : boolean = Array . from ( radioGroup ) . some (
85- ( radio : PfRadio ) => radio . checked
86- ) ;
87- if ( event . key === 'Tab' ) {
88- radioGroup . forEach ( ( radio : PfRadio ) => {
89- radio . tabIndex = radio . checked ? 0 : - 1 ;
90- } ) ;
91- if ( ! isRadioChecked ) {
92- radioGroup . forEach ( ( radio : PfRadio , index : number ) => {
93- radio . tabIndex = - 1 ;
94- if ( event . shiftKey ) {
95- if ( index === radioGroup . length - 1 ) {
96- radio . tabIndex = 0 ;
97- }
98- } else if ( index === 0 ) {
99- radio . tabIndex = 0 ;
100- }
101- } ) ;
102- }
103- }
104- }
105- } ;
106-
107126 // Function to handle keyboard navigation
108127 #onKeydown = ( event : KeyboardEvent ) => {
109128 const arrowKeys : string [ ] = [ 'ArrowDown' , 'ArrowRight' , 'ArrowUp' , 'ArrowLeft' ] ;
@@ -113,7 +132,6 @@ export class PfRadio extends LitElement {
113132 const radioGroup : NodeListOf < PfRadio > = root . querySelectorAll ( 'pf-radio' ) ;
114133 radioGroup . forEach ( ( radio : PfRadio , index : number ) => {
115134 this . checked = false ;
116- this . tabIndex = 0 ;
117135
118136 if ( radio === event . target ) {
119137 const isArrowDownOrRight : boolean = [ 'ArrowDown' , 'ArrowRight' ] . includes ( event . key ) ;
@@ -125,6 +143,9 @@ export class PfRadio extends LitElement {
125143 const nextIndex : number = ( index + direction + radioGroup . length ) % radioGroup . length ;
126144 radioGroup [ nextIndex ] . focus ( ) ;
127145 radioGroup [ nextIndex ] . checked = true ;
146+ // TODO: move this to an @observes
147+ // consider the api of this event.
148+ // do we add the group to it? do we fire from every element on every change?
128149 this . dispatchEvent ( new PfRadioChangeEvent ( event , radioGroup [ nextIndex ] . value ) ) ;
129150 }
130151 } ) ;
@@ -135,15 +156,15 @@ export class PfRadio extends LitElement {
135156 render ( ) : TemplateResult < 1 > {
136157 return html `
137158 < input
159+ id ="radio "
160+ type ="radio "
138161 @click =${ this . #onRadioButtonClick}
139- id =${ this . id }
140162 .name =${ this . name }
141- type='radio'
142163 value=${ this . value }
143- tabindex=${ this . tabIndex }
164+ tabindex=${ this . focusable ? 0 : - 1 }
144165 .checked=${ this . checked }
145- / >
146- < label for =${ this . id } > ${ this . label } </ label >
166+ >
167+ < label for =" radio " > ${ this . label } </ label >
147168 ` ;
148169 }
149170}
0 commit comments