Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/real-lamps-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/guide-cue': patch
---

This PR updates the GuideCue focus to be on the primary button instead of the close button when opened for an improved UX
33 changes: 33 additions & 0 deletions packages/guide-cue/src/GuideCue/GuideCue.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,20 @@ describe('packages/guide-cue', () => {
const body = getByText(guideCueChildren);
expect(body).toBeInTheDocument();
});

test('primary button is focusable for focus trap targeting', async () => {
const { getByRole } = renderGuideCue({
open: true,
});

await waitFor(() => {
const primaryButton = getByRole('button', { name: buttonTextDefault });
// Verify the button exists and is in the document
expect(primaryButton).toBeInTheDocument();
// Verify button can receive focus (tabindex is not -1)
expect(primaryButton).not.toHaveAttribute('tabindex', '-1');
});
});
});

describe('Multi-step tooltip', () => {
Expand Down Expand Up @@ -352,5 +366,24 @@ describe('packages/guide-cue', () => {
const numOfButtons = getAllByRole('button').length;
await waitFor(() => expect(numOfButtons).toEqual(2));
});

test('primary button is focusable for focus trap targeting', async () => {
const { getByRole } = renderGuideCue({
open: true,
numberOfSteps: 2,
currentStep: 1,
});
await act(async () => {
await waitForTimeout(timeout1);
});

await waitFor(() => {
const primaryButton = getByRole('button', { name: buttonTextDefault });
// Verify the button exists and is in the document
expect(primaryButton).toBeInTheDocument();
// Verify button can receive focus (tabindex is not -1)
expect(primaryButton).not.toHaveAttribute('tabindex', '-1');
});
});
});
});
8 changes: 4 additions & 4 deletions packages/guide-cue/src/GuideCue/GuideCue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function GuideCue({
: 'Next';

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

Expand All @@ -66,7 +66,7 @@ function GuideCue({
setPopoverOpen(true);
openTimeout = setTimeout(
() =>
// 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.
// 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.
flushSync(() => {
// tooltip opens a little after the beacon opens
setTooltipOpen(true);
Expand All @@ -77,7 +77,7 @@ function GuideCue({
// 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.
// tooltip closes first
setTooltipOpen(false);
// beacon closes a little after the tooltip cloese
// beacon closes a little after the tooltip close
closeTimeout = setTimeout(() => setPopoverOpen(false), timeout2);
}

Expand Down Expand Up @@ -143,7 +143,7 @@ function GuideCue({
// this is using the reference from the `refEl` prop to position itself against
<GuideCueTooltip {...tooltipContentProps}>{children}</GuideCueTooltip>
) : (
// Multistep tooltip
// Multi-step tooltip
<>
<Popover
active={popoverOpen}
Expand Down
28 changes: 23 additions & 5 deletions packages/guide-cue/src/GuideCueTooltip/GuideCueTooltip.styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { css } from '@leafygreen-ui/emotion';
import { css, cx } from '@leafygreen-ui/emotion';
import { Theme } from '@leafygreen-ui/lib';
import { palette } from '@leafygreen-ui/palette';

Expand All @@ -19,13 +19,13 @@ export const buttonStyles = css`
height: 28px;
`;

export const closeStyles = css`
const closeStyles = css`
position: absolute;
top: 10px;
right: 10px;
`;

export const closeHoverStyles = css`
const closeHoverStyles = css`
color: ${palette.gray.dark2};
&:hover,
&:active {
Expand All @@ -35,6 +35,11 @@ export const closeHoverStyles = css`
}
`;

export const getCloseButtonStyle = (isDarkMode?: boolean) =>
cx(closeStyles, {
[closeHoverStyles]: isDarkMode,
});

export const contentStyles = css`
margin-bottom: 16px;
`;
Expand All @@ -55,10 +60,23 @@ export const stepStyles: Record<Theme, string> = {
`,
};

export const tooltipMultistepStyles = css`
const tooltipMultiStepStyles = css`
padding: 32px 16px 16px;
`;

export const tooltipStyles = css`
const tooltipStyles = css`
cursor: auto;
`;

export const getTooltipStyles = ({
isStandalone,
tooltipClassName,
}: {
isStandalone?: boolean;
tooltipClassName?: string;
}) =>
cx(
{ [tooltipMultiStepStyles]: !isStandalone },
tooltipStyles,
tooltipClassName,
);
32 changes: 15 additions & 17 deletions packages/guide-cue/src/GuideCueTooltip/GuideCueTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React from 'react';
import React, { useRef } from 'react';
import { Options } from 'focus-trap';
import FocusTrap from 'focus-trap-react';

import { Button } from '@leafygreen-ui/button';
import { cx } from '@leafygreen-ui/emotion';
import { useIdAllocator } from '@leafygreen-ui/hooks';
import XIcon from '@leafygreen-ui/icon/dist/X';
import { IconButton } from '@leafygreen-ui/icon-button';
import { Theme } from '@leafygreen-ui/lib';
Expand All @@ -17,20 +15,21 @@ import {
bodyThemeStyles,
bodyTitleStyles,
buttonStyles,
closeHoverStyles,
closeStyles,
contentStyles,
footerStyles,
getCloseButtonStyle,
getTooltipStyles,
stepStyles,
tooltipMultistepStyles,
tooltipStyles,
} from './GuideCueTooltip.styles';

const ariaLabelledby = 'guide-cue-label';
const ariaDescribedby = 'guide-cue-desc';

const focusTrapOptions: Options = {
const getFocusTrapOptions = (
buttonRef: React.RefObject<HTMLButtonElement>,
): Options => ({
clickOutsideDeactivates: true,
initialFocus: () => buttonRef.current || false,
checkCanFocusTrap: async trapContainers => {
const results = trapContainers.map(trapContainer => {
return new Promise<void>(resolve => {
Expand All @@ -45,7 +44,7 @@ const focusTrapOptions: Options = {
// Return a promise that resolves when all the trap containers are able to receive focus
return Promise.all(results).then(() => undefined);
},
};
});

type GuideCueTooltipProps = Partial<GuideCueProps> & {
theme: Theme;
Expand Down Expand Up @@ -77,7 +76,7 @@ function GuideCueTooltip({
handleCloseClick,
...tooltipProps
}: GuideCueTooltipProps) {
const focusId = useIdAllocator({ prefix: 'guide-cue' });
const primaryButtonRef = useRef<HTMLButtonElement>(null);

return (
<>
Expand All @@ -88,22 +87,21 @@ function GuideCueTooltip({
justify={tooltipJustify}
align={tooltipAlign}
refEl={refEl}
className={cx(
{ [tooltipMultistepStyles]: !isStandalone },
tooltipStyles,
className={getTooltipStyles({
isStandalone,
tooltipClassName,
)}
})}
onClose={onEscClose}
role="dialog"
aria-labelledby={ariaLabelledby}
renderMode={RenderMode.TopLayer}
{...tooltipProps}
>
<FocusTrap focusTrapOptions={focusTrapOptions}>
<FocusTrap focusTrapOptions={getFocusTrapOptions(primaryButtonRef)}>
<div>
{!isStandalone && (
<IconButton
className={cx(closeStyles, { [closeHoverStyles]: darkMode })}
className={getCloseButtonStyle(darkMode)}
aria-label="Close Tooltip"
onClick={handleCloseClick}
darkMode={!darkMode}
Expand Down Expand Up @@ -135,11 +133,11 @@ function GuideCueTooltip({
</Disclaimer>
)}
<Button
ref={primaryButtonRef}
variant="primary"
onClick={() => handleButtonClick()}
darkMode={!darkMode}
className={buttonStyles}
id={focusId}
>
{buttonText}
</Button>
Expand Down
Loading