Merged in feat/sw-3218-move-sidepeek-to-design-system (pull request #2598)

feat(SW-3218): Move SidePeek to design-system

* Remove SidePeekProvider dependency on Next

* Remove dependency on i18n in sidepeek

* Inline types

* Move SidePeek to design-system

* Fix align-items value


Approved-by: Bianca Widstam
This commit is contained in:
Anton Gunnarsson
2025-08-06 08:35:34 +00:00
parent 75ffd5d10b
commit 7fb082f712
21 changed files with 152 additions and 65 deletions

View File

@@ -0,0 +1,43 @@
'use client'
import { useEffect, useState } from 'react'
import { SidePeekContext } from './index'
interface SidepeekProviderProps extends React.PropsWithChildren {
onOpen?: (sidePeek: string) => void
onClose?: () => void
searchParams: URLSearchParams
}
export default function SidePeekProvider({
children,
searchParams,
onOpen,
onClose,
}: SidepeekProviderProps) {
const [activeSidePeek, setActiveSidePeek] = useState<string | null>(null)
useEffect(() => {
const sidePeekParam = searchParams.get('s')
if (sidePeekParam !== activeSidePeek) {
setActiveSidePeek(sidePeekParam)
}
}, [searchParams, activeSidePeek])
useEffect(() => {
if (activeSidePeek && onOpen) {
onOpen(activeSidePeek)
}
}, [activeSidePeek, onOpen])
function handleClose(isOpen: boolean) {
if (!isOpen) {
onClose?.()
setActiveSidePeek(null)
}
}
return (
<SidePeekContext.Provider value={{ handleClose, activeSidePeek }}>
{children}
</SidePeekContext.Provider>
)
}

View File

@@ -0,0 +1,9 @@
'use client'
import { createContext } from 'react'
interface ISidePeekContext {
handleClose: (isOpen: boolean) => void
activeSidePeek: string | null
}
export const SidePeekContext = createContext<ISidePeekContext | null>(null)

View File

@@ -0,0 +1,18 @@
interface SidePeekSEOProps {
title: string
}
// Sidepeeks generally have important content that should be indexed by search engines.
// The content is hidden behind a modal, but it is still important for SEO.
// This component is used to provide SEO information for the sidepeek content.
export default function SidePeekSEO({
title,
children,
}: React.PropsWithChildren<SidePeekSEOProps>) {
return (
<div className="sr-only">
<h2>{title}</h2>
{children}
</div>
)
}

View File

@@ -0,0 +1,85 @@
'use client'
import { useContext, useRef } from 'react'
import { Dialog, Modal, ModalOverlay } from 'react-aria-components'
import { MaterialIcon } from '../Icons/MaterialIcon'
import { OldDSButton as Button } from '../OldDSButton'
import { Typography } from '../Typography'
import { SidePeekContext } from './SidePeekContext'
import SidePeekSEO from './SidePeekSEO'
import styles from './sidePeek.module.css'
interface SidePeekProps {
contentKey?: string
title: string
isOpen?: boolean
openInRoot?: boolean
handleClose?: (isOpen: boolean) => void
closeLabel: string
}
export default function SidePeek({
children,
title,
contentKey,
handleClose,
isOpen,
openInRoot = false,
closeLabel,
}: React.PropsWithChildren<SidePeekProps>) {
const rootDiv = useRef<HTMLDivElement>(null)
const context = useContext(SidePeekContext)
function onClose() {
const closeHandler = handleClose || context?.handleClose
closeHandler?.(false)
}
return (
<>
<div ref={openInRoot ? null : rootDiv}>
<ModalOverlay
UNSTABLE_portalContainer={rootDiv.current || undefined}
className={styles.overlay}
isOpen={
isOpen || (!!contentKey && contentKey === context?.activeSidePeek)
}
onOpenChange={onClose}
isDismissable
>
<Modal className={styles.modal}>
<Dialog className={styles.dialog} aria-label={title}>
<aside className={styles.sidePeek}>
<header className={styles.header}>
{title ? (
<Typography variant="Title/md">
<h2 className={styles.heading}>{title}</h2>
</Typography>
) : null}
<Button
aria-label={closeLabel}
className={styles.closeButton}
intent="text"
onPress={onClose}
>
<MaterialIcon
icon="close"
color="Icon/Interactive/Default"
/>
</Button>
</header>
<div className={styles.sidePeekContent}>{children}</div>
</aside>
</Dialog>
</Modal>
</ModalOverlay>
</div>
<SidePeekSEO title={title}>{children}</SidePeekSEO>
</>
)
}

View File

@@ -0,0 +1,91 @@
.modal {
--sidepeek-desktop-width: 560px;
}
@keyframes slide-in {
from {
right: calc(-1 * var(--sidepeek-desktop-width));
}
to {
right: 0;
}
}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: var(--sidepeek-z-index);
background-color: var(--UI-Opacity-Almost-Black-30);
}
.modal {
position: fixed;
top: 0;
right: auto;
bottom: 0;
width: 100%;
height: 100vh;
background-color: var(--Background-Primary);
z-index: var(--sidepeek-z-index);
outline: none;
}
.modal[data-entering] {
animation: slide-in 250ms;
}
.modal[data-exiting] {
animation: slide-in 250ms reverse;
}
.dialog {
height: 100%;
outline: none;
}
.sidePeek {
position: relative;
display: grid;
grid-template-rows: min-content auto;
height: 100dvh;
}
.header {
display: flex;
justify-content: flex-end;
border-bottom: 1px solid var(--Base-Border-Subtle);
align-items: flex-start;
padding: var(--Spacing-x4);
}
.header:has(> h2) {
justify-content: space-between;
}
.closeButton {
padding: 0;
}
.heading {
color: var(--Text-Heading);
text-wrap: balance;
hyphens: auto;
}
.sidePeekContent {
padding: var(--Spacing-x4);
overflow-y: auto;
}
@media screen and (min-width: 1367px) {
.modal {
top: 0;
right: 0px;
width: var(--sidepeek-desktop-width);
height: 100vh;
}
}