Skip to content

Commit

Permalink
feat(Radio, Fieldset): ✨ New Radio & Fieldset component (unreleas…
Browse files Browse the repository at this point in the history
…ed) (#666)

Co-authored-by: Albertlarsen <[email protected]>
  • Loading branch information
mimarz and Albertlarsen authored Jul 31, 2023
1 parent 75b8bd7 commit e2d6f89
Show file tree
Hide file tree
Showing 24 changed files with 1,274 additions and 17 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
],
"[ignore]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
}
},
"jest.jestCommandLine": "yarn test"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

display: inline-block;
margin: 0;
padding: 0;
color: var(--fds-semantic-text-neutral-default);
}

Expand Down
28 changes: 28 additions & 0 deletions packages/react/src/components/form/Fieldset/Fieldset.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.fieldset {
margin: 0;
padding: 0;
border: 0;
min-width: 0;
}

.fieldset > :not(:first-child, :empty) {
margin-top: var(--fds-spacing-2);
}

.description {
color: var(--fds-semantic-text-neutral-subtle);
font-weight: 400;
}

.readonly {
opacity: 0.3;
}

.readonly > .legend {
display: inline-flex;
}

.disabled > .legend,
.disabled > .description {
color: var(--fds-semantic-border-neutral-subtle);
}
51 changes: 51 additions & 0 deletions packages/react/src/components/form/Fieldset/Fieldset.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import { Fieldset } from './Fieldset';

describe('Fieldset', () => {
test('has correct legend and description', () => {
render(
<Fieldset
legend='test legend'
description='test description'
></Fieldset>,
);
const fieldset = screen.getByRole('group', { name: 'test legend' });
expect(fieldset).toBeDefined();
expect(fieldset).toHaveAccessibleDescription('test description');
});
test('is described by error message and invalid', () => {
render(
<Fieldset
legend='test legend'
description='test description'
error='test error'
></Fieldset>,
);

const errorFieldset = screen.getByRole('group', {
description: 'test description test error',
});
expect(errorFieldset).toBeDefined();
expect(errorFieldset).toHaveAccessibleDescription(
'test description test error',
);
expect(errorFieldset).toBeInvalid();
});
test('and its children are disabled', () => {
render(
<Fieldset disabled>
<input
value='test'
readOnly
/>
</Fieldset>,
);

const input = screen.getByDisplayValue<HTMLInputElement>('test');

expect(input).toBeDisabled();
expect(screen.getByRole('group')).toBeDisabled();
});
});
98 changes: 98 additions & 0 deletions packages/react/src/components/form/Fieldset/Fieldset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { FieldsetHTMLAttributes, ReactNode } from 'react';
import React, { useContext, forwardRef, createContext } from 'react';
import cn from 'classnames';
import { PadlockLockedFillIcon } from '@navikt/aksel-icons';

import { Label, Paragraph, ErrorMessage } from '../../Typography';

import { useFieldset } from './useFieldset';
import classes from './Fieldset.module.css';

export type FieldsetContextType = {
error?: ReactNode;
errorId?: string;
disabled?: boolean;
readOnly?: boolean;
size?: 'xsmall' | 'small' | 'medium';
};

export const FieldsetContext = createContext<FieldsetContextType | null>(null);

export type FieldsetProps = {
/** A description of the fieldset. This will appear below the legend. */
description?: ReactNode;
/** Toggle `disabled` all input fields within the fieldset. */
disabled?: boolean;
/** If set, this will diplay an error message at the bottom of the fieldset. */
error?: ReactNode;
/** The legend of the fieldset. */
legend?: ReactNode;
/** The size of the fieldset. */
size?: 'xsmall' | 'small' | 'medium';
/** Toggle `readOnly` on fieldset context.
* @note This does not prevent fieldset values from being submited */
readOnly?: boolean;
} & FieldsetHTMLAttributes<HTMLFieldSetElement>;

export const Fieldset = forwardRef<HTMLFieldSetElement, FieldsetProps>(
(props, ref) => {
const { children, legend, description, error, ...rest } = props;

const { fieldsetProps, size, readOnly, errorId, hasError, descriptionId } =
useFieldset(props);

const fieldset = useContext(FieldsetContext);

return (
<FieldsetContext.Provider
value={{
error: error ?? fieldset?.error,
errorId: hasError ? errorId : undefined,
size,
disabled: props?.disabled,
readOnly,
}}
>
<fieldset
{...rest}
{...fieldsetProps}
className={cn(
classes.fieldset,
readOnly && classes.readonly,
props?.disabled && classes.disabled,
rest.className,
)}
ref={ref}
>
<Label
as='legend'
size={size}
className={classes.legend}
>
{readOnly && <PadlockLockedFillIcon />}
{legend}
</Label>
{description && (
<Paragraph
id={descriptionId}
className={classes.description}
size={size}
as='div'
short
>
{description}
</Paragraph>
)}
{children}
<div
id={errorId}
aria-live='polite'
aria-relevant='additions removals'
>
{hasError && <ErrorMessage size={size}>{error}</ErrorMessage>}
</div>
</fieldset>
</FieldsetContext.Provider>
);
},
);
1 change: 1 addition & 0 deletions packages/react/src/components/form/Fieldset/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Fieldset';
17 changes: 17 additions & 0 deletions packages/react/src/components/form/Fieldset/useFieldset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useFormField } from '../useFormField';

import type { FieldsetProps } from './Fieldset';

/** Handles fieldset props and state */
export const useFieldset = (props: FieldsetProps) => {
const formField = useFormField(props, 'fieldset');
const { inputProps } = formField;

return {
...formField,
fieldsetProps: {
'aria-invalid': inputProps['aria-invalid'],
'aria-describedby': inputProps['aria-describedby'],
},
};
};
11 changes: 11 additions & 0 deletions packages/react/src/components/form/Radio/Group/Group.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.xsmall,
.small,
.medium {
margin-left: calc(var(--fds-spacing-2) * -1);
}

.inline {
display: flex;
flex-direction: row;
gap: var(--fds-spacing-2);
}
106 changes: 106 additions & 0 deletions packages/react/src/components/form/Radio/Group/Group.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useState } from 'react';
import type { Meta, StoryFn } from '@storybook/react';

import { Button, Paragraph } from '../../..';
import { Radio } from '../';

export default {
title: 'ikke utgitt/Radio/Group',
component: Radio.Group,
parameters: {
status: {
type: 'beta',
url: 'http://www.url.com/status',
},
},
} as Meta;

export const Preview: StoryFn<typeof Radio.Group> = (args) => (
<Radio.Group {...args}>
<Radio value='vanilje'>Vanilje</Radio>
<Radio value='jordbær'>Jordbær</Radio>
<Radio value='sjokolade'>Sjokolade</Radio>
<Radio value='spiser-ikke-is'>Jeg spiser ikke iskrem</Radio>
</Radio.Group>
);

Preview.args = {
legend: 'Hvilken iskremsmak er best?',
description: 'Velg din favorittsmak blant alternativene.',
readOnly: false,
disabled: false,
error: '',
};

export const Error: StoryFn<typeof Radio> = () => (
<Radio.Group
legend='Velg pizza (påkreved)'
description='Alle pizzaene er laget på våre egne nybakte bunner og serveres med kokkens egen osteblanding og tomatsaus.'
error='Du må velge en av våre pizzaer for å legge inn bestilling'
>
<Radio value='ost'>Bare ost</Radio>
<Radio
value='Dobbeldekker'
description='Chorizo spesial med kokkens luksuskylling'
>
Dobbeldekker
</Radio>
<Radio value='flammen'>Flammen</Radio>
<Radio value='snadder'>Snadder</Radio>
</Radio.Group>
);

export const Controlled: StoryFn<typeof Radio> = () => {
const [value, setValue] = useState<string>();

return (
<>
<span style={{ display: 'flex', gap: '1rem' }}>
<Button onClick={() => setValue('flammen')}>Velg Flammen</Button>
<Button onClick={() => setValue('snadder')}>Velg Snadder</Button>
<Paragraph spacing>Du har valgt: {value}</Paragraph>
</span>
<Radio.Group
legend='Velg pizza (påkreved)'
description='Alle pizzaene er laget på våre egne nybakte bunner og serveres med kokkens egen osteblanding og tomatsaus.'
value={value}
onChange={(e) => setValue(e.target.value)}
>
<Radio value='ost'>Bare ost</Radio>
<Radio
value='Dobbeldekker'
description='Chorizo spesial med kokkens luksuskylling'
>
Dobbeldekker
</Radio>
<Radio value='flammen'>Flammen</Radio>
<Radio value='snadder'>Snadder</Radio>
</Radio.Group>
</>
);
};

export const ReadOnly = Preview.bind({});

ReadOnly.args = {
...Preview.args,
readOnly: true,
};

export const Disabled = Preview.bind({});

Disabled.args = {
...Preview.args,
disabled: true,
};

export const Inline: StoryFn<typeof Radio.Group> = () => (
<Radio.Group
legend='Kontaktes på epost?'
description='Bekreft om du ønsker å bli kontaktet per epost. '
inline
>
<Radio value='ja'>Ja</Radio>
<Radio value='nei'>Nei</Radio>
</Radio.Group>
);
Loading

0 comments on commit e2d6f89

Please sign in to comment.