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 (
-
+
+
+
)
}
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;
+}