11open RecoilAtoms
2+
23module Loader = {
34 @react.component
45 let make = (~cardShimmerCount ) => {
@@ -70,6 +71,16 @@ let make = (
7071 let layoutClass = CardUtils .getLayoutClass (layout )
7172 let (showMore , setShowMore ) = React .useState (_ => false )
7273 let (selectedOption , setSelectedOption ) = Recoil .useRecoilState (selectedOptionAtom )
74+
75+ // Roving-tabindex focus management for the radiogroup. The primary list and the
76+ // "more" dropdown list are each treated as their own roving group: arrow keys move
77+ // DOM focus within the rendered list and wrap at its ends. `-1` means "nothing has
78+ // been focused yet", in which case the roving-tabbable item falls back to the
79+ // selected item (else the first item).
80+ let (cardFocusedIndex , setCardFocusedIndex ) = React .useState (_ => - 1 )
81+ let (dropDownFocusedIndex , setDropDownFocusedIndex ) = React .useState (_ => - 1 )
82+ let cardItemRefs = React .useRef ([])
83+ let dropDownItemRefs = React .useRef ([])
7384 let paymentMethodListValue = Recoil .useRecoilValueFromAtom (PaymentUtils .paymentMethodListValue )
7485 let {
7586 displayInSeparateScreen ,
@@ -140,6 +151,44 @@ let make = (
140151 }
141152 }
142153
154+ // Store/clear an item's DOM element in a list's ref registry at its index.
155+ let registerItemRef = (refs : React .ref <array <Nullable .t <Dom .element >>>, index , el ) => {
156+ while refs .current -> Array .length <= index {
157+ refs .current -> Array .push (Nullable .null )-> ignore
158+ }
159+ refs .current [index ] = el
160+ }
161+
162+ // The roving-tabbable index for a list: the explicitly-focused item if any,
163+ // otherwise the selected item, otherwise the first item.
164+ let getRovingIndex = (focusedIndex , list : array <PaymentMethodsRecord .paymentFieldsInfo >) =>
165+ if focusedIndex >= 0 {
166+ focusedIndex
167+ } else {
168+ switch list -> Array .findIndex (o => o .paymentMethodName == selectedOption ) {
169+ | - 1 => 0
170+ | selectedIndex => selectedIndex
171+ }
172+ }
173+
174+ // Move DOM focus within a single list (wrapping at the ends) and update its
175+ // focused-index state so the roving tabindex follows.
176+ let moveFocus = (
177+ refs : React .ref <array <Nullable .t <Dom .element >>>,
178+ setFocusedIndex ,
179+ length ,
180+ index ,
181+ delta ,
182+ ) =>
183+ if length > 0 {
184+ let nextIndex = mod (mod (index + delta , length ) + length , length )
185+ setFocusedIndex (_ => nextIndex )
186+ refs .current
187+ -> Array .get (nextIndex )
188+ -> Option .flatMap (Nullable .toOption )
189+ -> Option .forEach (el => el -> AccessibilityUtils .focus )
190+ }
191+
143192 React .useEffect0 (() => {
144193 let shouldAutoOpenSavedMethods =
145194 ! layoutClass .savedMethodCustomization .defaultCollapsed &&
@@ -155,6 +204,8 @@ let make = (
155204 <div className = "w-full" >
156205 <div
157206 className = "AccordionContainer flex flex-col overflow-auto no-scrollbar"
207+ role = "radiogroup"
208+ ariaLabel = {localeString .paymentMethodsGroupLabel }
158209 style = {
159210 marginTop : themeObj .spacingAccordionItem ,
160211 width : "-webkit-fill-available" ,
@@ -173,6 +224,17 @@ let make = (
173224 borderBottom = {(! showMore &&
174225 i == cardOptionDetails -> Array .length - 1 &&
175226 ! layoutClass .spacedAccordionItems ) || layoutClass .spacedAccordionItems }
227+ index = i
228+ isFocused = {i == getRovingIndex (cardFocusedIndex , cardOptionDetails )}
229+ registerItemRef = {registerItemRef (cardItemRefs , ... )}
230+ onArrowNav = {(index , delta ) =>
231+ moveFocus (
232+ cardItemRefs ,
233+ setCardFocusedIndex ,
234+ cardOptionDetails -> Array .length ,
235+ index ,
236+ delta ,
237+ )}
176238 />
177239 })
178240 -> React .array }
@@ -190,6 +252,17 @@ let make = (
190252 borderRadiusStyle = {borderRadiusStyle }
191253 borderBottom = {(i == dropDownOptionsDetails -> Array .length - 1 &&
192254 ! layoutClass .spacedAccordionItems ) || layoutClass .spacedAccordionItems }
255+ index = i
256+ isFocused = {i == getRovingIndex (dropDownFocusedIndex , dropDownOptionsDetails )}
257+ registerItemRef = {registerItemRef (dropDownItemRefs , ... )}
258+ onArrowNav = {(index , delta ) =>
259+ moveFocus (
260+ dropDownItemRefs ,
261+ setDropDownFocusedIndex ,
262+ dropDownOptionsDetails -> Array .length ,
263+ index ,
264+ delta ,
265+ )}
193266 />
194267 })
195268 -> React .array }
@@ -198,6 +271,9 @@ let make = (
198271 <RenderIf condition = {! showMore && dropDownOptionsDetails -> Array .length > 0 }>
199272 <button
200273 className = "AccordionMore flex overflow-auto no-scrollbar"
274+ type_ = "button"
275+ ariaExpanded = false
276+ ariaLabel = {localeString .morePaymentMethodsLabel }
201277 onClick = {_ => setShowMore (_ => ! showMore )}
202278 style = {
203279 borderRadius : themeObj .borderRadius ,
0 commit comments