diff --git a/.changeset/orange-flowers-trade.md b/.changeset/orange-flowers-trade.md new file mode 100644 index 00000000000..499aacac069 --- /dev/null +++ b/.changeset/orange-flowers-trade.md @@ -0,0 +1,5 @@ +--- +"@kaizen/components": patch +--- + +InlineNotification: allow ref to be passed in, and add focus styling diff --git a/packages/components/src/Notification/InlineNotification/InlineNotification.tsx b/packages/components/src/Notification/InlineNotification/InlineNotification.tsx index 43705bf4962..e84258f5516 100644 --- a/packages/components/src/Notification/InlineNotification/InlineNotification.tsx +++ b/packages/components/src/Notification/InlineNotification/InlineNotification.tsx @@ -1,4 +1,4 @@ -import React, { HTMLAttributes } from "react" +import React, { forwardRef, HTMLAttributes } from "react" import classnames from "classnames" import { HeadingProps } from "~components/Heading" import { OverrideClassName } from "~components/types/OverrideClassName" @@ -30,21 +30,30 @@ export type InlineNotificationProps = InlineNotificationBase & * {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3082093392/Inline+Notification Guidance} | * {@link https://cultureamp.design/storybook/?path=/docs/components-notifications-inline-notification--docs Storybook} */ -export const InlineNotification = ({ - isSubtle, - hideCloseIcon = false, - persistent = false, - classNameOverride, - ...otherProps -}: InlineNotificationProps): JSX.Element => ( - +export const InlineNotification = forwardRef< + HTMLDivElement, + InlineNotificationProps +>( + ( + { + isSubtle, + hideCloseIcon = false, + persistent = false, + classNameOverride, + ...otherProps + }, + ref + ): JSX.Element => ( + + ) ) InlineNotification.displayName = "InlineNotification" diff --git a/packages/components/src/Notification/InlineNotification/_docs/InlineNotification.stickersheet.stories.tsx b/packages/components/src/Notification/InlineNotification/_docs/InlineNotification.stickersheet.stories.tsx index eab8734452a..7ae4e0e970e 100644 --- a/packages/components/src/Notification/InlineNotification/_docs/InlineNotification.stickersheet.stories.tsx +++ b/packages/components/src/Notification/InlineNotification/_docs/InlineNotification.stickersheet.stories.tsx @@ -131,6 +131,19 @@ const VARIANTS_PROPS: Array<{ forceMultiline: true, }, }, + { + title: "Focus", + props: { + // @ts-ignore + "data-sb-pseudo-styles": "focus", + variant: "informative", + headingProps: { + variant: "heading-6", + tag: "span", + children: "Focused title", + }, + }, + }, ] const TYPE_PROPS: Array<{ @@ -206,6 +219,11 @@ const StickerSheetTemplate: StickerSheetStory = { ), + parameters: { + pseudo: { + focus: '[data-sb-pseudo-styles="focus"]', + }, + }, } export const StickerSheetDefault: StickerSheetStory = { diff --git a/packages/components/src/Notification/subcomponents/GenericNotification/GenericNotification.stories.tsx b/packages/components/src/Notification/subcomponents/GenericNotification/GenericNotification.stories.tsx index 5bdb2b66eb6..306c801acaa 100644 --- a/packages/components/src/Notification/subcomponents/GenericNotification/GenericNotification.stories.tsx +++ b/packages/components/src/Notification/subcomponents/GenericNotification/GenericNotification.stories.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react" +import React, { useRef, useState } from "react" import { Meta, StoryObj } from "@storybook/react" import { userEvent, within, expect, waitFor } from "@storybook/test" import { GenericNotification } from "./index" @@ -58,3 +58,46 @@ export const GenericNotificationTest: Story = { }) }, } + +export const RefTest: Story = { + render: () => { + const customRef = useRef(null) + const [isHidden, setIsHidden] = useState(false) + + return ( +
+ {isHidden ? "Hidden" : "Shown"} + setIsHidden(true)} + > + This is my positive notification + +
+ ) + }, + name: "Test: still renders and closes properly when custom ref passed in", + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const element = canvas.getByTestId("generic-notification") + const hiddenState = canvas.getByTestId("hidden-state") + + await waitFor(() => { + expect(element).toBeInTheDocument() + expect(hiddenState).toHaveTextContent("Shown") + }) + + await userEvent.click(canvas.getByTestId("close-button")) + + await waitFor(() => { + setTimeout(() => { + expect(hiddenState).toHaveTextContent("Hidden") + expect(element).not.toBeInTheDocument() + }, 1000) + }) + }, +} diff --git a/packages/components/src/Notification/subcomponents/GenericNotification/GenericNotification.tsx b/packages/components/src/Notification/subcomponents/GenericNotification/GenericNotification.tsx index ea86f0756ab..51787ca57c4 100644 --- a/packages/components/src/Notification/subcomponents/GenericNotification/GenericNotification.tsx +++ b/packages/components/src/Notification/subcomponents/GenericNotification/GenericNotification.tsx @@ -1,4 +1,10 @@ -import React, { HTMLAttributes, useEffect, useRef, useState } from "react" +import React, { + forwardRef, + HTMLAttributes, + useEffect, + useRef, + useState, +} from "react" import classnames from "classnames" import { HeadingProps } from "~components/Heading" import { @@ -6,6 +12,7 @@ import { NotificationVariant, } from "~components/Notification/types" import { OverrideClassName } from "~components/types/OverrideClassName" +import { isRefObject } from "~components/utils/isRefObject" import { CancelButton } from "../CancelButton" import { NotificationHeading } from "../NotificationHeading" import { @@ -49,89 +56,103 @@ export type GenericNotificationVariant = { export type GenericNotificationProps = GenericNotificationBase & (GenericNotificationType | GenericNotificationVariant) -export const GenericNotification = ({ - type, - variant, - style, - children, - title, - persistent = false, - onHide, - noBottomMargin, - forceMultiline, - headingProps, - classNameOverride, - ...restProps -}: GenericNotificationProps): JSX.Element | null => { - const [isHidden, setIsHidden] = useState(true) - const [isRemoved, setIsRemoved] = useState(false) +export const GenericNotification = forwardRef< + HTMLDivElement, + GenericNotificationProps +>( + ( + { + type, + variant, + style, + children, + title, + persistent = false, + onHide, + noBottomMargin, + forceMultiline, + headingProps, + classNameOverride, + ...restProps + }, + ref + ): JSX.Element | null => { + const [isHidden, setIsHidden] = useState(true) + const [isRemoved, setIsRemoved] = useState(false) - const containerRef = useRef(null) + const fallbackRef = useRef(null) + const containerRef = isRefObject(ref) ? ref : fallbackRef - useEffect(() => { - requestAnimationFrame(() => { - if (containerRef.current) { - setIsHidden(false) - } - }) - }, []) + useEffect(() => { + requestAnimationFrame(() => { + if (containerRef.current) { + setIsHidden(false) + } + }) + }, []) - const getMarginTop = (): string => { - if (isHidden && containerRef.current) { - return -containerRef.current.clientHeight + "px" + const getMarginTop = (): string => { + if (isHidden && containerRef.current) { + return -containerRef.current.clientHeight + "px" + } + return "0" } - return "0" - } - const onTransitionEnd = (e: React.TransitionEvent): void => { - // Be careful: this assumes the final CSS property to be animated is "margin-top". - if (isHidden && e.propertyName === "margin-top") { - setIsRemoved(true) - onHide?.() + const onTransitionEnd = ( + e: React.TransitionEvent + ): void => { + // Be careful: this assumes the final CSS property to be animated is "margin-top". + if (isHidden && e.propertyName === "margin-top") { + setIsRemoved(true) + onHide?.() + } } - } - if (isRemoved) { - return null - } + if (isRemoved) { + return null + } - return ( -
-
- {type ? ( - - ) : ( - - )} -
+ return (
- {style !== "global" && ( - - )} - {children &&
{children}
} +
+ {type ? ( + + ) : ( + + )} +
+
+ {style !== "global" && ( + + )} + {children &&
{children}
} +
+ {!persistent && setIsHidden(true)} />}
- {!persistent && setIsHidden(true)} />} -
- ) -} + ) + } +) GenericNotification.displayName = "GenericNotification" diff --git a/packages/components/src/Notification/subcomponents/GenericNotification/_mixins.scss b/packages/components/src/Notification/subcomponents/GenericNotification/_mixins.scss index ef39009336e..6b0a53261a3 100644 --- a/packages/components/src/Notification/subcomponents/GenericNotification/_mixins.scss +++ b/packages/components/src/Notification/subcomponents/GenericNotification/_mixins.scss @@ -22,6 +22,11 @@ $notification-slide-right: transform 300ms ease-out; box-sizing: border-box; pointer-events: all; + &:focus { + outline-offset: 1px; + outline: 2px solid var(--color-blue-500); + } + // Variants &%ca-notification---inline, &%ca-notification---toast {