[Glitch] Add components TextInput, TextArea, and FormStack
Port 218ca366533bb5b8c4d3eb163b6f2d4d1cc6f755 to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
3bbc8ecf92
commit
41dee6383f
@ -0,0 +1,7 @@
|
|||||||
|
.stack {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||||
|
|
||||||
|
import classes from './form_stack.module.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple wrapper for providing consistent spacing to a group of form fields.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const FormStack = polymorphicForwardRef<'div'>(
|
||||||
|
({ as: Element = 'div', children, className, ...otherProps }, ref) => (
|
||||||
|
<Element
|
||||||
|
ref={ref}
|
||||||
|
{...otherProps}
|
||||||
|
className={classNames(className, classes.stack)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Element>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
FormStack.displayName = 'FormStack';
|
||||||
@ -1,5 +1,6 @@
|
|||||||
export { TextInputField } from './text_input_field';
|
export { FormStack } from './form_stack';
|
||||||
export { TextAreaField } from './text_area_field';
|
export { TextInputField, TextInput } from './text_input_field';
|
||||||
|
export { TextAreaField, TextArea } from './text_area_field';
|
||||||
export { CheckboxField, Checkbox } from './checkbox_field';
|
export { CheckboxField, Checkbox } from './checkbox_field';
|
||||||
export { ToggleField, Toggle } from './toggle_field';
|
export { ToggleField, Toggle } from './toggle_field';
|
||||||
export { SelectField, Select } from './select_field';
|
export { SelectField, Select } from './select_field';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
import { TextAreaField } from './text_area_field';
|
import { TextAreaField, TextArea } from './text_area_field';
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Form Fields/TextAreaField',
|
title: 'Components/Form Fields/TextAreaField',
|
||||||
@ -9,14 +9,6 @@ const meta = {
|
|||||||
label: 'Label',
|
label: 'Label',
|
||||||
hint: 'This is a description of this form field',
|
hint: 'This is a description of this form field',
|
||||||
},
|
},
|
||||||
render(args) {
|
|
||||||
// Component styles require a wrapper class at the moment
|
|
||||||
return (
|
|
||||||
<div className='simple_form'>
|
|
||||||
<TextAreaField {...args} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
} satisfies Meta<typeof TextAreaField>;
|
} satisfies Meta<typeof TextAreaField>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -49,3 +41,17 @@ export const WithError: Story = {
|
|||||||
hasError: true,
|
hasError: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Plain: Story = {
|
||||||
|
render(args) {
|
||||||
|
return <TextArea {...args} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
...Plain,
|
||||||
|
args: {
|
||||||
|
disabled: true,
|
||||||
|
defaultValue: "This value can't be changed",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import type { ComponentPropsWithoutRef } from 'react';
|
import type { ComponentPropsWithoutRef } from 'react';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { FormFieldWrapper } from './form_field_wrapper';
|
import { FormFieldWrapper } from './form_field_wrapper';
|
||||||
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
||||||
|
import classes from './text_input.module.scss';
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
extends ComponentPropsWithoutRef<'textarea'>, CommonFieldWrapperProps {}
|
extends ComponentPropsWithoutRef<'textarea'>, CommonFieldWrapperProps {}
|
||||||
@ -23,9 +26,22 @@ export const TextAreaField = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
hasError={hasError}
|
hasError={hasError}
|
||||||
inputId={id}
|
inputId={id}
|
||||||
>
|
>
|
||||||
{(inputProps) => <textarea {...otherProps} {...inputProps} ref={ref} />}
|
{(inputProps) => <TextArea {...otherProps} {...inputProps} ref={ref} />}
|
||||||
</FormFieldWrapper>
|
</FormFieldWrapper>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
TextAreaField.displayName = 'TextAreaField';
|
TextAreaField.displayName = 'TextAreaField';
|
||||||
|
|
||||||
|
export const TextArea = forwardRef<
|
||||||
|
HTMLTextAreaElement,
|
||||||
|
ComponentPropsWithoutRef<'textarea'>
|
||||||
|
>(({ className, ...otherProps }, ref) => (
|
||||||
|
<textarea
|
||||||
|
{...otherProps}
|
||||||
|
className={classNames(className, classes.input)}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
TextArea.displayName = 'TextArea';
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
.input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
resize: vertical;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
outline-color: transparent;
|
||||||
|
outline-offset: -1px;
|
||||||
|
transition: outline-color 0.15s ease-out;
|
||||||
|
|
||||||
|
@media screen and (width <= 600px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline-color: var(--color-text-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:user-invalid,
|
||||||
|
&:required:user-invalid,
|
||||||
|
[data-has-error='true'] & {
|
||||||
|
outline-color: var(--color-text-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:required:user-valid {
|
||||||
|
outline-color: var(--color-text-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--color-text-disabled);
|
||||||
|
border-color: transparent;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
import { TextInputField } from './text_input_field';
|
import { TextInputField, TextInput } from './text_input_field';
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Form Fields/TextInputField',
|
title: 'Components/Form Fields/TextInputField',
|
||||||
@ -9,14 +9,6 @@ const meta = {
|
|||||||
label: 'Label',
|
label: 'Label',
|
||||||
hint: 'This is a description of this form field',
|
hint: 'This is a description of this form field',
|
||||||
},
|
},
|
||||||
render(args) {
|
|
||||||
// Component styles require a wrapper class at the moment
|
|
||||||
return (
|
|
||||||
<div className='simple_form'>
|
|
||||||
<TextInputField {...args} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
} satisfies Meta<typeof TextInputField>;
|
} satisfies Meta<typeof TextInputField>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -49,3 +41,17 @@ export const WithError: Story = {
|
|||||||
hasError: true,
|
hasError: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Plain: Story = {
|
||||||
|
render(args) {
|
||||||
|
return <TextInput {...args} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
...Plain,
|
||||||
|
args: {
|
||||||
|
disabled: true,
|
||||||
|
defaultValue: "This value can't be changed",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import type { ComponentPropsWithoutRef } from 'react';
|
import type { ComponentPropsWithoutRef } from 'react';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { FormFieldWrapper } from './form_field_wrapper';
|
import { FormFieldWrapper } from './form_field_wrapper';
|
||||||
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
||||||
|
import classes from './text_input.module.scss';
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
extends ComponentPropsWithoutRef<'input'>, CommonFieldWrapperProps {}
|
extends ComponentPropsWithoutRef<'input'>, CommonFieldWrapperProps {}
|
||||||
@ -15,10 +18,7 @@ interface Props
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const TextInputField = forwardRef<HTMLInputElement, Props>(
|
export const TextInputField = forwardRef<HTMLInputElement, Props>(
|
||||||
(
|
({ id, label, hint, hasError, required, ...otherProps }, ref) => (
|
||||||
{ id, label, hint, hasError, required, type = 'text', ...otherProps },
|
|
||||||
ref,
|
|
||||||
) => (
|
|
||||||
<FormFieldWrapper
|
<FormFieldWrapper
|
||||||
label={label}
|
label={label}
|
||||||
hint={hint}
|
hint={hint}
|
||||||
@ -26,11 +26,23 @@ export const TextInputField = forwardRef<HTMLInputElement, Props>(
|
|||||||
hasError={hasError}
|
hasError={hasError}
|
||||||
inputId={id}
|
inputId={id}
|
||||||
>
|
>
|
||||||
{(inputProps) => (
|
{(inputProps) => <TextInput {...otherProps} {...inputProps} ref={ref} />}
|
||||||
<input type={type} {...otherProps} {...inputProps} ref={ref} />
|
|
||||||
)}
|
|
||||||
</FormFieldWrapper>
|
</FormFieldWrapper>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
TextInputField.displayName = 'TextInputField';
|
TextInputField.displayName = 'TextInputField';
|
||||||
|
|
||||||
|
export const TextInput = forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
ComponentPropsWithoutRef<'input'>
|
||||||
|
>(({ type = 'text', className, ...otherProps }, ref) => (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
{...otherProps}
|
||||||
|
className={classNames(className, classes.input)}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
TextInput.displayName = 'TextInput';
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { Column } from 'flavours/glitch/components/column';
|
|||||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
import {
|
import {
|
||||||
CheckboxField,
|
CheckboxField,
|
||||||
|
FormStack,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
} from 'flavours/glitch/components/form_fields';
|
} from 'flavours/glitch/components/form_fields';
|
||||||
import { TextInputField } from 'flavours/glitch/components/form_fields/text_input_field';
|
import { TextInputField } from 'flavours/glitch/components/form_fields/text_input_field';
|
||||||
@ -132,88 +133,80 @@ const CollectionSettings: React.FC<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className='simple_form app-form' onSubmit={handleSubmit}>
|
<FormStack as='form' onSubmit={handleSubmit}>
|
||||||
<div className='fields-group'>
|
<TextInputField
|
||||||
<TextInputField
|
required
|
||||||
required
|
label={
|
||||||
label={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='collections.collection_name'
|
||||||
id='collections.collection_name'
|
defaultMessage='Name'
|
||||||
defaultMessage='Name'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
hint={
|
||||||
hint={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='collections.name_length_hint'
|
||||||
id='collections.name_length_hint'
|
defaultMessage='40 characters limit'
|
||||||
defaultMessage='40 characters limit'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
value={name}
|
||||||
value={name}
|
onChange={handleNameChange}
|
||||||
onChange={handleNameChange}
|
maxLength={40}
|
||||||
maxLength={40}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='fields-group'>
|
<TextAreaField
|
||||||
<TextAreaField
|
required
|
||||||
required
|
label={
|
||||||
label={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='collections.collection_description'
|
||||||
id='collections.collection_description'
|
defaultMessage='Description'
|
||||||
defaultMessage='Description'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
hint={
|
||||||
hint={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='collections.description_length_hint'
|
||||||
id='collections.description_length_hint'
|
defaultMessage='100 characters limit'
|
||||||
defaultMessage='100 characters limit'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
value={description}
|
||||||
value={description}
|
onChange={handleDescriptionChange}
|
||||||
onChange={handleDescriptionChange}
|
maxLength={100}
|
||||||
maxLength={100}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='fields-group'>
|
<TextInputField
|
||||||
<TextInputField
|
required={false}
|
||||||
required={false}
|
label={
|
||||||
label={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='collections.collection_topic'
|
||||||
id='collections.collection_topic'
|
defaultMessage='Topic'
|
||||||
defaultMessage='Topic'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
hint={
|
||||||
hint={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='collections.topic_hint'
|
||||||
id='collections.topic_hint'
|
defaultMessage='Add a hashtag that helps others understand the main topic of this collection.'
|
||||||
defaultMessage='Add a hashtag that helps others understand the main topic of this collection.'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
value={topic}
|
||||||
value={topic}
|
onChange={handleTopicChange}
|
||||||
onChange={handleTopicChange}
|
maxLength={40}
|
||||||
maxLength={40}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='fields-group'>
|
<CheckboxField
|
||||||
<CheckboxField
|
label={
|
||||||
label={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='collections.mark_as_sensitive'
|
||||||
id='collections.mark_as_sensitive'
|
defaultMessage='Mark as sensitive'
|
||||||
defaultMessage='Mark as sensitive'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
hint={
|
||||||
hint={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='collections.mark_as_sensitive_hint'
|
||||||
id='collections.mark_as_sensitive_hint'
|
defaultMessage="Hides the collection's description and accounts behind a content warning. The collection name will still be visible."
|
||||||
defaultMessage="Hides the collection's description and accounts behind a content warning. The collection name will still be visible."
|
/>
|
||||||
/>
|
}
|
||||||
}
|
checked={sensitive}
|
||||||
checked={sensitive}
|
onChange={handleSensitiveChange}
|
||||||
onChange={handleSensitiveChange}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='actions'>
|
<div className='actions'>
|
||||||
<Button type='submit'>
|
<Button type='submit'>
|
||||||
@ -224,7 +217,7 @@ const CollectionSettings: React.FC<{
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</FormStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user