Skip to content

Commit

Permalink
InlineNotification: allow ref to be passed in and add focus styling (#…
Browse files Browse the repository at this point in the history
…5076)

* Allow passing a ref to InlineNotification

* Add focus styling to GeneralNotification container

* Changeset

* Avoid conditional hook call

* Add test for custom ref

* Add focus example to stickersheet
  • Loading branch information
dougmacknz authored Sep 25, 2024
1 parent 7204fb9 commit e896e5f
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 89 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-flowers-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kaizen/components": patch
---

InlineNotification: allow ref to be passed in, and add focus styling
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 => (
<GenericNotification
style="inline"
persistent={persistent || hideCloseIcon}
classNameOverride={classnames(classNameOverride, [
isSubtle && styles.subtle,
])}
{...otherProps}
/>
export const InlineNotification = forwardRef<
HTMLDivElement,
InlineNotificationProps
>(
(
{
isSubtle,
hideCloseIcon = false,
persistent = false,
classNameOverride,
...otherProps
},
ref
): JSX.Element => (
<GenericNotification
style="inline"
persistent={persistent || hideCloseIcon}
classNameOverride={classnames(classNameOverride, [
isSubtle && styles.subtle,
])}
ref={ref}
{...otherProps}
/>
)
)

InlineNotification.displayName = "InlineNotification"
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand Down Expand Up @@ -206,6 +219,11 @@ const StickerSheetTemplate: StickerSheetStory = {
</StickerSheet>
</>
),
parameters: {
pseudo: {
focus: '[data-sb-pseudo-styles="focus"]',
},
},
}

export const StickerSheetDefault: StickerSheetStory = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -58,3 +58,46 @@ export const GenericNotificationTest: Story = {
})
},
}

export const RefTest: Story = {
render: () => {
const customRef = useRef<HTMLDivElement>(null)
const [isHidden, setIsHidden] = useState<boolean>(false)

return (
<div>
<span data-testid="hidden-state">{isHidden ? "Hidden" : "Shown"}</span>
<GenericNotification
ref={customRef}
variant="success"
style="inline"
title="Success"
data-testid="generic-notification"
onHide={() => setIsHidden(true)}
>
This is my positive notification
</GenericNotification>
</div>
)
},
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)
})
},
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
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 {
NotificationType,
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 {
Expand Down Expand Up @@ -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<boolean>(true)
const [isRemoved, setIsRemoved] = useState<boolean>(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<boolean>(true)
const [isRemoved, setIsRemoved] = useState<boolean>(false)

const containerRef = useRef<HTMLDivElement>(null)
const fallbackRef = useRef<HTMLDivElement>(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<HTMLDivElement>): 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<HTMLDivElement>
): 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 (
<div
ref={containerRef}
className={classnames(
styles.notification,
variant ? styles[variant] : styles[type],
styles[style],
isHidden && styles.hidden,
noBottomMargin && styles.noBottomMargin,
classNameOverride,
persistent && styles.persistent
)}
style={{ marginTop: getMarginTop() }}
onTransitionEnd={onTransitionEnd}
{...restProps}
>
<div className={styles.icon}>
{type ? (
<NotificationIconType type={type} />
) : (
<NotificationIconVariant variant={variant} />
)}
</div>
return (
<div
ref={containerRef}
className={classnames(
styles.textContainer,
forceMultiline && styles.forceMultiline
styles.notification,
variant ? styles[variant] : styles[type],
styles[style],
isHidden && styles.hidden,
noBottomMargin && styles.noBottomMargin,
classNameOverride,
persistent && styles.persistent
)}
style={{ marginTop: getMarginTop() }}
onTransitionEnd={onTransitionEnd}
{...restProps}
>
{style !== "global" && (
<NotificationHeading titleProp={title} headingProps={headingProps} />
)}
{children && <div className={styles.text}>{children}</div>}
<div className={styles.icon}>
{type ? (
<NotificationIconType type={type} />
) : (
<NotificationIconVariant variant={variant} />
)}
</div>
<div
className={classnames(
styles.textContainer,
forceMultiline && styles.forceMultiline
)}
>
{style !== "global" && (
<NotificationHeading
titleProp={title}
headingProps={headingProps}
/>
)}
{children && <div className={styles.text}>{children}</div>}
</div>
{!persistent && <CancelButton onClick={() => setIsHidden(true)} />}
</div>
{!persistent && <CancelButton onClick={() => setIsHidden(true)} />}
</div>
)
}
)
}
)

GenericNotification.displayName = "GenericNotification"
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit e896e5f

Please sign in to comment.