Skip to content

Commit 67f90c4

Browse files
feat(react,js-components): slot limit prop, exported <Block> (#680)
* feat(react,js): slot limit prop, exported `<Block>` * test: slot limit * docs: improve jsdoc --------- Co-authored-by: Ondřej Pešička <77627332+OPesicka@users.noreply.github.com>
1 parent 7358e6e commit 67f90c4

9 files changed

Lines changed: 90 additions & 8 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"shared": "pnpm --filter shared",
1212
"tsc": "pnpm -r tsc",
1313
"test": "pnpm -r test",
14+
"build": "pnpm -r build",
1415
"lint": "oxlint --type-aware",
1516
"commitlint": "commitlint --edit",
1617
"pre-push": "concurrently \"pnpm lint\" \"pnpm test\" \"pnpm tsc\" \"pnpm spellcheck\"",

workspaces/e2e/pages/js.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
addFloatingBlocksChangeListener,
77
fetchWorkflows,
88
} from "@flows/js";
9+
import type { FlowsSlot } from "@flows/js-components";
910
import { setupJsComponents } from "@flows/js-components";
1011
import * as _components from "@flows/js-components/components";
1112
import * as _tourComponents from "@flows/js-components/tour-components";
@@ -36,6 +37,7 @@ const customFetch =
3637
const noCurrentBlocks =
3738
new URLSearchParams(window.location.search).get("noCurrentBlocks") === "true";
3839
const language = new URLSearchParams(window.location.search).get("language") as LanguageOption;
40+
const slotLimit = new URLSearchParams(window.location.search).get("slotLimit");
3941

4042
class Card extends LitElement {
4143
@property({ type: String })
@@ -181,3 +183,6 @@ addFloatingBlocksChangeListener((blocks) => {
181183
document.querySelector("#fetchWorkflows")?.addEventListener("click", () => {
182184
void fetchWorkflows();
183185
});
186+
187+
const flowsSlotElement = document.querySelector<FlowsSlot>("flows-slot");
188+
if (flowsSlotElement) flowsSlotElement.limit = slotLimit ? Number(slotLimit) : undefined;

workspaces/e2e/pages/react.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const noCurrentBlocks =
4747
const language = new URLSearchParams(window.location.search).get("language") as LanguageOption;
4848
const enableLinkComponent =
4949
new URLSearchParams(window.location.search).get("LinkComponent") === "true";
50+
const slotLimit = new URLSearchParams(window.location.search).get("slotLimit");
5051

5152
const Card: FC<ComponentProps<{ text: string }>> = (props) => (
5253
<div
@@ -124,7 +125,11 @@ const Home: FC = () => {
124125
<h1>heading 1</h1>
125126
<h2>Subtitle</h2>
126127

127-
<FlowsSlot id="my-slot" placeholder={<p>Slot placeholder</p>} />
128+
<FlowsSlot
129+
id="my-slot"
130+
limit={slotLimit ? Number(slotLimit) : undefined}
131+
placeholder={<p>Slot placeholder</p>}
132+
/>
128133

129134
{!noCurrentBlocks && <p className="current-blocks">{JSON.stringify(floatingBlocks)}</p>}
130135

workspaces/e2e/tests/slot.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ const run = (packageName: string) => {
7373
"block number two",
7474
);
7575
});
76+
test(`${packageName} - should limit rendered blocks by slot limit`, async ({ page }) => {
77+
await mockBlocksEndpoint(page, [
78+
getCard({ text: "block number one" }),
79+
getCard({ text: "block number two" }),
80+
]);
81+
await page.goto(`/${packageName}.html?slotLimit=1`);
82+
await expect(page.getByText("block number one", { exact: true })).toBeVisible();
83+
await expect(page.getByText("block number two", { exact: true })).toBeHidden();
84+
await mockBlocksEndpoint(page, [
85+
getCard({ text: "block number one" }),
86+
getCard({ text: "block number two" }),
87+
]);
88+
await page.goto(`/${packageName}.html?slotLimit=2`);
89+
await expect(page.getByText("block number one", { exact: true })).toBeVisible();
90+
await expect(page.getByText("block number two", { exact: true })).toBeVisible();
91+
});
7692
};
7793

7894
run("js");

workspaces/e2e/tests/tour-trigger.spec.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { Block, TourTrigger, TourTriggerExpression, TourTriggerType } from "@flows/shared";
1+
import type {
2+
ApiSurvey,
3+
Block,
4+
TourTrigger,
5+
TourTriggerExpression,
6+
TourTriggerType,
7+
} from "@flows/shared";
28
import test, { expect } from "@playwright/test";
39
import { randomUUID } from "crypto";
410
import { mockBlocksEndpoint } from "./utils";
@@ -385,6 +391,21 @@ const run = (packageName: string) => {
385391
await page.reload();
386392
await expect(page.getByText("World", { exact: true })).toBeVisible();
387393
});
394+
test(`${packageName} - should be hidden with different block state ID`, async ({ page }) => {
395+
const block = getSurvey([{ type: "click", value: "h1" }]);
396+
await mockBlocksEndpoint(page, [block]);
397+
await page.goto(`/${packageName}.html`);
398+
await expect(page.getByText("Hello", { exact: true })).toBeHidden();
399+
await page.locator("h1").click();
400+
await expect(page.getByText("Hello", { exact: true })).toBeVisible();
401+
const surveyBlockWithDifferentBlockStateId: Block = {
402+
...block,
403+
survey: { ...(block.survey as ApiSurvey), blockStateId: randomUUID() },
404+
};
405+
await mockBlocksEndpoint(page, [surveyBlockWithDifferentBlockStateId]);
406+
await page.reload();
407+
await expect(page.getByText("Hello", { exact: true })).toBeHidden();
408+
});
388409
});
389410
};
390411

workspaces/js-components/src/slot.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ export class FlowsSlot extends LitElement {
1414
@property({ type: String, attribute: "data-slot-id" })
1515
slotId: string;
1616

17+
/**
18+
* Limit of how many blocks to render in this slot. Defaults to no limit.
19+
*
20+
* Useful when multiple blocks match the same slot.
21+
*
22+
* @default undefined
23+
*/
24+
@property({ type: Number })
25+
limit?: number;
26+
1727
connectedCallback(): void {
1828
super.connectedCallback();
1929

@@ -46,7 +56,10 @@ export class FlowsSlot extends LitElement {
4656
else this.placeholderElement.hidden = false;
4757
}
4858

49-
return repeat(this._blocks, getBlockRenderKey, (block) => {
59+
const blocksToRender =
60+
this.limit === undefined ? this._blocks : this._blocks.slice(0, this.limit);
61+
62+
return repeat(blocksToRender, getBlockRenderKey, (block) => {
5063
return Block({ block });
5164
});
5265
}

workspaces/react/src/components/block.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ interface Props {
66
block: ActiveBlock;
77
}
88

9+
/**
10+
* Renders a single `ActiveBlock` using the matching component from `components`, `tourComponents`, or `surveyComponents` registered in `FlowsProvider`.
11+
*
12+
* `ActiveBlock` can be obtained from `useCurrentFloatingBlocks` or `useCurrentSlotBlocks`.
13+
*
14+
* Useful for advanced block rendering implementations.
15+
*/
916
export const Block: FC<Props> = ({ block }) => {
1017
const { components, tourComponents, surveyComponents } = useFlowsContext();
1118

workspaces/react/src/components/flows-slot.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
1-
import { type FC, type ReactNode } from "react";
1+
import { useMemo, type FC, type ReactNode } from "react";
22
import { getBlockRenderKey } from "@flows/shared";
33
import { useCurrentSlotBlocks } from "../hooks/use-current-blocks";
44
import { Block } from "./block";
55

66
export interface FlowsSlotProps {
77
id: string;
88
placeholder?: ReactNode;
9+
/**
10+
* Limit of how many blocks to render in this slot. Defaults to no limit.
11+
*
12+
* Useful when multiple blocks match the same slot.
13+
*
14+
* @default undefined
15+
*/
16+
limit?: number;
917
}
1018

11-
export const FlowsSlot: FC<FlowsSlotProps> = ({ id, placeholder }) => {
12-
const items = useCurrentSlotBlocks(id);
19+
export const FlowsSlot: FC<FlowsSlotProps> = ({ id, placeholder, limit }) => {
20+
const blocks = useCurrentSlotBlocks(id);
1321

14-
if (items.length)
15-
return items.map((item) => {
22+
const blocksToRender = useMemo(
23+
() => (limit === undefined ? blocks : blocks.slice(0, limit)),
24+
[blocks, limit],
25+
);
26+
27+
if (blocksToRender.length)
28+
return blocksToRender.map((item) => {
1629
return <Block key={getBlockRenderKey(item)} block={item} />;
1730
});
1831

workspaces/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ export type {
3333
export * from "./components/flows-slot";
3434
export { FlowsProvider } from "./flows-provider";
3535
export * from "./methods";
36+
export { Block } from "./components/block";

0 commit comments

Comments
 (0)