Skip to content

Commit

Permalink
Feat/form dynamic zone animation example (#217)
Browse files Browse the repository at this point in the history
* refactor: renamed dynamic zone components

* feat: added example page with animations
  • Loading branch information
MorganeLeCaignec authored Sep 25, 2024
1 parent 2004835 commit 4538167
Show file tree
Hide file tree
Showing 24 changed files with 745 additions and 624 deletions.
6 changes: 6 additions & 0 deletions .changeset/olive-months-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'storybook-pages': minor
'@smile/haring-react': minor
---

Renamed components into Zone, DynamicZone, and the example page into FormDynamicZone, added example page with animations using AutoAnimate
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

133 changes: 133 additions & 0 deletions packages/haring-react/src/Components/DynamicZone/DynamicZone.mock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type {
IBaseBlock,
IBaseBlockFull,
IDynamicZoneBlock,
} from '../../types';
import type { ReactElement } from 'react';

import { Group } from '@mantine/core';
import { Cube, Leaf } from '@phosphor-icons/react';

export interface IExampleBlock extends IBaseBlock {
value?: string;
}

export const dynamicZoneBlocksMock: IBaseBlockFull<IExampleBlock>[] = [
{
blockHeader: (
<>
<Cube key="1" />
<span key="2">Example A</span>
</>
),
blockType: 'exampleA',
id: '0',
opened: true,
value: 'existing value',
},
{
blockHeader: (
<>
<Cube key="1" />
<span key="2">Example A</span>
</>
),
blockType: 'exampleA',
id: '1',
opened: false,
},
{
blockHeader: (
<>
<Leaf key="1" />
<span key="2">Example B</span>
</>
),
blockType: 'exampleB',
id: '2',
opened: true,
value: 'selectB',
},
];

export const dynamicZoneAvailableBlocksMock: IDynamicZoneBlock<IExampleBlock>[] =
[
{
block: {
blockType: 'exampleA',
opened: true,
value: '',
},
blockButtonOptions: {
blockType: 'exampleA',
label: 'Example A',
leftSection: <Cube />,
},
blockCardOptions: {
blockHeader: (
<>
<Cube key="1" />
<span key="2">Example A</span>
</>
),
},
renderFunc: (b: IExampleBlock, i: number): ReactElement => {
return (
<Group>
<input
key={b.id + 1}
defaultValue={b.value}
id={`example.${i}.input1`}
placeholder="nested field 1"
required
/>
<input
key={b.id + 2}
id={`example.${i}.input2`}
placeholder="nested field 2"
required
/>
</Group>
);
},
},
{
block: {
blockType: 'exampleB',
opened: true,
value: '',
},
blockButtonOptions: {
blockType: 'exampleB',
label: 'Example B',
leftSection: <Leaf />,
maxInstances: 1,
tooltipLabel: (b) => (b.disabled ? 'Cannot add more than 1' : ''),
variant: 'outline',
},
blockCardOptions: {
blockHeader: (
<>
<Leaf key="1" />
<span key="2">Example B</span>
</>
),
},
renderFunc: (b: IExampleBlock, i: number): ReactElement => {
return (
<select
key={b.id}
defaultValue={b.value}
id={`example.${i}`}
required
>
<option disabled value="">
-- Please choose an option --
</option>
<option value="selectA">Value A</option>
<option value="selectB">Value B</option>
</select>
);
},
},
];
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import type { IExampleBlock } from './FormDynamicZone.mock';
import type { IExampleBlock } from './DynamicZone.mock';
import type { Meta, StoryObj } from '@storybook/react';

import { action } from '@storybook/addon-actions';

import { FormDynamicZone as Cmp } from './FormDynamicZone';
import { availableBlocksMock, blocksMock } from './FormDynamicZone.mock';
import { DynamicZone as Cmp } from './DynamicZone';
import {
dynamicZoneAvailableBlocksMock,
dynamicZoneBlocksMock,
} from './DynamicZone.mock';

const meta = {
component: Cmp<IExampleBlock>,
tags: ['autodocs'],
title: '3-custom/Form/FormDynamicZone',
title: '3-custom/Components/DynamicZone',
} satisfies Meta<typeof Cmp<IExampleBlock>>;

export default meta;
type IStory = StoryObj<typeof meta>;

export const FormDynamicZone: IStory = {
export const DynamicZone: IStory = {
args: {
availableBlocks: availableBlocksMock,
blocksArray: blocksMock,
availableBlocks: dynamicZoneAvailableBlocksMock,
blocksArray: dynamicZoneBlocksMock,
onAppendUpdate: action('append'),
onRemoveUpdate: action('remove'),
onSwapUpdate: action('swap'),
Expand All @@ -28,8 +31,8 @@ export const FormDynamicZone: IStory = {

export const CustomInternalProps: IStory = {
args: {
availableBlocks: availableBlocksMock,
blocksArray: blocksMock,
availableBlocks: dynamicZoneAvailableBlocksMock,
blocksArray: dynamicZoneBlocksMock,
internalDynamicZoneProps: {
internalBlockComponentProps: {
headerActionListProps: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import type { IExampleBlock } from './FormDynamicZone.mock';
import type { IExampleBlock } from './DynamicZone.mock';

import { renderWithProviders } from '@smile/haring-react-shared/test-utils';
import { action } from '@storybook/addon-actions';

import { FormDynamicZone } from './FormDynamicZone';
import { availableBlocksMock, blocksMock } from './FormDynamicZone.mock';
import { DynamicZone } from './DynamicZone';
import {
dynamicZoneAvailableBlocksMock,
dynamicZoneBlocksMock,
} from './DynamicZone.mock';

describe('FormDynamicZone', () => {
describe('DynamicZone', () => {
it('matches snapshot', () => {
const { container } = renderWithProviders(
<FormDynamicZone<IExampleBlock>
availableBlocks={availableBlocksMock}
blocksArray={blocksMock}
<DynamicZone<IExampleBlock>
availableBlocks={dynamicZoneAvailableBlocksMock}
blocksArray={dynamicZoneBlocksMock}
onAppendUpdate={action('append')}
onRemoveUpdate={action('remove')}
onSwapUpdate={action('swap')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,51 @@ import type {
IBaseBlock,
IBaseBlockButtonOptions,
IBaseBlockFull,
IFormDynamicZoneBlock,
IDynamicZoneBlock,
} from '../../types';
import type { IDynamicZoneInternalComponentProps } from '../DynamicZone/DynamicZone';
import type { IBaseBlockType } from '@smile/haring-react';
import type { IZoneInternalComponentProps } from '../Zone/Zone';
import type {
IDynamicZoneBlockInternalComponentProps,
IDynamicZoneBlockReference,
} from '@smile/haring-react/src/Form/DynamicZone/DynamicZoneBlock/DynamicZoneBlock';
IZoneBlockInternalComponentProps,
IZoneBlockReference,
} from '../Zone/ZoneBlock/ZoneBlock';
import type { IBaseBlockType } from '@smile/haring-react';
import type { IAction } from '@smile/haring-react-shared';
import type { ReactElement } from 'react';

import { ArrowDown, ArrowUp, Trash } from '@phosphor-icons/react';
import { isNotNullNorEmpty } from '@smile/haring-react-shared';
import { useMemo } from 'react';

import { DynamicZone } from '../DynamicZone/DynamicZone';
import { Zone } from '../Zone/Zone';

interface IFormDynamicZoneActionLabels {
interface IDynamicZoneActionLabels {
deleteLabel: string;
moveDownLabel: string;
moveUpLabel: string;
}

const defaultActionLabels: IFormDynamicZoneActionLabels = {
const defaultActionLabels: IDynamicZoneActionLabels = {
deleteLabel: 'Delete',
moveDownLabel: 'Move Down',
moveUpLabel: 'Move Up',
};

export interface IFormDynamicZoneProps<Block extends IBaseBlock> {
actionLabels?: IFormDynamicZoneActionLabels;
availableBlocks: IFormDynamicZoneBlock<Block>[];
export interface IDynamicZoneProps<Block extends IBaseBlock> {
actionLabels?: IDynamicZoneActionLabels;
availableBlocks: IDynamicZoneBlock<Block>[];
blocksArray: Block[];
internalDynamicZoneProps?: {
internalBlockComponentProps?: IDynamicZoneBlockInternalComponentProps;
internalComponentProps?: IDynamicZoneInternalComponentProps;
internalBlockComponentProps?: IZoneBlockInternalComponentProps;
internalComponentProps?: IZoneInternalComponentProps;
};
onAppendUpdate: (newBlock: Block) => void;
onRemoveUpdate: (index: number) => void;
onSwapUpdate: (firstIndex: number, secondIndex: number) => void;
onToggleUpdate: (index: number, opened: boolean) => void;
}

export function FormDynamicZone<Block extends IBaseBlock>(
props: IFormDynamicZoneProps<Block>,
export function DynamicZone<Block extends IBaseBlock>(
props: IDynamicZoneProps<Block>,
): ReactElement {
const {
actionLabels = defaultActionLabels,
Expand Down Expand Up @@ -82,20 +82,17 @@ export function FormDynamicZone<Block extends IBaseBlock>(
);
if (correspondingType === undefined) {
throw Error(
`Could not render a block of blockType '${block.blockType} in given IFormDynamicZoneBlock[]'`,
`Could not render a block of blockType '${block.blockType} in given IDynamicZoneBlock[]'`,
);
}
return correspondingType.renderFunc(block, index);
}

function onRemove(ref: IDynamicZoneBlockReference): void {
function onRemove(ref: IZoneBlockReference): void {
onRemoveUpdate(ref.index);
}

function onSwap(
ref: IDynamicZoneBlockReference,
direction: 'down' | 'up',
): void {
function onSwap(ref: IZoneBlockReference, direction: 'down' | 'up'): void {
const secondIndex = ref.index + (direction === 'up' ? -1 : 1);
if (secondIndex >= 0 && secondIndex < ref.arrayLength) {
onSwapUpdate(ref.index, secondIndex);
Expand All @@ -111,15 +108,15 @@ export function FormDynamicZone<Block extends IBaseBlock>(
}

function isMoveDisabled(
ref: IDynamicZoneBlockReference,
ref: IZoneBlockReference,
direction: 'down' | 'up',
): boolean {
return direction === 'up'
? ref.index === 0
: ref.index === ref.arrayLength - 1;
}

const formDynamicZoneDefaultActions: IAction<IDynamicZoneBlockReference>[] = [
const dynamicZoneDefaultActions: IAction<IZoneBlockReference>[] = [
{
componentProps: (ref) => ({ disabled: isMoveDisabled(ref, 'up') }),
icon: <ArrowUp size={16} />,
Expand Down Expand Up @@ -148,7 +145,7 @@ export function FormDynamicZone<Block extends IBaseBlock>(
);
if (correspondingType === undefined) {
throw Error(
`Could not append a block of blockType '${type} in given IFormDynamicZoneBlock[]'`,
`Could not append a block of blockType '${type} in given IDynamicZoneBlock[]'`,
);
}
const newBlock = {
Expand All @@ -162,13 +159,13 @@ export function FormDynamicZone<Block extends IBaseBlock>(
.filter(isNotNullNorEmpty)
.map((b) => ({
...b,
blockActions: formDynamicZoneDefaultActions,
blockActions: dynamicZoneDefaultActions,
...availableBlocks.find((o) => o.block.blockType === b.blockType)
?.blockCardOptions,
}));

return (
<DynamicZone
<Zone
blockOptions={blockOptions}
blocks={blocksWithOptionsAndActions}
fluid
Expand Down
Loading

0 comments on commit 4538167

Please sign in to comment.