@@ -6,7 +6,7 @@ use bevy_ecs::{
66 entity:: Entity ,
77 hierarchy:: { ChildOf , Children } ,
88 observer:: On ,
9- query:: { Has , With } ,
9+ query:: { Has , With , Without } ,
1010 reflect:: ReflectComponent ,
1111 system:: { Commands , Query } ,
1212} ;
@@ -39,12 +39,18 @@ use crate::ValueChange;
3939#[ require( AccessibilityNode ( accesskit:: Node :: new( Role :: RadioGroup ) ) ) ]
4040pub struct RadioGroup ;
4141
42- /// Headless widget implementation for radio buttons. These should be enclosed within a
43- /// [`RadioGroup`] widget, which is responsible for the mutual exclusion logic.
42+ /// Headless widget implementation for radio buttons. They can be used independently,
43+ /// but enclosing them in a [`RadioGroup`] widget allows them to behave as a single,
44+ /// mutually exclusive unit.
4445///
4546/// According to the WAI-ARIA best practices document, radio buttons should not be focusable,
4647/// but rather the enclosing group should be focusable.
4748/// See <https://www.w3.org/WAI/ARIA/apg/patterns/radio>/
49+ ///
50+ /// The widget emits a [`ValueChange<bool>`] event with the value `true` whenever it becomes checked,
51+ /// either through a mouse click or when a [`RadioGroup`] checks the widget.
52+ /// If the [`RadioButton`] is focusable, it can also be checked using the `Enter` or `Space` keys,
53+ /// in which case the event will likewise be emitted.
4854#[ derive( Component , Debug ) ]
4955#[ require( AccessibilityNode ( accesskit:: Node :: new( Role :: RadioButton ) ) , Checkable ) ]
5056#[ derive( Reflect ) ]
@@ -132,7 +138,12 @@ fn radio_group_on_key_input(
132138
133139 let ( next_id, _) = radio_buttons[ next_index] ;
134140
135- // Trigger the on_change event for the newly checked radio button
141+ // Trigger the value change event on the radio button
142+ commands. trigger ( ValueChange :: < bool > {
143+ source : next_id,
144+ value : true ,
145+ } ) ;
146+ // Trigger the on_change event for the newly checked radio button on radio group
136147 commands. trigger ( ValueChange :: < Entity > {
137148 source : ev. focused_entity ,
138149 value : next_id,
@@ -141,82 +152,101 @@ fn radio_group_on_key_input(
141152 }
142153}
143154
144- fn radio_group_on_button_click (
145- mut ev : On < Pointer < Click > > ,
155+ // Provides functionality for standalone focusable [`RadioButton`] to react
156+ // on `Space` or `Enter` key press.
157+ fn radio_button_on_key_input (
158+ mut ev : On < FocusedInput < KeyboardInput > > ,
159+ q_radio_button : Query < Has < Checked > , ( With < RadioButton > , Without < InteractionDisabled > ) > ,
146160 q_group : Query < ( ) , With < RadioGroup > > ,
147- q_radio : Query < ( Has < Checked > , Has < InteractionDisabled > ) , With < RadioButton > > ,
148161 q_parents : Query < & ChildOf > ,
149- q_children : Query < & Children > ,
150162 mut commands : Commands ,
151163) {
152- if q_group. contains ( ev. entity ) {
153- // Starting with the original target, search upward for a radio button.
154- let radio_id = if q_radio. contains ( ev. original_event_target ( ) ) {
155- ev. original_event_target ( )
156- } else {
157- // Search ancestors for the first radio button
158- let mut found_radio = None ;
159- for ancestor in q_parents. iter_ancestors ( ev. original_event_target ( ) ) {
160- if q_group. contains ( ancestor) {
161- // We reached a radio group before finding a radio button, bail out
162- return ;
163- }
164- if q_radio. contains ( ancestor) {
165- found_radio = Some ( ancestor) ;
166- break ;
167- }
168- }
164+ let Ok ( checked) = q_radio_button. get ( ev. focused_entity ) else {
165+ // Not a radio button
166+ return ;
167+ } ;
168+
169+ // Radio button already checked
170+ if checked {
171+ return ;
172+ }
169173
170- match found_radio {
171- Some ( radio) => radio,
172- None => return , // No radio button found in the ancestor chain
173- }
174- } ;
174+ let event = & ev. event ( ) . input ;
175+ if event. state == ButtonState :: Pressed
176+ && !event. repeat
177+ && ( event. key_code == KeyCode :: Enter || event. key_code == KeyCode :: Space )
178+ {
179+ ev. propagate ( false ) ;
175180
176- // Radio button is disabled.
177- if q_radio. get ( radio_id) . unwrap ( ) . 1 {
178- return ;
179- }
181+ trigger_radio_button_and_radio_group_value_change (
182+ ev. focused_entity ,
183+ & q_group,
184+ & q_parents,
185+ & mut commands,
186+ ) ;
187+ }
188+ }
180189
181- // Gather all the enabled radio group descendants for exclusion.
182- let radio_buttons = q_children
183- . iter_descendants ( ev. entity )
184- . filter_map ( |child_id| match q_radio. get ( child_id) {
185- Ok ( ( checked, false ) ) => Some ( ( child_id, checked) ) ,
186- Ok ( ( _, true ) ) | Err ( _) => None ,
187- } )
188- . collect :: < Vec < _ > > ( ) ;
189-
190- if radio_buttons. is_empty ( ) {
191- return ; // No enabled radio buttons in the group
192- }
190+ fn radio_button_on_click (
191+ mut ev : On < Pointer < Click > > ,
192+ q_group : Query < ( ) , With < RadioGroup > > ,
193+ q_radio : Query < Has < Checked > , ( With < RadioButton > , Without < InteractionDisabled > ) > ,
194+ q_parents : Query < & ChildOf > ,
195+ mut commands : Commands ,
196+ ) {
197+ let Ok ( checked) = q_radio. get ( ev. entity ) else {
198+ // Not a radio button
199+ return ;
200+ } ;
193201
194- // Pick out the radio button that is currently checked.
195- ev. propagate ( false ) ;
196- let current_radio = radio_buttons
197- . iter ( )
198- . find ( |( _, checked) | * checked)
199- . map ( |( id, _) | * id) ;
200-
201- if current_radio == Some ( radio_id) {
202- // If they clicked the currently checked radio button, do nothing
203- return ;
204- }
202+ ev. propagate ( false ) ;
203+
204+ // Radio button is already checked
205+ if checked {
206+ return ;
207+ }
205208
206- // Trigger the on_change event for the newly checked radio button
209+ trigger_radio_button_and_radio_group_value_change (
210+ ev. entity ,
211+ & q_group,
212+ & q_parents,
213+ & mut commands,
214+ ) ;
215+ }
216+
217+ fn trigger_radio_button_and_radio_group_value_change (
218+ radio_button : Entity ,
219+ q_group : & Query < ( ) , With < RadioGroup > > ,
220+ q_parents : & Query < & ChildOf > ,
221+ commands : & mut Commands ,
222+ ) {
223+ commands. trigger ( ValueChange :: < bool > {
224+ source : radio_button,
225+ value : true ,
226+ } ) ;
227+
228+ // Find if radio button is inside radio group
229+ let radio_group = q_parents
230+ . iter_ancestors ( radio_button)
231+ . find ( |ancestor| q_group. contains ( * ancestor) ) ;
232+
233+ // If is inside radio group
234+ if let Some ( radio_group) = radio_group {
235+ // Trigger event for radio group
207236 commands. trigger ( ValueChange :: < Entity > {
208- source : ev . entity ,
209- value : radio_id ,
237+ source : radio_group ,
238+ value : radio_button ,
210239 } ) ;
211240 }
212241}
213242
214- /// Plugin that adds the observers for the [`RadioGroup`] widget.
243+ /// Plugin that adds the observers for [`RadioButton`] and [`RadioGroup`] widget.
215244pub struct RadioGroupPlugin ;
216245
217246impl Plugin for RadioGroupPlugin {
218247 fn build ( & self , app : & mut App ) {
219248 app. add_observer ( radio_group_on_key_input)
220- . add_observer ( radio_group_on_button_click) ;
249+ . add_observer ( radio_button_on_click)
250+ . add_observer ( radio_button_on_key_input) ;
221251 }
222252}
0 commit comments