Skip to content

Commit

Permalink
Add partial match support to usePreview helpers isSelected and `i…
Browse files Browse the repository at this point in the history
…sHovered` (#3009)

Co-authored-by: Stefanie Kaltenhauser <stefanie.kaltenhauser@vivid-planet.com>
  • Loading branch information
stekalt and Stefanie Kaltenhauser authored Jan 24, 2025
1 parent ce3aaf6 commit f60b636
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 24 deletions.
8 changes: 8 additions & 0 deletions .changeset/fast-ties-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@comet/cms-site": minor
---

Extend the `usePreview`-helpers `isSelected` and `isHovered` with optional partial match support

- When `exactMatch` is set to `true` (default), the function checks for exact URL matches.
- When `exactMatch` is set to `false`, the function checks if the selected route starts with the given URL.
72 changes: 64 additions & 8 deletions demo/site/src/common/blocks/AccordionBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,74 @@
import { ListBlock, PropsWithData, withPreview } from "@comet/cms-site";
import { isWithPreviewPropsData, PropsWithData, usePreview, withPreview } from "@comet/cms-site";
import { AccordionBlockData } from "@src/blocks.generated";
import { AccordionItemBlock } from "@src/common/blocks/AccordionItemBlock";
import { PageLayout } from "@src/layout/PageLayout";
import { useEffect, useMemo, useState } from "react";
import styled from "styled-components";

import { AccordionItemBlock } from "./AccordionItemBlock";

type AccordionBlockProps = PropsWithData<AccordionBlockData>;

export const AccordionBlock = withPreview(
({ data }: AccordionBlockProps) => (
<Root>
<ListBlock data={data} block={(block) => <AccordionItemBlock data={block} />} />
</Root>
),
({ data }: AccordionBlockProps) => {
const openByDefaultBlockKeys = useMemo(
() => data.blocks.filter((block) => block.props.openByDefault).map((block) => block.key),
[data.blocks],
);

const [expandedItems, setExpandedItems] = useState<Set<string>>(() => {
// Create a Set containing the keys of blocks where openByDefault is set to true
return new Set(openByDefaultBlockKeys);
});

const { showPreviewSkeletons, isSelected, isHovered } = usePreview();

useEffect(() => {
if (showPreviewSkeletons) {
const getFocusedBlockKey = () => {
const focusedBlock = data.blocks.find((block) => {
if (!isWithPreviewPropsData(block)) {
return false;
}

const url = block.adminMeta?.route;

return url && (isSelected(url, { exactMatch: false }) || isHovered(url, { exactMatch: false }));
});

return focusedBlock?.key;
};

const expandedItemsInPreview = new Set<string>(openByDefaultBlockKeys);
const focusedBlockKey = getFocusedBlockKey();

if (focusedBlockKey) {
expandedItemsInPreview.add(focusedBlockKey);
}

setExpandedItems(expandedItemsInPreview);
}
}, [showPreviewSkeletons, data.blocks, isSelected, isHovered, openByDefaultBlockKeys]);

const handleChange = (itemKey: string) => {
const newExpandedItems = new Set(expandedItems);

newExpandedItems.has(itemKey) ? newExpandedItems.delete(itemKey) : newExpandedItems.add(itemKey);

setExpandedItems(newExpandedItems);
};

return (
<Root>
{data.blocks.map((block) => (
<AccordionItemBlock
key={block.key}
data={block.props}
onChange={() => handleChange(block.key)}
isExpanded={expandedItems.has(block.key)}
/>
))}
</Root>
);
},
{ label: "Accordion" },
);

Expand Down
21 changes: 11 additions & 10 deletions demo/site/src/common/blocks/AccordionItemBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { BlocksBlock, PropsWithData, SupportedBlocks, withPreview } from "@comet/cms-site";
import { AccordionContentBlockData, AccordionItemBlockData } from "@src/blocks.generated";
import { useState } from "react";
import { RichTextBlock } from "@src/common/blocks/RichTextBlock";
import { SpaceBlock } from "@src/common/blocks/SpaceBlock";
import { StandaloneCallToActionListBlock } from "@src/common/blocks/StandaloneCallToActionListBlock";
import { StandaloneHeadingBlock } from "@src/common/blocks/StandaloneHeadingBlock";
import { SvgUse } from "@src/common/helpers/SvgUse";
import { useIntl } from "react-intl";
import styled, { css } from "styled-components";

import { Typography } from "../components/Typography";
import { SvgUse } from "../helpers/SvgUse";
import { RichTextBlock } from "./RichTextBlock";
import { SpaceBlock } from "./SpaceBlock";
import { StandaloneCallToActionListBlock } from "./StandaloneCallToActionListBlock";
import { StandaloneHeadingBlock } from "./StandaloneHeadingBlock";

const supportedBlocks: SupportedBlocks = {
richtext: (props) => <RichTextBlock data={props} />,
Expand All @@ -25,20 +24,22 @@ const AccordionContentBlock = withPreview(
{ label: "Accordion Content" },
);

type AccordionItemBlockProps = PropsWithData<AccordionItemBlockData>;
type AccordionItemBlockProps = PropsWithData<AccordionItemBlockData> & {
isExpanded: boolean;
onChange: () => void;
};

export const AccordionItemBlock = withPreview(
({ data: { title, content, openByDefault } }: AccordionItemBlockProps) => {
({ data: { title, content }, isExpanded, onChange }: AccordionItemBlockProps) => {
const intl = useIntl();
const [isExpanded, setIsExpanded] = useState<boolean>(openByDefault);

const ariaLabelText = isExpanded
? intl.formatMessage({ id: "accordionBlock.ariaLabel.expanded", defaultMessage: "Collapse accordion item" })
: intl.formatMessage({ id: "accordionBlock.ariaLabel.collapsed", defaultMessage: "Expand accordion item" });

return (
<>
<TitleWrapper onClick={() => setIsExpanded(!isExpanded)} aria-label={ariaLabelText}>
<TitleWrapper onClick={() => onChange()} aria-label={ariaLabelText}>
<Typography variant="h350">{title}</Typography>
<IconWrapper>
<AnimatedChevron href="/assets/icons/chevron-down.svg#root" $isExpanded={isExpanded} />
Expand Down
32 changes: 26 additions & 6 deletions packages/site/cms-site/src/preview/usePreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,43 @@ import { useIFrameBridge } from "../iframebridge/useIFrameBridge";
import { PreviewContext, PreviewContextOptions } from "./PreviewContext";

export interface PreviewHookReturn extends PreviewContextOptions {
isSelected: (url: string) => boolean;
isHovered: (url: string) => boolean;
isSelected: (url: string, options?: { exactMatch?: boolean }) => boolean;
isHovered: (url: string, options?: { exactMatch?: boolean }) => boolean;
}

export function usePreview(): PreviewHookReturn {
const iFrameBridge = useIFrameBridge();
const previewContext = useContext(PreviewContext);
const isSelected = useCallback(
(url: string) => {
return url === iFrameBridge.selectedAdminRoute;
(url: string, options?: { exactMatch?: boolean }) => {
if (!iFrameBridge.selectedAdminRoute) {
return false;
}

const exactMatch = options?.exactMatch ?? true;

if (exactMatch) {
return url === iFrameBridge.selectedAdminRoute;
} else {
return iFrameBridge.selectedAdminRoute?.startsWith(url);
}
},
[iFrameBridge.selectedAdminRoute],
);

const isHovered = useCallback(
(url: string) => {
return url === iFrameBridge.hoveredAdminRoute;
(url: string, options?: { exactMatch?: boolean }) => {
if (!iFrameBridge.hoveredAdminRoute) {
return false;
}

const exactMatch = options?.exactMatch ?? true;

if (exactMatch) {
return url === iFrameBridge.hoveredAdminRoute;
} else {
return iFrameBridge.hoveredAdminRoute?.startsWith(url);
}
},
[iFrameBridge.hoveredAdminRoute],
);
Expand Down

0 comments on commit f60b636

Please sign in to comment.