Skip to content

Commit b9811ed

Browse files
authored
[react-interactions] Add wrapping support to FocusList/FocusTable (#16903)
1 parent 49b0cb6 commit b9811ed

4 files changed

Lines changed: 160 additions & 34 deletions

File tree

‎packages/react-interactions/accessibility/src/FocusList.js‎

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ import {useKeyboard} from 'react-interactions/events/keyboard';
1515

1616
typeFocusItemProps={
1717
children?: React.Node,
18+
onKeyDown?: KeyboardEvent=>void,
1819
};
1920

2021
typeFocusListProps={|
2122
children: React.Node,
2223
portrait: boolean,
24+
wrap?: boolean,
2325
|};
2426

2527
const{useRef}=React;
2628

27-
functionfocusListItem(cell: ReactScopeMethods): void{
29+
functionfocusListItem(cell: ReactScopeMethods,event: KeyboardEvent): void{
2830
consttabbableNodes=cell.getScopedNodes();
2931
if(tabbableNodes!==null&&tabbableNodes.length>0){
3032
tabbableNodes[0].focus();
33+
event.preventDefault();
3134
}
3235
}
3336

@@ -38,7 +41,10 @@ function getPreviousListItem(
3841
constitems=list.getChildren();
3942
if(items!==null){
4043
constcurrentItemIndex=items.indexOf(currentItem);
41-
if(currentItemIndex>0){
44+
constwrap=getListWrapProp(currentItem);
45+
if(currentItemIndex===0&&wrap){
46+
returnitems[items.length-1]||null;
47+
}elseif(currentItemIndex>0){
4248
returnitems[currentItemIndex-1]||null;
4349
}
4450
}
@@ -52,25 +58,38 @@ function getNextListItem(
5258
constitems=list.getChildren();
5359
if(items!==null){
5460
constcurrentItemIndex=items.indexOf(currentItem);
55-
if(currentItemIndex!==-1&&currentItemIndex!==items.length-1){
61+
constwrap=getListWrapProp(currentItem);
62+
constend=currentItemIndex===items.length-1;
63+
if(end&&wrap){
64+
returnitems[0]||null;
65+
}elseif(currentItemIndex!==-1&&!end){
5666
returnitems[currentItemIndex+1]||null;
5767
}
5868
}
5969
returnnull;
6070
}
6171

72+
functiongetListWrapProp(currentItem: ReactScopeMethods): boolean{
73+
constlist=currentItem.getParent();
74+
if(list!==null){
75+
constlistProps=list.getProps();
76+
return(listProps.type==='list'&&listProps.wrap)||false;
77+
}
78+
returnfalse;
79+
}
80+
6281
exportfunctioncreateFocusList(scope: ReactScope): Array<React.Component>{
6382
constTableScope=React.unstable_createScope(scope.fn);
6483

65-
functionList({children, portrait}): FocusListProps{
84+
functionList({children, portrait, wrap}): FocusListProps{
6685
return(
67-
<TableScopetype="list"portrait={portrait}>
86+
<TableScopetype="list"portrait={portrait}wrap={wrap}>
6887
{children}
6988
</TableScope>
7089
);
7190
}
7291

73-
functionItem({children}): FocusItemProps{
92+
functionItem({children, onKeyDown}): FocusItemProps{
7493
constscopeRef=useRef(null);
7594
constkeyboard=useKeyboard({
7695
onKeyDown(event: KeyboardEvent): void{
@@ -88,8 +107,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
88107
currentItem,
89108
);
90109
if(previousListItem){
91-
event.preventDefault();
92-
focusListItem(previousListItem);
110+
focusListItem(previousListItem,event);
93111
return;
94112
}
95113
}
@@ -99,8 +117,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
99117
if(portrait){
100118
constnextListItem=getNextListItem(list,currentItem);
101119
if(nextListItem){
102-
event.preventDefault();
103-
focusListItem(nextListItem);
120+
focusListItem(nextListItem,event);
104121
return;
105122
}
106123
}
@@ -113,8 +130,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
113130
currentItem,
114131
);
115132
if(previousListItem){
116-
event.preventDefault();
117-
focusListItem(previousListItem);
133+
focusListItem(previousListItem,event);
118134
return;
119135
}
120136
}
@@ -124,8 +140,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
124140
if(!portrait){
125141
constnextListItem=getNextListItem(list,currentItem);
126142
if(nextListItem){
127-
event.preventDefault();
128-
focusListItem(nextListItem);
143+
focusListItem(nextListItem,event);
129144
return;
130145
}
131146
}
@@ -134,6 +149,9 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
134149
}
135150
}
136151
}
152+
if(onKeyDown){
153+
onKeyDown(event);
154+
}
137155
event.continuePropagation();
138156
},
139157
});

‎packages/react-interactions/accessibility/src/FocusTable.js‎

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {useKeyboard} from 'react-interactions/events/keyboard';
1515

1616
typeFocusCellProps={
1717
children?: React.Node,
18+
onKeyDown?: KeyboardEvent=>void,
1819
};
1920

2021
typeFocusRowProps={
@@ -28,6 +29,7 @@ type FocusTableProps = {|
2829
direction: 'left'|'right'|'up'|'down',
2930
focusTableByID: (id: string)=>void,
3031
)=>void,
32+
wrap?: boolean,
3133
|};
3234

3335
const{useRef}=React;
@@ -54,19 +56,26 @@ export function focusFirstCellOnTable(table: ReactScopeMethods): void {
5456
}
5557
}
5658

57-
functionfocusCell(cell: ReactScopeMethods): void{
59+
functionfocusCell(cell: ReactScopeMethods,event?: KeyboardEvent): void{
5860
const tabbableNodes =cell.getScopedNodes();
5961
if(tabbableNodes!==null&&tabbableNodes.length>0){
6062
tabbableNodes[0].focus();
63+
if(event){
64+
event.preventDefault();
65+
}
6166
}
6267
}
6368

64-
functionfocusCellByIndex(row: ReactScopeMethods,cellIndex: number): void{
69+
functionfocusCellByIndex(
70+
row: ReactScopeMethods,
71+
cellIndex: number,
72+
event?: KeyboardEvent,
73+
): void{
6574
const cells =row.getChildren();
6675
if(cells!==null){
6776
const cell=cells[cellIndex];
6877
if(cell){
69-
focusCell(cell);
78+
focusCell(cell,event);
7079
}
7180
}
7281
}
@@ -130,12 +139,27 @@ function triggerNavigateOut(
130139
event.continuePropagation();
131140
}
132141

142+
functiongetTableWrapProp(currentCell: ReactScopeMethods): boolean{
143+
constrow=currentCell.getParent();
144+
if(row!==null&&row.getProps().type==='row'){
145+
consttable=row.getParent();
146+
if(table!==null){
147+
returntable.getProps().wrap||false;
148+
}
149+
}
150+
returnfalse;
151+
}
152+
133153
exportfunctioncreateFocusTable(scope: ReactScope): Array<React.Component>{
134154
constTableScope=React.unstable_createScope(scope.fn);
135155

136-
functionTable({children, onKeyboardOut, id}): FocusTableProps{
156+
functionTable({children, onKeyboardOut, id, wrap}): FocusTableProps{
137157
return(
138-
<TableScopetype="table"onKeyboardOut={onKeyboardOut}id={id}>
158+
<TableScope
159+
type="table"
160+
onKeyboardOut={onKeyboardOut}
161+
id={id}
162+
wrap={wrap}>
139163
{children}
140164
</TableScope>
141165
);
@@ -145,7 +169,7 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
145169
return<TableScopetype="row">{children}</TableScope>;
146170
}
147171

148-
functionCell({children}): FocusCellProps{
172+
functionCell({children, onKeyDown}): FocusCellProps{
149173
constscopeRef=useRef(null);
150174
constkeyboard=useKeyboard({
151175
onKeyDown(event: KeyboardEvent): void{
@@ -162,10 +186,15 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
162186
if(rows!==null){
163187
if(rowIndex>0){
164188
constrow=rows[rowIndex-1];
165-
focusCellByIndex(row,cellIndex);
166-
event.preventDefault();
189+
focusCellByIndex(row,cellIndex,event);
167190
}elseif(rowIndex===0){
168-
triggerNavigateOut(currentCell,'up',event);
191+
const wrap=getTableWrapProp(currentCell);
192+
if(wrap){
193+
const row=rows[rows.length-1];
194+
focusCellByIndex(row,cellIndex,event);
195+
}else{
196+
triggerNavigateOut(currentCell,'up',event);
197+
}
169198
}
170199
}
171200
}
@@ -178,11 +207,16 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
178207
if(rows!==null){
179208
if(rowIndex!==-1){
180209
if(rowIndex===rows.length-1){
181-
triggerNavigateOut(currentCell,'down',event);
210+
constwrap=getTableWrapProp(currentCell);
211+
if(wrap){
212+
constrow=rows[0];
213+
focusCellByIndex(row,cellIndex,event);
214+
}else{
215+
triggerNavigateOut(currentCell,'down',event);
216+
}
182217
}else{
183218
constrow=rows[rowIndex+1];
184-
focusCellByIndex(row,cellIndex);
185-
event.preventDefault();
219+
focusCellByIndex(row,cellIndex,event);
186220
}
187221
}
188222
}
@@ -196,7 +230,12 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
196230
focusCell(cells[rowIndex-1]);
197231
event.preventDefault();
198232
}elseif(rowIndex===0){
199-
triggerNavigateOut(currentCell,'left',event);
233+
constwrap=getTableWrapProp(currentCell);
234+
if(wrap){
235+
focusCell(cells[cells.length-1],event);
236+
}else{
237+
triggerNavigateOut(currentCell, 'left',event);
238+
}
200239
}
201240
}
202241
return;
@@ -206,16 +245,23 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
206245
if(cells!==null){
207246
if(rowIndex!==-1){
208247
if(rowIndex===cells.length-1){
209-
triggerNavigateOut(currentCell,'right',event);
248+
constwrap=getTableWrapProp(currentCell);
249+
if(wrap){
250+
focusCell(cells[0],event);
251+
}else{
252+
triggerNavigateOut(currentCell,'right', event);
253+
}
210254
}else{
211-
focusCell(cells[rowIndex+1]);
212-
event.preventDefault();
255+
focusCell(cells[rowIndex+1],event);
213256
}
214257
}
215258
}
216259
return;
217260
}
218261
}
262+
if(onKeyDown){
263+
onKeyDown(event);
264+
}
219265
},
220266
});
221267
return(

‎packages/react-interactions/accessibility/src/__tests__/FocusList-test.internal.js‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ describe('FocusList', () => {
4343
functioncreateFocusListComponent(){
4444
const[FocusList,FocusItem]=createFocusList(TabbableScope);
4545

46-
return({portrait})=>(
47-
<FocusListportrait={portrait}>
46+
return({portrait, wrap})=>(
47+
<FocusListportrait={portrait}wrap={wrap}>
4848
<ul>
4949
<FocusItem>
5050
<litabIndex={0}>Item 1</li>
@@ -125,5 +125,36 @@ describe('FocusList', () => {
125125
});
126126
expect(document.activeElement.textContent).toBe('Item 3');
127127
});
128+
129+
it('handles keyboard arrow operations (portrait) with wrapping enabled',()=>{
130+
constTest=createFocusListComponent();
131+
132+
ReactDOM.render(<Testportrait={true}wrap={true}/>,container);
133+
constlistItems=document.querySelectorAll('li');
134+
letfirstListItem=createEventTarget(listItems[0]);
135+
firstListItem.focus();
136+
firstListItem.keydown({
137+
key: 'ArrowDown',
138+
});
139+
expect(document.activeElement.textContent).toBe('Item 2');
140+
141+
constsecondListItem=createEventTarget(document.activeElement);
142+
secondListItem.keydown({
143+
key: 'ArrowDown',
144+
});
145+
expect(document.activeElement.textContent).toBe('Item 3');
146+
147+
constthirdListItem=createEventTarget(document.activeElement);
148+
thirdListItem.keydown({
149+
key: 'ArrowDown',
150+
});
151+
expect(document.activeElement.textContent).toBe('Item 1');
152+
153+
firstListItem=createEventTarget(document.activeElement);
154+
firstListItem.keydown({
155+
key: 'ArrowUp',
156+
});
157+
expect(document.activeElement.textContent).toBe('Item 3');
158+
});
128159
});
129160
});

‎packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ describe('FocusTable', () => {
4545
TabbableScope,
4646
);
4747

48-
return({onKeyboardOut, id})=>(
49-
<FocusTableonKeyboardOut={onKeyboardOut}id={id}>
48+
return({onKeyboardOut, id, wrap})=>(
49+
<FocusTableonKeyboardOut={onKeyboardOut}id={id}wrap={wrap}>
5050
<table>
5151
<tbody>
5252
<FocusTableRow>
@@ -326,5 +326,36 @@ describe('FocusTable', () => {
326326
});
327327
expect(document.activeElement.placeholder).toBe('B1');
328328
});
329+
330+
it('handles keyboard arrow operations with wrapping enabled',()=>{
331+
constTest=createFocusTableComponent();
332+
333+
ReactDOM.render(<Testwrap={true}/>,container);
334+
constbuttons=document.querySelectorAll('button');
335+
leta1=createEventTarget(buttons[0]);
336+
a1.focus();
337+
a1.keydown({
338+
key: 'ArrowRight',
339+
});
340+
expect(document.activeElement.textContent).toBe('A2');
341+
342+
consta2=createEventTarget(document.activeElement);
343+
a2.keydown({
344+
key: 'ArrowRight',
345+
});
346+
expect(document.activeElement.textContent).toBe('A3');
347+
348+
consta3=createEventTarget(document.activeElement);
349+
a3.keydown({
350+
key: 'ArrowRight',
351+
});
352+
expect(document.activeElement.textContent).toBe('A1');
353+
354+
a1=createEventTarget(document.activeElement);
355+
a1.keydown({
356+
key: 'ArrowLeft',
357+
});
358+
expect(document.activeElement.textContent).toBe('A3');
359+
});
329360
});
330361
});

0 commit comments

Comments
 (0)