diff --git a/cypress/components/message/message.cy.tsx b/cypress/components/message/message.cy.tsx index 4fbb163104..5b6e9e5223 100644 --- a/cypress/components/message/message.cy.tsx +++ b/cypress/components/message/message.cy.tsx @@ -1,7 +1,8 @@ +/* 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 { @@ -9,9 +10,11 @@ import { messageChildren, messageTitle, messageDismissIcon, + messageDismissIconButton, messageContent, variantPreview, } from "../../locators/message/index"; +import { buttonDataComponent } from "../../locators/button"; import { VALIDATION, CHARACTERS, @@ -71,6 +74,25 @@ context("Tests for Message component", () => { } ); + it("should focus component when open is true and component has a ref", () => { + CypressMountWithProviders(); + + 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(); + + buttonDataComponent().click(); + messagePreview().should("be.focused"); + messagePreview().tab(); + messageDismissIconButton().should("be.focused"); + }); + it.each(testData)( "should check %s title for Message component", (title) => { diff --git a/cypress/locators/message/index.js b/cypress/locators/message/index.js index ae5780b440..e70e14f7cd 100644 --- a/cypress/locators/message/index.js +++ b/cypress/locators/message/index.js @@ -3,6 +3,7 @@ import { MESSAGE_CHILDREN, MESSAGE_TITLE, MESSAGE_DISMISS_ICON, + MESSAGE_DISMISS_ICON_BUTTON, MESSAGE_CONTENT, VARIANT_PREVIEW, } from "./locators"; @@ -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); diff --git a/cypress/locators/message/locators.js b/cypress/locators/message/locators.js index 35d27e77ca..af4153e512 100644 --- a/cypress/locators/message/locators.js +++ b/cypress/locators/message/locators.js @@ -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)'; diff --git a/src/components/message/message-test.stories.tsx b/src/components/message/message-test.stories.tsx index 06711fe010..644d28416d 100644 --- a/src/components/message/message-test.stories.tsx +++ b/src/components/message/message-test.stories.tsx @@ -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: { @@ -66,3 +67,26 @@ export const MessageComponent = (props: MessageProps) => { ); }; + +export const MessageComponentWithRef = (props: MessageProps) => { + const [isOpen, setIsOpen] = useState(false); + const messageRef: React.Ref = useRef(null); + + useEffect(() => { + if (isOpen) messageRef.current?.focus(); + }); + + return ( +
+ {!isOpen && } + setIsOpen(false)} + ref={messageRef} + {...props} + > + Some custom message + +
+ ); +}; diff --git a/src/components/message/message.component.tsx b/src/components/message/message.component.tsx index 7a6cae73fb..064032b4ad 100644 --- a/src/components/message/message.component.tsx +++ b/src/components/message/message.component.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useRef } from "react"; import { MarginProps } from "styled-system"; import MessageStyle from "./message.style"; @@ -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( + ( + { + open = true, + transparent = false, + title, + variant = "info", + children, + onDismiss, + id, + className, + closeButtonAriaLabel, + showCloseIcon = true, + ...props + }: MessageProps, + ref + ) => { + const messageRef = useRef(null); + const refToPass = ref || messageRef; - return ( - { + if (!showCloseIcon || !onDismiss) return null; + + return ( + + + + ); + }; + + return open ? ( + - - - ); - }; + + + {children} + + {renderCloseIcon()} + + ) : null; + } +); - return open ? ( - - - - {children} - - {renderCloseIcon()} - - ) : null; -}; +Message.displayName = "Message"; export default Message; diff --git a/src/components/message/message.spec.tsx b/src/components/message/message.spec.tsx index f5017e80c3..852fbef85e 100644 --- a/src/components/message/message.spec.tsx +++ b/src/components/message/message.spec.tsx @@ -201,4 +201,36 @@ describe("Message", () => { describe("styled-system", () => { testStyledSystemMargin((props) => ); }); + + describe("ref", () => { + it("passes ref to component", () => { + const ref = { current: null }; + + const wrapper = mount( + {}} ref={ref}> + foobar + + ); + + 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( + {}} ref={testCallbackRef}> + foobar + + ); + + const message = wrapper.find(MessageStyle); + + expect(testCallbackRef).toHaveBeenCalledWith(message.getDOMNode()); + }); + }); + }); }); diff --git a/src/components/message/message.stories.mdx b/src/components/message/message.stories.mdx index be382e8492..b082cf9726 100644 --- a/src/components/message/message.stories.mdx +++ b/src/components/message/message.stories.mdx @@ -100,6 +100,14 @@ To see a full list of available margin props, please visit the props table at th +### With focus + +Since it is possible to pass a ref to this component, it is also possible to programmatically focus the component. + + + + + ## Props ### Message diff --git a/src/components/message/message.stories.tsx b/src/components/message/message.stories.tsx index d5e4dcfda9..50896ce109 100644 --- a/src/components/message/message.stories.tsx +++ b/src/components/message/message.stories.tsx @@ -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 "."; @@ -107,33 +107,82 @@ export const WithRichContent: ComponentStory = () => { }; export const WithMargin: ComponentStory = () => { - const [isOpen, setIsOpen] = useState(true); + const [isOpen, setIsOpen] = useState({ + MessageOne: true, + MessageTwo: true, + MessageThree: true, + }); + + const displayButton = Object.values({ ...isOpen }).every( + (value) => value === false + ); return ( <> - {!isOpen && } + {displayButton && ( + + )} setIsOpen(false)} + open={isOpen.MessageOne} + onDismiss={() => setIsOpen({ ...isOpen, MessageOne: false })} variant="warning" m={1} > - Some custom message + This is message one. setIsOpen(false)} + open={isOpen.MessageTwo} + onDismiss={() => setIsOpen({ ...isOpen, MessageTwo: false })} variant="warning" m={3} > - Some custom message + This is message two. setIsOpen(false)} + open={isOpen.MessageThree} + onDismiss={() => setIsOpen({ ...isOpen, MessageThree: false })} variant="warning" m="16px" > - Some custom message + This is message three. + + + ); +}; + +export const WithFocus: ComponentStory = () => { + const [isMessageOpen, setIsMessageOpen] = useState(true); + + const messageRef: React.Ref = useRef(null); + + useEffect(() => { + if (isMessageOpen) { + messageRef.current?.focus(); + } + }, [isMessageOpen]); + + return ( + <> + {!isMessageOpen && ( + + )} + setIsMessageOpen(false)} + variant="error" + mb={1} + ref={messageRef} + > + This is message one. ); diff --git a/src/components/message/message.style.ts b/src/components/message/message.style.ts index 4d04349c9d..8f289a5ce6 100644 --- a/src/components/message/message.style.ts +++ b/src/components/message/message.style.ts @@ -28,6 +28,10 @@ const MessageStyle = styled.div` background-color: var(--colorsUtilityYang100); min-height: 38px; + :focus { + outline: none; + } + ${({ transparent }) => transparent && css`