Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(menu-full-screen, modal): ensure the call to action element is focused on close #6986

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

tomdavies73
Copy link
Contributor

@tomdavies73 tomdavies73 commented Sep 24, 2024

fix #6870

Proposed behaviour

Ensures focus is moved back onto the active element before a modal/fullscreen menu is opened.

Current behaviour

Currently, once a modal is closed, focus is typically returned to the document, which can be disorientating for users.

Checklist

  • Commits follow our style guide
  • Related issues linked in commit messages if required
  • Screenshots are included in the PR if useful
  • All themes are supported if required
  • Unit tests added or updated if required
  • Playwright automation tests added or updated if required
  • Storybook added or updated if required
  • Translations added or updated (including creating or amending translation keys table in storybook) if required
  • Typescript d.ts file added or updated if required
  • Related docs have been updated if required

QA

  • Tested in provided StackBlitz sandbox/Storybook
  • Add new Playwright test coverage if required
  • Carbon implementation matches Design System/designs
  • UI Tests GitHub check reviewed if required

Additional context

Testing instructions

@tomdavies73 tomdavies73 requested review from a team as code owners September 24, 2024 14:19
@tomdavies73 tomdavies73 changed the title fix(modal): ensure the call to action element is focused on close **work in progress** fix(modal): ensure the call to action element is focused on close (work in progress) Sep 24, 2024
@tomdavies73 tomdavies73 marked this pull request as draft September 24, 2024 14:19
@tomdavies73 tomdavies73 self-assigned this Sep 25, 2024
@tomdavies73 tomdavies73 force-pushed the FE-6728 branch 2 times, most recently from 7c428ef to cd15a0e Compare October 2, 2024 13:01
@tomdavies73 tomdavies73 changed the title fix(modal): ensure the call to action element is focused on close (work in progress) fix(menu-full-screen, modal): ensure the call to action element is focused on close (work in progress) Oct 2, 2024
@tomdavies73 tomdavies73 force-pushed the FE-6728 branch 5 times, most recently from 720eeef to 8c7988c Compare October 8, 2024 09:38
@tomdavies73 tomdavies73 changed the title fix(menu-full-screen, modal): ensure the call to action element is focused on close (work in progress) fix(menu-full-screen, modal): ensure the call to action element is focused on close Oct 8, 2024
@@ -54,6 +54,93 @@ export const DialogFullScreenComponent = ({
);
};

export const DefaultStory = ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: do we need these test cases added? It looks like we could use some existing ones an pass an open prop which initialises the state

@@ -80,17 +95,31 @@ const useModalManager = ({
if (modalRegistered.current) {
ModalManager.removeModal(ref, triggerRefocusOnClose);

if (lastFocusedElement.current) {
setTimeout(() => {
lastFocusedElement.current?.focus();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lastFocusedElement.current?.focus();
lastFocusedElement.current.focus();

Copy link
Contributor

@Parsium Parsium Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may be necessary since the call is made within an asynchronous function. Although it’s unlikely the ref will change during the very short timeout, TypeScript may still consider it possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, unfortunately I'm still getting a possibly null warning, to avoid it I'd have to do have an if statement asserting lastFocusedElement.current again within the setTimeout to avoid it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, it won't ever be null if it passes the guard as the only point that happens is in the callback the timeout is executing but I get why TS can't guarantee it when it computes the types

modalRegistered.current = false;
}
},
[triggerRefocusOnClose]
);

useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I think we can initialise this earlier like so but if it doesn't work you could use a layout effect

const modalOpenOnRender = useRef(open); // init it straightaway 

...


useEffect(() => {
    // block and reset etc
    if (modalOpenOnRender.current) {
       modalOpenOnRender.current = false;
       return;
    }
    if (
      !lastFocusedElement.current &&
      focusCallToActionElement &&
      open
    ) {
      lastFocusedElement.current = focusCallToActionElement;
    }
  }, [open, focusCallToActionElement]);
  useLayoutEffect(() => {
     modalOpenOnRender.current = true;
  }, []);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think due to the modalOpenOnRender being made redundant now, we can avoid the useLayoutEffect and reduce the useEffect too

@Parsium Parsium self-requested a review October 11, 2024 09:16
Copy link
Contributor

@Parsium Parsium left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this @tomdavies73 👍🏼

@@ -54,6 +54,58 @@ export const DialogFullScreenComponent = ({
);
};

export const DefaultStory = ({ open = false }: { open?: boolean }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: were you not able to tweak DialogComponent to achieve the same functionality as this story? It seems like you could just add a button to it etc

);
};

export const DefaultNestedStory = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: isn't this pretty much the same as the NestedStory below?

@@ -210,8 +210,8 @@ export const DialogComponentFocusableSelectors = (
);
};

export const DefaultStory = () => {
const [isOpen, setIsOpen] = useState(defaultOpenState);
export const DefaultStory = ({ open }: { open?: boolean }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking etc

Suggested change
export const DefaultStory = ({ open }: { open?: boolean }) => {
export const DefaultStory = ({ open = defaultOpenState }: { open?: boolean }) => {
const [isOpen, setIsOpen] = useState(open);

@@ -304,6 +304,92 @@ export const MenuComponentFullScreen = (
);
};

export const MenuComponentFullScreenOpen = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: apologies if I missed this on the first review, it just seems to me that this way more complex than it needs to be:

  • do we need to support all the menuTypes? To me it's not important here we could just have two of the default
  • do we need to concern ourselves with the breakpoints here? To me we could just render it as it's also superfluous to what we're testing etc
  • I'd also reduce the menu-items down to a couple etc but that's less of an issue

@@ -29,6 +29,63 @@ export const Default = (args: Partial<SidebarProps>) => {
);
};

export const DefaultClosed = (args: Partial<SidebarProps>) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: I think this one could be replaced by adding an open prop to SidebarComponent with a default of true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this has been done in the most recent commit

await expect(button).toBeFocused();
});

test("when nested Sidebar's are open/closed their respective call to action elements should be focused correctly", async ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: open-> opened

@@ -80,17 +95,31 @@ const useModalManager = ({
if (modalRegistered.current) {
ModalManager.removeModal(ref, triggerRefocusOnClose);

if (lastFocusedElement.current) {
setTimeout(() => {
lastFocusedElement.current?.focus();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, it won't ever be null if it passes the guard as the only point that happens is in the callback the timeout is executing but I get why TS can't guarantee it when it computes the types

await expect(button).toBeFocused();
});

test("when nested Dialog's are open/closed their respective call to action elements should be focused correctly", async ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test("when nested Dialog's are open/closed their respective call to action elements should be focused correctly", async ({
test("when nested Dialog's are opened/closed their respective call to action elements should be focused correctly", async ({

await expect(button).toBeFocused();
});

test("when nested Dialog's are open/closed their respective call to action elements should be focused correctly", async ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test("when nested Dialog's are open/closed their respective call to action elements should be focused correctly", async ({
test("when nested Dialog's are opened/closed their respective call to action elements should be focused correctly", async ({

Copy link
Contributor

@Parsium Parsium left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the new changes @tomdavies73 👍🏼 Just one small question from me in addition to @edleeks87's comments

src/components/menu/component.test-pw.tsx Outdated Show resolved Hide resolved
…cused on close

Ensures the element which opens `Modal` or a fullscreen `Menu` is focused when the
respective component closes.

fix #6870
@harpalsingh
Copy link
Member

I did an @desk demo with @tempertemper and this also passes accessibility review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

Return focus on the button which opened Confirm modal
4 participants