diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/ArraySelection/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/ArraySelection/Examples.tsx index 6866550cdac..64788d863b0 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/ArraySelection/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/ArraySelection/Examples.tsx @@ -46,7 +46,7 @@ export const FieldArraySelectionAndOption = () => { - + @@ -67,7 +67,7 @@ export const FieldArraySelectionPath = () => { > - + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection.mdx new file mode 100644 index 00000000000..4ebbe475a1e --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection.mdx @@ -0,0 +1,27 @@ +--- +title: 'Selection' +description: '`Value.Selection` is a component for displaying a string value based on a user selection.' +componentType: 'base-value' +hideInMenu: true +showTabs: true +tabs: + - title: Info + key: '/info' + - title: Demos + key: '/demos' + - title: Properties + key: '/properties' +breadcrumb: + - text: Forms + href: /uilib/extensions/forms/ + - text: Value + href: /uilib/extensions/forms/Value/ + - text: Selection + href: /uilib/extensions/forms/Value/Selection/ +--- + +import Info from 'Docs/uilib/extensions/forms/Value/Selection/info' +import Demos from 'Docs/uilib/extensions/forms/Value/Selection/demos' + + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/Examples.tsx new file mode 100644 index 00000000000..acde58534e9 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/Examples.tsx @@ -0,0 +1,101 @@ +import ComponentBox from '../../../../../../shared/tags/ComponentBox' +import { Flex, P } from '@dnb/eufemia/src' +import { Field, Form, Value } from '@dnb/eufemia/src/extensions/forms' + +export const Placeholder = () => { + return ( + + + + ) +} + +export const WithValue = () => { + return ( + + + + ) +} + +export const Label = () => { + return ( + + + + ) +} + +export const LabelAndValue = () => { + return ( + + + + ) +} + +export const Inline = () => { + return ( + + + This is before the component + + This is after the component + + + ) +} + +export const FieldSelectionPath = () => { + return ( + + + + + + + + + ) +} + +export const FieldSelectionAndOption = () => { + return ( + + + + + + + + + + + + + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/demos.mdx new file mode 100644 index 00000000000..56f36690894 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/demos.mdx @@ -0,0 +1,39 @@ +--- +showTabs: true +--- + +import * as Examples from './Examples' + +## Demos + +### Placeholder + + + +### Value + + + +### Label + + + +### Label and value + + + +### Inline + + + +### Field.Selection with path + +When using the same `path` as on a `Field.Selection`, the title will be used as the displayed value. + + + +### Field.Option and Field.Selection + +When using the same `path` as on a `Field.Selection`, the `Field.Option` title will be used as the displayed value. + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/info.mdx new file mode 100644 index 00000000000..530087d274e --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/info.mdx @@ -0,0 +1,14 @@ +--- +showTabs: true +--- + +## Description + +`Value.Selection` is a component for displaying a string value based on a user selection. + +There is a corresponding [Field.Selection](/uilib/extensions/forms/base-fields/Selection) component. + +```jsx +import { Value } from '@dnb/eufemia/extensions/forms' +render() +``` diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/properties.mdx new file mode 100644 index 00000000000..ec45400a322 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/Selection/properties.mdx @@ -0,0 +1,18 @@ +--- +showTabs: true +--- + +import TranslationsTable from 'dnb-design-system-portal/src/shared/parts/TranslationsTable' +import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable' +import { SelectionProperties } from '@dnb/eufemia/src/extensions/forms/Value/Selection/SelectionDocs' +import { ValueProperties } from '@dnb/eufemia/src/extensions/forms/Value/ValueDocs' + +## Properties + +### Field-specific props + + + +### General props + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Selection/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Selection/info.mdx index 46785026c80..874d6a71ef6 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Selection/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Selection/info.mdx @@ -8,6 +8,8 @@ showTabs: true [Field.Option](/uilib/extensions/forms/base-fields/Option/) is a related component. +There is a corresponding [Value.Selection](/uilib/extensions/forms/Value/Selection) component. + ```tsx import { Field } from '@dnb/eufemia/extensions/forms' render( diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/changelog.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/changelog.mdx index 88c91ff79eb..a411a3ea6b3 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/changelog.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/changelog.mdx @@ -17,6 +17,7 @@ Change log for the Eufemia Forms extension. - Added [Iterate.PushContainer](/uilib/extensions/forms/Iterate/PushContainer/) to create new items in an array. - Added [Value.ArraySelection](/uilib/extensions/forms/Value/ArraySelection/) component to render an array of values. +- Added [Value.Selection](/uilib/extensions/forms/Value/Selection/) component to render a selection value. ## v10.43 diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx index d05e638e029..0f55a004e82 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx @@ -1,13 +1,12 @@ import React, { useCallback, useContext, useMemo } from 'react' import { Checkbox, HelpButton, ToggleButton } from '../../../../components' import classnames from 'classnames' -import OptionField from '../Option' import FieldBlock from '../../FieldBlock' import { useFieldProps } from '../../hooks' import { ReturnAdditional } from '../../hooks/useFieldProps' import { FieldHelpProps, FieldProps, FormError } from '../../types' import { pickSpacingProps } from '../../../../components/flex/utils' -import { getStatus } from '../Selection' +import { getStatus, mapOptions } from '../Selection' import { HelpButtonProps } from '../../../../components/HelpButton' import ToggleButtonGroupContext from '../../../../components/toggle-button/ToggleButtonGroupContext' import DataContext from '../../DataContext/Context' @@ -219,6 +218,7 @@ export function useCheckboxOrToggleOptions({ /> ) }, + // eslint-disable-next-line react-hooks/exhaustive-deps [ disabled, emptyValue, @@ -234,29 +234,7 @@ export function useCheckboxOrToggleOptions({ ] ) - const mapOptions = useCallback( - (children: React.ReactNode) => { - return React.Children.toArray(children).map( - (child: React.ReactElement, i) => { - if (React.isValidElement(child)) { - if (child.type === OptionField) { - return createOption(child.props, i) - } - - if (child.props.children) { - const nestedChildren = mapOptions(child.props.children) - return React.cloneElement(child, child.props, nestedChildren) - } - } - - return child - } - ) - }, - [createOption] - ) - - const result = mapOptions(children) + const result = mapOptions(children, { createOption }) if (path) { setFieldProps?.(path + '/arraySelectionData', collectedData) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Selection/Selection.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Selection/Selection.tsx index ce98e0241fb..1fd73bcbca1 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Selection/Selection.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Selection/Selection.tsx @@ -324,10 +324,6 @@ function renderRadioItems({ const optionsCount = React.Children.count(children) + (dataList?.length || 0) - const Component = ( - variant === 'radio' ? Radio : ToggleButton - ) as typeof Radio & typeof ToggleButton - const createOption = (props: OptionProps, i: number) => { const { error, title, help, children, ...rest } = props @@ -339,6 +335,10 @@ function renderRadioItems({ ) : undefined + const Component = ( + variant === 'radio' ? Radio : ToggleButton + ) as typeof Radio & typeof ToggleButton + return ( { - return React.Children.map( - children, - (child: React.ReactElement, i) => { - if (React.isValidElement(child)) { - if (child.type === OptionField) { - return createOption(child.props, i) - } - - if (child.props.children) { - const nestedChildren = mapOptions(child.props.children) - return React.cloneElement(child, child.props, nestedChildren) - } - } - - return child - } - ) - } - return [ ...(dataList || []).map((props, i) => { return createOption(props as OptionProps, i) }), - ...(mapOptions(children) || []), + ...(mapOptions(children, { createOption }) || []), ].filter(Boolean) } +export function mapOptions(children: React.ReactNode, { createOption }) { + return React.Children.map( + children, + (child: React.ReactElement, i) => { + if (React.isValidElement(child)) { + if (child.type === OptionField) { + return createOption(child.props, i) + } + + if (child.props.children) { + const nestedChildren = mapOptions(child.props.children, { + createOption, + }) + return React.cloneElement(child, child.props, nestedChildren) + } + } + + return child + } + ) +} + export function makeOptions( children: React.ReactNode ): T { diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/ArraySelection.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/ArraySelection.tsx index 3258fda46c6..f5d4244512e 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/ArraySelection.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/ArraySelection.tsx @@ -21,16 +21,18 @@ function ArraySelection(props: Props) { const { fieldPropsRef } = useContext(Context) || {} const { path, value, format, className, ...rest } = useValueProps(props) - const valueFromField = useMemo(() => { - if (path && value) { + const list = useMemo(() => { + if (path) { const data = fieldPropsRef?.current?.[ path + '/arraySelectionData' ] as Array<{ value: string title: string | React.ReactNode }> - return data?.map(({ title }) => convertJsxToString(title)) + return data?.map?.(({ title }) => convertJsxToString(title)) || value } + + return value }, [fieldPropsRef, path, value]) return ( @@ -38,7 +40,7 @@ function ArraySelection(props: Props) { className={classnames('dnb-forms-value-array-selection', className)} {...rest} > - {listFormat(valueFromField ?? value, { locale, format })} + {listFormat(list, { locale, format })} ) } diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/stories/ArraySelection.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/stories/ArraySelection.stories.tsx index cd2a3dfcacc..51cf6297f20 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/stories/ArraySelection.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/stories/ArraySelection.stories.tsx @@ -21,7 +21,7 @@ export function ArraySelectionValue() { - + setCount(count + 1)}> {count} diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Selection/Selection.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/Selection.tsx new file mode 100644 index 00000000000..55fecb08d48 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/Selection.tsx @@ -0,0 +1,44 @@ +import React, { useContext, useMemo } from 'react' +import StringValue from '../String' +import { Path, ValueProps } from '../../types' +import { useValueProps } from '../../hooks' +import useDataValue from '../../hooks/useDataValue' +import Context from '../../DataContext/Context' +import { convertJsxToString } from '../../../../shared/component-helper' +import { Data } from '../../Field/Selection' + +export type Props = ValueProps & { + dataPath?: Path +} + +function Selection(props: Props) { + const { fieldPropsRef } = useContext(Context) || {} + const { path, dataPath, value, ...rest } = useValueProps(props) + const { getValueByPath } = useDataValue() + + const valueToDisplay = useMemo(() => { + const fieldProp = fieldPropsRef?.current?.[path] + + if (path || dataPath) { + let list = getValueByPath(dataPath)?.map?.((props) => ({ props })) + + if (!list) { + list = fieldProp?.['children'] as Array< + Omit & { props: Data[number] } + > + } + + const title = list?.find?.((child) => child.props.value === value) + ?.props?.title + + return title ? convertJsxToString(title) : value + } + + return value + }, [dataPath, fieldPropsRef, getValueByPath, path, value]) + + return +} + +Selection._supportsSpacingProps = true +export default Selection diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Selection/SelectionDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/SelectionDocs.ts new file mode 100644 index 00000000000..e97008b1bc9 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/SelectionDocs.ts @@ -0,0 +1,9 @@ +import { PropertiesTableProps } from '../../../../shared/types' + +export const SelectionProperties: PropertiesTableProps = { + dataPath: { + doc: 'The path to the context data (Form.Handler). The object needs to have a `value` and a `title` property. The generated options will be placed above given JSX based children.', + type: 'string', + status: 'optional', + }, +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Selection/__tests__/Selection.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/__tests__/Selection.test.tsx new file mode 100644 index 00000000000..4f280bea076 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/__tests__/Selection.test.tsx @@ -0,0 +1,225 @@ +import React from 'react' +import { screen, render } from '@testing-library/react' +import { Value, Form, Field } from '../../..' +import userEvent from '@testing-library/user-event' + +describe('Value.Selection', () => { + it('renders value', () => { + render() + + expect( + document.querySelector( + '.dnb-forms-value-string .dnb-forms-value-block__content' + ) + ).toHaveTextContent('Bankaxept') + }) + + it('renders label when showEmpty is true', () => { + render() + expect(document.querySelector('.dnb-form-label')).toHaveTextContent( + 'My label' + ) + }) + + it('renders value and label', () => { + render() + expect( + document.querySelector( + '.dnb-forms-value-string .dnb-forms-value-block__content' + ) + ).toHaveTextContent('Visa') + + expect(document.querySelector('.dnb-form-label')).toHaveTextContent( + 'My selections' + ) + }) + + it('renders custom label', () => { + render() + expect(document.querySelector('.dnb-form-label')).toHaveTextContent( + 'Custom label' + ) + }) + + it('renders placeholder', () => { + render() + expect(screen.getByText('Please select a value')).toBeInTheDocument() + }) + + it('renders value from path', () => { + render( + + + + ) + + expect( + document.querySelector( + '.dnb-forms-value-string .dnb-forms-value-block__content' + ) + ).toHaveTextContent('Mastercard') + }) + + it('should use Field.Option title rendered in Field.Selection', () => { + render( + + + + + + + + + + ) + + expect( + document.querySelector( + '.dnb-forms-value-string .dnb-forms-value-block__content' + ) + ).toHaveTextContent('Foo title') + }) + + it('should use Field.Option title, when title is JSX', async () => { + render( + + + + Bar title} /> + + + + + + ) + + expect( + document.querySelector( + '.dnb-forms-value-string .dnb-forms-value-block__content' + ) + ).toHaveTextContent('Bar title') + }) + + it('should use Field.Option title rendered in Field.Selection interactively', async () => { + render( + + + + + + + + + + ) + + const element = document.querySelector( + '.dnb-forms-value-string .dnb-forms-value-block__content' + ) + + expect(element).toHaveTextContent('Foo title') + + await userEvent.click(screen.getAllByText('Bar title')[0]) + expect(element).toHaveTextContent('Bar title') + + await userEvent.click(screen.getAllByText('Baz title')[0]) + expect(element).toHaveTextContent('Baz title') + + await userEvent.click(screen.getAllByText('Foo title')[0]) + expect(element).toHaveTextContent('Foo title') + }) + + it('should use title from data context interactively', async () => { + render( + + + + + ) + + const element = document.querySelector( + '.dnb-forms-value-string .dnb-forms-value-block__content' + ) + + expect(element).toHaveTextContent('Foo title') + + await userEvent.click(screen.getAllByText('Bar title')[0]) + expect(element).toHaveTextContent('Bar title') + + await userEvent.click(screen.getAllByText('Baz title')[0]) + expect(element).toHaveTextContent('Baz title') + + await userEvent.click(screen.getAllByText('Foo title')[0]) + expect(element).toHaveTextContent('Foo title') + }) + + it('should use data from context when "dataPath" is defined', () => { + render( + + + + ) + + const element = document.querySelector( + '.dnb-forms-value-string .dnb-forms-value-block__content' + ) + + expect(element).toHaveTextContent('Bar title') + }) + + it('should use data from context when "dataPath" is defined without "path"', () => { + render( + + + + ) + + const element = document.querySelector( + '.dnb-forms-value-string .dnb-forms-value-block__content' + ) + + expect(element).toHaveTextContent('Bar title') + }) +}) diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Selection/index.ts b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/index.ts new file mode 100644 index 00000000000..795b7ae19b7 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/index.ts @@ -0,0 +1,2 @@ +export { default } from './Selection' +export * from './Selection' diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Selection/stories/Selection.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/stories/Selection.stories.tsx new file mode 100644 index 00000000000..371b027e041 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Value/Selection/stories/Selection.stories.tsx @@ -0,0 +1,49 @@ +import { Field, Form, Value } from '../../..' +import { Flex } from '../../../../../components' + +export default { + title: 'Eufemia/Extensions/Forms/Value/Selection', +} + +export function ValueFromProps() { + return ( + + + + + + + + + + ) +} + +export function ValueFromPath() { + return ( + + + + + + + ) +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/index.ts b/packages/dnb-eufemia/src/extensions/forms/Value/index.ts index 620c00ac600..4469a10abd0 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Value/index.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Value/index.ts @@ -13,3 +13,4 @@ export { default as OrganizationNumber } from './OrganizationNumber' export { default as SummaryList } from './SummaryList' export { default as Composition } from './Composition' export { default as ArraySelection } from './ArraySelection' +export { default as Selection } from './Selection'
+ This is before the component + + This is after the component +