Skip to content

Commit 5f8b75b

Browse files
fix(compass-connections-navigation): fix for action button not being tabbable when tabbing through navigation tree (#5923)
1 parent e3473fa commit 5f8b75b

File tree

6 files changed

+97
-24
lines changed

6 files changed

+97
-24
lines changed

packages/compass-connections-navigation/src/base-navigation-item.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type NavigationBaseItemProps = {
2626

2727
canExpand: boolean;
2828
isExpanded: boolean;
29+
isFocused: boolean;
2930
onExpand: (toggle: boolean) => void;
3031

3132
actionProps: {
@@ -58,6 +59,7 @@ export const NavigationBaseItem = ({
5859
dataAttributes,
5960
canExpand,
6061
isExpanded,
62+
isFocused,
6163
onExpand,
6264
}: NavigationBaseItemProps) => {
6365
const [hoverProps, isHovered] = useHoverState();
@@ -88,7 +90,7 @@ export const NavigationBaseItem = ({
8890
</ItemLabel>
8991
</ItemButtonWrapper>
9092
<ItemActionControls<Actions>
91-
isVisible={isActive || isHovered}
93+
isVisible={isActive || isHovered || isFocused}
9294
data-testid="sidebar-navigation-item-actions"
9395
iconSize="small"
9496
{...actionProps}

packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,39 @@ describe('ConnectionsNavigationTree', function () {
252252
});
253253
});
254254

255+
it('should render the action items for the tabbed navigation item', async function () {
256+
await renderConnectionsNavigationTree({
257+
expanded: {},
258+
activeWorkspace: null,
259+
});
260+
261+
// Tab to the first element
262+
userEvent.tab();
263+
await waitFor(() => {
264+
// Virtual list will be the one to grab the focus first, but will
265+
// immediately forward it to the element and mocking raf here breaks
266+
// virtual list implementatin, waitFor is to accomodate for that
267+
expect(document.querySelector('[data-id="connection_ready"]')).to.eq(
268+
document.activeElement
269+
);
270+
return true;
271+
});
272+
let tabbedItem = screen.getByTestId('connection_ready');
273+
expect(within(tabbedItem).getByLabelText('Show actions')).to.be.visible;
274+
275+
// Go down to the second element
276+
userEvent.keyboard('{arrowdown}');
277+
await waitFor(() => {
278+
expect(document.querySelector('[data-id="connection_initial"]')).to.eq(
279+
document.activeElement
280+
);
281+
return true;
282+
});
283+
284+
tabbedItem = screen.getByTestId('connection_initial');
285+
expect(within(tabbedItem).getByLabelText('Show actions')).to.be.visible;
286+
});
287+
255288
describe('when connection is writable', function () {
256289
it('should show all connection actions', async function () {
257290
await renderConnectionsNavigationTree();

packages/compass-connections-navigation/src/connections-navigation-tree.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,18 @@ const ConnectionsNavigationTree: React.FunctionComponent<
160160
onDefaultAction={onDefaultAction}
161161
onExpandedChange={onItemExpand}
162162
getItemKey={(item) => item.id}
163-
renderItem={({ item }) => (
164-
<NavigationItem
165-
item={item}
166-
activeItemId={activeItemId}
167-
getItemActions={getItemActions}
168-
onItemExpand={onItemExpand}
169-
onItemAction={onItemAction}
170-
/>
171-
)}
163+
renderItem={({ item, isActive, isFocused }) => {
164+
return (
165+
<NavigationItem
166+
item={item}
167+
isActive={isActive}
168+
isFocused={isFocused}
169+
getItemActions={getItemActions}
170+
onItemExpand={onItemExpand}
171+
onItemAction={onItemAction}
172+
/>
173+
);
174+
}}
172175
/>
173176
)}
174177
</AutoSizer>

packages/compass-connections-navigation/src/navigation-item.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { ConnectionStatus } from '@mongodb-js/compass-connections/provider';
1212

1313
type NavigationItemProps = {
1414
item: SidebarTreeItem;
15-
activeItemId?: string;
15+
isActive: boolean;
16+
isFocused: boolean;
1617
getItemActions: (item: SidebarTreeItem) => NavigationItemActions;
1718
onItemAction: (
1819
item: SidebarActionableItem,
@@ -23,7 +24,8 @@ type NavigationItemProps = {
2324

2425
export function NavigationItem({
2526
item,
26-
activeItemId,
27+
isActive,
28+
isFocused,
2729
onItemAction,
2830
onItemExpand,
2931
getItemActions,
@@ -119,7 +121,8 @@ export function NavigationItem({
119121
<PlaceholderItem level={item.level} />
120122
) : (
121123
<NavigationBaseItem
122-
isActive={item.id === activeItemId}
124+
isActive={isActive}
125+
isFocused={isFocused}
123126
isExpanded={!!item.isExpanded}
124127
icon={itemIcon}
125128
name={item.name}

packages/compass-connections-navigation/src/virtual-list/use-virtual-navigation-tree.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function useVirtualNavigationTree<T extends HTMLElement = HTMLElement>({
130130
activeItemId?: string;
131131
onExpandedChange(item: VirtualTreeItem, isExpanded: boolean): void;
132132
onFocusMove?: (item: VirtualTreeItem) => void;
133-
}): [React.HTMLProps<T>, string | undefined] {
133+
}): [React.HTMLProps<T>, string | undefined, boolean] {
134134
const rootRef = useRef<T | null>(null);
135135
const activeId = activeItemId || findFirstItem(items)?.id;
136136
const [currentTabbable, setCurrentTabbable] = useState(activeId);
@@ -303,5 +303,7 @@ export function useVirtualNavigationTree<T extends HTMLElement = HTMLElement>({
303303
...focusProps,
304304
};
305305

306-
return [rootProps, currentTabbable];
306+
const isTreeItemFocused = focusState === FocusState.FocusWithinVisible;
307+
308+
return [rootProps, currentTabbable, isTreeItemFocused];
307309
}

packages/compass-connections-navigation/src/virtual-list/virtual-list.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ function useDefaultAction<T extends VirtualTreeItem>(
5050
}
5151

5252
type NotPlaceholderTreeItem<T> = T extends { type: 'placeholder' } ? never : T;
53-
type RenderItem<T> = (props: { index: number; item: T }) => React.ReactNode;
53+
type RenderItem<T> = (props: {
54+
index: number;
55+
isActive: boolean;
56+
isFocused: boolean;
57+
item: T;
58+
}) => React.ReactNode;
5459
export type OnDefaultAction<T> = (
5560
item: T,
5661
evt: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>
@@ -113,25 +118,33 @@ export function VirtualTree<T extends VirtualItem>({
113118
},
114119
[items]
115120
);
116-
const [rootProps, currentTabbable] = useVirtualNavigationTree<HTMLDivElement>(
117-
{
121+
const [rootProps, currentTabbable, isTreeItemFocused] =
122+
useVirtualNavigationTree<HTMLDivElement>({
118123
items,
119124
activeItemId,
120125
onExpandedChange,
121126
onFocusMove,
122-
}
123-
);
127+
});
124128

125129
const id = useId();
126130

127131
const itemData = useMemo(() => {
128132
return {
129133
items,
130134
currentTabbable,
135+
isTreeItemFocused,
136+
activeItemId,
131137
renderItem,
132138
onDefaultAction,
133139
};
134-
}, [items, renderItem, currentTabbable, onDefaultAction]);
140+
}, [
141+
items,
142+
renderItem,
143+
currentTabbable,
144+
onDefaultAction,
145+
activeItemId,
146+
isTreeItemFocused,
147+
]);
135148

136149
const getItemKey = useCallback(
137150
(index: number, data: VirtualItemData<T>) => {
@@ -169,7 +182,9 @@ export function VirtualTree<T extends VirtualItem>({
169182

170183
type VirtualItemData<T extends VirtualItem> = {
171184
items: T[];
185+
isTreeItemFocused: boolean;
172186
currentTabbable?: string;
187+
activeItemId?: string;
173188
renderItem: RenderItem<T>;
174189
onDefaultAction: OnDefaultAction<NotPlaceholderTreeItem<T>>;
175190
};
@@ -178,13 +193,28 @@ function TreeItem<T extends VirtualItem>({
178193
data,
179194
style,
180195
}: ListChildComponentProps<VirtualItemData<T>>) {
181-
const { renderItem, items } = data;
196+
const { renderItem, items, activeItemId } = data;
182197
const item = useMemo(() => items[index], [items, index]);
183198
const focusRingProps = useFocusRing();
184199

185200
const component = useMemo(() => {
186-
return renderItem({ index, item });
187-
}, [renderItem, index, item]);
201+
return renderItem({
202+
index,
203+
item,
204+
isActive: !isPlaceholderItem(item) && item.id === activeItemId,
205+
isFocused:
206+
data.isTreeItemFocused &&
207+
!isPlaceholderItem(item) &&
208+
item.id === data.currentTabbable,
209+
});
210+
}, [
211+
renderItem,
212+
index,
213+
item,
214+
activeItemId,
215+
data.currentTabbable,
216+
data.isTreeItemFocused,
217+
]);
188218

189219
const actionProps = useDefaultAction(
190220
item as NotPlaceholderTreeItem<T>,

0 commit comments

Comments
 (0)