Skip to content

Commit 55580b8

Browse files
authored
[LG-2957] feat: GuideCue - Update Button Focus (#3277)
* [LG-2957] feat: GuideCue - Update Button Focus * comment cleanup * udpated changelog, udpated test assertion
1 parent d8485a9 commit 55580b8

File tree

5 files changed

+80
-26
lines changed

5 files changed

+80
-26
lines changed

.changeset/real-lamps-teach.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/guide-cue': patch
3+
---
4+
5+
The GuideCue is updated for the focus to be on the primary button instead of the close button when opened for an improved UX

packages/guide-cue/src/GuideCue/GuideCue.spec.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,20 @@ describe('packages/guide-cue', () => {
185185
const body = getByText(guideCueChildren);
186186
expect(body).toBeInTheDocument();
187187
});
188+
189+
test('primary button is focusable for focus trap targeting', async () => {
190+
const { getByRole } = renderGuideCue({
191+
open: true,
192+
});
193+
194+
const primaryButton = getByRole('button', { name: buttonTextDefault });
195+
expect(primaryButton).toBeInTheDocument();
196+
197+
userEvent.tab();
198+
await waitFor(() => {
199+
expect(primaryButton).toHaveFocus();
200+
});
201+
});
188202
});
189203

190204
describe('Multi-step tooltip', () => {
@@ -352,5 +366,24 @@ describe('packages/guide-cue', () => {
352366
const numOfButtons = getAllByRole('button').length;
353367
await waitFor(() => expect(numOfButtons).toEqual(2));
354368
});
369+
370+
test('primary button is focusable for focus trap targeting', async () => {
371+
const { getByRole } = renderGuideCue({
372+
open: true,
373+
numberOfSteps: 2,
374+
currentStep: 1,
375+
});
376+
await act(async () => {
377+
await waitForTimeout(timeout1);
378+
});
379+
380+
const primaryButton = getByRole('button', { name: buttonTextDefault });
381+
expect(primaryButton).toBeInTheDocument();
382+
383+
userEvent.tab();
384+
await waitFor(() => {
385+
expect(primaryButton).toHaveFocus();
386+
});
387+
});
355388
});
356389
});

packages/guide-cue/src/GuideCue/GuideCue.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function GuideCue({
5353
: 'Next';
5454

5555
/**
56-
* Determines if the stand-alone tooltip should be shown. If there are multiple steps the multip-step tooltip will be shown.
56+
* Determines if the stand-alone tooltip should be shown. If there are multiple steps the multi-step tooltip will be shown.
5757
*/
5858
const isStandalone = numberOfSteps <= 1;
5959

@@ -66,7 +66,7 @@ function GuideCue({
6666
setPopoverOpen(true);
6767
openTimeout = setTimeout(
6868
() =>
69-
// React 18 automatically batches all updates which appears to break the opening transition. flushSync prevents this state update from automically batching. Instead updates are made synchronously.
69+
// React 18 automatically batches all updates which appears to break the opening transition. flushSync prevents this state update from automatically batching. Instead updates are made synchronously.
7070
flushSync(() => {
7171
// tooltip opens a little after the beacon opens
7272
setTooltipOpen(true);
@@ -77,7 +77,7 @@ function GuideCue({
7777
// Adding a timeout to the popover because if we close both the tooltip and the popover at the same time the transition is not visible. Only applies to multi-step tooltip.
7878
// tooltip closes first
7979
setTooltipOpen(false);
80-
// beacon closes a little after the tooltip cloese
80+
// beacon closes a little after the tooltip close
8181
closeTimeout = setTimeout(() => setPopoverOpen(false), timeout2);
8282
}
8383

@@ -143,7 +143,7 @@ function GuideCue({
143143
// this is using the reference from the `refEl` prop to position itself against
144144
<GuideCueTooltip {...tooltipContentProps}>{children}</GuideCueTooltip>
145145
) : (
146-
// Multistep tooltip
146+
// Multi-step tooltip
147147
<>
148148
<Popover
149149
active={popoverOpen}

packages/guide-cue/src/GuideCueTooltip/GuideCueTooltip.styles.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { css } from '@leafygreen-ui/emotion';
1+
import { css, cx } from '@leafygreen-ui/emotion';
22
import { Theme } from '@leafygreen-ui/lib';
33
import { palette } from '@leafygreen-ui/palette';
44

@@ -19,13 +19,13 @@ export const buttonStyles = css`
1919
height: 28px;
2020
`;
2121

22-
export const closeStyles = css`
22+
const closeStyles = css`
2323
position: absolute;
2424
top: 10px;
2525
right: 10px;
2626
`;
2727

28-
export const closeHoverStyles = css`
28+
const closeHoverStyles = css`
2929
color: ${palette.gray.dark2};
3030
&:hover,
3131
&:active {
@@ -35,6 +35,11 @@ export const closeHoverStyles = css`
3535
}
3636
`;
3737

38+
export const getCloseButtonStyle = (isDarkMode?: boolean) =>
39+
cx(closeStyles, {
40+
[closeHoverStyles]: isDarkMode,
41+
});
42+
3843
export const contentStyles = css`
3944
margin-bottom: 16px;
4045
`;
@@ -55,10 +60,23 @@ export const stepStyles: Record<Theme, string> = {
5560
`,
5661
};
5762

58-
export const tooltipMultistepStyles = css`
63+
const tooltipMultiStepStyles = css`
5964
padding: 32px 16px 16px;
6065
`;
6166

62-
export const tooltipStyles = css`
67+
const tooltipStyles = css`
6368
cursor: auto;
6469
`;
70+
71+
export const getTooltipStyles = ({
72+
isStandalone,
73+
tooltipClassName,
74+
}: {
75+
isStandalone?: boolean;
76+
tooltipClassName?: string;
77+
}) =>
78+
cx(
79+
{ [tooltipMultiStepStyles]: !isStandalone },
80+
tooltipStyles,
81+
tooltipClassName,
82+
);

packages/guide-cue/src/GuideCueTooltip/GuideCueTooltip.tsx

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import React from 'react';
1+
import React, { useRef } from 'react';
22
import { Options } from 'focus-trap';
33
import FocusTrap from 'focus-trap-react';
44

55
import { Button } from '@leafygreen-ui/button';
6-
import { cx } from '@leafygreen-ui/emotion';
7-
import { useIdAllocator } from '@leafygreen-ui/hooks';
86
import XIcon from '@leafygreen-ui/icon/dist/X';
97
import { IconButton } from '@leafygreen-ui/icon-button';
108
import { Theme } from '@leafygreen-ui/lib';
@@ -17,20 +15,21 @@ import {
1715
bodyThemeStyles,
1816
bodyTitleStyles,
1917
buttonStyles,
20-
closeHoverStyles,
21-
closeStyles,
2218
contentStyles,
2319
footerStyles,
20+
getCloseButtonStyle,
21+
getTooltipStyles,
2422
stepStyles,
25-
tooltipMultistepStyles,
26-
tooltipStyles,
2723
} from './GuideCueTooltip.styles';
2824

2925
const ariaLabelledby = 'guide-cue-label';
3026
const ariaDescribedby = 'guide-cue-desc';
3127

32-
const focusTrapOptions: Options = {
28+
const getFocusTrapOptions = (
29+
buttonRef: React.RefObject<HTMLButtonElement>,
30+
): Options => ({
3331
clickOutsideDeactivates: true,
32+
initialFocus: () => buttonRef.current || false,
3433
checkCanFocusTrap: async trapContainers => {
3534
const results = trapContainers.map(trapContainer => {
3635
return new Promise<void>(resolve => {
@@ -45,7 +44,7 @@ const focusTrapOptions: Options = {
4544
// Return a promise that resolves when all the trap containers are able to receive focus
4645
return Promise.all(results).then(() => undefined);
4746
},
48-
};
47+
});
4948

5049
type GuideCueTooltipProps = Partial<GuideCueProps> & {
5150
theme: Theme;
@@ -77,7 +76,7 @@ function GuideCueTooltip({
7776
handleCloseClick,
7877
...tooltipProps
7978
}: GuideCueTooltipProps) {
80-
const focusId = useIdAllocator({ prefix: 'guide-cue' });
79+
const primaryButtonRef = useRef<HTMLButtonElement>(null);
8180

8281
return (
8382
<>
@@ -88,22 +87,21 @@ function GuideCueTooltip({
8887
justify={tooltipJustify}
8988
align={tooltipAlign}
9089
refEl={refEl}
91-
className={cx(
92-
{ [tooltipMultistepStyles]: !isStandalone },
93-
tooltipStyles,
90+
className={getTooltipStyles({
91+
isStandalone,
9492
tooltipClassName,
95-
)}
93+
})}
9694
onClose={onEscClose}
9795
role="dialog"
9896
aria-labelledby={ariaLabelledby}
9997
renderMode={RenderMode.TopLayer}
10098
{...tooltipProps}
10199
>
102-
<FocusTrap focusTrapOptions={focusTrapOptions}>
100+
<FocusTrap focusTrapOptions={getFocusTrapOptions(primaryButtonRef)}>
103101
<div>
104102
{!isStandalone && (
105103
<IconButton
106-
className={cx(closeStyles, { [closeHoverStyles]: darkMode })}
104+
className={getCloseButtonStyle(darkMode)}
107105
aria-label="Close Tooltip"
108106
onClick={handleCloseClick}
109107
darkMode={!darkMode}
@@ -135,11 +133,11 @@ function GuideCueTooltip({
135133
</Disclaimer>
136134
)}
137135
<Button
136+
ref={primaryButtonRef}
138137
variant="primary"
139138
onClick={() => handleButtonClick()}
140139
darkMode={!darkMode}
141140
className={buttonStyles}
142-
id={focusId}
143141
>
144142
{buttonText}
145143
</Button>

0 commit comments

Comments
 (0)