mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
[web] Create DSFormControl Input components (#29647)
* Create DS version for Bootstrap Input form elements * Move DS Button Storybook to DS component folder * Use phosphor icons * Add ds-focus-outline mixin * Use math.div GitOrigin-RevId: e50934212ec5949f0f7abc7880eb73933fce2a9b
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { Form, FormControlProps } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
|
||||
interface ButtonProps extends FormControlProps {
|
||||
size?: 'lg'
|
||||
}
|
||||
|
||||
const DSFormControl = forwardRef<HTMLInputElement, ButtonProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Form.Control
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={classnames('form-control-ds', className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
DSFormControl.displayName = 'DSFormControl'
|
||||
|
||||
export default DSFormControl
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Form } from 'react-bootstrap'
|
||||
import FormText from '@/shared/components/form/form-text'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
export type FormFeedbackProps = Pick<
|
||||
ComponentProps<typeof Form.Control.Feedback>,
|
||||
'type' | 'className' | 'children'
|
||||
>
|
||||
|
||||
function DSFormFeedback(props: FormFeedbackProps) {
|
||||
return (
|
||||
<Form.Control.Feedback {...props}>
|
||||
<FormText type={props.type === 'invalid' ? 'error' : 'success'}>
|
||||
{props.children}
|
||||
</FormText>
|
||||
</Form.Control.Feedback>
|
||||
)
|
||||
}
|
||||
|
||||
export default DSFormFeedback
|
||||
@@ -0,0 +1,18 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { Form, FormGroupProps } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const DSFormGroup = forwardRef<typeof Form.Group, FormGroupProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Form.Group
|
||||
className={classnames('form-group-ds', className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
DSFormGroup.displayName = 'DSFormGroup'
|
||||
|
||||
export default DSFormGroup
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ComponentProps } from 'react'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
|
||||
function DSFormLabel({
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof Form.Label>) {
|
||||
return (
|
||||
<Form.Label {...props} className={classnames('form-label-ds', className)} />
|
||||
)
|
||||
}
|
||||
|
||||
export default DSFormLabel
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Form, FormTextProps as BS5FormTextProps } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
import { MergeAndOverride } from '@ol-types/utils'
|
||||
import { CheckCircle, WarningCircle } from '@phosphor-icons/react'
|
||||
|
||||
type TextType = 'success' | 'error'
|
||||
|
||||
export type FormTextProps = MergeAndOverride<
|
||||
BS5FormTextProps,
|
||||
{ type?: TextType }
|
||||
>
|
||||
|
||||
const typeClasses = {
|
||||
error: 'text-danger',
|
||||
success: 'text-success',
|
||||
} as const
|
||||
|
||||
export const getFormTextClass = (type?: TextType) =>
|
||||
type && type in typeClasses
|
||||
? typeClasses[type as keyof typeof typeClasses]
|
||||
: undefined
|
||||
|
||||
function FormTextIcon({ type }: { type?: TextType }) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return <CheckCircle className="ciam-form-text-icon" />
|
||||
case 'error':
|
||||
return <WarningCircle className="ciam-form-text-icon" />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function DSFormText({ type, children, className, ...rest }: FormTextProps) {
|
||||
return (
|
||||
<Form.Text
|
||||
className={classnames('form-text-ds', className, getFormTextClass(type))}
|
||||
{...rest}
|
||||
>
|
||||
<span className="form-text-inner-ds">
|
||||
<FormTextIcon type={type} />
|
||||
{children}
|
||||
</span>
|
||||
</Form.Text>
|
||||
)
|
||||
}
|
||||
|
||||
export default DSFormText
|
||||
@@ -9,7 +9,7 @@ export const Button = (args: Args) => {
|
||||
}
|
||||
|
||||
const meta: Meta<typeof DSButton> = {
|
||||
title: 'Shared / Components / DS Button',
|
||||
title: 'Shared / DS Components',
|
||||
component: DSButton,
|
||||
args: {
|
||||
children: 'Button',
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Meta } from '@storybook/react'
|
||||
import { figmaDesignUrl } from '../../../.storybook/utils/figma-design-url'
|
||||
import DSFormControl from '@/shared/components/ds/ds-form-control'
|
||||
import DSFormText from '@/shared/components/ds/ds-form-text'
|
||||
import DSFormGroup from '@/shared/components/ds/ds-form-group'
|
||||
import DSFormLabel from '@/shared/components/ds/ds-form-label'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
type Args = ComponentProps<typeof DSFormControl> & {
|
||||
textType: ComponentProps<typeof DSFormText>['type']
|
||||
}
|
||||
|
||||
export const FormControl = ({ textType, value, ...args }: Args) => {
|
||||
return (
|
||||
<DSFormGroup>
|
||||
<DSFormLabel htmlFor="form-control-id">Form input label</DSFormLabel>
|
||||
<DSFormControl
|
||||
id="form-control-id"
|
||||
name="form-control-name"
|
||||
{...args}
|
||||
value={value || undefined}
|
||||
/>
|
||||
<DSFormText type={textType}>Form input feedback</DSFormText>
|
||||
</DSFormGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof FormControl> = {
|
||||
title: 'Shared / DS Components',
|
||||
component: FormControl,
|
||||
argTypes: {
|
||||
disabled: { control: 'boolean' },
|
||||
isInvalid: { control: 'boolean' },
|
||||
placeholder: { control: 'text' },
|
||||
readOnly: { control: 'boolean' },
|
||||
value: { control: 'text' },
|
||||
size: { control: 'radio', options: ['lg', undefined] },
|
||||
textType: { control: 'radio', options: ['error', 'success', undefined] },
|
||||
},
|
||||
parameters: {
|
||||
controls: {
|
||||
include: [
|
||||
'disabled',
|
||||
'isInvalid',
|
||||
'placeholder',
|
||||
'readOnly',
|
||||
'size',
|
||||
'value',
|
||||
'textType',
|
||||
],
|
||||
},
|
||||
...figmaDesignUrl(
|
||||
'https://www.figma.com/design/aJQlecvqCS9Ry8b6JA1lQN/DS---Components?node-id=6318-428&t=pcx9KKzhlzpRmA4S-0'
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
@@ -1 +1,2 @@
|
||||
@import 'button';
|
||||
@import 'form-control';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
@import '../mixins';
|
||||
|
||||
.btn.btn-ds {
|
||||
--bs-btn-font-family: var(--ds-font-family-sans);
|
||||
--bs-btn-border-radius: var(--ds-border-radius-200);
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--ds-color-yellow-500);
|
||||
outline-offset: 2px;
|
||||
box-shadow: none;
|
||||
@include ds-focus-outline;
|
||||
}
|
||||
|
||||
// Default size
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
@use 'sass:math';
|
||||
@import '../mixins';
|
||||
|
||||
.form-control-ds,
|
||||
.form-group-ds,
|
||||
.form-text-ds,
|
||||
.form-label-ds {
|
||||
--bs-body-font-family: var(--ds-font-family-sans), sans-serif;
|
||||
--bs-success-rgb: 25, 117, 76; // #19754c
|
||||
--bs-danger-rgb: 195, 9, 43; // #c3092b
|
||||
--content-placeholder: var(--ds-color-text-disabled);
|
||||
|
||||
// Without this, it inherits the body's --bs-body-font-family which isn't the DS font
|
||||
font-family: var(--ds-font-family-sans), sans-serif;
|
||||
}
|
||||
|
||||
input.form-control.form-control-ds {
|
||||
border-radius: var(--ds-border-radius-200);
|
||||
border-color: var(--ds-color-neutral-300);
|
||||
color: var(--ds-color-neutral-950);
|
||||
padding: calc(var(--ds-spacing-250) - 1px) calc(var(--ds-spacing-300) - 1px);
|
||||
|
||||
&.form-control-lg {
|
||||
padding: calc(var(--ds-spacing-350) - 1px) calc(var(--ds-spacing-300) - 1px);
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
background: var(--ds-color-neutral-200);
|
||||
color: var(--ds-color-text-secondary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--ds-color-text-disabled);
|
||||
background: var(--ds-color-neutral-100);
|
||||
}
|
||||
|
||||
&:not(:disabled, :focus-visible) {
|
||||
&:hover,
|
||||
&.hover {
|
||||
border-color: var(--ds-color-neutral-400);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
&:focus {
|
||||
@include ds-focus-outline;
|
||||
|
||||
border-color: var(--ds-color-neutral-950);
|
||||
}
|
||||
|
||||
&.is-invalid,
|
||||
.was-validated &:invalid {
|
||||
border-color: var(--ds-color-red-500);
|
||||
}
|
||||
}
|
||||
|
||||
.form-label.form-label-ds {
|
||||
@include ds-body-md-semibold;
|
||||
|
||||
color: var(--ds-color-text-primary);
|
||||
|
||||
&:has(+ .form-control:read-only) {
|
||||
color: var(--ds-color-text-secondary);
|
||||
}
|
||||
|
||||
&:has(+ .form-control:disabled) {
|
||||
color: var(--ds-color-text-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.form-text.form-text-ds {
|
||||
color: var(--ds-color-text-secondary);
|
||||
|
||||
.form-text-inner-ds {
|
||||
display: flex;
|
||||
gap: var(--ds-spacing-200);
|
||||
margin-top: var(--ds-spacing-150);
|
||||
|
||||
@include ds-body-sm-regular;
|
||||
}
|
||||
|
||||
.ciam-form-text-icon {
|
||||
font-size: math.div(20em, 14);
|
||||
}
|
||||
}
|
||||
@@ -107,3 +107,10 @@
|
||||
font-weight: var(--ds-font-weight-semibold);
|
||||
line-height: var(--ds-font-line-height-1200);
|
||||
}
|
||||
|
||||
// Focus outline
|
||||
@mixin ds-focus-outline() {
|
||||
box-shadow: none;
|
||||
outline: 2px solid var(--ds-color-yellow-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user