Skip to content

Commit

Permalink
Add GridGroup
Browse files Browse the repository at this point in the history
  • Loading branch information
robphoenix committed Jun 17, 2024
1 parent e1c70ad commit 357b771
Show file tree
Hide file tree
Showing 12 changed files with 4,088 additions and 5,142 deletions.
49 changes: 49 additions & 0 deletions packages/web-ui/src/Checkbox/BaseCheckboxGroup.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ComponentPropsWithoutRef, ReactNode } from 'react';
import { CheckboxGroupContextValue } from './CheckboxGroup.context';
import { BoxProps } from '../Box';

export interface BaseCheckboxGroupProps extends ComponentPropsWithoutRef<'fieldset'> {
name?: CheckboxGroupContextValue['name'];
required?: boolean;
disabled?: boolean;
defaultValue?: Array<string>;
value?: CheckboxGroupContextValue['value'];
onValueChange?: (value: Array<string>) => void;
/**
* The label for the radio group. This should contain the question being
* answered by the radio group.
*
* If you don't include a label you need to ensure you use the `aria-label`
* or `aria-labelledby` prop to properly associate a label with the radio
* group.
*/
label?: ReactNode;
/**
* Helper text for the radio group. Provides a hint such as specific
* requirements for what to choose. When displayed, child `Radio` or
* `RadioTile` components will not display `helperText`.
*/
helperText?: ReactNode;
/**
* Position of the helper text.
* @default 'top'
*/
helperTextPosition?: 'top' | 'bottom';
/**
* Set whether to display the helper text icon.
*/
showHelperTextIcon?: boolean;
/** Controls whether the error message is displayed. */
error?: boolean;
/** The error message to be displayed. */
errorMessage?: ReactNode;
/**
* Set whether to display the error message icon.
*/
showErrorMessageIcon?: boolean;
/**
* Set the width of the RadioGroup children, separate to the width of the
* entire RadioGroup.
*/
contentWidth?: BoxProps['width'];
}
125 changes: 125 additions & 0 deletions packages/web-ui/src/Checkbox/BaseCheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import * as React from 'react';
import { useControllableState } from '@radix-ui/react-use-controllable-state';
import { Fieldset } from '../Fieldset';
import { FieldsetLegend } from '../FieldsetLegend';
import { HelperText } from '../HelperText';
import { Flex } from '../Flex';
import { mergeIds } from '../utils';
import { CheckboxGroupContext } from './CheckboxGroup.context';
import { useIds } from '../hooks';
import { CheckboxGroupProps } from './CheckboxGroup.props';
import { BaseCheckboxGroupProps } from './BaseCheckboxGroup.props';

const componentName = 'BaseCheckboxGroup';

const BaseCheckboxGroup = React.forwardRef<HTMLFieldSetElement, BaseCheckboxGroupProps>(
(
{
name,
defaultValue,
value: valueProp,
required = false,
disabled = false,
onValueChange,
id: providedId,
label,
helperText,
helperTextPosition = 'top',
showHelperTextIcon,
error,
errorMessage,
showErrorMessageIcon,
'aria-labelledby': ariaLabelledby,
'aria-describedby': ariaDescribedby,
'aria-errormessage': ariaErrorMessage,
children,
...props
},
ref
) => {
const { id, labelId, helperTextId, errorMessageId } = useIds({
providedId,
componentPrefix: 'checkboxgroup',
});
const showErrorMessage = Boolean(error && errorMessage);
const fieldDirection = helperTextPosition === 'top' ? 'column' : 'column-reverse';

// With useControllableState, you can pass an initial state (using
// defaultValue) implying the component is uncontrolled, or you can pass a
// controlled value (using value) implying the component is controlled.
const [value = [], setValue] = useControllableState({
prop: valueProp,
defaultProp: defaultValue,
onChange: onValueChange,
});

const handleItemCheck = React.useCallback(
(itemValue: string) => setValue((prevValue = []) => [...prevValue, itemValue]),
[setValue]
);

const handleItemUncheck = React.useCallback(
(itemValue: string) =>
setValue((prevValue = []) => prevValue.filter(value => value !== itemValue)),
[setValue]
);

const providerValue = {
name,
required,
disabled,
value,
onItemCheck: handleItemCheck,
onItemUncheck: handleItemUncheck,
hasGroupHelperText: !!helperText,
'aria-describedby': mergeIds(
ariaDescribedby || !!helperText ? helperTextId : undefined,
ariaErrorMessage || showErrorMessage ? errorMessageId : undefined
),
};

return (
<Fieldset
ref={ref}
{...props}
disabled={disabled}
id={id}
data-disabled={disabled ? '' : undefined}
aria-errormessage={ariaErrorMessage || showErrorMessage ? errorMessageId : undefined}
aria-labelledby={ariaLabelledby || !!label ? labelId : undefined}
aria-invalid={showErrorMessage}
>
{label ? (
<FieldsetLegend id={labelId} disabled={disabled}>
{label}
</FieldsetLegend>
) : null}
<Flex gap={2} direction={fieldDirection}>
{helperText ? (
<HelperText id={helperTextId} disabled={disabled} showIcon={showHelperTextIcon}>
{helperText}
</HelperText>
) : null}

<CheckboxGroupContext.Provider value={providerValue}>
{children}
</CheckboxGroupContext.Provider>
</Flex>
{showErrorMessage ? (
<HelperText
validationStatus="invalid"
showIcon={showErrorMessageIcon}
id={errorMessageId}
>
{errorMessage}
</HelperText>
) : null}
</Fieldset>
);
}
);

BaseCheckboxGroup.displayName = componentName;

export { BaseCheckboxGroup };
export type { CheckboxGroupProps };
7 changes: 7 additions & 0 deletions packages/web-ui/src/Checkbox/CheckboxGridGroup.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { StackProps } from '../Stack';
import { BaseCheckboxGroupProps } from './BaseCheckboxGroup.props';

export interface CheckboxGridGroupProps extends Omit<BaseCheckboxGroupProps, 'direction'> {
/** Sets the number of columns to display the contents in. */
columns?: StackProps['spacing'];
}
58 changes: 58 additions & 0 deletions packages/web-ui/src/Checkbox/CheckboxGridGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { CheckboxGridGroup } from './CheckboxGridGroup';
import { CheckboxTile } from './CheckboxTile';

const meta: Meta<typeof CheckboxGridGroup> = {
title: 'Web UI / Components / Checkbox / CheckboxGridGroup',
component: CheckboxGridGroup,
argTypes: {
direction: {
options: ['column', 'row'],
control: { type: 'radio' },
},
helperText: { control: { type: 'text' } },
helperTextPosition: { options: ['top', 'bottom'], control: { type: 'radio' } },
showHelperTextIcon: { control: { type: 'boolean' } },
label: { control: { type: 'text' } },
error: { control: { type: 'boolean' } },
errorMessage: { control: { type: 'text' } },
showErrorMessageIcon: { control: { type: 'boolean' } },
disabled: { control: { type: 'boolean' } },
contentWidth: { control: { type: 'text' } },
columns: { control: { type: 'number' } },
},
args: {
label: 'Label',
defaultValue: ['1', '2'],
columns: 2,
direction: 'column',
disabled: false,
helperText: 'Helper text',
helperTextPosition: 'top',
showHelperTextIcon: false,
error: false,
errorMessage: 'There is an error',
showErrorMessageIcon: true,
contentWidth: undefined,
},
};

export default meta;
type Story = StoryObj<typeof CheckboxGridGroup>;

export const Workshop: Story = {
name: 'CheckboxGridGroup',
render: args => (
<form>
<CheckboxGridGroup {...args} name="checkbox-tiles-story">
<CheckboxTile value="1" label="One" />
<CheckboxTile value="2" label="Two" />
<CheckboxTile value="3" label="Three" />
<CheckboxTile value="4" label="Four" />
<CheckboxTile value="5" label="Five" />
<CheckboxTile value="6" label="Six" />
</CheckboxGridGroup>
</form>
),
};
29 changes: 29 additions & 0 deletions packages/web-ui/src/Checkbox/CheckboxGridGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import { getColumns, withGlobalPrefix } from '../utils';
import clsx from 'clsx';
import { BaseCheckboxGroup } from './BaseCheckboxGroup';
import { Box } from '../Box';
import { CheckboxGridGroupProps } from './CheckboxGridGroup.props';

const componentName = 'CheckboxGridGroup';
const componentClassName = withGlobalPrefix(componentName);

export const CheckboxGridGroup = React.forwardRef<HTMLFieldSetElement, CheckboxGridGroupProps>(
({ children, contentWidth = 'fit-content', columns = 2, className, ...props }, ref) => {
return (
<BaseCheckboxGroup ref={ref} className={clsx(componentClassName, className)} {...props}>
<Box
display="grid"
gap={2}
gridTemplateColumns={getColumns(columns)}
minWidth="fit-content"
width={contentWidth}
>
{children}
</Box>
</BaseCheckboxGroup>
);
}
);

CheckboxGridGroup.displayName = componentName;
49 changes: 2 additions & 47 deletions packages/web-ui/src/Checkbox/CheckboxGroup.props.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,6 @@
import { ComponentPropsWithoutRef, ReactNode } from 'react';
import { CheckboxGroupContextValue } from './CheckboxGroup.context';
import { BoxProps } from '../Box';
import { BaseCheckboxGroupProps } from './BaseCheckboxGroup.props';

export interface CheckboxGroupProps extends ComponentPropsWithoutRef<'fieldset'> {
name?: CheckboxGroupContextValue['name'];
required?: boolean;
disabled?: boolean;
defaultValue?: Array<string>;
value?: CheckboxGroupContextValue['value'];
onValueChange?: (value: Array<string>) => void;
export interface CheckboxGroupProps extends BaseCheckboxGroupProps {
/** The direction of the radios, will also set the aria-orientation value. */
direction?: 'column' | 'row';
/**
* Set the width of the RadioGroup children, separate to the width of the
* entire RadioGroup.
*/
contentWidth?: BoxProps['width'];
/**
* The label for the radio group. This should contain the question being
* answered by the radio group.
*
* If you don't include a label you need to ensure you use the `aria-label`
* or `aria-labelledby` prop to properly associate a label with the radio
* group.
*/
label?: ReactNode;
/**
* Helper text for the radio group. Provides a hint such as specific
* requirements for what to choose. When displayed, child `Radio` or
* `RadioTile` components will not display `helperText`.
*/
helperText?: ReactNode;
/**
* Position of the helper text.
* @default 'top'
*/
helperTextPosition?: 'top' | 'bottom';
/**
* Set whether to display the helper text icon.
*/
showHelperTextIcon?: boolean;
/** Controls whether the error message is displayed. */
error?: boolean;
/** The error message to be displayed. */
errorMessage?: ReactNode;
/**
* Set whether to display the error message icon.
*/
showErrorMessageIcon?: boolean;
}
Loading

0 comments on commit 357b771

Please sign in to comment.