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

feat(message): enable component to be programmatically focused - FE-5725 #6033

Merged
merged 3 commits into from
Jun 14, 2023
Merged
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
24 changes: 23 additions & 1 deletion cypress/components/message/message.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
/* eslint-disable jest/no-disabled-tests */
/* eslint-disable jest/valid-expect, no-unused-expressions */
import React from "react";
import Message, { MessageProps } from "../../../src/components/message";
import { MessageComponent } from "../../../src/components/message/message-test.stories";
import { MessageComponent, MessageComponentWithRef } from "../../../src/components/message/message-test.stories";
import CypressMountWithProviders from "../../support/component-helper/cypress-mount";
import { getDataElementByValue } from "../../locators";
import {
messagePreview,
messageChildren,
messageTitle,
messageDismissIcon,
messageDismissIconButton,
messageContent,
variantPreview,
} from "../../locators/message/index";
import { buttonDataComponent } from "../../locators/button";
import {
VALIDATION,
CHARACTERS,
Expand Down Expand Up @@ -71,6 +74,25 @@ context("Tests for Message component", () => {
}
);

it("should focus component when open is true and component has a ref", () => {
CypressMountWithProviders(<MessageComponentWithRef />);

buttonDataComponent().click();
messagePreview().should("be.focused");
});

// Unable to run this test due to the Message component having a tabIndex of -1.
// cypress-plugin-tab does not allow tabbing on elements with an tabIndex of -1,
// however this test has been kept incase the bug is ever addressed. https://github.com/kuceb/cypress-plugin-tab/issues/18
it.skip("should focus icon button when open is true for Message component and tab key is pressed", () => {
CypressMountWithProviders(<MessageComponentWithRef />);

buttonDataComponent().click();
messagePreview().should("be.focused");
messagePreview().tab();
messageDismissIconButton().should("be.focused");
});

it.each(testData)(
"should check %s title for Message component",
(title) => {
Expand Down
3 changes: 3 additions & 0 deletions cypress/locators/message/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
MESSAGE_CHILDREN,
MESSAGE_TITLE,
MESSAGE_DISMISS_ICON,
MESSAGE_DISMISS_ICON_BUTTON,
MESSAGE_CONTENT,
VARIANT_PREVIEW,
} from "./locators";
Expand All @@ -12,5 +13,7 @@ export const messagePreview = () => cy.get(MESSAGE_PREVIEW);
export const messageChildren = () => cy.get(MESSAGE_CHILDREN);
export const messageTitle = () => cy.get(MESSAGE_TITLE);
export const messageDismissIcon = () => cy.get(MESSAGE_DISMISS_ICON);
export const messageDismissIconButton = () =>
cy.get(MESSAGE_DISMISS_ICON_BUTTON);
export const messageContent = () => cy.get(MESSAGE_CONTENT);
export const variantPreview = () => cy.get(VARIANT_PREVIEW);
1 change: 1 addition & 0 deletions cypress/locators/message/locators.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const MESSAGE_PREVIEW = 'div[data-component="Message"]';
export const MESSAGE_CHILDREN = 'div[data-element="content-body"]';
export const MESSAGE_TITLE = '[data-element="content-title"]';
export const MESSAGE_DISMISS_ICON = 'span[data-element="close"]';
export const MESSAGE_DISMISS_ICON_BUTTON = 'button[data-element="close"]';
export const MESSAGE_CONTENT = '[data-element="message-content"]';
export const VARIANT_PREVIEW =
'div[data-component="Message"] > div:nth-child(1)';
26 changes: 25 additions & 1 deletion src/components/message/message-test.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState } from "react";
import React, { useState, useRef, useEffect } from "react";
import { action } from "@storybook/addon-actions";
import Button from "../button";
import Message, { MessageProps } from "./message.component";

export default {
title: "Message/Test",
includeStories: ["Default"],
parameters: {
info: { disable: true },
chromatic: {
Expand Down Expand Up @@ -66,3 +67,26 @@ export const MessageComponent = (props: MessageProps) => {
</div>
);
};

export const MessageComponentWithRef = (props: MessageProps) => {
const [isOpen, setIsOpen] = useState(false);
const messageRef: React.Ref<HTMLDivElement> = useRef(null);

useEffect(() => {
if (isOpen) messageRef.current?.focus();
});

return (
<div>
{!isOpen && <Button onClick={() => setIsOpen(true)}>Open Message</Button>}
<Message
open={isOpen}
onDismiss={() => setIsOpen(false)}
ref={messageRef}
{...props}
>
Some custom message
</Message>
</div>
);
};
103 changes: 58 additions & 45 deletions src/components/message/message.component.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useRef } from "react";
import { MarginProps } from "styled-system";

import MessageStyle from "./message.style";
Expand Down Expand Up @@ -39,52 +39,65 @@ export interface MessageProps extends MarginProps {
variant?: MessageVariant;
}

export const Message = ({
open = true,
transparent = false,
title,
variant = "info",
children,
onDismiss,
id,
className,
closeButtonAriaLabel,
showCloseIcon = true,
...props
}: MessageProps) => {
const marginProps = filterStyledSystemMarginProps(props);
const l = useLocale();
const renderCloseIcon = () => {
if (!showCloseIcon || !onDismiss) return null;
export const Message = React.forwardRef<HTMLDivElement, MessageProps>(
(
{
open = true,
transparent = false,
title,
variant = "info",
children,
onDismiss,
id,
className,
closeButtonAriaLabel,
showCloseIcon = true,
...props
}: MessageProps,
ref
Parsium marked this conversation as resolved.
Show resolved Hide resolved
) => {
const messageRef = useRef<HTMLDivElement | null>(null);
const refToPass = ref || messageRef;

return (
<IconButton
data-element="close"
aria-label={closeButtonAriaLabel || l.message.closeButtonAriaLabel()}
onClick={onDismiss}
const marginProps = filterStyledSystemMarginProps(props);
const l = useLocale();

const renderCloseIcon = () => {
if (!showCloseIcon || !onDismiss) return null;

return (
<IconButton
data-element="close"
aria-label={closeButtonAriaLabel || l.message.closeButtonAriaLabel()}
onClick={onDismiss}
>
<Icon type="close" />
</IconButton>
);
};

return open ? (
<MessageStyle
{...tagComponent("Message", props)}
className={className}
transparent={transparent}
variant={variant}
role="status"
id={id}
ref={refToPass}
{...marginProps}
tabIndex={-1}
>
<Icon type="close" />
</IconButton>
);
};
<TypeIcon variant={variant} transparent={transparent} />
<MessageContent showCloseIcon={showCloseIcon} title={title}>
{children}
</MessageContent>
{renderCloseIcon()}
</MessageStyle>
) : null;
}
);

return open ? (
<MessageStyle
{...tagComponent("Message", props)}
className={className}
transparent={transparent}
variant={variant}
role="status"
id={id}
{...marginProps}
>
<TypeIcon variant={variant} transparent={transparent} />
<MessageContent showCloseIcon={showCloseIcon} title={title}>
{children}
</MessageContent>
{renderCloseIcon()}
</MessageStyle>
) : null;
};
Message.displayName = "Message";

export default Message;
32 changes: 32 additions & 0 deletions src/components/message/message.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,36 @@ describe("Message", () => {
describe("styled-system", () => {
testStyledSystemMargin((props) => <Message {...props} />);
});

describe("ref", () => {
it("passes ref to component", () => {
robinzigmond marked this conversation as resolved.
Show resolved Hide resolved
const ref = { current: null };

const wrapper = mount(
<Message onDismiss={() => {}} ref={ref}>
foobar
</Message>
);

const message = wrapper.find(MessageStyle);

expect(ref.current).toBe(message.getDOMNode());
});

describe("callback ref", () => {
it("allows a callback ref is be passed to component", () => {
const testCallbackRef = jest.fn();

const wrapper = mount(
<Message onDismiss={() => {}} ref={testCallbackRef}>
foobar
</Message>
);

const message = wrapper.find(MessageStyle);

expect(testCallbackRef).toHaveBeenCalledWith(message.getDOMNode());
});
});
});
});
8 changes: 8 additions & 0 deletions src/components/message/message.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ To see a full list of available margin props, please visit the props table at th
<Story name="with margin" story={stories.WithMargin} />
</Canvas>

### With focus

Since it is possible to pass a ref to this component, it is also possible to programmatically focus the component.

<Canvas>
<Story name="with focus" story={stories.WithFocus} />
</Canvas>

## Props

### Message
Expand Down
73 changes: 61 additions & 12 deletions src/components/message/message.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
import React, { useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { ComponentStory } from "@storybook/react";

import Message from ".";
Expand Down Expand Up @@ -107,33 +107,82 @@ export const WithRichContent: ComponentStory<typeof Message> = () => {
};

export const WithMargin: ComponentStory<typeof Message> = () => {
const [isOpen, setIsOpen] = useState(true);
const [isOpen, setIsOpen] = useState({
robinzigmond marked this conversation as resolved.
Show resolved Hide resolved
MessageOne: true,
MessageTwo: true,
MessageThree: true,
});

const displayButton = Object.values({ ...isOpen }).every(
(value) => value === false
);
return (
<>
{!isOpen && <Button onClick={() => setIsOpen(true)}>Open Message</Button>}
{displayButton && (
<Button
onClick={() =>
setIsOpen({
MessageOne: true,
MessageTwo: true,
MessageThree: true,
})
}
>
Open Message
</Button>
)}
<Message
open={isOpen}
onDismiss={() => setIsOpen(false)}
open={isOpen.MessageOne}
onDismiss={() => setIsOpen({ ...isOpen, MessageOne: false })}
variant="warning"
m={1}
>
Some custom message
This is message one.
</Message>
<Message
open={isOpen}
onDismiss={() => setIsOpen(false)}
open={isOpen.MessageTwo}
onDismiss={() => setIsOpen({ ...isOpen, MessageTwo: false })}
variant="warning"
m={3}
>
Some custom message
This is message two.
</Message>
<Message
open={isOpen}
onDismiss={() => setIsOpen(false)}
open={isOpen.MessageThree}
onDismiss={() => setIsOpen({ ...isOpen, MessageThree: false })}
variant="warning"
m="16px"
>
Some custom message
This is message three.
</Message>
</>
);
};

export const WithFocus: ComponentStory<typeof Message> = () => {
const [isMessageOpen, setIsMessageOpen] = useState(true);

const messageRef: React.Ref<HTMLDivElement> = useRef(null);

useEffect(() => {
if (isMessageOpen) {
messageRef.current?.focus();
}
}, [isMessageOpen]);

return (
<>
{!isMessageOpen && (
<Button onClick={() => setIsMessageOpen(true)}>Open Message</Button>
)}
<Message
open={isMessageOpen}
onDismiss={() => setIsMessageOpen(false)}
variant="error"
mb={1}
ref={messageRef}
>
This is message one.
</Message>
</>
);
Expand Down
4 changes: 4 additions & 0 deletions src/components/message/message.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const MessageStyle = styled.div<MessageStyleProps & MarginProps>`
background-color: var(--colorsUtilityYang100);
min-height: 38px;

:focus {
outline: none;
}

${({ transparent }) =>
transparent &&
css`
Expand Down