mirror of
https://github.com/mastodon/mastodon.git
synced 2026-08-11 06:15:43 -05:00
Redesign components: Radio buttons (#39943)
This commit is contained in:
@@ -139,9 +139,10 @@ a.base {
|
||||
|
||||
// Helper classes
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
.icon,
|
||||
.loading {
|
||||
width: var(--fs-xl);
|
||||
height: var(--fs-xl);
|
||||
}
|
||||
|
||||
// Adds padding to the text label. This is supposed to be there even if there is no icon.
|
||||
@@ -156,15 +157,9 @@ a.base {
|
||||
|
||||
.iconOnly {
|
||||
aspect-ratio: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: inherit;
|
||||
height: 0.8em;
|
||||
width: 0.8em;
|
||||
|
||||
.iconOnly & {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,24 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
|
||||
export const CheckboxField = forwardRef<
|
||||
HTMLInputElement,
|
||||
Props & CommonFieldWrapperProps
|
||||
>(({ id, label, hint, status, required, ...otherProps }, ref) => (
|
||||
<FormFieldWrapper
|
||||
label={label}
|
||||
hint={hint}
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
inputPlacement='inline-start'
|
||||
>
|
||||
{(inputProps) => <Checkbox {...otherProps} {...inputProps} ref={ref} />}
|
||||
</FormFieldWrapper>
|
||||
));
|
||||
>(
|
||||
(
|
||||
{ id, label, hint, status, required, wrapperClassName, ...otherProps },
|
||||
ref,
|
||||
) => (
|
||||
<FormFieldWrapper
|
||||
label={label}
|
||||
hint={hint}
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
inputPlacement='inline-start'
|
||||
className={wrapperClassName}
|
||||
>
|
||||
{(inputProps) => <Checkbox {...otherProps} {...inputProps} ref={ref} />}
|
||||
</FormFieldWrapper>
|
||||
),
|
||||
);
|
||||
|
||||
CheckboxField.displayName = 'CheckboxField';
|
||||
|
||||
|
||||
@@ -180,7 +180,15 @@ export const ComboboxFieldWithRef = <
|
||||
Item extends ComboboxItem,
|
||||
GroupKey extends string,
|
||||
>(
|
||||
{ id, label, hint, status, required, ...otherProps }: Props<Item, GroupKey>,
|
||||
{
|
||||
id,
|
||||
label,
|
||||
hint,
|
||||
status,
|
||||
required,
|
||||
wrapperClassName,
|
||||
...otherProps
|
||||
}: Props<Item, GroupKey>,
|
||||
ref: React.ForwardedRef<HTMLInputElement>,
|
||||
) => (
|
||||
<FormFieldWrapper
|
||||
@@ -189,6 +197,7 @@ export const ComboboxFieldWithRef = <
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
className={wrapperClassName}
|
||||
>
|
||||
{(inputProps) => <Combobox {...otherProps} {...inputProps} ref={ref} />}
|
||||
</FormFieldWrapper>
|
||||
|
||||
@@ -22,7 +22,17 @@ interface CopyLinkFieldProps extends CommonFieldWrapperProps, TextInputProps {
|
||||
|
||||
export const CopyLinkField = forwardRef<HTMLInputElement, CopyLinkFieldProps>(
|
||||
(
|
||||
{ id, label, hint, status, value, required, className, ...otherProps },
|
||||
{
|
||||
id,
|
||||
label,
|
||||
hint,
|
||||
status,
|
||||
value,
|
||||
required,
|
||||
className,
|
||||
wrapperClassName,
|
||||
...otherProps
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
@@ -50,6 +60,7 @@ export const CopyLinkField = forwardRef<HTMLInputElement, CopyLinkFieldProps>(
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
className={wrapperClassName}
|
||||
>
|
||||
{(inputProps) => (
|
||||
<div className={classes.wrapper}>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import type { ReactNode, FC } from 'react';
|
||||
import { createContext, useId } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { A11yLiveRegion } from 'mastodon/components/a11y_live_region';
|
||||
import { CalloutInline } from 'mastodon/components/callout_inline';
|
||||
|
||||
@@ -17,6 +19,7 @@ interface FieldsetProps {
|
||||
name?: string;
|
||||
status?: FieldStatus | FieldStatus['variant'];
|
||||
layout?: 'vertical' | 'horizontal';
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -33,6 +36,7 @@ export const Fieldset: FC<FieldsetProps> = ({
|
||||
name,
|
||||
status,
|
||||
layout,
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
const uniqueId = useId();
|
||||
@@ -54,23 +58,23 @@ export const Fieldset: FC<FieldsetProps> = ({
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
className={classes.fieldset}
|
||||
className={classNames(classes.fieldset, className)}
|
||||
data-has-error={status === 'error'}
|
||||
aria-labelledby={labelId}
|
||||
aria-describedby={descriptionIds}
|
||||
>
|
||||
<div className={formFieldWrapperClasses.labelWrapper}>
|
||||
<div id={labelId} className={formFieldWrapperClasses.label}>
|
||||
<div id={labelId} className={formFieldWrapperClasses.label} data-label>
|
||||
{legend}
|
||||
</div>
|
||||
{hasHint && (
|
||||
<p id={hintId} className={formFieldWrapperClasses.hint}>
|
||||
<p id={hintId} className={formFieldWrapperClasses.hint} data-hint>
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={classes.fieldsWrapper} data-layout={layout}>
|
||||
<div className={classes.fieldsWrapper} data-layout={layout} data-fields>
|
||||
<FieldsetNameContext.Provider value={fieldsetName}>
|
||||
{children}
|
||||
</FieldsetNameContext.Provider>
|
||||
|
||||
@@ -83,7 +83,9 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
|
||||
};
|
||||
|
||||
const input = (
|
||||
<div className={classes.inputWrapper}>{children(inputProps)}</div>
|
||||
<div className={classes.inputWrapper} data-input>
|
||||
{children(inputProps)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -105,7 +107,7 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
|
||||
</label>
|
||||
|
||||
{hasHint && (
|
||||
<span className={classes.hint} id={hintId}>
|
||||
<span className={classes.hint} id={hintId} data-hint>
|
||||
{hint}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import type { ComponentPropsWithoutRef, CSSProperties } from 'react';
|
||||
import { forwardRef, useContext } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { FieldsetNameContext } from './fieldset';
|
||||
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
||||
import { FormFieldWrapper } from './form_field_wrapper';
|
||||
@@ -15,41 +17,50 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
|
||||
export const RadioButtonField = forwardRef<
|
||||
HTMLInputElement,
|
||||
Props & CommonFieldWrapperProps
|
||||
>(({ id, label, hint, status, required, ...otherProps }, ref) => {
|
||||
const fieldsetName = useContext(FieldsetNameContext);
|
||||
>(
|
||||
(
|
||||
{ id, label, hint, status, required, wrapperClassName, ...otherProps },
|
||||
ref,
|
||||
) => {
|
||||
const fieldsetName = useContext(FieldsetNameContext);
|
||||
|
||||
return (
|
||||
<FormFieldWrapper
|
||||
label={label}
|
||||
hint={hint}
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
inputPlacement='inline-start'
|
||||
>
|
||||
{(inputProps) => (
|
||||
<RadioButton
|
||||
{...otherProps}
|
||||
{...inputProps}
|
||||
name={otherProps.name || fieldsetName}
|
||||
ref={ref}
|
||||
/>
|
||||
)}
|
||||
</FormFieldWrapper>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<FormFieldWrapper
|
||||
label={label}
|
||||
hint={hint}
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
className={wrapperClassName}
|
||||
inputPlacement='inline-start'
|
||||
>
|
||||
{(inputProps) => (
|
||||
<RadioButton
|
||||
{...otherProps}
|
||||
{...inputProps}
|
||||
name={otherProps.name || fieldsetName}
|
||||
ref={ref}
|
||||
/>
|
||||
)}
|
||||
</FormFieldWrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
RadioButtonField.displayName = 'RadioButtonField';
|
||||
|
||||
export const RadioButton = forwardRef<HTMLInputElement, Props>(
|
||||
({ className, size, ...otherProps }, ref) => (
|
||||
<input
|
||||
{...otherProps}
|
||||
type='radio'
|
||||
className={classes.radioButton}
|
||||
style={size ? ({ '--size': `${size}px` } as CSSProperties) : undefined}
|
||||
ref={ref}
|
||||
/>
|
||||
({ className, size, children, ...otherProps }, ref) => (
|
||||
<>
|
||||
{children}
|
||||
<input
|
||||
{...otherProps}
|
||||
type='radio'
|
||||
className={classNames(classes.radioButton, className)}
|
||||
style={size ? ({ '--size': `${size}px` } as CSSProperties) : undefined}
|
||||
ref={ref}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
// Radio
|
||||
|
||||
.radioWrapper {
|
||||
[data-input] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition:
|
||||
color 200ms,
|
||||
opacity 200ms;
|
||||
}
|
||||
|
||||
.icon:has(+ :checked) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: var(--fs-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: var(--fs-lg);
|
||||
height: var(--fs-lg);
|
||||
}
|
||||
|
||||
// Text Input
|
||||
|
||||
.inputWrapper {
|
||||
label {
|
||||
font-size: var(--fs-sm);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { TextInputField, ToggleField } from './redesign';
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
|
||||
import { Fieldset } from './fieldset';
|
||||
import { RadioButtonField, TextInputField, ToggleField } from './redesign';
|
||||
|
||||
interface FieldProps {
|
||||
label: string;
|
||||
@@ -28,6 +31,17 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const RadioButton: Story = {
|
||||
render(args) {
|
||||
return (
|
||||
<Fieldset name='test' legend='Test radio'>
|
||||
<RadioButtonField {...args} icon={CheckIcon} defaultChecked />
|
||||
<RadioButtonField {...args} icon={CheckIcon} />
|
||||
</Fieldset>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const TextInput: Story = {
|
||||
render(args) {
|
||||
return <TextInputField {...args} />;
|
||||
|
||||
@@ -2,6 +2,10 @@ import classNames from 'classnames';
|
||||
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
import { Icon } from '../icon';
|
||||
import type { IconProp } from '../icon';
|
||||
|
||||
import { RadioButtonField as OldRadioButtonField } from './radio_button_field';
|
||||
import classes from './redesign.module.scss';
|
||||
import {
|
||||
TextInput as OldTextInput,
|
||||
@@ -18,6 +22,31 @@ type RedesignComponent<
|
||||
P = object,
|
||||
> = React.FC<Merge<React.ComponentProps<T>, P>>;
|
||||
|
||||
// Radio button field
|
||||
|
||||
interface RadioButtonProps {
|
||||
icon?: IconProp | null;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
export const RadioButtonField: RedesignComponent<
|
||||
typeof OldRadioButtonField,
|
||||
RadioButtonProps
|
||||
> = ({ icon, iconClassName, wrapperClassName, ...props }) => (
|
||||
<OldRadioButtonField
|
||||
{...props}
|
||||
wrapperClassName={classNames(wrapperClassName, classes.radioWrapper)}
|
||||
>
|
||||
{icon && (
|
||||
<Icon
|
||||
id='checked'
|
||||
icon={icon}
|
||||
className={classNames(classes.icon, iconClassName)}
|
||||
/>
|
||||
)}
|
||||
</OldRadioButtonField>
|
||||
);
|
||||
|
||||
// Text field
|
||||
|
||||
export const TextInputField: RedesignComponent<typeof OldTextInputField> = ({
|
||||
@@ -71,5 +100,6 @@ export const ToggleField: RedesignComponent<
|
||||
classes.toggle,
|
||||
size === 'sm' && classes.toggleSmall,
|
||||
)}
|
||||
size={size === 'sm' ? 14 : 20}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -14,18 +14,24 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
|
||||
export const ToggleField = forwardRef<
|
||||
HTMLInputElement,
|
||||
Props & CommonFieldWrapperProps
|
||||
>(({ id, label, hint, status, required, ...otherProps }, ref) => (
|
||||
<FormFieldWrapper
|
||||
label={label}
|
||||
hint={hint}
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
inputPlacement='inline-end'
|
||||
>
|
||||
{(inputProps) => <Toggle {...otherProps} {...inputProps} ref={ref} />}
|
||||
</FormFieldWrapper>
|
||||
));
|
||||
>(
|
||||
(
|
||||
{ id, label, hint, status, required, wrapperClassName, ...otherProps },
|
||||
ref,
|
||||
) => (
|
||||
<FormFieldWrapper
|
||||
label={label}
|
||||
hint={hint}
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
inputPlacement='inline-end'
|
||||
className={wrapperClassName}
|
||||
>
|
||||
{(inputProps) => <Toggle {...otherProps} {...inputProps} ref={ref} />}
|
||||
</FormFieldWrapper>
|
||||
),
|
||||
);
|
||||
|
||||
ToggleField.displayName = 'ToggleField';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user