diff --git a/src/assets/icons/ICircleFilled.svg b/src/assets/icons/ICircleFilled.svg new file mode 100644 index 00000000..90f07ed9 --- /dev/null +++ b/src/assets/icons/ICircleFilled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/icons.stories.mdx b/src/assets/icons/icons.stories.mdx index 28ed7d5e..f4bb9d95 100644 --- a/src/assets/icons/icons.stories.mdx +++ b/src/assets/icons/icons.stories.mdx @@ -26,6 +26,7 @@ import Proxy from './Proxy.svg' import Sidecar from './Sidecar.svg' import Import from './Import.svg' import Export from './Export.svg' +import IIconFilled from './IIconFilled.svg' @@ -66,4 +67,7 @@ import { IconName } from "@mia-platform-internal/console-design-system-react/ico + + + diff --git a/src/components/Form/FormItem/FormItem.module.css b/src/components/Form/FormItem/FormItem.module.css new file mode 100644 index 00000000..4994ab35 --- /dev/null +++ b/src/components/Form/FormItem/FormItem.module.css @@ -0,0 +1,45 @@ +/** + * Copyright 2024 Mia srl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +.labelContainer { + display: flex; + justify-content: center; + align-items: center; + color: var(--palette-text-neutral-subtler, #898989); + gap: var(--spacing-gap-xs, 4px); +} + +.extraContainer { + display: flex; + align-items: center; + color: var(--palette-text-neutral-main, #636363); + padding-top: var(--spacing-gap-sm, 8px); + gap: var(--spacing-gap-xs, 4px); +} + +.tooltipContainer { + display: inline-flex; +} + +.docLinkContainer { + width: var(--shape-size-md, 16px); + height: var(--shape-size-md, 16px); + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/components/Form/FormItem/FormItem.stories.tsx b/src/components/Form/FormItem/FormItem.stories.tsx index b178f5bc..2fe4ee95 100644 --- a/src/components/Form/FormItem/FormItem.stories.tsx +++ b/src/components/Form/FormItem/FormItem.stories.tsx @@ -17,6 +17,7 @@ */ import { Meta, type StoryObj } from '@storybook/react' +import { PiCircleHalfTilt } from 'react-icons/pi' import { Button } from '../../Button' import { Checkbox as CheckboxComponent } from '../../Checkbox' @@ -77,6 +78,48 @@ export const Input: Story = { }, } +export const Required: Story = { + args: { + name: 'input', + isRequired: true, + children: , + }, +} + +export const InputWithTooltip: Story = { + args: { + name: 'input', + tooltip: { title: 'title' }, + children: , + }, +} + +export const InputWithDoclink: Story = { + args: { + name: 'input', + docLink: '#', + children: , + }, +} + +export const InputWithDoclinkAndTooltip: Story = { + args: { + name: 'input', + docLink: '#', + tooltip: { title: 'title' }, + children: , + }, +} + +export const InputWithExtra: Story = { + args: { + name: 'input', + extra: 'Extra', + extraIcon: PiCircleHalfTilt, + children: , + }, +} + export const InputWithAddon: Story = { args: { name: 'inputAddon', diff --git a/src/components/Form/FormItem/FormItem.test.tsx b/src/components/Form/FormItem/FormItem.test.tsx index d46ab896..69a941df 100644 --- a/src/components/Form/FormItem/FormItem.test.tsx +++ b/src/components/Form/FormItem/FormItem.test.tsx @@ -17,6 +17,7 @@ */ import { RenderResult, waitFor, within } from '@testing-library/react' +import { PiCircleHalfTilt } from 'react-icons/pi' import { ReactElement } from 'react' import { fireEvent } from '@testing-library/dom' @@ -84,6 +85,43 @@ describe('FormItem Component', () => { await waitFor(() => expect(asFragment()).toMatchSnapshot()) }) + test('renders input required FormItem correctly', async() => { + const { asFragment } = renderItem({ + name: 'input', + isRequired: true, + children: , + }) + await waitFor(() => expect(asFragment()).toMatchSnapshot()) + }) + + test('renders input with tooltip FormItem correctly', async() => { + const { asFragment } = renderItem({ + name: 'input', + tooltip: { title: 'tooltip' }, + children: , + }) + await waitFor(() => expect(asFragment()).toMatchSnapshot()) + }) + + test('renders input with docLink FormItem correctly', async() => { + const { asFragment } = renderItem({ + name: 'input', + tooltip: { title: 'tooltip' }, + children: , + }) + await waitFor(() => expect(asFragment()).toMatchSnapshot()) + }) + + test('renders input with extra FormItem correctly', async() => { + const { asFragment } = renderItem({ + name: 'input', + extra: 'Extra', + extraIcon: PiCircleHalfTilt, + children: , + }) + await waitFor(() => expect(asFragment()).toMatchSnapshot()) + }) + test('renders inputAddon FormItem correctly', async() => { const { asFragment } = renderItem({ name: 'inputAddon', @@ -174,6 +212,21 @@ describe('FormItem Component', () => { }) }) + test('click on docLink button should open a new window', async() => { + const openLink = jest.fn() + jest.spyOn(window, 'open').mockImplementationOnce(openLink) + + renderItem({ + name: 'input', + docLink: '#', + children: , + }) + + const button = screen.getByRole('button', { name: 'doc-link' }) + await userEvent.click(button) + expect(openLink).toHaveBeenCalledWith('#', '_blank') + }) + describe('onChange', () => { test('input should display value and change correctly', async() => { renderItem({ diff --git a/src/components/Form/FormItem/FormItem.tsx b/src/components/Form/FormItem/FormItem.tsx index b00d7aeb..56ddda53 100644 --- a/src/components/Form/FormItem/FormItem.tsx +++ b/src/components/Form/FormItem/FormItem.tsx @@ -16,15 +16,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ReactElement, isValidElement, useMemo } from 'react' +import { ReactElement, isValidElement, useCallback, useMemo } from 'react' import { Form as AntForm } from 'antd' +import { PiBookOpen } from 'react-icons/pi' +import { Button } from '../../Button' import { Checkbox } from '../../Checkbox' import { FormItemProps } from '../props.ts' +import ICircleFilled from '../../../assets/icons/ICircleFilled.svg' +import { Icon } from '../../Icon' import { Input } from '../../Input' import { RadioGroup } from '../../RadioGroup' import { Switch } from '../../Switch' +import { Tooltip } from '../../Tooltip' import log from '../../../utils/log.ts' +import styles from './FormItem.module.css' const defaults = { span: 1, @@ -63,11 +69,17 @@ export const FormItem = ( span = defaults.span, justify, isFullWidth = defaults.isFullWidth, - label = name, + label: labelProp = name, rules, valuePropName, getValueFromEvent, shouldUpdate, + dependencies, + isRequired, + docLink, + tooltip, + extra: extraProp, + extraIcon: extraIconProp, }: FormItemProps ): ReactElement => { const form = AntForm.useFormInstance() @@ -107,14 +119,61 @@ export const FormItem = ( log.error('inputElement must be a valid element or a function') }, [form, name, children]) + const onClickDocLink = useCallback(() => { + window.open(docLink, '_blank') + }, [docLink]) + + const label = useMemo(() => { + if (labelProp || tooltip || docLink) { + return ( +
+ {labelProp} + {tooltip && ( + +
+ +
+
+ )} + {docLink && ( +
+
+ )} +
+ ) + } + }, [docLink, labelProp, onClickDocLink, tooltip]) + + const extra = useMemo(() => { + if (extraIconProp || extraProp) { + return ( +
+ {extraIconProp && ( + + )} + {extraProp} +
+ ) + } + }, [extraIconProp, extraProp]) + return ( diff --git a/src/components/Form/FormItem/__snapshots__/FormItem.test.tsx.snap b/src/components/Form/FormItem/__snapshots__/FormItem.test.tsx.snap index 54bff5b3..d05a02ad 100644 --- a/src/components/Form/FormItem/__snapshots__/FormItem.test.tsx.snap +++ b/src/components/Form/FormItem/__snapshots__/FormItem.test.tsx.snap @@ -19,9 +19,13 @@ exports[`FormItem Component snapshots renders checkbox FormItem correctly 1`] =
- checkboxGroup +
+ checkboxGroup +
- input +
+ input +
+ +
+
+
+
+ +
+
+
+ + +
+
+
+
+
+ +
+
+
+
+
+ + +`; + +exports[`FormItem Component snapshots renders input required FormItem correctly 1`] = ` + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; + +exports[`FormItem Component snapshots renders input with docLink FormItem correctly 1`] = ` + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; + +exports[`FormItem Component snapshots renders input with extra FormItem correctly 1`] = ` + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + + + Extra +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; + +exports[`FormItem Component snapshots renders input with tooltip FormItem correctly 1`] = ` + +
+
+
+
+
- inputAddon +
+ inputAddon +
- number +
+ number +
- radioGroup +
+ radioGroup +
- custom +
+ custom +
- search +
+ search +
- select +
+ select +
- switch +
+ switch +
- textarea +
+ textarea +
- firstName +
+ firstName +
- lastName +
+ lastName +
- firstName +
+ firstName +
- lastName +
+ lastName +
- firstName +
+ firstName +
- lastName +
+ lastName +
- firstName +
+ firstName +
- lastName +
+ lastName +
- firstName +
+ firstName +
- lastName +
+ lastName +
- firstName +
+ firstName +
- lastName +
+ lastName +
- firstName +
+ firstName +
- lastName +
+ lastName +
- firstName +
+ firstName +
- lastName +
+ lastName +
ReactNode); /** - * Whether the FormItem should update when the state of other fields changes. + * Additional content displayed below the FormItem, similar to `help` but for more general purposes. */ - shouldUpdate?: boolean; + tooltip?: Omit /** - * List of fields that this FormItem depends on, used to trigger conditional updates. + * Additional content displayed below the FormItem, similar to `help` but for more general purposes. */ - dependencies?: string[]; + docLink?: string /** - * Help text displayed below the form field. + * Additional content displayed below the FormItem, similar to `help` but for more general purposes. */ - help?: ReactNode; + extra?: ReactNode; /** * Additional content displayed below the FormItem, similar to `help` but for more general purposes. */ - extra?: ReactNode; + extraIcon?: IconComponent, + + /** + * Whether the FormItem should update when the state of other fields changes. + */ + shouldUpdate?: boolean; + + /** + * List of fields that this FormItem depends on, used to trigger conditional updates. + */ + dependencies?: string[]; /** * Whether the field is required, showing an asterisk and applying validation. */ - required?: boolean; + isRequired?: boolean; };