Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MOON-475: Add components Fieldset & DynamicFieldset #478

Merged
merged 4 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import clsx from 'clsx';
import '../Fieldset.scss';
import type {ControlledDynamicFieldsetProps} from './DynamicFieldset.types';
import {Switch, Typography} from '~/components';

export const ControlledDynamicFieldset = React.forwardRef<HTMLFieldSetElement, ControlledDynamicFieldsetProps>(({
id,
label,
helper,
children,
className,
buttons,
checked = false,
onChange,
...props
}, ref) => {
return (
<fieldset
ref={ref}
id={id}
Eevolee marked this conversation as resolved.
Show resolved Hide resolved
className={clsx(
'moonstone-dynamic-fieldset',
checked && 'moonstone-dynamic-fieldset_open',
'flexCol_nowrap',
className
)}
aria-checked={checked}
{...props}
>
<legend className={clsx('flexRow_nowrap', 'flexFluid', 'alignCenter')}>
<Typography isNowrap className="flexRow_nowrap flexFluid alignCenter" component="label" variant="heading" weight="bold">{label}</Typography>
<Switch id="moonstone-dynamic-fieldset-switch" checked={checked} onChange={onChange}/>
{buttons && buttons}
</legend>
{helper &&
<Typography variant="caption" className={clsx('moonstone-fieldset_helper')}>{helper}</Typography>}
<div className={clsx('moonstone-fieldset_children', 'flexCol_nowrap')}>
Eevolee marked this conversation as resolved.
Show resolved Hide resolved
{(checked && children) && children}
</div>
</fieldset>
);
});

ControlledDynamicFieldset.displayName = 'ControlledDynamicFieldset';
Empty file.
77 changes: 77 additions & 0 deletions src/components/Fieldset/DynamicFieldset/DynamicFieldset.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import {DynamicFieldset} from './index';
import {Button, Input, Field, FieldSelector} from '~/components';
import {Add, Love} from '~/icons';

describe('DynamicFieldset', () => {
it('should display additional class names', () => {
render(<DynamicFieldset data-testid="dynamic-fieldset" className="extra"><FieldSelector selector={<textarea placeholder="Input value"/>}/></DynamicFieldset>);
expect(screen.getByTestId('dynamic-fieldset')).toHaveClass('extra');
});

it('should display label', () => {
render(<DynamicFieldset label="Dynamic fieldset label"/>);
expect(screen.queryByText('Dynamic fieldset label')).toBeInTheDocument();
});

it('should display helper', () => {
render(<DynamicFieldset helper="Dynamic fieldset helper"/>);
expect(screen.queryByText('Dynamic fieldset helper')).toBeInTheDocument();
});

it('should display buttons', () => {
render(<DynamicFieldset buttons={<Button label="Click me"/>}/>);
expect(screen.queryByText('Click me')).toBeInTheDocument();
});

it('should display multiple buttons', () => {
render(<DynamicFieldset buttons={<><Button icon={<Add/>} label="Click me"/><Button icon={<Love/>} label="Click me"/></>}/>);
expect(screen.getAllByText('Click me')).toHaveLength(2);
});

it('should display children when the switch is clicked', async () => {
const user = userEvent.setup();

render(<DynamicFieldset><Field id="field" label="Field" helper="information"><FieldSelector selector={<Input size="big" value="Input value"/>}/></Field></DynamicFieldset>);
await user.click(screen.getByRole('checkbox'), '1');

expect(screen.queryByDisplayValue('Input value')).toBeInTheDocument();
});
});

describe('UncontrolledDynamicFieldset', () => {
it('should display children when defaultChecked is set', () => {
render(<DynamicFieldset defaultChecked><Field id="field" label="Field" helper="information"><FieldSelector selector={<Input size="big" value="Input value"/>}/></Field></DynamicFieldset>);
expect(screen.queryByDisplayValue('Input value')).toBeInTheDocument();
});

it('should call specified onChange function', async () => {
const user = userEvent.setup();
const handleChange = jest.fn();

render(<DynamicFieldset defaultChecked data-testid="dynamic-fieldset" onChange={handleChange}/>);
await user.click(screen.getByRole('checkbox'), '1');

expect(handleChange).toHaveBeenCalledTimes(1);
});
});

describe('ControlledDynamicFieldset', () => {
it('should display children when checked', () => {
render(<DynamicFieldset checked><Field id="field" label="Field" helper="information"><FieldSelector selector={<Input size="big" value="Input value"/>}/></Field></DynamicFieldset>);
expect(screen.queryByDisplayValue('Input value')).toBeInTheDocument();
});

it('should call specified onChange function', async () => {
const user = userEvent.setup();
const handleChange = jest.fn();

render(<DynamicFieldset checked data-testid="dynamic-fieldset" onChange={handleChange}/>);
await user.click(screen.getByRole('checkbox'), '1');

expect(handleChange).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, {useState} from 'react';
import {StoryObj, Meta} from '@storybook/react';

import {DynamicFieldset} from './index';
import {Field, FieldSelector} from '~/components';
import markdownNotes from './DynamicFieldset.md';
import {Button, Chip, Input} from '~/components';
import {Add, Language, MoreVert} from '~/icons';

const meta: Meta<typeof DynamicFieldset> = {
title: 'Components/Fieldset/DynamicFieldset',
component: DynamicFieldset,
tags: ['beta'],

parameters: {
layout: 'padded',
actions: {argTypesRegex: '^on.*'},
notes: {markdown: markdownNotes}
},
args: {
id: 'dynamic-fieldset',
label: 'Dynamic fieldset',
helper: 'dynamic fieldset information',
buttons: <Button icon={<MoreVert/>} variant="ghost"/>,
children: <Field id="field" label="Field" chips={<><Chip color="accent" label="Required"/><Chip icon={<Language/>} label="Shared by all languages"/></>} buttons={<><Button icon={<Add/>} label="Add"/><Button icon={<MoreVert/>} variant="ghost"/></>} helper="information"><FieldSelector selector={<Input size="big" placeholder="Input value"/>}/></Field>
},
argTypes: {
buttons: {
control: false
},
children: {
control: false
}
}
};
export default meta;

type Story = StoryObj<typeof DynamicFieldset>;

export const Uncontrolled: Story = {};

export const Controlled: Story = {
render: args => {
const [checked, setChecked] = useState(false);
return (
<DynamicFieldset
checked={checked}
onChange={e => setChecked(e.currentTarget.checked)}
{...args}
/>
);
}
};
14 changes: 14 additions & 0 deletions src/components/Fieldset/DynamicFieldset/DynamicFieldset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import type {DynamicFieldsetProps} from './DynamicFieldset.types';
import {UncontrolledDynamicFieldset} from './UncontrolledDynamicFieldset';
import {ControlledDynamicFieldset} from './ControlledDynamicFieldset';

export const DynamicFieldset: React.FC<DynamicFieldsetProps> = ({checked, onChange, ...props}) => {
if (typeof checked === 'undefined') {
return <UncontrolledDynamicFieldset onChange={onChange} {...props}/>;
}

return <ControlledDynamicFieldset checked={checked} onChange={onChange} {...props}/>;
};

DynamicFieldset.displayName = 'DynamicFieldset';
37 changes: 37 additions & 0 deletions src/components/Fieldset/DynamicFieldset/DynamicFieldset.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react';
import type {FieldsetProps} from '../Fieldset.types';

type BaseProps = Omit<FieldsetProps, 'children'> & {
/**
* Define fieldset field(s)
*/
children?: React.ReactElement;
};

type ControlledProps = {
/**
* Whether dynamic fieldset is checked or not
*/
checked: boolean;

/**
* Dynamic fieldset's function onChange
*/
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

type UncontrolledProps = {
/**
* Whether dynamic fieldset is checked by default
*/
defaultChecked?: boolean;

/**
* Dynamic fieldset's function onChange
*/
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

export type DynamicFieldsetProps = BaseProps & Partial<ControlledProps> & Partial<UncontrolledProps>;
export type ControlledDynamicFieldsetProps = BaseProps & ControlledProps;
export type UncontrolledDynamicFieldsetProps = BaseProps & UncontrolledProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, {useState} from 'react';
import type {UncontrolledDynamicFieldsetProps} from './DynamicFieldset.types';
import {ControlledDynamicFieldset} from './ControlledDynamicFieldset';

export const UncontrolledDynamicFieldset: React.FC<UncontrolledDynamicFieldsetProps> = ({defaultChecked, onChange, ...props}) => {
const [checked, setChecked] = useState(defaultChecked);

const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.currentTarget.checked);

if (typeof onChange !== 'undefined') {
onChange(event);
}
};

return <ControlledDynamicFieldset checked={checked} onChange={handleOnChange} {...props}/>;
};

UncontrolledDynamicFieldset.displayName = 'UncontrolledDynamicFieldset';
1 change: 1 addition & 0 deletions src/components/Fieldset/DynamicFieldset/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './DynamicFieldset';
Empty file.
22 changes: 22 additions & 0 deletions src/components/Fieldset/Fieldset.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.moonstone-fieldset,
.moonstone-dynamic-fieldset {
padding-left: 32px;

border-left: 3px solid transparent;

& legend {
width: 100%;
}
}

.moonstone-fieldset_helper {
margin-top: 2px;
padding-right: calc(var(--spacing-large) + var(--spacing-nano));

color: var(--color-gray_dark_plain60);
}

.moonstone-fieldset_children {
gap: var(--spacing-large);
margin-top: var(--spacing-small);
}
43 changes: 43 additions & 0 deletions src/components/Fieldset/Fieldset.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import {render, screen} from '@testing-library/react';

import {Fieldset} from './index';
import {Button, Input, Field, FieldSelector} from '~/components';
import {Add, Love} from '~/icons';

describe('Fieldset', () => {
it('should display additional class names', () => {
render(<Fieldset data-testid="fieldset" className="extra"><FieldSelector selector={<textarea placeholder="Input value"/>}/></Fieldset>);
expect(screen.getByTestId('fieldset')).toHaveClass('extra');
});

it('should display label', () => {
render(<Fieldset label="Fieldset label"/>);
expect(screen.queryByText('Fieldset label')).toBeInTheDocument();
});

it('should display helper', () => {
render(<Fieldset helper="Fieldset helper"/>);
expect(screen.queryByText('Fieldset helper')).toBeInTheDocument();
});

it('should display children', () => {
render(<Fieldset><Field id="field" label="Field" helper="information"><FieldSelector selector={<Input size="big" value="Input value"/>}/></Field></Fieldset>);
expect(screen.queryByDisplayValue('Input value')).toBeInTheDocument();
});

it('should display multiple children', () => {
render(<Fieldset><Field id="field" label="Field" helper="information"><FieldSelector selector={<Input size="big" value="Input value"/>}/></Field><Field id="field" label="Field" helper="information"><FieldSelector selector={<Input size="big" value="Input value"/>}/></Field></Fieldset>);
expect(screen.getAllByDisplayValue('Input value')).toHaveLength(2);
});

it('should display buttons', () => {
render(<Fieldset buttons={<Button label="Click me"/>}/>);
expect(screen.queryByText('Click me')).toBeInTheDocument();
});

it('should display multiple buttons', () => {
render(<Fieldset buttons={<><Button icon={<Add/>} label="Click me"/><Button icon={<Love/>} label="Click me"/></>}/>);
expect(screen.getAllByText('Click me')).toHaveLength(2);
});
});
Loading
Loading