From 51d7d8df846cc9e77c7e2e6d0d228bdae3b231a3 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:40:21 +0300 Subject: [PATCH] Merge pull request #19366 from overleaf/ii-bs5-projects-list-search [web] BS5 projects list search GitOrigin-RevId: e2545f43ac3a50e58f7e97a2038e5b768c909e4f --- .../clone-project-modal-content.jsx | 2 +- .../components/project-list-root.tsx | 1 - .../project-list/components/search-form.tsx | 87 ++++---- .../bootstrap-3/form/form-control.tsx | 26 +++ .../bootstrap-5/form/form-control.tsx | 44 ++++ .../bootstrap-5/form/form-group.tsx | 14 ++ .../ui/components/ol/ol-form-control.tsx | 13 +- .../ui/components/ol/ol-form-group.tsx | 13 +- .../js/features/ui/components/ol/ol-form.tsx | 3 +- .../ui/form/form-input-bs5.stories.tsx | 192 +++++++++++++----- .../ui/form/form-select-bs5.stories.tsx | 73 +++---- .../ui/form/form-textarea-bs5.stories.tsx | 98 ++++----- .../stylesheets/app/project-list.less | 4 +- .../bootstrap-5/components/form.scss | 66 ++++++ .../bootstrap-5/pages/project-list.scss | 10 + 15 files changed, 455 insertions(+), 191 deletions(-) create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-3/form/form-control.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/form/form-control.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/form/form-group.tsx diff --git a/services/web/frontend/js/features/clone-project-modal/components/clone-project-modal-content.jsx b/services/web/frontend/js/features/clone-project-modal/components/clone-project-modal-content.jsx index 6975df6e8b..84d8151f0f 100644 --- a/services/web/frontend/js/features/clone-project-modal/components/clone-project-modal-content.jsx +++ b/services/web/frontend/js/features/clone-project-modal/components/clone-project-modal-content.jsx @@ -102,7 +102,7 @@ export default function CloneProjectModalContent({ {clonedProjectTags.length > 0 && ( {t('tags')}:
diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx index 9f280eee99..28a160edd7 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-root.tsx @@ -97,7 +97,6 @@ function ProjectListPageContent() { filter={filter} selectedTag={selectedTag} className="overflow-hidden" - formGroupProps={{ className: 'mb-0' }} />
diff --git a/services/web/frontend/js/features/project-list/components/search-form.tsx b/services/web/frontend/js/features/project-list/components/search-form.tsx index 891fd23c7f..710ac4c15c 100644 --- a/services/web/frontend/js/features/project-list/components/search-form.tsx +++ b/services/web/frontend/js/features/project-list/components/search-form.tsx @@ -1,36 +1,38 @@ import { useTranslation } from 'react-i18next' -import { - Form, - FormGroup, - FormGroupProps, - Col, - FormControl, -} from 'react-bootstrap' +import { FormControl } from 'react-bootstrap' import Icon from '../../../shared/components/icon' import * as eventTracking from '../../../infrastructure/event-tracking' import classnames from 'classnames' import { Tag } from '../../../../../app/src/Features/Tags/types' +import { MergeAndOverride } from '../../../../../types/utils' import { Filter } from '../context/project-list-context' import { isSmallDevice } from '../../../infrastructure/event-tracking' +import OLForm from '@/features/ui/components/ol/ol-form' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLCol from '@/features/ui/components/ol/ol-col' +import OLFormControl from '@/features/ui/components/ol/ol-form-control' +import MaterialIcon from '@/shared/components/material-icon' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { bsVersion } from '@/features/utils/bootstrap-5' type SearchFormOwnProps = { inputValue: string setInputValue: (input: string) => void filter: Filter selectedTag: Tag | undefined - formGroupProps?: FormGroupProps & - Omit, keyof FormGroupProps> } -type SearchFormProps = SearchFormOwnProps & - Omit, keyof SearchFormOwnProps> +type SearchFormProps = MergeAndOverride< + React.ComponentProps, + SearchFormOwnProps +> function SearchForm({ inputValue, setInputValue, filter, selectedTag, - formGroupProps, + className, ...props }: SearchFormProps) { const { t } = useTranslation() @@ -57,8 +59,6 @@ function SearchForm({ } } const placeholder = `${placeholderMessage}…` - const { className: formGroupClassName, ...restFormGroupProps } = - formGroupProps || {} const handleChange = ( e: React.ChangeEvent< @@ -75,44 +75,49 @@ function SearchForm({ const handleClear = () => setInputValue('') return ( -
e.preventDefault()} + bs3Props={{ horizontal: true }} {...props} > - - - + , + bs5: , + })} + append={ + inputValue.length > 0 && ( + + ) + } /> - - {inputValue.length ? ( -
- -
- ) : null} - -
-
+ +
+ ) } diff --git a/services/web/frontend/js/features/ui/components/bootstrap-3/form/form-control.tsx b/services/web/frontend/js/features/ui/components/bootstrap-3/form/form-control.tsx new file mode 100644 index 0000000000..1fe3fb826e --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-3/form/form-control.tsx @@ -0,0 +1,26 @@ +import { + FormControl as BS3FormControl, + FormControlProps as BS3FormControlProps, +} from 'react-bootstrap' + +type FormControlProps = BS3FormControlProps & { + prepend?: React.ReactNode + append?: React.ReactNode +} + +function FormControl({ + prepend, + append, + className, + ...props +}: FormControlProps) { + return ( + <> + {prepend &&
{prepend}
} + + {append &&
{append}
} + + ) +} + +export default FormControl diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-control.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-control.tsx new file mode 100644 index 0000000000..17ea179157 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-control.tsx @@ -0,0 +1,44 @@ +import { forwardRef } from 'react' +import { Form, FormControlProps } from 'react-bootstrap-5' +import classnames from 'classnames' + +type OLFormControlProps = FormControlProps & { + prepend?: React.ReactNode + append?: React.ReactNode +} + +const FormControl = forwardRef( + ({ prepend, append, className, ...props }, ref) => { + if (prepend || append) { + const wrapperClassNames = classnames('form-control-wrapper', { + 'form-control-wrapper-sm': props.size === 'sm', + 'form-control-wrapper-lg': props.size === 'lg', + 'form-control-wrapper-disabled': props.disabled, + }) + + const formControlClassNames = classnames(className, { + 'form-control-offset-start': prepend, + 'form-control-offset-end': append, + }) + + return ( +
+ {prepend && ( + {prepend} + )} + + {append && {append}} +
+ ) + } + + return + } +) +FormControl.displayName = 'FormControl' + +export default FormControl diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-group.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-group.tsx new file mode 100644 index 0000000000..19b88cdc65 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/form/form-group.tsx @@ -0,0 +1,14 @@ +import { forwardRef } from 'react' +import { FormGroup as BS5FormGroup, FormGroupProps } from 'react-bootstrap-5' +import classnames from 'classnames' + +const FormGroup = forwardRef( + ({ className, ...props }, ref) => { + const classNames = classnames('form-group', className) + + return + } +) +FormGroup.displayName = 'FormGroup' + +export default FormGroup diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx index ed1109d9ad..3543d49cf3 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx @@ -1,11 +1,12 @@ import { forwardRef } from 'react' -import { Form } from 'react-bootstrap-5' -import { FormControl as BS3FormControl } from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' +import BS3FormControl from '@/features/ui/components/bootstrap-3/form/form-control' +import FormControl from '@/features/ui/components/bootstrap-5/form/form-control' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -type OLFormControlProps = React.ComponentProps<(typeof Form)['Control']> & { +type OLFormControlProps = React.ComponentProps & { bs3Props?: Record + 'data-ol-dirty'?: unknown } const OLFormControl = forwardRef( @@ -37,6 +38,8 @@ const OLFormControl = forwardRef( ref.current = inputElement } }, + prepend: rest.prepend, + append: rest.append, ...bs3Props, } @@ -49,7 +52,7 @@ const OLFormControl = forwardRef( return ( } - bs5={} + bs5={} /> ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx index 65ec53f85b..5517d5d1ed 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx @@ -1,27 +1,26 @@ -import { Form } from 'react-bootstrap-5' +import { FormGroupProps } from 'react-bootstrap-5' import { FormGroup as BS3FormGroup } from 'react-bootstrap' +import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group' import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -type OLFormGroupProps = React.ComponentProps<(typeof Form)['Group']> & { +type OLFormGroupProps = FormGroupProps & { bs3Props?: Record } function OLFormGroup(props: OLFormGroupProps) { - const { bs3Props, className, ...rest } = props - - const classNames = className ?? 'mb-3' + const { bs3Props, ...rest } = props const bs3FormGroupProps: React.ComponentProps = { children: rest.children, controlId: rest.controlId, - className, + className: rest.className, ...bs3Props, } return ( } - bs5={} + bs5={} /> ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form.tsx index dbdc0a6c24..566d1ba64d 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form.tsx @@ -11,10 +11,11 @@ function OLForm(props: OLFormProps) { const bs3FormProps: React.ComponentProps = { componentClass: rest.as, - bsClass: rest.className, children: rest.children, id: rest.id, onSubmit: rest.onSubmit as React.FormEventHandler | undefined, + className: rest.className, + role: rest.role, ...bs3Props, } diff --git a/services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx b/services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx index 4750e7df2b..e37e031f63 100644 --- a/services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx +++ b/services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx @@ -1,39 +1,42 @@ import { Form } from 'react-bootstrap-5' import type { Meta, StoryObj } from '@storybook/react' +import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group' import FormText from '@/features/ui/components/bootstrap-5/form/form-text' +import FormControl from '@/features/ui/components/bootstrap-5/form/form-control' +import MaterialIcon from '@/shared/components/material-icon' -const meta: Meta<(typeof Form)['Control']> = { +const meta: Meta> = { title: 'Shared / Components / Bootstrap 5 / Form / Input', - component: Form.Control, + component: FormControl, parameters: { bootstrap5: true, }, } export default meta -type Story = StoryObj<(typeof Form)['Control']> +type Story = StoryObj> export const Default: Story = { render: args => { return ( <> - + Label - + Helper - +
- + Label - + Helper - +
- + Label - + Helper - + ) }, @@ -46,37 +49,37 @@ export const Info: Story = { render: args => { return ( <> - + Label - Info - +
- + Label - Info - +
- + Label - Info - + ) }, @@ -86,9 +89,9 @@ export const Error: Story = { render: args => { return ( <> - + Label - Error - +
- + Label - Error - +
- + Label - Error - + ) }, @@ -129,37 +132,37 @@ export const Warning: Story = { render: args => { return ( <> - + Label - Warning - +
- + Label - Warning - +
- + Label - Warning - + ) }, @@ -169,37 +172,130 @@ export const Success: Story = { render: args => { return ( <> - + Label - Success - +
- + Label - Success - +
- + Label - Success - + + + ) + }, +} + +export const WithIcons: Story = { + render: args => { + const handleClear = () => { + alert('Clicked clear button') + } + + return ( + <> + + Label + } + append={ + + } + size="lg" + {...args} + /> + +
+ + Label + } + append={ + + } + {...args} + /> + +
+ + Label + } + append={ + + } + size="sm" + {...args} + /> + +
+
+ + Disabled state + } + append={ + + } + disabled + {...args} + /> + ) }, diff --git a/services/web/frontend/stories/ui/form/form-select-bs5.stories.tsx b/services/web/frontend/stories/ui/form/form-select-bs5.stories.tsx index c80187adf2..148ea9597f 100644 --- a/services/web/frontend/stories/ui/form/form-select-bs5.stories.tsx +++ b/services/web/frontend/stories/ui/form/form-select-bs5.stories.tsx @@ -1,8 +1,9 @@ -import { Form } from 'react-bootstrap-5' +import { Form, FormSelectProps } from 'react-bootstrap-5' import type { Meta, StoryObj } from '@storybook/react' +import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group' import FormText from '@/features/ui/components/bootstrap-5/form/form-text' -const meta: Meta<(typeof Form)['Select']> = { +const meta: Meta = { title: 'Shared / Components / Bootstrap 5 / Form / Select', component: Form.Select, parameters: { @@ -11,13 +12,13 @@ const meta: Meta<(typeof Form)['Select']> = { } export default meta -type Story = StoryObj<(typeof Form)['Select']> +type Story = StoryObj export const Default: Story = { render: args => { return ( <> - + Label @@ -26,9 +27,9 @@ export const Default: Story = { Helper - +
- + Label @@ -37,9 +38,9 @@ export const Default: Story = { Helper - +
- + Label @@ -48,7 +49,7 @@ export const Default: Story = { Helper - + ) }, @@ -61,7 +62,7 @@ export const Info: Story = { render: args => { return ( <> - + Label @@ -70,9 +71,9 @@ export const Info: Story = { Info - +
- + Label @@ -81,9 +82,9 @@ export const Info: Story = { Info - +
- + Label @@ -92,7 +93,7 @@ export const Info: Story = { Info - + ) }, @@ -102,7 +103,7 @@ export const Error: Story = { render: args => { return ( <> - + Label @@ -111,9 +112,9 @@ export const Error: Story = { Error - +
- + Label @@ -122,9 +123,9 @@ export const Error: Story = { Error - +
- + Label @@ -133,7 +134,7 @@ export const Error: Story = { Error - + ) }, @@ -143,7 +144,7 @@ export const Warning: Story = { render: args => { return ( <> - + Label @@ -152,9 +153,9 @@ export const Warning: Story = { Warning - +
- + Label @@ -163,9 +164,9 @@ export const Warning: Story = { Warning - +
- + Label @@ -174,7 +175,7 @@ export const Warning: Story = { Warning - + ) }, @@ -184,38 +185,38 @@ export const Success: Story = { render: args => { return ( <> - + Label - + Success - +
- + Label - + Success - +
- + Label - + Success - + ) }, diff --git a/services/web/frontend/stories/ui/form/form-textarea-bs5.stories.tsx b/services/web/frontend/stories/ui/form/form-textarea-bs5.stories.tsx index 2ac58f5d6b..df72bc7ffa 100644 --- a/services/web/frontend/stories/ui/form/form-textarea-bs5.stories.tsx +++ b/services/web/frontend/stories/ui/form/form-textarea-bs5.stories.tsx @@ -1,49 +1,51 @@ import { Form } from 'react-bootstrap-5' import type { Meta, StoryObj } from '@storybook/react' +import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group' import FormText from '@/features/ui/components/bootstrap-5/form/form-text' +import FormControl from '@/features/ui/components/bootstrap-5/form/form-control' -const meta: Meta<(typeof Form)['Control']> = { +const meta: Meta> = { title: 'Shared / Components / Bootstrap 5 / Form / Textarea', - component: Form.Control, + component: FormControl, parameters: { bootstrap5: true, }, } export default meta -type Story = StoryObj<(typeof Form)['Control']> +type Story = StoryObj> export const Default: Story = { render: args => { return ( <> - + Label - Helper - +
- + Label - + Helper - +
- + Label - Helper - + ) }, @@ -56,9 +58,9 @@ export const Info: Story = { render: args => { return ( <> - + Label - Info - +
- + Label - Info - +
- + Label - Info - + ) }, @@ -99,9 +101,9 @@ export const Error: Story = { render: args => { return ( <> - + Label - Error - +
- + Label - Error - +
- + Label - Error - + ) }, @@ -145,9 +147,9 @@ export const Warning: Story = { render: args => { return ( <> - + Label - Warning - +
- + Label - Warning - +
- + Label - Warning - + ) }, @@ -188,9 +190,9 @@ export const Success: Story = { render: args => { return ( <> - + Label - Success - +
- + Label - Success - +
- + Label - Success - + ) }, diff --git a/services/web/frontend/stylesheets/app/project-list.less b/services/web/frontend/stylesheets/app/project-list.less index df5e821041..32507e3dbc 100644 --- a/services/web/frontend/stylesheets/app/project-list.less +++ b/services/web/frontend/stylesheets/app/project-list.less @@ -386,9 +386,7 @@ ul.folders-menu { form.project-search { .form-group { - @media (min-width: @screen-md) { - margin-bottom: 0; - } + margin-bottom: 0; } } diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/form.scss b/services/web/frontend/stylesheets/bootstrap-5/components/form.scss index 0200ede1c8..1ec07571bf 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/form.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/form.scss @@ -84,3 +84,69 @@ .form-group { margin-bottom: var(--spacing-06); } + +.form-control-wrapper { + position: relative; + + &.form-control-wrapper-disabled { + .form-control-start-icon, + .form-control-end-icon { + & > * { + color: var(--content-disabled); + } + } + } + + .form-control-start-icon, + .form-control-end-icon { + position: absolute; + top: 0; + height: 100%; + display: flex; + align-items: center; + font-size: 0; + } + + .form-control-start-icon { + left: 0; + } + .form-control-end-icon { + right: 0; + } + + --icon-width: 20px; + --form-control-padding-x: var(--spacing-04); + --form-control-icon-offset-y: var(--spacing-04); + + &.form-control-wrapper-sm { + --form-control-padding-x: var(--spacing-03); + --form-control-icon-offset-y: var(--spacing-02); + } + + &.form-control-wrapper-lg { + --form-control-padding-x: var(--spacing-05); + --form-control-icon-offset-y: var(--spacing-05); + } + + .form-control-start-icon { + padding-left: calc(var(--form-control-padding-x) + var(--bs-border-width)); + } + + .form-control-end-icon { + padding-right: calc(var(--form-control-padding-x) + var(--bs-border-width)); + } + + .form-control-offset-start { + padding-left: calc( + var(--form-control-padding-x) + var(--form-control-icon-offset-y) + + var(--icon-width) + ); + } + + .form-control-offset-end { + padding-right: calc( + var(--form-control-padding-x) + var(--form-control-icon-offset-y) + + var(--icon-width) + ); + } +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss index 082c2bc574..23af3a2e9a 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss @@ -464,3 +464,13 @@ .project-list-load-more-button { margin-bottom: var(--spacing-05); } + +form.project-search { + .form-group { + margin-bottom: 0; + } +} + +.project-search-clear-btn { + @include reset-button; +}