From b6c3dadea922fe59db0a74c6ca6778183ef48ebd Mon Sep 17 00:00:00 2001 From: Anders Date: Wed, 8 Jan 2025 17:07:08 +0100 Subject: [PATCH 01/61] feat(Forms): deprecate `continuousValidation` in favor of `validateContinuously` (#4441) --- .../releases/eufemia/v11-info.mdx | 8 + .../extensions/forms/Form/Visibility/info.mdx | 2 +- .../forms/best-practices-on-forms.mdx | 2 +- .../create-component/useFieldProps/info.mdx | 2 +- .../extensions/forms/getting-started.mdx | 2 +- .../src/components/accordion/Accordion.tsx | 2 +- .../__tests__/FieldBoundaryProvider.test.tsx | 39 +++++ .../Provider/__tests__/Provider.test.tsx | 126 ++++++++++++++- .../__tests__/Composition.test.tsx | 81 ++++++++++ .../Field/Number/stories/Number.stories.tsx | 2 +- .../forms/Field/PhoneNumber/PhoneNumber.tsx | 5 +- .../Handler/stories/FormHandler.stories.tsx | 2 +- .../Form/Section/stories/Section.stories.tsx | 2 +- .../forms/Form/Visibility/Visibility.tsx | 8 + .../forms/Form/Visibility/VisibilityDocs.ts | 2 +- .../__tests__/useVisibility.test.tsx | 45 ++++++ .../forms/Form/Visibility/useVisibility.tsx | 3 +- .../forms/Iterate/Array/ArrayDocs.ts | 2 +- .../extensions/forms/Iterate/Array/types.ts | 1 + .../forms/hooks/DataValueWritePropsDocs.ts | 2 +- .../hooks/__tests__/useFieldProps.test.tsx | 151 ++++++++++++++++++ .../extensions/forms/hooks/useFieldProps.ts | 16 +- .../dnb-eufemia/src/extensions/forms/types.ts | 7 + 23 files changed, 488 insertions(+), 24 deletions(-) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx index 300565f693e..135d015046b 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx @@ -206,4 +206,12 @@ const errorMessages = { - replace type `DrawerListDataObjectUnion` with `DrawerListDataArrayItem`. - replace type `DrawerListDataObject` with `DrawerListDataArrayObject`. +## `Form.Visibility` + +- replace `continuousValidation` with `validateContinuously`. + +## `Field.*` components + +- replace `continuousValidation` with `validateContinuously`. + _February, 6. 2024_ diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility/info.mdx index 652b98ad820..44a05209f31 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility/info.mdx @@ -67,7 +67,7 @@ render( ) ``` -To prevent visibility changes during user interactions like typing, it shows the children first when the field both has no errors and has lost focus (blurred). You can use the `continuousValidation: true` property to immediately show the children when the field has no errors. +To prevent visibility changes during user interactions like typing, it shows the children first when the field both has no errors and has lost focus (blurred). You can use the `validateContinuously: true` property to immediately show the children when the field has no errors. ## Accessibility diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/best-practices-on-forms.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/best-practices-on-forms.mdx index 12275df8184..f20fb437fb4 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/best-practices-on-forms.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/best-practices-on-forms.mdx @@ -41,7 +41,7 @@ This document provides a set of best practices to follow when creating forms for - The `tab` (Tab) key should be used to navigate between form fields. It should NOT trigger validation. - Required fields should have `aria-required="true"` attribute. Use the `required` property for that. -- Validation should be triggered on `submit` events and on `blur` – if the user has made changes. In some cases, it is appreciated to trigger validation on `change` events. This behavior can be changed if needed by using `validateInitially`, `validateUnchanged` and `continuousValidation`. More info about these properties can be found in the [useFieldProps](/uilib/extensions/forms/create-component/useFieldProps/) documentation. +- Validation should be triggered on `submit` events and on `blur` – if the user has made changes. In some cases, it is appreciated to trigger validation on `change` events. This behavior can be changed if needed by using `validateInitially`, `validateUnchanged` and `validateContinuously`. More info about these properties can be found in the [useFieldProps](/uilib/extensions/forms/create-component/useFieldProps/) documentation. ```jsx { expect(contextRef.current.errorsRef.current).toMatchObject({}) }) + it('should set error in context with validateContinuously', async () => { + const contextRef: React.MutableRefObject = + React.createRef() + + const Contexts = ({ children }) => { + contextRef.current = useContext(FieldBoundaryContext) + return <>{children} + } + + render( + + + + + + + + + ) + + await userEvent.click(document.querySelector('button')) + + expect(contextRef.current.hasError).toBe(true) + expect(contextRef.current.hasSubmitError).toBe(true) + expect(contextRef.current.hasVisibleError).toBe(true) + expect(contextRef.current.errorsRef.current).toMatchObject({ + '/bar': true, + }) + + const inputElement = document.querySelector('input') + await userEvent.type(inputElement, 'b') + await userEvent.click(document.querySelector('button')) + + expect(contextRef.current.hasError).toBe(false) + expect(contextRef.current.hasSubmitError).toBe(false) + expect(contextRef.current.hasVisibleError).toBe(false) + expect(contextRef.current.errorsRef.current).toMatchObject({}) + }) + it('should set number for showBoundaryErrors as a truthy value', async () => { const showBoundaryErrors = { view: null, diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/__tests__/Provider.test.tsx b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/__tests__/Provider.test.tsx index e87c9352ec9..0de80d238f4 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/__tests__/Provider.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/__tests__/Provider.test.tsx @@ -42,7 +42,7 @@ if (isCI) { } function TestField(props: StringFieldProps) { - return + return } describe('DataContext.Provider', () => { @@ -2892,7 +2892,7 @@ describe('DataContext.Provider', () => { log.mockRestore() }) - it('should revalidate with provided schema based on changes in external data', () => { + it('should revalidate with provided schema based on changes in external data using deprecated continuousValidation', () => { const log = jest.spyOn(console, 'error').mockImplementation() const schema: JSONSchema = { @@ -2947,7 +2947,7 @@ describe('DataContext.Provider', () => { log.mockRestore() }) - it('should revalidate correctly based on changes in provided schema', () => { + it('should revalidate correctly based on changes in provided schema using deprecated continuousValidation', () => { const log = jest.spyOn(console, 'error').mockImplementation() const schema1: JSONSchema = { @@ -3010,6 +3010,124 @@ describe('DataContext.Provider', () => { log.mockRestore() }) + it('should revalidate with provided schema based on changes in external data', () => { + const log = jest.spyOn(console, 'error').mockImplementation() + + const schema: JSONSchema = { + type: 'object', + properties: { + myKey: { + type: 'string', + }, + }, + } + const validData = { + myKey: 'some-value', + } + const invalidData = { + myKey: 123, + } + const { rerender } = render( + + + + ) + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + + rerender( + + + + ) + + expect(screen.queryByRole('alert')).toBeInTheDocument() + + rerender( + + + + ) + + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + + log.mockRestore() + }) + + it('should revalidate correctly based on changes in provided schema', () => { + const log = jest.spyOn(console, 'error').mockImplementation() + + const schema1: JSONSchema = { + type: 'object', + properties: { + myKey: { + type: 'number', + }, + }, + } + const schema2: JSONSchema = { + type: 'object', + properties: { + myKey: { + type: 'string', + }, + }, + } + const data = { + myKey: 'some-value', + } + const { rerender } = render( + + + + ) + expect(screen.queryByRole('alert')).toBeInTheDocument() + expect(screen.queryByRole('alert')).toHaveTextContent( + 'The field at path="/myKey" value (some-value) type must be number' + ) + + rerender( + + + + ) + + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + + rerender( + + + + ) + + expect(screen.queryByRole('alert')).toBeInTheDocument() + + log.mockRestore() + }) + it('should accept custom ajv instance', async () => { const ajv = new Ajv({ strict: true, @@ -3041,7 +3159,7 @@ describe('DataContext.Provider', () => { path="/myKey" value="1" validateInitially - continuousValidation + validateContinuously /> ) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Composition/__tests__/Composition.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Composition/__tests__/Composition.test.tsx index 461f9415882..9cb4938dacd 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Composition/__tests__/Composition.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Composition/__tests__/Composition.test.tsx @@ -710,5 +710,86 @@ describe('FieldBlock', () => { nb.StringField.errorMinLength.replace('{minLength}', '6') ) }) + + it('should validate error continuously when validateContinuously is given', async () => { + const schema: JSONSchema = { + type: 'object', + properties: { + first: { + type: 'string', + minLength: 3, + }, + last: { + type: 'string', + minLength: 6, + }, + }, + } + + render( + + + + + + + ) + + const [first, last] = Array.from(document.querySelectorAll('input')) + const statusMessages = document.querySelectorAll('.dnb-form-status') + expect(statusMessages).toHaveLength(1) + + const statusMessage = statusMessages[0] + + expect(statusMessage).toHaveTextContent( + nb.StringField.errorMinLength.replace('{minLength}', '3') + ) + expect(statusMessage).toHaveTextContent( + nb.StringField.errorMinLength.replace('{minLength}', '6') + ) + + await userEvent.type(first, 'i') + + expect(statusMessage).toHaveTextContent( + nb.StringField.errorMinLength.replace('{minLength}', '3') + ) + + await userEvent.type(first, 'rst') + + expect(statusMessage).not.toHaveTextContent( + nb.StringField.errorMinLength.replace('{minLength}', '3') + ) + expect(statusMessage).toHaveTextContent( + nb.StringField.errorMinLength.replace('{minLength}', '6') + ) + + await userEvent.type(last, 'ast name') + + expect(statusMessage).not.toHaveTextContent( + nb.StringField.errorMinLength.replace('{minLength}', '3') + ) + expect(statusMessage).not.toHaveTextContent( + nb.StringField.errorMinLength.replace('{minLength}', '6') + ) + + expect(document.querySelectorAll('.dnb-form-status')).toHaveLength(0) + + await userEvent.type(last, '{Backspace>4}') + + expect(document.querySelectorAll('.dnb-form-status')).toHaveLength(1) + expect(document.querySelector('.dnb-form-status')).toHaveTextContent( + nb.StringField.errorMinLength.replace('{minLength}', '6') + ) + }) }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Number/stories/Number.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Number/stories/Number.stories.tsx index d74f85e5e5f..59fc3ce41e9 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Number/stories/Number.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Number/stories/Number.stories.tsx @@ -74,7 +74,7 @@ export const WithFreshValidator = () => { onChangeValidator={validator} defaultValue={2} // validateInitially - // continuousValidation + // validateContinuously // validateUnchanged path="/myNumberWithOnChangeValidator" /> diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/PhoneNumber/PhoneNumber.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/PhoneNumber/PhoneNumber.tsx index 44c490d08a4..7920b8801ca 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/PhoneNumber/PhoneNumber.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/PhoneNumber/PhoneNumber.tsx @@ -198,6 +198,7 @@ function PhoneNumber(props: Props) { required, validateInitially, continuousValidation, + validateContinuously, validateUnchanged, omitCountryCodeField, setHasFocus, @@ -437,7 +438,9 @@ function PhoneNumber(props: Props) { required={required} errorMessages={errorMessages} validateInitially={validateInitially} - continuousValidation={continuousValidation} + validateContinuously={ + continuousValidation || validateContinuously + } validateUnchanged={validateUnchanged} inputMode="tel" /> diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Handler/stories/FormHandler.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Handler/stories/FormHandler.stories.tsx index 0ee999297de..dd459a0219d 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Handler/stories/FormHandler.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Handler/stories/FormHandler.stories.tsx @@ -136,7 +136,7 @@ export function AdvancedForm() { path="/fieldA" onBlurValidator={firstValidator} // onChangeValidator={firstValidator} - // continuousValidation + // validateContinuously // validateInitially // validateUnchanged /> diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Section/stories/Section.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Section/stories/Section.stories.tsx index 844388e63d6..608a3ef6a97 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Section/stories/Section.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Section/stories/Section.stories.tsx @@ -23,7 +23,7 @@ const MySection = (props: SectionProps<{ lastName?: FieldNameProps }>) => { // required // value="x" // minLength={4} - // continuousValidation + // validateContinuously // validateInitially /> diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx index 1032ce5f9ef..fa62a9057d0 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx @@ -26,12 +26,20 @@ export type VisibleWhen = | { path: Path isValid: boolean + /** + * @deprecated – Replaced with validateContinuously, continuousValidation can be removed in v11. + */ continuousValidation?: boolean + validateContinuously?: boolean } | { itemPath: Path isValid: boolean + /** + * @deprecated – Replaced with validateContinuously, continuousValidation can be removed in v11. + */ continuousValidation?: boolean + validateContinuously?: boolean } /** diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/VisibilityDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/VisibilityDocs.ts index f6bd075dba0..8721bb53bd1 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/VisibilityDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/VisibilityDocs.ts @@ -3,7 +3,7 @@ import { HeightAnimationEvents } from '../../../../components/height-animation/H export const VisibilityProperties: PropertiesTableProps = { visibleWhen: { - doc: 'Provide a `path` or `itemPath` and a `hasValue` method that returns a boolean or the excepted value in order to show children. The first parameter is the value of the path. You can also use `isValid` instead of `hasValue` to only show the children when the field has no errors and has lost focus (blurred). You can change that behavior by using the `continuousValidation` property.', + doc: 'Provide a `path` or `itemPath` and a `hasValue` method that returns a boolean or the excepted value in order to show children. The first parameter is the value of the path. You can also use `isValid` instead of `hasValue` to only show the children when the field has no errors and has lost focus (blurred). You can change that behavior by using the `validateContinuously` property.', type: 'object', status: 'optional', }, diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/__tests__/useVisibility.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/__tests__/useVisibility.test.tsx index 73d39756ab2..1d4c7911c9e 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/__tests__/useVisibility.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/__tests__/useVisibility.test.tsx @@ -427,6 +427,51 @@ describe('useVisibility', () => { }) ).toBe(true) }) + + it('should return true immediately when "validateContinuously" is true', () => { + const { result } = renderHook(useVisibility, { + wrapper: ({ children }) => ( + + + {children} + + ), + }) + + expect( + result.current.check({ + visibleWhen: { + path: '/myPath', + isValid: true, + validateContinuously: true, + }, + }) + ).toBe(false) + + fireEvent.focus(document.querySelector('input')) + fireEvent.change(document.querySelector('input'), { + target: { value: '2' }, + }) + expect( + result.current.check({ + visibleWhen: { + path: '/myPath', + isValid: true, + validateContinuously: true, + }, + }) + ).toBe(true) + + fireEvent.blur(document.querySelector('input')) + expect( + result.current.check({ + visibleWhen: { + path: '/myPath', + isValid: true, + }, + }) + ).toBe(true) + }) }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/useVisibility.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/useVisibility.tsx index 34c8c671d92..9d1c55b198d 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/useVisibility.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/useVisibility.tsx @@ -63,7 +63,8 @@ export default function useVisibility(props?: Partial) { return visibleWhenNot ? true : false } const result = - (visibleWhen.continuousValidation + (visibleWhen.continuousValidation || + visibleWhen.validateContinuously ? true : item.isFocused !== true) && hasFieldError(path) === false return visibleWhenNot ? !result : result diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/ArrayDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/ArrayDocs.ts index 90a8abb1d12..7d466fe3c3a 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/ArrayDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/ArrayDocs.ts @@ -54,7 +54,7 @@ export const ArrayProperties: PropertiesTableProps = { }, onChangeValidator: DataValueWritePropsProperties.onChangeValidator, validateInitially: DataValueWritePropsProperties.validateInitially, - continuousValidation: DataValueWritePropsProperties.continuousValidation, + validateContinuously: DataValueWritePropsProperties.validateContinuously, containerMode: { doc: 'Defines the container mode for all nested containers. Can be `view`, `edit` or `auto`. When using `auto`, it will automatically open if there is an error in the container. When a new item is added, the item before it will change to `view` mode, if it had no validation errors. Defaults to `auto`.', type: 'string', diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/types.ts b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/types.ts index 6e8eeaa9e68..8b297fe3b21 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/types.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/types.ts @@ -18,6 +18,7 @@ export type Props = Omit< | 'onChange' | 'validateInitially' | 'continuousValidation' + | 'validateContinuously' > & { children: ElementChild | Array path?: Path diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/DataValueWritePropsDocs.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/DataValueWritePropsDocs.ts index e31d5ee2e1b..83d9952594b 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/DataValueWritePropsDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/DataValueWritePropsDocs.ts @@ -66,7 +66,7 @@ export const DataValueWritePropsProperties: PropertiesTableProps = { type: 'boolean', status: 'optional', }, - continuousValidation: { + validateContinuously: { doc: 'Set to `true` to show validation based errors continuously while writing, not just when blurring the field.', type: 'boolean', status: 'optional', diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx index 6bf7210da35..3d8585afe3f 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx @@ -3187,6 +3187,71 @@ describe('useFieldProps', () => { }) }) }) + + describe('validateContinuously', () => { + it('should show not show error message initially', async () => { + const validator = jest.fn(validatorFn) + + render( + + + + + + ) + + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + }) + + it('should hide and show error message while typing', async () => { + const validator = jest.fn(validatorFn) + + render( + + + + + + ) + + const [inputWithRefValue] = Array.from( + document.querySelectorAll('input') + ) + + // Show error message + fireEvent.submit(document.querySelector('form')) + + await waitFor(() => { + expect(screen.queryByRole('alert')).toBeInTheDocument() + expect(screen.queryByRole('alert')).toHaveTextContent( + 'The amount should be greater than 2' + ) + }) + + await userEvent.type(inputWithRefValue, '{Backspace}') + + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + + await userEvent.type(inputWithRefValue, '3') + + await waitFor(() => { + expect(screen.queryByRole('alert')).toBeInTheDocument() + expect(screen.queryByRole('alert')).toHaveTextContent( + 'The amount should be greater than 3' + ) + }) + }) + }) }) describe('validators given as an array', () => { @@ -4475,6 +4540,71 @@ describe('useFieldProps', () => { }) }) }) + + describe('validateContinuously', () => { + it('should show not show error message initially', async () => { + const onChangeValidator = jest.fn(onChangeValidatorFn) + + render( + + + + + + ) + + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + }) + + it('should hide and show error message while typing', async () => { + const onChangeValidator = jest.fn(onChangeValidatorFn) + + render( + + + + + + ) + + const [inputWithRefValue] = Array.from( + document.querySelectorAll('input') + ) + + // Show error message + fireEvent.submit(document.querySelector('form')) + + await waitFor(() => { + expect(screen.queryByRole('alert')).toBeInTheDocument() + expect(screen.queryByRole('alert')).toHaveTextContent( + 'The amount should be greater than 2' + ) + }) + + await userEvent.type(inputWithRefValue, '{Backspace}') + + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + + await userEvent.type(inputWithRefValue, '3') + + await waitFor(() => { + expect(screen.queryByRole('alert')).toBeInTheDocument() + expect(screen.queryByRole('alert')).toHaveTextContent( + 'The amount should be greater than 3' + ) + }) + }) + }) }) describe('onChangeValidators given as an array', () => { @@ -5410,6 +5540,27 @@ describe('useFieldProps', () => { expect(screen.queryByRole('alert')).not.toBeInTheDocument() }) }) + + describe('validateContinuously', () => { + it('should show not show error message initially', async () => { + const onBlurValidator = jest.fn(onBlurValidatorFn) + + render( + + + + + + ) + + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + }) + }) }) describe('exportValidators', () => { diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts index f40748481d7..bcce809c9b1 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts @@ -128,7 +128,9 @@ export default function useFieldProps( schema, validateInitially, validateUnchanged, + // Deprecated – can be removed in v11 continuousValidation, + validateContinuously = continuousValidation, transformIn = (external: unknown) => external as Value, transformOut = (internal: Value) => internal, toInput = (value: Value) => value, @@ -618,7 +620,7 @@ export default function useFieldProps( if ( localErrorRef.current || validateUnchanged || - continuousValidation + validateContinuously ) { runOnChangeValidator() } @@ -834,7 +836,7 @@ export default function useFieldProps( if ( (validateInitially && !changedRef.current) || validateUnchanged || - continuousValidation || + validateContinuously || runAsync // Because it's a better UX to show the error when the validation is async/delayed ) { // Because we first need to throw the error to be able to display it, we delay the showError call @@ -856,7 +858,7 @@ export default function useFieldProps( } }, [ - continuousValidation, + validateContinuously, defineAsyncProcess, persistErrorState, revealError, @@ -1188,18 +1190,18 @@ export default function useFieldProps( const handleError = useCallback(() => { if ( - continuousValidation || - (continuousValidation !== false && !hasFocusRef.current) + validateContinuously || + (validateContinuously !== false && !hasFocusRef.current) ) { // When there is a change to the value without there having been any focus callback beforehand, it is likely // to believe that the blur callback will not be called either, which would trigger the display of the error. - // The error is therefore displayed immediately (unless instructed not to with continuousValidation set to false). + // The error is therefore displayed immediately (unless instructed not to with validateContinuously set to false). revealError() } else { // When changing the value, hide errors to avoid annoying the user before they are finished filling in that value hideError() } - }, [continuousValidation, hideError, revealError]) + }, [continuousValidation, validateContinuously, hideError, revealError]) const getEventArgs = useCallback( ({ diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts index 89bbcf96a57..7ea7fb5d731 100644 --- a/packages/dnb-eufemia/src/extensions/forms/types.ts +++ b/packages/dnb-eufemia/src/extensions/forms/types.ts @@ -340,7 +340,14 @@ export interface UseFieldProps< /** * Should validation be done while writing, not just when blurring the field? */ + /** + * @deprecated – Replaced with validateContinuously, continuousValidation can be removed in v11. + */ continuousValidation?: boolean + /** + * Should validation be done while writing, not just when blurring the field? + */ + validateContinuously?: boolean /** * Provide custom error messages for the field */ From 21489163a1875da6b4bb9dd2d76663f45bf97e9b Mon Sep 17 00:00:00 2001 From: Snorre Kim Date: Thu, 9 Jan 2025 10:01:34 +0100 Subject: [PATCH 02/61] docs(Form.MainHeading, Form.SubHeading): add `help` property to properties tabs (#4442) Co-authored-by: -l --- .../forms/Form/MainHeading/properties.mdx | 9 ++++---- .../forms/Form/SubHeading/properties.mdx | 9 ++++---- .../forms/Form/MainHeading/MainHeadingDocs.ts | 21 +++++++++++++++++++ .../forms/Form/SubHeading/SubHeadingDocs.ts | 21 +++++++++++++++++++ 4 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/MainHeading/MainHeadingDocs.ts create mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/SubHeading/SubHeadingDocs.ts diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/MainHeading/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/MainHeading/properties.mdx index 3700bb98513..d48093fdd60 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/MainHeading/properties.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/MainHeading/properties.mdx @@ -2,10 +2,9 @@ showTabs: true --- +import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable' +import { MainHeadingProperties } from '@dnb/eufemia/src/extensions/forms/Form/MainHeading/MainHeadingDocs' + ## Properties -| Property | Type | Description | -| --------------------------------------- | ------------ | -------------------------------------------------------------------------------------------- | -| `level` | `number` | _(optional)_ Define a specific level value to ensure correct level hierarchy. Defaults to 2. | -| `children` | `React.Node` | _(optional)_ Heading text / contents. | -| [Space](/uilib/layout/space/properties) | Various | _(optional)_ Spacing properties like `top` or `bottom` are supported. | + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/SubHeading/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/SubHeading/properties.mdx index 706ff25f6bb..c252ae1b0a9 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/SubHeading/properties.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/SubHeading/properties.mdx @@ -2,10 +2,9 @@ showTabs: true --- +import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable' +import { SubHeadingProperties } from '@dnb/eufemia/src/extensions/forms/Form/SubHeading/SubHeadingDocs' + ## Properties -| Property | Type | Description | -| --------------------------------------- | ------------ | -------------------------------------------------------------------------------------------- | -| `level` | `number` | _(optional)_ Define a specific level value to ensure correct level hierarchy. Defaults to 3. | -| `children` | `React.Node` | _(optional)_ Heading text / contents. | -| [Space](/uilib/layout/space/properties) | Various | _(optional)_ Spacing properties like `top` or `bottom` are supported. | + diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/MainHeading/MainHeadingDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Form/MainHeading/MainHeadingDocs.ts new file mode 100644 index 00000000000..fba9b9cdf3c --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Form/MainHeading/MainHeadingDocs.ts @@ -0,0 +1,21 @@ +import { PropertiesTableProps } from '../../../../shared/types' +import { FieldProperties } from '../../Field/FieldDocs' + +export const MainHeadingProperties: PropertiesTableProps = { + level: { + doc: 'Define a specific level value to ensure correct level hierarchy. Defaults to `2`.', + type: 'number', + status: 'optional', + }, + help: FieldProperties.help, + children: { + doc: 'Heading text / contents.', + type: 'React.Node', + status: 'optional', + }, + '[Space](/uilib/layout/space/properties)': { + doc: 'Spacing properties like `top` or `bottom` are supported.', + type: ['string', 'object'], + status: 'optional', + }, +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/SubHeading/SubHeadingDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Form/SubHeading/SubHeadingDocs.ts new file mode 100644 index 00000000000..0d2c1ac5772 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Form/SubHeading/SubHeadingDocs.ts @@ -0,0 +1,21 @@ +import { PropertiesTableProps } from '../../../../shared/types' +import { FieldProperties } from '../../Field/FieldDocs' + +export const SubHeadingProperties: PropertiesTableProps = { + level: { + doc: 'Define a specific level value to ensure correct level hierarchy. Defaults to `3`.', + type: 'number', + status: 'optional', + }, + help: FieldProperties.help, + children: { + doc: 'Heading text / contents.', + type: 'React.Node', + status: 'optional', + }, + '[Space](/uilib/layout/space/properties)': { + doc: 'Spacing properties like `top` or `bottom` are supported.', + type: ['string', 'object'], + status: 'optional', + }, +} From 744781b611473b87acb45d7c2d4cc9fba6270fa3 Mon Sep 17 00:00:00 2001 From: Anders Date: Fri, 10 Jan 2025 11:49:14 +0100 Subject: [PATCH 03/61] fix(Autocomplete): does not fail when clicking show all button (#4445) fixes https://github.com/dnbexperience/eufemia/issues/2101 --- .../dnb-eufemia/src/components/autocomplete/Autocomplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js b/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js index effdb7a8d71..6574534a9fb 100644 --- a/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js +++ b/packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js @@ -1679,7 +1679,7 @@ class AutocompleteInstance extends React.PureComponent { this.setInputValue(inputValue) } - if (typeof args.data.render === 'function') { + if (typeof args.data?.render === 'function') { delete args.data.render } From 2efc62315ee800b1b8b410f2d1c2b3d15494eb6d Mon Sep 17 00:00:00 2001 From: Anders Date: Mon, 13 Jan 2025 10:25:10 +0100 Subject: [PATCH 04/61] fix(InputMasked): should work without any properties (#4446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR aims to make InputMasked work without providing any props. Reason why it should work without providing any props, is because all its props is optional. Defaults to using number mask, as a mask is required. fixes https://github.com/dnbexperience/eufemia/issues/3134 --------- Co-authored-by: Tobias Høegh --- .../components/input-masked/InputMaskedDocs.ts | 2 +- .../components/input-masked/InputMaskedHooks.js | 2 +- .../input-masked/__tests__/InputMasked.test.tsx | 16 ++++++++++++++++ .../input-masked/stories/InputMasked.stories.tsx | 4 ++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/dnb-eufemia/src/components/input-masked/InputMaskedDocs.ts b/packages/dnb-eufemia/src/components/input-masked/InputMaskedDocs.ts index d615ee45b9c..96fefffa1d8 100644 --- a/packages/dnb-eufemia/src/components/input-masked/InputMaskedDocs.ts +++ b/packages/dnb-eufemia/src/components/input-masked/InputMaskedDocs.ts @@ -42,7 +42,7 @@ export const inputMaskedProperties: PropertiesTableProps = { status: 'optional', }, mask: { - doc: 'A mask can be defined both as a [RegExp style of characters](https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#readme) or a callback function. Example below.', + doc: 'A mask can be defined both as a [RegExp style of characters](https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#readme) or a callback function. Example below. Defaults to number mask.', type: ['RegExp', 'function'], status: 'optional', }, diff --git a/packages/dnb-eufemia/src/components/input-masked/InputMaskedHooks.js b/packages/dnb-eufemia/src/components/input-masked/InputMaskedHooks.js index 3722899dc20..f1942fcc214 100644 --- a/packages/dnb-eufemia/src/components/input-masked/InputMaskedHooks.js +++ b/packages/dnb-eufemia/src/components/input-masked/InputMaskedHooks.js @@ -239,7 +239,7 @@ export const useInputElement = () => { inputRef={ref} inputElement={inputElementRef.current} pipe={pipe} - mask={mask || []} + mask={mask || createNumberMask()} showMask={showMask} guide={showGuide} keepCharPositions={keepCharPositions} diff --git a/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx b/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx index 59277eb5e53..82bb2be8cff 100644 --- a/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx +++ b/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx @@ -917,6 +917,22 @@ describe('InputMasked component as_percent', () => { }) }) +describe('InputMasked component without any properties', () => { + it('defaults to number mask', () => { + const newValue = '1' + + render() + + expect(document.querySelector('input').value).toBe('') + + fireEvent.change(document.querySelector('input'), { + target: { value: newValue }, + }) + + expect(document.querySelector('input').value).toBe(newValue) + }) +}) + describe('InputMasked component as_number', () => { it('should create a "number_mask" accordingly the defined properties', () => { const { rerender } = render( diff --git a/packages/dnb-eufemia/src/components/input-masked/stories/InputMasked.stories.tsx b/packages/dnb-eufemia/src/components/input-masked/stories/InputMasked.stories.tsx index dfd562f348f..d14b928f13c 100644 --- a/packages/dnb-eufemia/src/components/input-masked/stories/InputMasked.stories.tsx +++ b/packages/dnb-eufemia/src/components/input-masked/stories/InputMasked.stories.tsx @@ -27,6 +27,10 @@ export function TypeNumber() { return } +export function NoProps() { + return +} + export function Sandbox() { const [locale, setLocale] = React.useState('nb-NO') return ( From d4c586d3a8ea7b442670596b1deb06268ab4e3bd Mon Sep 17 00:00:00 2001 From: Anders Date: Mon, 13 Jan 2025 10:26:30 +0100 Subject: [PATCH 05/61] chore: replace defaultProps in function components (#4447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a few warnings reported here, not all I believe: https://github.com/dnbexperience/eufemia/issues/4145 --------- Co-authored-by: Tobias Høegh --- packages/dnb-design-system-portal/src/html.js | 8 +--- .../src/shared/menu/SidebarMenu.tsx | 4 +- .../src/shared/menu/StickyMenuBar.tsx | 4 -- .../src/shared/tags/Img.js | 26 ++++------- .../src/shared/tags/Intro.tsx | 6 +-- .../src/shared/tags/Tabbar.tsx | 7 +-- .../src/components/button/Button.js | 27 ++++-------- .../dropdown/stories/Dropdown.stories.tsx | 10 +++-- .../src/components/form-row/FormRow.js | 9 +--- .../src/components/form-status/FormStatus.js | 26 ++++------- .../Field/String/__tests__/String.test.tsx | 3 +- .../extensions/payment-card/PaymentCard.js | 18 ++------ .../src/fragments/drawer-list/DrawerList.js | 44 +++++-------------- .../dnb-eufemia/src/shared/AlignmentHelper.js | 4 -- tools/react-live-ssr/dist/index.js | 5 +-- tools/react-live-ssr/dist/index.mjs | 5 +-- 16 files changed, 58 insertions(+), 148 deletions(-) diff --git a/packages/dnb-design-system-portal/src/html.js b/packages/dnb-design-system-portal/src/html.js index 4668923991d..8411b16b940 100644 --- a/packages/dnb-design-system-portal/src/html.js +++ b/packages/dnb-design-system-portal/src/html.js @@ -20,8 +20,8 @@ const mainColor = properties['--color-sea-green'] export default class HTML extends React.PureComponent { render() { const { - htmlAttributes, - bodyAttributes, + htmlAttributes = null, + bodyAttributes = null, headComponents, preBodyComponents, postBodyComponents, @@ -94,7 +94,3 @@ HTML.propTypes = { body: PropTypes.string.isRequired, postBodyComponents: PropTypes.array.isRequired, } -HTML.defaultProps = { - htmlAttributes: null, - bodyAttributes: null, -} diff --git a/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx b/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx index abb8f855952..0650a5648ba 100644 --- a/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx +++ b/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx @@ -30,7 +30,7 @@ import { } from '@dnb/eufemia/src/shared/helpers' import PortalToolsMenu from './PortalToolsMenu' import { navStyle } from './SidebarMenu.module.scss' -import { defaultTabs } from '../tags/Tabbar' +import { defaultTabsValue } from '../tags/Tabbar' const showAlwaysMenuItems = [] // like "uilib" something like that @@ -722,7 +722,7 @@ function checkIfActiveItem( // In addition, because we show the info.mdx without /info // we don't want the "parent" to be marked as active as well. // So we get tabs and check for that state as well - const found = (tabs || defaultTabs).some(({ key }) => { + const found = (tabs || defaultTabsValue).some(({ key }) => { return '/' + lastSlug === key }) diff --git a/packages/dnb-design-system-portal/src/shared/menu/StickyMenuBar.tsx b/packages/dnb-design-system-portal/src/shared/menu/StickyMenuBar.tsx index b01a6d9e1cf..957c7e51df7 100644 --- a/packages/dnb-design-system-portal/src/shared/menu/StickyMenuBar.tsx +++ b/packages/dnb-design-system-portal/src/shared/menu/StickyMenuBar.tsx @@ -137,7 +137,3 @@ StickyMenuBar.propTypes = { hideSidebarToggleButton: PropTypes.bool, preventBarVisibility: PropTypes.bool, } -StickyMenuBar.defaultProps = { - hideSidebarToggleButton: false, - preventBarVisibility: false, -} diff --git a/packages/dnb-design-system-portal/src/shared/tags/Img.js b/packages/dnb-design-system-portal/src/shared/tags/Img.js index 7d43304e9ae..9458e9042de 100644 --- a/packages/dnb-design-system-portal/src/shared/tags/Img.js +++ b/packages/dnb-design-system-portal/src/shared/tags/Img.js @@ -4,14 +4,14 @@ import classnames from 'classnames' import { Img as Image } from '@dnb/eufemia/src' const Img = ({ - className, - alt, - src, - children, - size, - width, - height, - caption, + className = null, + alt = null, + src = null, + children = null, + size = null, + width = null, + height = null, + caption = null, ...rest }) => { if (size === 'auto') { @@ -47,15 +47,5 @@ Img.propTypes = { width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), caption: PropTypes.string, } -Img.defaultProps = { - className: null, - caption: null, - alt: null, - src: null, - size: null, - height: null, - width: null, - children: null, -} export default Img diff --git a/packages/dnb-design-system-portal/src/shared/tags/Intro.tsx b/packages/dnb-design-system-portal/src/shared/tags/Intro.tsx index f1d8c7e1d21..326868821c5 100644 --- a/packages/dnb-design-system-portal/src/shared/tags/Intro.tsx +++ b/packages/dnb-design-system-portal/src/shared/tags/Intro.tsx @@ -13,7 +13,7 @@ import { startPageTransition } from './Transition' import { Link } from './Anchor' const ref = React.createRef() -const Intro = ({ children }) => { +const Intro = ({ children = undefined }) => { React.useEffect(() => { const onKeyDownHandler = (e) => { if (/textarea|input/i.test(document.activeElement.tagName)) { @@ -53,9 +53,8 @@ const Intro = ({ children }) => { Intro.propTypes = { children: PropTypes.node.isRequired, } -Intro.defaultProps = {} -export const IntroFooter = ({ href, text }) => ( +export const IntroFooter = ({ href = undefined, text = undefined }) => ( ( <> diff --git a/packages/dnb-design-system-portal/src/shared/tags/Tabbar.tsx b/packages/dnb-design-system-portal/src/shared/tags/Tabbar.tsx index c586a0f027d..7fe726d49f2 100644 --- a/packages/dnb-design-system-portal/src/shared/tags/Tabbar.tsx +++ b/packages/dnb-design-system-portal/src/shared/tags/Tabbar.tsx @@ -10,7 +10,7 @@ import AutoLinkHeader from './AutoLinkHeader' import { tabsWrapperStyle } from './Tabbar.module.scss' import { Link } from './Anchor' -export const defaultTabs = [ +export const defaultTabsValue = [ { title: 'Info', key: '/info' }, { title: 'Demos', key: '/demos' }, { title: 'Properties', key: '/properties' }, @@ -34,7 +34,7 @@ export default function Tabbar({ hideTabs, rootPath, tabs, - defaultTabs, + defaultTabs = defaultTabsValue, children, }: TabbarProps) { const [wasFullscreen, setFullscreen] = React.useState( @@ -150,9 +150,6 @@ export default function Tabbar({ ) } -Tabbar.defaultProps = { - defaultTabs, -} Tabbar.ContentWrapper = (props) => ( ) diff --git a/packages/dnb-eufemia/src/components/button/Button.js b/packages/dnb-eufemia/src/components/button/Button.js index 90708f0be6e..6ede0bf6be2 100644 --- a/packages/dnb-eufemia/src/components/button/Button.js +++ b/packages/dnb-eufemia/src/components/button/Button.js @@ -393,14 +393,14 @@ Button.defaultProps = { } function Content({ - title, - content, - custom_content, - icon, - icon_size, - bounding, - skeleton, - isIconOnly, + title = null, + content = null, + custom_content = null, + icon = null, + icon_size = 'default', + bounding = null, + skeleton = null, + isIconOnly = null, }) { return ( <> @@ -481,16 +481,5 @@ Content.propTypes = { isIconOnly: PropTypes.bool, } -Content.defaultProps = { - custom_content: null, - title: null, - content: null, - icon: null, - icon_size: 'default', - bounding: null, - skeleton: null, - isIconOnly: null, -} - Button._formElement = true Button._supportsSpacingProps = true diff --git a/packages/dnb-eufemia/src/components/dropdown/stories/Dropdown.stories.tsx b/packages/dnb-eufemia/src/components/dropdown/stories/Dropdown.stories.tsx index e6640c42534..e614a201ba4 100644 --- a/packages/dnb-eufemia/src/components/dropdown/stories/Dropdown.stories.tsx +++ b/packages/dnb-eufemia/src/components/dropdown/stories/Dropdown.stories.tsx @@ -727,7 +727,12 @@ const dropdownDataScrollable = [ const Flag = () => <>COUNTRY FLAG // These <> are Fragments, like React.Fragment // This component populates the dropdown and handles the reset if, and only if, the value is undefined -function CurrencySelector({ currencies, onChange, value, ...props }) { +function CurrencySelector({ + currencies, + onChange, + value = null, + ...props +}) { let itemIndex = currencies.indexOf(value) itemIndex = itemIndex > -1 ? itemIndex : null return ( @@ -759,9 +764,6 @@ CurrencySelector.propTypes = { currencies: PropTypes.array.isRequired, onChange: PropTypes.func.isRequired, } -CurrencySelector.defaultProps = { - value: null, -} function DropdownStatesSync() { const [state, setState] = React.useState({}) diff --git a/packages/dnb-eufemia/src/components/form-row/FormRow.js b/packages/dnb-eufemia/src/components/form-row/FormRow.js index 9292f714a3a..bf84d233159 100644 --- a/packages/dnb-eufemia/src/components/form-row/FormRow.js +++ b/packages/dnb-eufemia/src/components/form-row/FormRow.js @@ -287,8 +287,8 @@ export default class FormRow extends React.PureComponent { } const Fieldset = ({ - useFieldset, - children, + useFieldset = false, + children = null, className = null, ...props }) => { @@ -318,10 +318,5 @@ Fieldset.propTypes = { useFieldset: PropTypes.bool, className: PropTypes.string, } -Fieldset.defaultProps = { - children: null, - useFieldset: false, - className: null, -} FormRow._supportsSpacingProps = true diff --git a/packages/dnb-eufemia/src/components/form-status/FormStatus.js b/packages/dnb-eufemia/src/components/form-status/FormStatus.js index 5a2e9fc5a50..c2dd61e51a6 100644 --- a/packages/dnb-eufemia/src/components/form-status/FormStatus.js +++ b/packages/dnb-eufemia/src/components/form-status/FormStatus.js @@ -451,6 +451,7 @@ export default class FormStatus extends React.PureComponent { } export const ErrorIcon = (props) => { + const { title = 'error' } = props || {} const isSbankenTheme = useTheme()?.name === 'sbanken' const fill = isSbankenTheme ? properties.sbanken['--sb-color-magenta'] @@ -461,7 +462,7 @@ export const ErrorIcon = (props) => { return ( - {props && props.title && {props.title}} + {title} { ErrorIcon.propTypes = { title: PropTypes.string, } -ErrorIcon.defaultProps = { - title: 'error', -} export const WarnIcon = (props) => { + const { title = 'error' } = props || {} const isSbankenTheme = useTheme()?.name === 'sbanken' const fill = isSbankenTheme ? properties.sbanken['--sb-color-yellow-dark'] @@ -498,7 +497,7 @@ export const WarnIcon = (props) => { return ( - {props && props.title && {props.title}} + {title} { WarnIcon.propTypes = { title: PropTypes.string, } -WarnIcon.defaultProps = { - title: 'error', -} export const InfoIcon = (props) => { + const { title = 'info' } = props || {} const isSbankenTheme = useTheme()?.name === 'sbanken' let fill = isSbankenTheme ? properties.sbanken['--sb-color-violet-light'] @@ -541,7 +538,7 @@ export const InfoIcon = (props) => { return ( - {props && props.title && {props.title}} + {title} { } InfoIcon.propTypes = { title: PropTypes.string, - state: PropTypes.string, -} -InfoIcon.defaultProps = { - title: 'info', - state: 'info', } export const MarketingIcon = (props) => { + const { title = 'marketing' } = props || {} const isSbankenTheme = useTheme()?.name === 'sbanken' const fill = isSbankenTheme ? properties.sbanken['--sb-color-violet-light'] @@ -582,7 +575,7 @@ export const MarketingIcon = (props) => { xmlns="http://www.w3.org/2000/svg" {...props} > - {props && props.title && {props.title}} + {title} { MarketingIcon.propTypes = { title: PropTypes.string, } -MarketingIcon.defaultProps = { - title: 'marketing', -} export function setMaxWidthToElement({ element, diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/String/__tests__/String.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/String/__tests__/String.test.tsx index 509a660a2b5..be1fb800f3c 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/String/__tests__/String.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/String/__tests__/String.test.tsx @@ -698,7 +698,8 @@ describe('Field.String', () => { '.dnb-forms-submit-indicator' ) - await userEvent.type(input, 'foo') + // Use fireEvent over userEvent to avoid async related delays + fireEvent.change(input, { target: { value: 'foo' } }) await waitFor(() => { expect(input).not.toBeDisabled() diff --git a/packages/dnb-eufemia/src/extensions/payment-card/PaymentCard.js b/packages/dnb-eufemia/src/extensions/payment-card/PaymentCard.js index ed4b41701b9..db0e93206eb 100644 --- a/packages/dnb-eufemia/src/extensions/payment-card/PaymentCard.js +++ b/packages/dnb-eufemia/src/extensions/payment-card/PaymentCard.js @@ -221,10 +221,6 @@ const defaultCard = (productCode) => ({ StatusOverlay.propTypes = { cardStatus: PropTypes.string.isRequired, translations: PropTypes.object.isRequired, - skeleton: PropTypes.bool, -} -StatusOverlay.defaultProps = { - skeleton: false, } const BlockingOverlay = ({ cardStatus, text }, skeleton) => { @@ -295,17 +291,13 @@ NormalCard.propTypes = { cardNumber: PropTypes.string.isRequired, translations: PropTypes.object.isRequired, } -NormalCard.defaultProps = { - id: null, - skeleton: null, -} function NormalCard({ data, cardStatus, cardNumber, - id, - skeleton, + id = null, + skeleton = null, translations, }) { return ( @@ -348,11 +340,7 @@ function NormalCard({ /> - + ) } diff --git a/packages/dnb-eufemia/src/fragments/drawer-list/DrawerList.js b/packages/dnb-eufemia/src/fragments/drawer-list/DrawerList.js index 71d46ee73fe..5a82d2a63be 100644 --- a/packages/dnb-eufemia/src/fragments/drawer-list/DrawerList.js +++ b/packages/dnb-eufemia/src/fragments/drawer-list/DrawerList.js @@ -409,10 +409,10 @@ DrawerList.Options = React.memo( React.forwardRef((props, ref) => { const { children, - className, + className = null, triangleRef = null, - cache_hash, // eslint-disable-line - showFocusRing, + cache_hash = null, // eslint-disable-line + showFocusRing = false, ...rest } = props @@ -451,24 +451,18 @@ DrawerList.Options.propTypes = { className: PropTypes.string, triangleRef: PropTypes.object, } -DrawerList.Options.defaultProps = { - cache_hash: null, - showFocusRing: false, - className: null, - triangleRef: null, -} // DrawerList Item DrawerList.Item = React.forwardRef((props, ref) => { const { - role, // eslint-disable-line - hash, // eslint-disable-line + role = 'option', // eslint-disable-line + hash = '', // eslint-disable-line children, // eslint-disable-line - className, // eslint-disable-line - on_click, // eslint-disable-line + className = null, // eslint-disable-line + on_click = null, // eslint-disable-line selected, // eslint-disable-line - active, // eslint-disable-line - value, // eslint-disable-line + active = null, // eslint-disable-line + value = null, // eslint-disable-line disabled, // eslint-disable-line ...rest } = props @@ -529,17 +523,8 @@ DrawerList.Item.propTypes = { value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), disabled: PropTypes.bool, } -DrawerList.Item.defaultProps = { - role: 'option', - hash: '', - className: null, - on_click: null, - selected: null, - active: null, - value: null, -} -export function ItemContent({ hash = '', children }) { +export function ItemContent({ hash = '', children = undefined }) { let content = null if (Array.isArray(children.content || children)) { @@ -582,12 +567,8 @@ ItemContent.propTypes = { hash: PropTypes.string, children: PropTypes.oneOfType([PropTypes.node, PropTypes.object]), } -ItemContent.defaultProps = { - hash: '', - children: undefined, -} -DrawerList.HorizontalItem = ({ className, ...props }) => ( +DrawerList.HorizontalItem = ({ className = null, ...props }) => ( ( DrawerList.HorizontalItem.propTypes = { className: PropTypes.string, } -DrawerList.HorizontalItem.defaultProps = { - className: null, -} class OnMounted extends React.PureComponent { static propTypes = { diff --git a/packages/dnb-eufemia/src/shared/AlignmentHelper.js b/packages/dnb-eufemia/src/shared/AlignmentHelper.js index edfd2a36763..8bfedfea801 100644 --- a/packages/dnb-eufemia/src/shared/AlignmentHelper.js +++ b/packages/dnb-eufemia/src/shared/AlignmentHelper.js @@ -29,7 +29,3 @@ AlignmentHelper.propTypes = { children: PropTypes.node, className: PropTypes.string, } -AlignmentHelper.defaultProps = { - children: null, - className: null, -} diff --git a/tools/react-live-ssr/dist/index.js b/tools/react-live-ssr/dist/index.js index 23323a95e00..5f460b834f0 100644 --- a/tools/react-live-ssr/dist/index.js +++ b/tools/react-live-ssr/dist/index.js @@ -101,7 +101,7 @@ var CodeEditor = (props) => { code: origCode, className, style, - tabMode, + tabMode="indentation", theme: origTheme, prism, language, @@ -174,9 +174,6 @@ var CodeEditor = (props) => { } ) }); }; -CodeEditor.defaultProps = { - tabMode: "indentation" -}; var Editor_default = CodeEditor; // src/components/Live/LiveProvider.tsx diff --git a/tools/react-live-ssr/dist/index.mjs b/tools/react-live-ssr/dist/index.mjs index 4cf841fc3db..e5f9e1cdaa7 100644 --- a/tools/react-live-ssr/dist/index.mjs +++ b/tools/react-live-ssr/dist/index.mjs @@ -60,7 +60,7 @@ var CodeEditor = (props) => { code: origCode, className, style, - tabMode, + tabMode="indentation", theme: origTheme, prism, language, @@ -133,9 +133,6 @@ var CodeEditor = (props) => { } ) }); }; -CodeEditor.defaultProps = { - tabMode: "indentation" -}; var Editor_default = CodeEditor; // src/components/Live/LiveProvider.tsx From ad13e235d119fc28b693d8eb702a2203cbdbf375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Mon, 13 Jan 2025 14:20:47 +0100 Subject: [PATCH 06/61] feat(Forms): add support for conditional function based `info`, `warning` and `error` props to all `Field.*` components (#4421) The PR updates the `info`, `warning`, and `error` props to accept functions. This makes it easier to create conditional messages without relying on making a field "controlled." Additionally, it ensures the same user experience for displaying these messages (by using the `conditionally` function), consistent with how error messages are handled by default today. ```tsx { return conditionally(() => 'Show this message, with this value ' + getValueByPath('/otherField')) }} /> ``` Here's an [example](https://eufemia-git-feat-forms-conditional-infos-eufemia.vercel.app/uilib/extensions/forms/base-fields/Number/#displaying-messages---conditional-info-message). --------- Co-authored-by: Anders --- .../forms/base-fields/Number/Examples.tsx | 97 +++++- .../forms/base-fields/Number/demos.mdx | 50 +++ .../extensions/forms/getting-started.mdx | 66 ++++ .../extensions/forms/DataContext/Context.ts | 22 +- .../forms/DataContext/Provider/Provider.tsx | 82 ++--- .../Field/ArraySelection/ArraySelection.tsx | 4 +- .../Indeterminate/useDependencePaths.tsx | 11 +- .../Field/Number/__tests__/Number.test.tsx | 304 +++++++++++++++++- .../Field/Number/stories/Number.stories.tsx | 93 +++++- .../forms/FieldBlock/FieldBlock.tsx | 18 +- .../extensions/forms/Tools/GenerateSchema.tsx | 16 +- .../extensions/forms/Tools/ListAllProps.tsx | 12 +- .../Value/ArraySelection/ArraySelection.tsx | 8 +- .../forms/Value/Selection/Selection.tsx | 8 +- .../Wizard/Container/WizardContainer.tsx | 5 +- .../forms/hooks/DataValueWritePropsDocs.ts | 12 +- .../extensions/forms/hooks/useFieldProps.ts | 194 ++++++++--- .../extensions/forms/hooks/useValueProps.ts | 10 +- .../dnb-eufemia/src/extensions/forms/types.ts | 31 +- 19 files changed, 898 insertions(+), 145 deletions(-) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Number/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Number/Examples.tsx index f52023fe382..8e44564cf26 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Number/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Number/Examples.tsx @@ -1,7 +1,7 @@ -import ComponentBox from '../../../../../../shared/tags/ComponentBox' -import { Slider, Grid, Flex } from '@dnb/eufemia/src' -import { Field, Form } from '@dnb/eufemia/src/extensions/forms' import React from 'react' +import ComponentBox from '../../../../../../shared/tags/ComponentBox' +import { Slider, Grid, Flex, Anchor } from '@dnb/eufemia/src' +import { Field, Form, FormError } from '@dnb/eufemia/src/extensions/forms' export const Placeholder = () => { return ( @@ -421,3 +421,94 @@ export const WithSlider = () => ( }} ) + +export const ConditionalInfo = () => { + return ( + + {() => { + return ( + { + console.log('onSubmit', data) + }} + > + + +
+ Defines the maximum amount possible to be entered. + + } + path="/maximum" + required + info={( + maximum, + { conditionally, getValueByPath, getFieldByPath }, + ) => { + return conditionally(() => { + if (maximum < getValueByPath('/amount')) { + const { props, id } = getFieldByPath('/amount') + const anchor = props?.label && ( + { + event.preventDefault() + const el = document.getElementById( + id + '-label', + ) + el?.scrollIntoView() + }} + > + {props.label} + + ) + + return ( + anchor && ( + <> + Remember to adjust the {anchor} to be {maximum}{' '} + or lower. + + ) + ) + } + }) + }} + /> + +
+ Should be same or lower than maximum. + + } + path="/amount" + required + onBlurValidator={(amount: number, { connectWithPath }) => { + const maximum = connectWithPath('/maximum').getValue() + + if (amount > maximum) { + return new FormError('NumberField.errorMaximum', { + messageValues: { + maximum: String(maximum), + }, + }) + } + }} + /> +
+ + +
+ ) + }} +
+ ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Number/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Number/demos.mdx index b1f88130e3d..e05edb54b84 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Number/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/Number/demos.mdx @@ -78,6 +78,56 @@ You can also use a function as a prefix or suffix. +### Displaying messages - Conditional info message + +You can provide a function to the `info`, `warning` or `error` props that returns a message based on your conditions. + +```tsx + { + if (value === '123') { + return 'The value is 123' + } + }} +/> +``` + +Optionally, use the `conditionally` higher order function to show the message only when the field got changed (onChange) and blurred (onBlur). + +```tsx + { + if (value === '123') { + // Show this message only when the field got changed and blurred. + return conditionally(() => 'The value is 123') + } + }} +/> +``` + +You can also pass options to the `conditionally` function: + +- `showInitially` – display the message when the field is first rendered. + +```tsx + { + if (value === '123') { + // Show this message only when the field got changed and blurred. + return conditionally(() => 'The value is 123', { + showInitially: true, + }) + } + }} +/> +``` + +Down below you can see an example of how to use the `conditionally` function. There are two input fields which depend on each other. Here we use `info` to show a message when the value of the first field is too low. While we use an error message when the value of the second field is more than what the first field has. The `info` on the first field will only be shown when the user has changed the value and blurred the field. + +Read more about [validation and the user experience](/uilib/extensions/forms/getting-started/#validation-and-the-user-experience-ux). + + + ### Percentage diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx index dc725a9f231..059a6bedab2 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx @@ -39,6 +39,8 @@ import AsyncChangeExample from './Form/Handler/parts/async-change-example.mdx' - [onChange and autosave](#onchange-and-autosave) - [Async field validation](#async-field-validation) - [Validation and error handling](#validation-and-error-handling) + - [Validation and the user experience (UX)](#validation-and-the-user-experience-ux) + - [Validation order](#validation-order) - [Error messages](#error-messages) - [Summary for errors](#summary-for-errors) - [required](#required) @@ -48,6 +50,8 @@ import AsyncChangeExample from './Form/Handler/parts/async-change-example.mdx' - [Connect with another field](#connect-with-another-field) - [Async validation](#async-validation) - [Async validator with debounce](#async-validator-with-debounce) + - [error](#error) + - [Conditionally display messages](#conditionally-display-messages) - [Localization and translation](#localization-and-translation) - [Customize translations](#customize-translations) - [How to customize translations in a form](#how-to-customize-translations-in-a-form) @@ -440,6 +444,30 @@ Fields which have the `disabled` property or the `readOnly` property, will skip For monitoring and setting your form errors, you can use the [useValidation](/uilib/extensions/forms/Form/useValidation) hook. +#### Validation and the user experience (UX) + +Eufemia Forms provides a built-in validation system that is based on the type of data it handles. This validation is automatically applied to the field when the user interacts with it. The validation is also applied when the user submits the form. + +In general, the validation is based on the following principles: + +- Trust the user and assume they are doing their best. +- Avoid interrupting the user while they are entering data. +- Provide clear and concise error messages, ending with a period. + +The validation system is flexible and can be customized to fit your specific needs. You can add your own validation to a field component by using the `required`, `pattern`, `schema`, `onChangeValidator` or `onBlurValidation` property. + +#### Validation order + +If there are multiple errors, they will be shown in a specific order, and only the first error in that order will appear. + +The validation order is as follows: + +1. `required` +2. `error` (with `conditionally`) +3. `schema` (including `pattern`) +4. `onChangeValidator` +5. `onBlurValidator` + #### Error messages Eufemia Forms comes with built-in error messages. But you can also customize and override these messages by using the `errorMessages` property both on [fields](/uilib/extensions/forms/all-fields/) (field level) and on the [Form.Handler](/uilib/extensions/forms/Form/Handler/) (global level). @@ -617,6 +645,44 @@ const onChangeValidator = debounceAsync(async function myValidator(value) { render() ``` +#### error + +The `error` property is a controlled error message that is always displayed by default. However, you can provide a function to conditionally change this behavior based on specific requirements. + +Using this `conditional` function, the error message will only be shown after the user interacts with the field — for instance, after they change its value and move focus away (blur). This allows you to display error messages dynamically, ensuring they appear only when the field's value is invalid and the user has engaged with it. + +```tsx +render( + { + if (value === 123) { + return conditionally(() => new Error('Invalid value')) + } + }} + />, +) +``` + +### Conditionally display messages + +You can provide a function to the `info`, `warning` and `error` properties. + +```tsx +render( + { + if (value === 123) { + return conditionally(() => <>Invalid value) + } + }} + />, +) +``` + +**NB:** When using `error` and returning a React.Node, the error message will be shown, but it will not trigger the error state of the form. So the form can be submitted, even if the field looks invalid. + +More info about it in the [Field.Number](/uilib/extensions/forms/base-fields/Number/) component. + ### Localization and translation You can set the locale for your form by using the `locale` property on the [Form.Handler](/uilib/extensions/forms/Form/Handler/) component. This will ensure that the correct language is used for all the fields in your form. diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts b/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts index edfe34510f1..55a73e14c28 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts @@ -83,6 +83,20 @@ export type HandleSubmitCallback = ({ export type FieldConnections = { setEventResult?: (status: EventStateObject) => void } +export type FieldInternalsRefProps = + | (FieldProps & { children: unknown }) + | undefined +export type FieldInternalsRef = Record< + Path, + { + props: FieldInternalsRefProps + id: string | undefined + } +> +export type ValueInternalsRef = Record< + Path, + { props: ValueProps | undefined } +> export interface ContextState { id?: SharedStateId @@ -150,8 +164,8 @@ export interface ContextState { callback: EventListenerCall['callback'] ) => void setVisibleError?: (path: Path, hasError: boolean) => void - setFieldProps?: (path: Path, props: unknown) => void - setValueProps?: (path: Path, props: unknown) => void + setFieldInternals?: (path: Path, props: unknown, id?: string) => void + setValueInternals?: (path: Path, props: unknown) => void setHandleSubmit?: ( callback: HandleSubmitCallback, params?: { remove?: boolean } @@ -161,8 +175,8 @@ export interface ContextState { fieldDisplayValueRef?: React.MutableRefObject< Record > - fieldPropsRef?: React.MutableRefObject> - valuePropsRef?: React.MutableRefObject> + fieldInternalsRef?: React.MutableRefObject + valueInternalsRef?: React.MutableRefObject fieldConnectionsRef?: React.RefObject> mountedFieldsRef?: React.MutableRefObject> snapshotsRef?: React.MutableRefObject< diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx index f1b4f27a8e2..8cee9d2a18e 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx @@ -17,7 +17,6 @@ import { import { GlobalErrorMessagesWithPaths, AllJSONSchemaVersions, - FieldProps, SubmitState, Path, EventStateObject, @@ -43,12 +42,15 @@ import useTranslation from '../../hooks/useTranslation' import DataContext, { ContextState, EventListenerCall, + FieldInternalsRef, + ValueInternalsRef, FilterData, FilterDataHandler, HandleSubmitCallback, MountState, TransformData, VisibleDataHandler, + FieldInternalsRefProps, } from '../Context' /** @@ -434,26 +436,28 @@ export default function Provider( } if (typeof handler === 'function') { - Object.entries(fieldPropsRef.current).forEach(([path, props]) => { - const exists = pointer.has(data, path) - if (exists) { - const value = pointer.get(data, path) - const displayValue = fieldDisplayValueRef.current[path] - const internal = { - error: fieldErrorRef.current?.[path], + Object.entries(fieldInternalsRef.current).forEach( + ([path, { props }]) => { + const exists = pointer.has(data, path) + if (exists) { + const value = pointer.get(data, path) + const displayValue = fieldDisplayValueRef.current[path] + const internal = { + error: fieldErrorRef.current?.[path], + } + const result = handler({ + path, + value, + displayValue, + label: props.label, + data: internalDataRef.current, + props, + internal, + }) + mutate(path, result) } - const result = handler({ - path, - value, - displayValue, - label: props.label, - data: internalDataRef.current, - props, - internal, - }) - mutate(path, result) } - }) + ) return data } else if (handler) { @@ -461,7 +465,7 @@ export default function Provider( const exists = pointer.has(data, path) if (exists) { const value = pointer.get(data, path) - const props = fieldPropsRef.current[path] + const props = fieldInternalsRef.current[path]?.props const internal = { error: fieldErrorRef.current?.[path] } const result = typeof condition === 'function' @@ -587,29 +591,35 @@ export default function Provider( [] ) - const fieldPropsRef = useRef>({}) - const setFieldProps = useCallback( - (path: Path, props: Record) => { - fieldPropsRef.current[path] = props + const fieldInternalsRef = useRef({}) + const setFieldInternals = useCallback( + (path: Path, props: FieldInternalsRefProps, id: string) => { + fieldInternalsRef.current[path] = Object.assign( + fieldInternalsRef.current[path] || {}, + { props, id } + ) }, [] ) - const valuePropsRef = useRef>({}) - const setValueProps = useCallback( - (path: Path, props: Record) => { - valuePropsRef.current[path] = props + const valueInternalsRef = useRef({}) + const setValueInternals = useCallback( + (path: Path, props: ValueProps) => { + valueInternalsRef.current[path] = Object.assign( + valueInternalsRef.current[path] || {}, + { props } + ) }, [] ) const hasFieldWithAsyncValidator = useCallback(() => { - for (const path in fieldPropsRef.current) { + for (const path in fieldInternalsRef.current) { if (mountedFieldsRef.current[path]?.isMounted) { - const props = fieldPropsRef.current[path] + const props = fieldInternalsRef.current[path]?.props if ( - isAsync(props.onChangeValidator) || - isAsync(props.onBlurValidator) + isAsync(props?.onChangeValidator) || + isAsync(props?.onBlurValidator) ) { return true } @@ -1331,8 +1341,8 @@ export default function Provider( setFieldState, setFieldError, setFieldConnection, - setFieldProps, - setValueProps, + setFieldInternals, + setValueInternals, hasErrors, hasFieldError, hasFieldState, @@ -1361,8 +1371,8 @@ export default function Provider( hasVisibleError: Object.keys(hasVisibleErrorRef.current).length > 0, fieldConnectionsRef, fieldDisplayValueRef, - fieldPropsRef, - valuePropsRef, + fieldInternalsRef, + valueInternalsRef, mountedFieldsRef, snapshotsRef, existingFieldsRef, 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 0a2978b9c80..c09471877bb 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx @@ -161,7 +161,7 @@ export function useCheckboxOrToggleOptions({ handleChange?: ReturnAdditional['handleChange'] handleActiveData?: (item: { labels: Array }) => void }) { - const { setFieldProps } = useContext(DataContext) + const { setFieldInternals } = useContext(DataContext) const optionsCount = useMemo( () => React.Children.count(children) + (dataList?.length || 0), [dataList, children] @@ -261,7 +261,7 @@ export function useCheckboxOrToggleOptions({ } if (path) { - setFieldProps?.(path + '/arraySelectionData', activeData) + setFieldInternals?.(path + '/arraySelectionData', activeData) } return result diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Indeterminate/useDependencePaths.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Indeterminate/useDependencePaths.tsx index 8eef025d62d..98519469044 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Indeterminate/useDependencePaths.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Indeterminate/useDependencePaths.tsx @@ -7,7 +7,7 @@ export default function useDependencePaths( dependencePaths: Props['dependencePaths'], propagateIndeterminateState: Props['propagateIndeterminateState'] ) { - const { data, fieldPropsRef, handlePathChange } = + const { data, fieldInternalsRef, handlePathChange } = useContext(DataContext) || {} const { allOn, allOff, indeterminate } = useMemo(() => { @@ -22,7 +22,7 @@ export default function useDependencePaths( if ( // When value is undefined, we should also consider it as off (whenUndefined ? typeof value === 'undefined' : false) || - value === fieldPropsRef?.current?.[path]?.[key] + value === fieldInternalsRef?.current?.[path]?.props?.[key] ) { return true } @@ -39,7 +39,7 @@ export default function useDependencePaths( allOff, indeterminate, } - }, [data, dependencePaths, fieldPropsRef]) + }, [data, dependencePaths, fieldInternalsRef]) const keepStateRef = useRef() useMemo(() => { @@ -58,11 +58,12 @@ export default function useDependencePaths( (checked: boolean) => { dependencePaths?.forEach((path) => { const fieldProp = checked ? 'valueOn' : 'valueOff' - const value = fieldPropsRef?.current?.[path]?.[fieldProp] + const value = + fieldInternalsRef?.current?.[path]?.props?.[fieldProp] handlePathChange?.(path, value) }) }, - [dependencePaths, fieldPropsRef, handlePathChange] + [dependencePaths, fieldInternalsRef, handlePathChange] ) return { diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Number/__tests__/Number.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Number/__tests__/Number.test.tsx index 4cbb1b16de4..706d9135094 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Number/__tests__/Number.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Number/__tests__/Number.test.tsx @@ -143,11 +143,305 @@ describe('Field.Number', () => { ) }) - it('renders error', () => { - render() - expect( - screen.getByText('This is what went wrong') - ).toBeInTheDocument() + describe('error', () => { + it('renders error', () => { + render( + + ) + expect( + screen.getByText('This is what went wrong') + ).toBeInTheDocument() + }) + + it('renders error given as a function', () => { + render( + new Error('This is what went wrong')} + /> + ) + expect( + screen.getByText('This is what went wrong') + ).toBeInTheDocument() + }) + + it('renders error given as a function with value', () => { + render( + + new Error('This is what went wrong ' + value) + } + value={123} + /> + ) + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 123') + }) + + describe('conditionally', () => { + it('renders message when field gets blurred', async () => { + render( + { + return conditionally(() => { + return new Error('This is what went wrong ' + value) + }) + }} + /> + ) + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + + await userEvent.type(document.querySelector('input'), '123') + await userEvent.tab() + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 123') + + await userEvent.type(document.querySelector('input'), '4') + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 1234') + }) + + it('renders message conditionally on every value change', async () => { + render( + { + if (value === 123) { + return undefined + } + + return new Error('This is what went wrong ' + value) + }} + /> + ) + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 0') + + await userEvent.type(document.querySelector('input'), '12') + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 12') + + await userEvent.type(document.querySelector('input'), '3') + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + + await userEvent.type(document.querySelector('input'), '4') + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 1234') + }) + + it('showInitially: renders message initially', async () => { + render( + { + return conditionally( + () => { + if (value === 123) { + return undefined + } + + return new Error('This is what went wrong ' + value) + }, + { showInitially: true } + ) + }} + /> + ) + + const input = document.querySelector('input') + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 0') + + await userEvent.type(input, '1') + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 1') + + await userEvent.type(input, '2') + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 12') + + await userEvent.type(input, '3') + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + + await userEvent.type(input, '4') + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + + await userEvent.tab() + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 1234') + + await userEvent.type(input, '{Backspace}') + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + + await userEvent.type(input, '4') + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + + await userEvent.type(input, '5') + await userEvent.tab() + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 12345') + }) + }) + }) + + describe('warning', () => { + it('renders warning', () => { + render() + expect( + screen.getByText('This is what went wrong') + ).toBeInTheDocument() + }) + + it('renders warning given as a function', () => { + render( + 'This is what went wrong ' + value} + value={123} + /> + ) + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 123') + }) + + describe('getValueByPath', () => { + it('renders message with value from other path', async () => { + render( + + { + return String(value) + getValueByPath('/bar') + }} + /> + + ) + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('123456') + + await userEvent.type(document.querySelector('input'), '0') + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('1230456') + }) + }) + }) + + describe('info', () => { + it('renders info', () => { + render() + expect( + screen.getByText('This is what went wrong') + ).toBeInTheDocument() + }) + + it('renders info given as a function', () => { + render( + 'This is what went wrong ' + value} + value={123} + /> + ) + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent('This is what went wrong 123') + }) + + it('renders summarized messages given by an array from a function return', async () => { + render( + { + return ['Foo', 'Bar'] + }} + /> + ) + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent(nb.Field.stateSummary + 'FooBar') + }) + + describe('getFieldByPath', () => { + it('renders message with value from other path', async () => { + render( + + { + const field = getFieldByPath('/bar') + const props = field.props + const id = field.id + + if (props) { + const label = props.label + return JSON.stringify({ value, id, label }) + } + }} + id="foo" + /> + + + + ) + + expect( + document.querySelector('.dnb-form-status') + ).toHaveTextContent( + JSON.stringify({ + value: 123, + id: 'bar-id', + label: 'Bar Label', + }) + ) + }) + }) }) it('shows error border', () => { diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Number/stories/Number.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Number/stories/Number.stories.tsx index 59fc3ce41e9..e38a3c46170 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Number/stories/Number.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Number/stories/Number.stories.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import { Field, Form, UseFieldProps } from '../../..' -import { Flex } from '../../../../../components' +import { Field, Form, FormError, UseFieldProps } from '../../..' +import { Anchor, Flex } from '../../../../../components' export default { title: 'Eufemia/Extensions/Forms/Number', @@ -84,3 +84,92 @@ export const WithFreshValidator = () => { ) } + +export const ConditionalInfo = () => { + const conditionalInfo: UseFieldProps['info'] = ( + maximum: number, + { conditionally, getValueByPath, getFieldByPath } + ) => { + return conditionally( + () => { + if (maximum < getValueByPath('/amount')) { + const { props, id } = getFieldByPath('/amount') + + const anchor = props && ( + ) => { + event.preventDefault() + const el = document.getElementById(`${id}-label`) + el?.scrollIntoView() + }} + > + {props?.label} + + ) + + return ( + anchor && ( + <> + Remember to adjust the {anchor} to be {maximum} or lower. + + ) + ) + } + }, + { + showInitially: true, + } + ) + } + + const onBlurValidator: UseFieldProps['onBlurValidator'] = ( + amount: number, + { connectWithPath } + ) => { + const maximum = connectWithPath('/maximum').getValue() + + if (amount > maximum) { + return new FormError('NumberField.errorMaximum', { + messageValues: { maximum: String(maximum) }, + }) + } + } + + return ( + + + + + + + + + ) +} diff --git a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlock.tsx b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlock.tsx index 932b73119dd..33a06817ec8 100644 --- a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlock.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlock.tsx @@ -179,7 +179,7 @@ function FieldBlock(props: Props) { const { index: iterateIndex } = iterateItemContext ?? {} const blockId = useId(props.id) - const [wasUpdated, forceUpdate] = useReducer(() => ({}), {}) + const [salt, forceUpdate] = useReducer(() => ({}), {}) const mountedFieldsRef = useRef({}) const fieldStateRef = useRef(null) const stateRecordRef = useRef({}) @@ -440,17 +440,18 @@ function FieldBlock(props: Props) { } return acc - }, {}) as StatusContent - - // eslint-disable-next-line react-hooks/exhaustive-deps + }, salt) as StatusContent }, [ - info, - warning, errorProp, - nestedFieldBlockContext, + warning, + info, + salt, setInternalRecord, blockId, - wasUpdated, // wasUpdated is needed to get the current errors + hasInitiallyErrorProp, + props.id, + forId, + label, ]) // Handle the error prop from outside @@ -498,6 +499,7 @@ function FieldBlock(props: Props) { }) const labelProps: FormLabelAllProps = { + id: `${id}-label`, className: 'dnb-forms-field-block__label', element: enableFieldset ? 'legend' : 'label', forId: enableFieldset ? undefined : forId, diff --git a/packages/dnb-eufemia/src/extensions/forms/Tools/GenerateSchema.tsx b/packages/dnb-eufemia/src/extensions/forms/Tools/GenerateSchema.tsx index 86199f361f7..797404a1c69 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Tools/GenerateSchema.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Tools/GenerateSchema.tsx @@ -31,15 +31,15 @@ export const schemaParams = [ export default function GenerateSchema(props: GenerateSchemaProps) { const { generateRef, filterData, log, children } = props || {} - const { fieldPropsRef, valuePropsRef, data, hasContext } = + const { fieldInternalsRef, valueInternalsRef, data, hasContext } = useContext(DataContext) const dataRef = useRef({}) dataRef.current = data const generate = useCallback(() => { - const schema = Object.entries(fieldPropsRef?.current || {}).reduce( - (acc, [path, props]) => { + const schema = Object.entries(fieldInternalsRef?.current || {}).reduce( + (acc, [path, { props }]) => { if (path.startsWith('/')) { const objectKey = path.substring(1) @@ -113,8 +113,8 @@ export default function GenerateSchema(props: GenerateSchemaProps) { ) const propsOfFields = Object.entries( - fieldPropsRef?.current || {} - ).reduce((acc, [path, props]) => { + fieldInternalsRef?.current || {} + ).reduce((acc, [path, { props }]) => { if (path.startsWith('/')) { const propertyValue = {} @@ -134,8 +134,8 @@ export default function GenerateSchema(props: GenerateSchemaProps) { }, {}) const propsOfValues = Object.entries( - valuePropsRef?.current || {} - ).reduce((acc, [path, props]) => { + valueInternalsRef?.current || {} + ).reduce((acc, [path, { props }]) => { if (path.startsWith('/')) { const propertyValue = {} @@ -164,7 +164,7 @@ export default function GenerateSchema(props: GenerateSchemaProps) { propsOfFields, propsOfValues, } as GenerateSchemaReturn - }, [fieldPropsRef, filterData, valuePropsRef]) + }, [fieldInternalsRef, filterData, valueInternalsRef]) if (hasContext) { if (log) { diff --git a/packages/dnb-eufemia/src/extensions/forms/Tools/ListAllProps.tsx b/packages/dnb-eufemia/src/extensions/forms/Tools/ListAllProps.tsx index 0fda1113732..c71aabdb85b 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Tools/ListAllProps.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Tools/ListAllProps.tsx @@ -19,7 +19,7 @@ export default function ListAllProps( props: ListAllPropsProps ) { const { log, generateRef, filterData, children } = props || {} - const { fieldPropsRef, valuePropsRef, data, hasContext } = + const { fieldInternalsRef, valueInternalsRef, data, hasContext } = useContext(DataContext) const dataRef = useRef({}) @@ -27,8 +27,8 @@ export default function ListAllProps( const generate = useCallback(() => { const propsOfFields = Object.entries( - fieldPropsRef?.current || {} - ).reduce((acc, [path, props]) => { + fieldInternalsRef?.current || {} + ).reduce((acc, [path, { props }]) => { if (path.startsWith('/')) { const propertyValue = {} @@ -51,8 +51,8 @@ export default function ListAllProps( }, {}) const propsOfValues = Object.entries( - valuePropsRef?.current || {} - ).reduce((acc, [path, props]) => { + valueInternalsRef?.current || {} + ).reduce((acc, [path, { props }]) => { if (path.startsWith('/')) { const propertyValue = {} @@ -75,7 +75,7 @@ export default function ListAllProps( }, {}) return { propsOfFields, propsOfValues } as ListAllPropsReturn - }, [fieldPropsRef, filterData, valuePropsRef]) + }, [fieldInternalsRef, filterData, valueInternalsRef]) if (hasContext) { if (log) { 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 4d2a3fcb604..3fe06e9e278 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/ArraySelection.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Value/ArraySelection/ArraySelection.tsx @@ -12,7 +12,7 @@ import ListFormat, { export type Props = ValueProps> & ListFormatProps function ArraySelection(props: Props) { - const { fieldPropsRef } = useContext(Context) || {} + const { fieldInternalsRef } = useContext(Context) || {} const { path, value, @@ -27,9 +27,9 @@ function ArraySelection(props: Props) { let valueToUse = value if (path) { - const data = fieldPropsRef?.current?.[ + const data = fieldInternalsRef?.current?.[ path + '/arraySelectionData' - ] as Array<{ + ]?.props as unknown as Array<{ value: string title: string | React.ReactNode }> @@ -50,7 +50,7 @@ function ArraySelection(props: Props) { listType={listType} /> ) - }, [fieldPropsRef, path, value, variant, listType]) + }, [fieldInternalsRef, path, value, variant, listType]) return ( & { } function Selection(props: Props) { - const { fieldPropsRef } = useContext(Context) || {} + const { fieldInternalsRef } = useContext(Context) || {} const { path, dataPath, value, ...rest } = useValueProps(props) const { getValueByPath } = useDataValue() const valueToDisplay = useMemo(() => { - const fieldProp = fieldPropsRef?.current?.[path] + const fieldProp = fieldInternalsRef?.current?.[path]?.props if (path || dataPath) { let list = getValueByPath(dataPath)?.map?.((props) => ({ props })) if (!list) { - list = fieldProp?.['children'] as Array< + list = fieldProp?.children as Array< Omit & { props: Data[number] } > } @@ -35,7 +35,7 @@ function Selection(props: Props) { } return value - }, [dataPath, fieldPropsRef, getValueByPath, path, value]) + }, [dataPath, fieldInternalsRef, getValueByPath, path, value]) return } diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx index fb1cd366bc2..d6caadc0c58 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx @@ -583,7 +583,8 @@ function WizardPortal({ children }) { } function PrerenderFieldPropsProvider({ children }) { - const { data, setFieldProps, updateDataValue } = useContext(DataContext) + const { data, setFieldInternals, updateDataValue } = + useContext(DataContext) return ( '], + doc: "Info message shown below / after the field. When provided as a function, the function will be called with the current value as argument. The second parameter is an object with `{ conditionally, getValueByPath, getFieldByPath }`. To show the message first after the user has interacted with the field, you can call and return `conditionally` function with a callback and with options: `conditionally(() => 'Your message', { showInitially: true })`", + type: ['React.Node', 'Array', 'function'], status: 'optional', }, warning: { - doc: 'Warning message shown below / after the field.', - type: ['React.Node', 'Array'], + doc: "Warning message shown below / after the field. When provided as a function, the function will be called with the current value as argument. The second parameter is an object with `{ conditionally, getValueByPath, getFieldByPath }`. To show the message first after the user has interacted with the field, you can call and return `conditionally` function with a callback and with options: `conditionally(() => 'Your message', { showInitially: true })`", + type: ['React.Node', 'Array', 'function'], status: 'optional', }, error: { - doc: 'Error message shown below / after the field.', - type: ['Error', 'FormError', 'Array'], + doc: "Error message shown below / after the field. When provided as a function, the function will be called with the current value as argument. The second parameter is an object with `{ conditionally, getValueByPath, getFieldByPath }`. To show the message first after the user has interacted with the field, you can call and return `conditionally` function with a callback and with options: `conditionally(() => 'Your message', { showInitially: true })`", + type: ['Error', 'FormError', 'Array', 'function'], status: 'optional', }, disabled: { diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts index bcce809c9b1..aa16271568d 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts @@ -25,6 +25,10 @@ import { ValidatorAdditionalArgs, Validator, Identifier, + MessageProp, + MessageTypes, + MessagePropParams, + UseFieldProps, } from '../types' import { Context as DataContext, ContextState } from '../DataContext' import { clearedData } from '../DataContext/Provider/Provider' @@ -113,8 +117,8 @@ export default function useFieldProps( emptyValue, required: requiredProp, disabled: disabledProp, - info, - warning, + info: infoProp, + warning: warningProp, error: errorProp, errorMessages, onFocus, @@ -176,6 +180,7 @@ export default function useFieldProps( useContext(SnapshotContext) || {} const { isVisible } = useContext(VisibilityContext) || {} + const { getValueByPath } = useDataValue() const translation = useTranslation() const { formatMessage } = translation const translationRef = useRef(translation) @@ -200,7 +205,7 @@ export default function useFieldProps( validateData: validateDataDataContext, setFieldState: setFieldStateDataContext, setFieldError: setFieldErrorDataContext, - setFieldProps: setFieldPropsDataContext, + setFieldInternals: setFieldInternalsDataContext, setFieldConnection: setFieldConnectionDataContext, setVisibleError: setVisibleErrorDataContext, setMountedFieldState: setMountedFieldStateDataContext, @@ -210,6 +215,7 @@ export default function useFieldProps( contextErrorMessages, fieldDisplayValueRef, existingFieldsRef, + fieldInternalsRef, } = dataContext || {} const onChangeContext = dataContext?.props?.onChange @@ -271,6 +277,10 @@ export default function useFieldProps( const changedRef = useRef() const hasFocusRef = useRef() + // - Should errors received through validation be shown initially. Assume that providing a direct prop to + // the component means it is supposed to be shown initially. + const revealErrorRef = useRef(null) + const required = useMemo(() => { if (typeof requiredProp !== 'undefined') { return requiredProp @@ -326,13 +336,107 @@ export default function useFieldProps( sectionPath, ]) - // Error handling - // - Should errors received through validation be shown initially. Assume that providing a direct prop to - // the component means it is supposed to be shown initially. - const revealErrorRef = useRef( - validateInitially ?? Boolean(errorProp) + const getFieldByPath: MessagePropParams< + Value, + unknown + >['getFieldByPath'] = useCallback( + (path) => { + return ( + fieldInternalsRef.current?.[path] || { + props: undefined, + id: undefined, + } + ) + }, + [fieldInternalsRef] + ) + + const messageCacheRef = useRef<{ + isSet: boolean + message: MessageTypes + }>({ isSet: false, message: undefined }) + const executeMessage = useCallback( + >( + message: MessageProp + ): ReturnValue => { + if (typeof message === 'function') { + const ALWAYS = 4 + const INITIALLY = 8 + + let currentMode = ALWAYS + + const msg = message(valueRef.current, { + conditionally: (callback, options) => { + currentMode &= ~ALWAYS + + if (options?.showInitially) { + currentMode |= INITIALLY + } + + return callback() + }, + getValueByPath, + getFieldByPath, + }) + + if (msg === undefined) { + messageCacheRef.current.message = undefined + return null // hide the message + } + + const isError = + msg instanceof Error || + msg instanceof FormError || + (Array.isArray(msg) && checkForError(msg)) + + if ( + (!messageCacheRef.current.isSet && currentMode & INITIALLY) || + currentMode & ALWAYS || + hasFocusRef.current === false || + // Ensure we don't remove the message when the value is e.g. empty string + messageCacheRef.current.message + ) { + if ( + // Ensure to only update the message when component did re-render internally + isInternalRerenderRef.current || + currentMode & ALWAYS || + (!messageCacheRef.current.isSet && currentMode & INITIALLY) + ) { + if (msg) { + messageCacheRef.current.isSet = true + } + if (msg || !hasFocusRef.current || currentMode & ALWAYS) { + messageCacheRef.current.message = msg + } + } + + message = messageCacheRef.current.message as ReturnValue + + if (isError && message) { + revealErrorRef.current = true + } + + if (!isError && !message) { + return null // hide the message + } + } else { + return undefined // no message + } + } + + return message + }, + [getFieldByPath, getValueByPath] ) + const error = executeMessage(errorProp) + const warning = executeMessage(warningProp) + const info = executeMessage(infoProp) + + if (revealErrorRef.current === null) { + revealErrorRef.current = validateInitially ?? Boolean(errorProp) + } + // - Local errors are errors based on validation instructions received by const errorMethodRef = useRef< Partial> @@ -602,19 +706,22 @@ export default function useFieldProps( revealErrorRef.current = true } - const error = revealErrorRef.current - ? prepareError(errorProp) ?? + const bufferedError = revealErrorRef.current + ? prepareError(error) ?? localErrorRef.current ?? contextErrorRef.current + : error === null + ? null : undefined const hasVisibleError = - Boolean(error) || (inFieldBlock && fieldBlockContext.hasErrorProp) + Boolean(bufferedError) || + (inFieldBlock && fieldBlockContext.hasErrorProp) const hasError = useCallback(() => { return Boolean( - errorProp ?? localErrorRef.current ?? contextErrorRef.current + error ?? localErrorRef.current ?? contextErrorRef.current ) - }, [errorProp]) + }, [error]) const connectWithPathListenerRef = useRef(async () => { if ( @@ -630,7 +737,6 @@ export default function useFieldProps( } }) - const { getValueByPath } = useDataValue() const exportValidatorsRef = useRef(exportValidators) exportValidatorsRef.current = exportValidators const additionalArgs = useMemo(() => { @@ -810,11 +916,15 @@ export default function useFieldProps( } }, [persistErrorState]) + const setChanged = useCallback((state: boolean) => { + changedRef.current = state + }, []) + const removeError = useCallback(() => { - changedRef.current = false + setChanged(false) hideError() clearErrorState() - }, [clearErrorState, hideError]) + }, [clearErrorState, hideError, setChanged]) const validatorCacheRef = useRef({ onChangeValidator: null, @@ -1201,7 +1311,7 @@ export default function useFieldProps( // When changing the value, hide errors to avoid annoying the user before they are finished filling in that value hideError() } - }, [continuousValidation, validateContinuously, hideError, revealError]) + }, [validateContinuously, hideError, revealError]) const getEventArgs = useCallback( ({ @@ -1526,10 +1636,6 @@ export default function useFieldProps( ] ) - const setChanged = (state: boolean) => { - changedRef.current = state - } - const setDisplayValue = useCallback( (path: Identifier, content: React.ReactNode) => { if (!path || !fieldDisplayValueRef?.current) { @@ -1558,7 +1664,7 @@ export default function useFieldProps( } // Must be set before validation - changedRef.current = true + setChanged(true) if (asyncBehaviorIsEnabled) { hideError() @@ -1621,17 +1727,18 @@ export default function useFieldProps( await runPool() }, [ + addToPool, asyncBehaviorIsEnabled, + defineAsyncProcess, + getEventArgs, + hasError, + hideError, onChange, runPool, - hideError, + setChanged, + setEventResult, updateValue, - addToPool, - getEventArgs, yieldAsyncProcess, - defineAsyncProcess, - hasError, - setEventResult, ] ) @@ -1639,7 +1746,7 @@ export default function useFieldProps( const handleBlur = useCallback(() => setHasFocus(false), [setHasFocus]) // Put props into the surrounding data context as early as possible - setFieldPropsDataContext?.(identifier, props) + setFieldInternalsDataContext?.(identifier, props, id) const { activeIndex, activeIndexRef } = wizardContext || {} const activeIndexTmpRef = useRef(activeIndex) @@ -2005,11 +2112,11 @@ export default function useFieldProps( useLayoutEffect(() => { if (isEmptyData()) { defaultValueRef.current = defaultValue - changedRef.current = false + setChanged(false) hideError() clearErrorState() } - }, [clearErrorState, defaultValue, hideError, isEmptyData]) + }, [clearErrorState, defaultValue, hideError, isEmptyData, setChanged]) useMemo(() => { if (updateContextDataInSync && !isEmptyData()) { @@ -2100,7 +2207,7 @@ export default function useFieldProps( setBlockRecord?.({ identifier, type: 'error', - content: errorProp, + content: error, showInitially: true, show: true, }) @@ -2128,7 +2235,7 @@ export default function useFieldProps( } } }, [ - errorProp, + error, identifier, inFieldBlock, info, @@ -2137,13 +2244,14 @@ export default function useFieldProps( warning, ]) - const infoRef = useRef(info) - const warningRef = useRef(warning) - useUpdateEffect(() => { + const infoRef = useRef(info) + const warningRef = useRef(warning) + if (typeof info !== 'undefined') { infoRef.current = info + } + if (typeof warning !== 'undefined') { warningRef.current = warning - forceUpdate() - }, [info, warning]) + } const connections = useMemo(() => { return { @@ -2165,8 +2273,8 @@ export default function useFieldProps( ) }, [props]) - if (error) { - htmlAttributes['aria-invalid'] = error ? 'true' : 'false' + if (bufferedError) { + htmlAttributes['aria-invalid'] = bufferedError ? 'true' : 'false' } if (required) { htmlAttributes['aria-required'] = 'true' @@ -2184,7 +2292,7 @@ export default function useFieldProps( htmlAttributes['aria-describedby'] = combineDescribedBy( htmlAttributes, [ - error && stateIds.error, + bufferedError && stateIds.error, warning && stateIds.warning, info && stateIds.info, ].filter(Boolean) @@ -2192,7 +2300,7 @@ export default function useFieldProps( } } else { const ids = [ - (error || errorProp) && `${id}-form-status--error`, + (bufferedError || error) && `${id}-form-status--error`, warning && `${id}-form-status--warning`, info && `${id}-form-status--info`, ].filter(Boolean) @@ -2217,7 +2325,7 @@ export default function useFieldProps( /** Documented APIs */ info: !inFieldBlock ? infoRef.current : undefined, warning: !inFieldBlock ? warningRef.current : undefined, - error: !inFieldBlock ? error : undefined, + error: !inFieldBlock ? bufferedError : undefined, required, label: props.label, labelDescription: props.labelDescription, diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useValueProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useValueProps.ts index dcfe9e1bffd..f8305ee29ea 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useValueProps.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useValueProps.ts @@ -56,12 +56,12 @@ export default function useValueProps< }) ?? defaultValue const { - fieldPropsRef, + fieldInternalsRef, mountedFieldsRef, - setValueProps, + setValueInternals, setFieldEventListener, } = useContext(DataContext) || {} - setValueProps?.(path, props) + setValueInternals?.(path, props) useEffect(() => { if (inheritLabel || inheritVisibility) { @@ -94,7 +94,9 @@ export default function useValueProps< const label = props.label ?? - (inheritLabel ? fieldPropsRef?.current?.[path]?.label : undefined) + (inheritLabel + ? fieldInternalsRef?.current?.[path]?.props?.label + : undefined) return { ...props, label, value } } diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts index 7ea7fb5d731..b79f6ced46c 100644 --- a/packages/dnb-eufemia/src/extensions/forms/types.ts +++ b/packages/dnb-eufemia/src/extensions/forms/types.ts @@ -13,6 +13,7 @@ import { FormsTranslationFlat, FormsTranslationLocale, } from './hooks/useTranslation' +import { GetValueByPath } from './hooks/useDataValue' export type * from 'json-schema' export type JSONSchema = JSONSchema7 @@ -265,6 +266,30 @@ export type DataValueReadWriteComponentProps< DataValueReadProps & DataValueWriteProps +export type MessagePropParams = { + conditionally: ( + callback: () => ReturnValue | void, + options?: { + showInitially?: boolean + } + ) => ReturnValue + getValueByPath: GetValueByPath + getFieldByPath: (path: Path) => { + props: FieldProps + id: Identifier + } +} +export type MessageProp = + | ReturnValue + | (( + value: Value, + options: MessagePropParams + ) => ReturnValue) +export type MessageTypes = + | UseFieldProps['info'] + | UseFieldProps['warning'] + | UseFieldProps['error'] + export interface UseFieldProps< Value = unknown, EmptyValue = undefined | unknown, @@ -300,9 +325,9 @@ export interface UseFieldProps< props?: Record // - Used by useFieldProps and FieldBlock - info?: React.ReactNode - warning?: React.ReactNode - error?: Error | FormError | Array + info?: MessageProp> + warning?: MessageProp> + error?: MessageProp> // - Validation required?: boolean From b7639cb258f99cd0644897fa98c4e0406a584c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Mon, 13 Jan 2025 14:21:05 +0100 Subject: [PATCH 07/61] docs(Forms): improve documentation on TypeScript type handling (#4444) Closes #4443 --- .../extensions/forms/Form/Handler/info.mdx | 41 ++++---- .../extensions/forms/Form/useData/info.mdx | 11 ++- .../extensions/forms/getting-started.mdx | 20 ++-- .../Form/Handler/__tests__/Handler.test.tsx | 94 +++++++++++++++++++ 4 files changed, 135 insertions(+), 31 deletions(-) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/info.mdx index 8482711bd81..049f4cb2ad4 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/info.mdx @@ -28,22 +28,23 @@ function MyForm() { ### TypeScript support -You can define the TypeScript type structure for your data like so: +You can define the TypeScript type structure for your form data. This will help you to get better code completion and type checking. + +**NB:** Use `type` instead of `interface` for the type definition. ```tsx import { Form } from '@dnb/eufemia/extensions/forms' -type MyDataContext = { +type MyData = { firstName?: string } // Method #1 – without initial data function MyForm() { return ( - + onSubmit={(data) => { - // "firstName" is of type string - console.log(data.firstName) + console.log(data.firstName satisfies string) }} > ... @@ -52,7 +53,7 @@ function MyForm() { } // Method #2 – with data (initial values) -const existingData: MyDataContext = { +const existingData: MyData = { firstName: 'Nora', } function MyForm() { @@ -60,8 +61,7 @@ function MyForm() { { - // "firstName" is of type string - console.log(data.firstName) + console.log(data.firstName satisfies string) }} > ... @@ -69,23 +69,26 @@ function MyForm() { ) } -// Method #3 – type definition on the event parameter -const submitHandler = (data: MyDataContext) => { - // "firstName" is of type string - console.log(data.firstName) +// Method #3 – type definition for the submit handler +import type { OnSubmit } from '@dnb/eufemia/extensions/forms' +const submitHandler: OnSubmit = (data) => { + console.log(data.firstName satisfies string) } function MyForm() { return ... } -// Method #4 – type definition for the submit handler -import type { OnSubmit } from '@dnb/eufemia/extensions/forms' -const submitHandler: OnSubmit = (data) => { - // "firstName" is of type string - console.log(data.firstName) -} +// Method #4 – type definition on the event parameter function MyForm() { - return ... + return ( + { + console.log(data.firstName satisfies string) + }} + > + ... + + ) } ``` diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx index 19f6733aaba..e092ce1b054 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx @@ -46,12 +46,17 @@ You can use the `Form.useData` hook with or without an `id` (string, function, o ### TypeScript support -You can define the TypeScript type structure for your data like so: +You can define the TypeScript type structure for your form data. This will help you to get better code completion and type checking. + +**NB:** Use `type` instead of `interface` for the type definition. ```tsx -type Data = { foo: string } +type MyData = { firstName: string } -const { data } = Form.useData() +const MyComponent = () => { + const { data } = Form.useData() + return data.firstName +} ``` ### Without an `id` property diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx index 059a6bedab2..674bd809c71 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx @@ -70,29 +70,31 @@ The needed styles are included in the Eufemia core package via `dnb-ui-component ### TypeScript support -You can define the TypeScript type structure for your data like so: +You can define the TypeScript type structure for your form data. This will help you to get better code completion and type checking. + +**NB:** Use `type` instead of `interface` for the type definition. ```tsx -import { Form } from '@dnb/eufemia/extensions/forms' +import { Form, OnSubmit } from '@dnb/eufemia/extensions/forms' -type MyDataContext = { +type MyData = { firstName?: string } +const submitHandler: OnSubmit = (data) => { + console.log(data.firstName satisfies string) +} + function MyForm() { return ( - - onSubmit={(data) => { - console.log(data.firstName) // "firstName" is of type string - }} - > + onSubmit={submitHandler}> ) } const MyComponent = () => { - const { data } = Form.useData() + const { data } = Form.useData() return data.firstName } ``` diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Handler/__tests__/Handler.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Handler/__tests__/Handler.test.tsx index 890bd94fbf9..2b0fc7566e6 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Handler/__tests__/Handler.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Handler/__tests__/Handler.test.tsx @@ -1287,3 +1287,97 @@ describe('Form.Handler', () => { }) }) }) + +describe('Form.Handler TypeScript type validation', () => { + describe('for the OnSubmit type', () => { + it('should correctly support a named type', () => { + type MyInterface = { + foo: string + } + + const submitHandler: OnSubmit = (data) => { + data.foo satisfies string + } + + render(...) + }) + + it('should correctly support an array type', () => { + type MyInterface = Array + + const submitHandler: OnSubmit = (data) => { + data satisfies Array + } + + render(...) + }) + + it('should throw a type error when interface is used', () => { + interface MyInterface { + foo: string + } + + const submitHandler: OnSubmit = (data) => { + data.foo satisfies string + } + + render( + + ... + + ) + }) + }) + + describe('for direct type annotation', () => { + it('should correctly support a named type', () => { + type MyInterface = { + foo: string + } + + render( + + onSubmit={(data) => { + data.foo satisfies string + }} + > + ... + + ) + }) + + it('should correctly support an array type', () => { + type MyInterface = Array + + render( + + onSubmit={(data) => { + data satisfies Array + }} + > + ... + + ) + }) + + it('should throw a type error when interface is used', () => { + interface MyInterface { + foo: string + } + + render( + // @ts-expect-error + + onSubmit={(data) => { + data.foo satisfies string + }} + > + ... + + ) + }) + }) +}) From 22f439f51e3735a48d72fe66a571f8fcd1f0b4b0 Mon Sep 17 00:00:00 2001 From: Anders Date: Mon, 13 Jan 2025 21:11:32 +0100 Subject: [PATCH 08/61] chore(Accordion): replace defaultProps (#4449) - Fixes https://github.com/dnbexperience/eufemia/issues/4145 - In relation to #4447 and #4448 --- packages/dnb-eufemia/src/components/accordion/Accordion.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/dnb-eufemia/src/components/accordion/Accordion.tsx b/packages/dnb-eufemia/src/components/accordion/Accordion.tsx index 904ca1f212b..43a5b0c8ef9 100644 --- a/packages/dnb-eufemia/src/components/accordion/Accordion.tsx +++ b/packages/dnb-eufemia/src/components/accordion/Accordion.tsx @@ -435,9 +435,6 @@ function Accordion({ ) } -// TEMPORARY SOLUTION (defaultProps will be deprecated at one point). Needs to replacement with default prop parameters for example "({expanded: null})" -// Only solved this way to prevent tests from failing, for when expanded is undefined instead of null -Accordion.defaultProps = accordionDefaultProps export type GroupProps = AccordionProps & { allow_close_all?: boolean From 34d109ef2e3cac9e0669c6970c15a69964a2ce2f Mon Sep 17 00:00:00 2001 From: Anders Date: Mon, 13 Jan 2025 21:12:21 +0100 Subject: [PATCH 09/61] fix: refactor defaultProps to ES6 default parameters (#4448) - Fixes https://github.com/dnbexperience/eufemia/issues/4145 - In relation to #4447 --- .../dnb-eufemia/src/components/input-masked/InputMasked.js | 4 ++-- .../src/components/input-masked/InputMaskedUtils.js | 7 ++++++- .../components/input-masked/__tests__/InputMasked.test.tsx | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/dnb-eufemia/src/components/input-masked/InputMasked.js b/packages/dnb-eufemia/src/components/input-masked/InputMasked.js index a8f486c25cb..0cee0017819 100644 --- a/packages/dnb-eufemia/src/components/input-masked/InputMasked.js +++ b/packages/dnb-eufemia/src/components/input-masked/InputMasked.js @@ -30,7 +30,7 @@ const InputMasked = (props) => { const contextAndProps = React.useMemo(() => { return extendPropsWithContext( props, - InputMasked.defaultProps, + defaultProps, context?.InputMasked ) }, [context?.InputMasked, props]) @@ -89,7 +89,7 @@ InputMasked.propTypes = { ...inputPropTypes, } -InputMasked.defaultProps = { +const defaultProps = { ...Input.defaultProps, mask: null, diff --git a/packages/dnb-eufemia/src/components/input-masked/InputMaskedUtils.js b/packages/dnb-eufemia/src/components/input-masked/InputMaskedUtils.js index b89f8ce8f47..5cacf523f71 100644 --- a/packages/dnb-eufemia/src/components/input-masked/InputMaskedUtils.js +++ b/packages/dnb-eufemia/src/components/input-masked/InputMaskedUtils.js @@ -65,7 +65,12 @@ export const correctNumberValue = ({ locale, maskParams, }) => { - let value = props.value === null ? null : String(props.value) + let value = + props.value === null + ? null + : props.value === undefined + ? undefined + : String(props.value) if (isNaN(parseFloat(value))) { return value diff --git a/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx b/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx index 82bb2be8cff..5cc3b09f431 100644 --- a/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx +++ b/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx @@ -549,7 +549,7 @@ describe('InputMasked component', () => { it('should show placeholder with both value null and undefined', () => { const { rerender } = render( - + ) expect( From 813f4749dab69067ec3d30b543ba26fb300ae38f Mon Sep 17 00:00:00 2001 From: Anders Date: Tue, 14 Jan 2025 10:46:33 +0100 Subject: [PATCH 10/61] chore: rename Tabbar -> TabBar (#4454) --- .../src/core/PortalLayout.tsx | 8 ++++---- .../src/docs/uilib/components/tabs/Examples.tsx | 2 +- .../src/e2e/pageHeading.spec.ts | 8 ++++---- .../src/e2e/pageLists.spec.ts | 6 +++--- .../src/shared/menu/SidebarMenu.tsx | 2 +- .../tags/{Tabbar.module.scss => TabBar.module.scss} | 2 +- .../src/shared/tags/{Tabbar.tsx => TabBar.tsx} | 12 ++++++------ .../src/components/tabs/stories/Tabs.stories.tsx | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) rename packages/dnb-design-system-portal/src/shared/tags/{Tabbar.module.scss => TabBar.module.scss} (94%) rename packages/dnb-design-system-portal/src/shared/tags/{Tabbar.tsx => TabBar.tsx} (93%) diff --git a/packages/dnb-design-system-portal/src/core/PortalLayout.tsx b/packages/dnb-design-system-portal/src/core/PortalLayout.tsx index 16513e002ca..80ae54de742 100644 --- a/packages/dnb-design-system-portal/src/core/PortalLayout.tsx +++ b/packages/dnb-design-system-portal/src/core/PortalLayout.tsx @@ -7,14 +7,14 @@ import React from 'react' import { MDXProvider } from '@mdx-js/react' import { graphql, useStaticQuery } from 'gatsby' import Layout from '../shared/parts/Layout' -import Tabbar from '../shared/tags/Tabbar' +import TabBar from '../shared/tags/TabBar' import { Link } from '../shared/tags/Anchor' import tags from '../shared/tags' import { resetLevels } from '@dnb/eufemia/src/components/Heading' import { setPortalHeadData, usePortalHead } from './PortalHead' import { Breadcrumb } from '@dnb/eufemia/src' -const ContentWrapper = Tabbar.ContentWrapper +const ContentWrapper = TabBar.ContentWrapper type Frontmatter = { title: string @@ -157,8 +157,8 @@ export default function PortalLayout(props: PortalLayoutProps) { )} {currentFm.showTabs && ( - ( flex-shrink: 0; ` const RightArea = styled.div` - /* Ensure the tabbar is hidden outside this area */ + /* Ensure the tab bar is hidden outside this area */ overflow: hidden; /* Ensure the focus ring is visible! (because of overflow: hidden) */ diff --git a/packages/dnb-design-system-portal/src/e2e/pageHeading.spec.ts b/packages/dnb-design-system-portal/src/e2e/pageHeading.spec.ts index 22d0a3d9e57..9e99dae9fbe 100644 --- a/packages/dnb-design-system-portal/src/e2e/pageHeading.spec.ts +++ b/packages/dnb-design-system-portal/src/e2e/pageHeading.spec.ts @@ -15,19 +15,19 @@ test.describe('Page Heading', () => { expect(h1Count).toBe(1) const firstElementTagName = await page.$eval( - '#tabbar-content > *', + '#tab-bar-content > *', (element) => element.tagName, ) expect(firstElementTagName).toBe('H1') const secondElementTagName = await page.$eval( - '#tabbar-content > h1 ~ p ~ *', + '#tab-bar-content > h1 ~ p ~ *', (element) => element.tagName, ) expect(secondElementTagName).toBe('H2') const thirdElementTagName = await page.$eval( - '#tabbar-content > h1 ~ p ~ *', + '#tab-bar-content > h1 ~ p ~ *', (element) => element.tagName, ) expect(thirdElementTagName).toBe('H2') @@ -38,7 +38,7 @@ test.describe('Page Heading', () => { ) const reRenderedElementTagName = await page.$eval( - '#tabbar-content > h1 ~ p ~ *', + '#tab-bar-content > h1 ~ p ~ *', (element) => element.tagName, ) expect(reRenderedElementTagName).toBe('H2') diff --git a/packages/dnb-design-system-portal/src/e2e/pageLists.spec.ts b/packages/dnb-design-system-portal/src/e2e/pageLists.spec.ts index c4e556fe32f..eacf1a8934c 100644 --- a/packages/dnb-design-system-portal/src/e2e/pageLists.spec.ts +++ b/packages/dnb-design-system-portal/src/e2e/pageLists.spec.ts @@ -41,7 +41,7 @@ test.describe('Page Lists', () => { await expect( page.locator( - '#tabbar-content h2:has(a[href*="/uilib/components/"]:not([aria-hidden]))', + '#tab-bar-content h2:has(a[href*="/uilib/components/"]:not([aria-hidden]))', ), ).toHaveCount(listLength) }) @@ -72,7 +72,7 @@ test.describe('Page Lists', () => { await expect( page.locator( - '#tabbar-content h2:has(a[href*="/uilib/extensions/"]:not([aria-hidden]))', + '#tab-bar-content h2:has(a[href*="/uilib/extensions/"]:not([aria-hidden]))', ), ).toHaveCount(listLength) }) @@ -102,7 +102,7 @@ test.describe('Page Lists', () => { .count() await expect( page.locator( - '#tabbar-content ul li:has(a[href*="/uilib/elements/"]:not([aria-hidden]))', + '#tab-bar-content ul li:has(a[href*="/uilib/elements/"]:not([aria-hidden]))', ), ).toHaveCount(listLength) }) diff --git a/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx b/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx index 0650a5648ba..674ec873255 100644 --- a/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx +++ b/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx @@ -30,7 +30,7 @@ import { } from '@dnb/eufemia/src/shared/helpers' import PortalToolsMenu from './PortalToolsMenu' import { navStyle } from './SidebarMenu.module.scss' -import { defaultTabsValue } from '../tags/Tabbar' +import { defaultTabsValue } from '../tags/TabBar' const showAlwaysMenuItems = [] // like "uilib" something like that diff --git a/packages/dnb-design-system-portal/src/shared/tags/Tabbar.module.scss b/packages/dnb-design-system-portal/src/shared/tags/TabBar.module.scss similarity index 94% rename from packages/dnb-design-system-portal/src/shared/tags/Tabbar.module.scss rename to packages/dnb-design-system-portal/src/shared/tags/TabBar.module.scss index 6903aa99398..549f671a946 100644 --- a/packages/dnb-design-system-portal/src/shared/tags/Tabbar.module.scss +++ b/packages/dnb-design-system-portal/src/shared/tags/TabBar.module.scss @@ -27,7 +27,7 @@ top: 0; /* - Will align the tabbar to the browser edges + Will align the tab bar to the browser edges .dnb-tabs__tabs.dnb-tabs--has-scrollbar { margin: 0 -2rem; } diff --git a/packages/dnb-design-system-portal/src/shared/tags/Tabbar.tsx b/packages/dnb-design-system-portal/src/shared/tags/TabBar.tsx similarity index 93% rename from packages/dnb-design-system-portal/src/shared/tags/Tabbar.tsx rename to packages/dnb-design-system-portal/src/shared/tags/TabBar.tsx index 7fe726d49f2..2ab78547de1 100644 --- a/packages/dnb-design-system-portal/src/shared/tags/Tabbar.tsx +++ b/packages/dnb-design-system-portal/src/shared/tags/TabBar.tsx @@ -7,7 +7,7 @@ import React from 'react' import { Button, Tabs } from '@dnb/eufemia/src/components' import { fullscreen as fullscreenIcon } from '@dnb/eufemia/src/icons' import AutoLinkHeader from './AutoLinkHeader' -import { tabsWrapperStyle } from './Tabbar.module.scss' +import { tabsWrapperStyle } from './TabBar.module.scss' import { Link } from './Anchor' export const defaultTabsValue = [ @@ -28,7 +28,7 @@ type TabbarProps = { children?: React.ReactNode } -export default function Tabbar({ +export default function TabBar({ location, title, hideTabs, @@ -102,14 +102,14 @@ export default function Tabbar({ ].join('') return ( -
+
{title && ( {title} )} ( - +TabBar.ContentWrapper = (props) => ( + ) diff --git a/packages/dnb-eufemia/src/components/tabs/stories/Tabs.stories.tsx b/packages/dnb-eufemia/src/components/tabs/stories/Tabs.stories.tsx index b85e6f6eeae..4c9c8dacd4a 100644 --- a/packages/dnb-eufemia/src/components/tabs/stories/Tabs.stories.tsx +++ b/packages/dnb-eufemia/src/components/tabs/stories/Tabs.stories.tsx @@ -375,7 +375,7 @@ const LeftArea = styled.div` flex-shrink: 0; ` const RightArea = styled.div` - /* Ensure the tabbar is hidden outside this area */ + /* Ensure the tab bar is hidden outside this area */ overflow: hidden; /* Ensure the focus ring is visible! (because of overflow: hidden) */ From 9dfd6b49eaf4d1c4db07bfed16fdb4b8d55409f0 Mon Sep 17 00:00:00 2001 From: Anders Date: Tue, 14 Jan 2025 20:51:36 +0100 Subject: [PATCH 11/61] fix(Blockquote): contrast font color of code as child (#4453) fixes https://github.com/dnbexperience/eufemia/issues/3730 --- .../docs/uilib/elements/blockquote/Examples.tsx | 11 ++++++++++- .../docs/uilib/elements/blockquote/demos.mdx | 5 +++++ .../__tests__/Blockquote.screenshot.test.ts | 8 ++++++++ ...match-blockquote-with-code-as-child.snap.png | Bin 0 -> 9959 bytes ...match-blockquote-with-code-as-child.snap.png | Bin 0 -> 10864 bytes .../blockquote/stories/Blockquote.stories.tsx | 7 +++++++ .../blockquote/style/blockquote-mixins.scss | 3 +++ .../__snapshots__/Elements.test.js.snap | 3 +++ 8 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 packages/dnb-eufemia/src/elements/blockquote/__tests__/__image_snapshots__/blockquote-for-sbanken-have-to-match-blockquote-with-code-as-child.snap.png create mode 100644 packages/dnb-eufemia/src/elements/blockquote/__tests__/__image_snapshots__/blockquote-for-ui-have-to-match-blockquote-with-code-as-child.snap.png diff --git a/packages/dnb-design-system-portal/src/docs/uilib/elements/blockquote/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/elements/blockquote/Examples.tsx index b30bfd3c15d..aef9253a80b 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/elements/blockquote/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/elements/blockquote/Examples.tsx @@ -5,7 +5,7 @@ import React from 'react' import ComponentBox from '../../../../shared/tags/ComponentBox' -import { Anchor, Blockquote } from '@dnb/eufemia/src' +import { Anchor, Blockquote, Code } from '@dnb/eufemia/src' export const BlockquoteDefaultExample = () => ( @@ -55,3 +55,12 @@ export const BlockquoteTransparentOnTopExample = () => ( ) + +export const BlockquoteWithCodeExample = () => ( + +
+ display and background-color are CSS + properties +
+
+) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/elements/blockquote/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/elements/blockquote/demos.mdx index a7d6e3a19ba..2e60b03a116 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/elements/blockquote/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/elements/blockquote/demos.mdx @@ -7,6 +7,7 @@ import { BlockquoteGraphicsExample, BlockquoteTransparentExample, BlockquoteTransparentOnTopExample, + BlockquoteWithCodeExample, } from 'Docs/uilib/elements/blockquote/Examples' ## Demos @@ -26,3 +27,7 @@ import { ### Blockquote with transparent background and graphics on top + +### Blockquote with [Code](/uilib/elements/code/) + + diff --git a/packages/dnb-eufemia/src/elements/blockquote/__tests__/Blockquote.screenshot.test.ts b/packages/dnb-eufemia/src/elements/blockquote/__tests__/Blockquote.screenshot.test.ts index c7b36abafda..a34be87fe13 100644 --- a/packages/dnb-eufemia/src/elements/blockquote/__tests__/Blockquote.screenshot.test.ts +++ b/packages/dnb-eufemia/src/elements/blockquote/__tests__/Blockquote.screenshot.test.ts @@ -49,6 +49,14 @@ describe.each(['ui', 'sbanken'])('Blockquote for %s', (themeName) => { }) expect(screenshot).toMatchImageSnapshot() }) + + it('have to match "blockquote" with code as child', async () => { + const screenshot = await makeScreenshot({ + style, + selector: '[data-visual-test="blockquote-with-code"]', + }) + expect(screenshot).toMatchImageSnapshot() + }) }) describe.each(['sbanken'])('Blockquote on mobile for %s', (themeName) => { diff --git a/packages/dnb-eufemia/src/elements/blockquote/__tests__/__image_snapshots__/blockquote-for-sbanken-have-to-match-blockquote-with-code-as-child.snap.png b/packages/dnb-eufemia/src/elements/blockquote/__tests__/__image_snapshots__/blockquote-for-sbanken-have-to-match-blockquote-with-code-as-child.snap.png new file mode 100644 index 0000000000000000000000000000000000000000..f2bcfdc5999abfc26b20f4c75b9aad2cfa239790 GIT binary patch literal 9959 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 literal 0 HcmV?d00001 diff --git a/packages/dnb-eufemia/src/elements/blockquote/stories/Blockquote.stories.tsx b/packages/dnb-eufemia/src/elements/blockquote/stories/Blockquote.stories.tsx index 0de123b47d9..fdfe01920e9 100644 --- a/packages/dnb-eufemia/src/elements/blockquote/stories/Blockquote.stories.tsx +++ b/packages/dnb-eufemia/src/elements/blockquote/stories/Blockquote.stories.tsx @@ -7,6 +7,7 @@ import React from 'react' import { Wrapper, Box } from 'storybook-utils/helpers' import styled from '@emotion/styled' import Blockquote from '../Blockquote' +import { Code } from '../..' const CustomStyles = styled.div` a { @@ -61,5 +62,11 @@ export const BlockquoteSandbox = () => (
Figcaption Reference
+ +
+ display and background-color are CSS + properties +
+
) diff --git a/packages/dnb-eufemia/src/elements/blockquote/style/blockquote-mixins.scss b/packages/dnb-eufemia/src/elements/blockquote/style/blockquote-mixins.scss index 46897467d0a..9bee60f598b 100644 --- a/packages/dnb-eufemia/src/elements/blockquote/style/blockquote-mixins.scss +++ b/packages/dnb-eufemia/src/elements/blockquote/style/blockquote-mixins.scss @@ -92,6 +92,9 @@ .dnb-anchor { font-size: inherit; } + .dnb-code { + color: var(--color-black-80); + } } @mixin blockquoteTag() { diff --git a/packages/dnb-eufemia/src/style/elements/__tests__/__snapshots__/Elements.test.js.snap b/packages/dnb-eufemia/src/style/elements/__tests__/__snapshots__/Elements.test.js.snap index 05c84747c72..c373ad8ad8f 100644 --- a/packages/dnb-eufemia/src/style/elements/__tests__/__snapshots__/Elements.test.js.snap +++ b/packages/dnb-eufemia/src/style/elements/__tests__/__snapshots__/Elements.test.js.snap @@ -91,6 +91,9 @@ exports[`Elements scss has to match style dependencies css 1`] = ` .dnb-blockquote .dnb-anchor { font-size: inherit; } +.dnb-blockquote .dnb-code { + color: var(--color-black-80); +} .dnb-spacing .dnb-blockquote:not([class*=dnb-space__top]) { margin-top: 0; From f434a8eec5a454b4ec572ac29920fc9e7d06d1f8 Mon Sep 17 00:00:00 2001 From: Anders Date: Tue, 14 Jan 2025 21:33:09 +0100 Subject: [PATCH 12/61] docs(GlobalStatus): add example on how to add custom icon (#4457) fixes https://github.com/dnbexperience/eufemia/issues/3298 --- .../components/global-status/Examples.tsx | 19 ++++++++++++ .../uilib/components/global-status/demos.mdx | 5 ++++ .../__tests__/GlobalStatus.screenshot.test.ts | 9 ++++++ .../__tests__/GlobalStatus.test.tsx | 28 ++++++++++++++++++ ...ken-have-to-match-the-custom-icon.snap.png | Bin 0 -> 4730 bytes ...-ui-have-to-match-the-custom-icon.snap.png | Bin 0 -> 5391 bytes 6 files changed, 61 insertions(+) create mode 100644 packages/dnb-eufemia/src/components/global-status/__tests__/__image_snapshots__/globalstatus-for-sbanken-have-to-match-the-custom-icon.snap.png create mode 100644 packages/dnb-eufemia/src/components/global-status/__tests__/__image_snapshots__/globalstatus-for-ui-have-to-match-the-custom-icon.snap.png diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/Examples.tsx index 6f7e663d319..4b1df88ae39 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/Examples.tsx @@ -11,7 +11,10 @@ import { Input, Section, ToggleButton, + Icon, } from '@dnb/eufemia/src' +import { confetti_medium } from '@dnb/eufemia/src/icons' + import { Provider } from '@dnb/eufemia/src/shared' export const GlobalInfoOverlayError = () => ( @@ -81,6 +84,22 @@ export const GlobalInfoOverlaySuccess = () => ( ) +export const GlobalInfoCustomIcon = () => ( + + } + show={true} + autoscroll={false} + no_animation={true} + omit_set_focus={true} + id="demo-icon" + /> + +) + export const GlobalStatusCoupling = () => ( {() => { diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/demos.mdx index b65d94a08d9..10b64767611 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/demos.mdx @@ -11,6 +11,7 @@ import { GlobalStatusCoupling, GlobalStatusAddRemoveItems, GlobalStatusScrolling, + GlobalInfoCustomIcon, } from 'Docs/uilib/components/global-status/Examples' ## Demos @@ -33,6 +34,10 @@ import { +### GlobalStatus custom icon + + + ### To showcase the automated coupling between **FormStatus** and **GlobalStatus** diff --git a/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.screenshot.test.ts b/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.screenshot.test.ts index b3e5f5c5bd5..859ac344a80 100644 --- a/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.screenshot.test.ts +++ b/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.screenshot.test.ts @@ -33,6 +33,15 @@ describe.each(['ui', 'sbanken'])('GlobalStatus for %s', (themeName) => { expect(screenshot).toMatchImageSnapshot() }) + it('have to match the custom icon', async () => { + const screenshot = await makeScreenshot({ + style, + selector: + '[data-visual-test="global-status-icon"] .dnb-global-status', + }) + expect(screenshot).toMatchImageSnapshot() + }) + if (themeName !== 'sbanken') { it('have to match the close button in focus state', async () => { const screenshot = await makeScreenshot({ diff --git a/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.test.tsx b/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.test.tsx index 7850f9492d3..bd24b9f1895 100644 --- a/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.test.tsx +++ b/packages/dnb-eufemia/src/components/global-status/__tests__/GlobalStatus.test.tsx @@ -13,10 +13,12 @@ import Autocomplete from '../../autocomplete/Autocomplete' import { fireEvent, render, waitFor } from '@testing-library/react' import { Provider } from '../../../shared' import { P } from '../../../elements' +import { Icon } from '../../../components' import { initializeTestSetup, simulateAnimationEnd, } from '../../height-animation/__tests__/HeightAnimationUtils' +import { confetti_medium as ConfettiIcon } from '../../../icons' const text = 'text' const items = [ @@ -919,6 +921,32 @@ describe('GlobalStatus component', () => { expect(element.classList).toContain('dnb-global-status--warning') }) + it('should support removing icon', () => { + render( + + ) + + expect(document.querySelector('.dnb-icon')).not.toBeInTheDocument() + }) + + it('should support setting icon as Icon', () => { + render( + + } + show + no_animation + hide_close_button + /> + ) + + expect(document.querySelector('.dnb-icon')).toBeInTheDocument() + expect( + document.querySelector('span.dnb-icon').getAttribute('data-testid') + ).toBe('custom-icon-testid') + }) + it('should validate with ARIA rules', async () => { const Comp = render() expect(await axeComponent(Comp)).toHaveNoViolations() diff --git a/packages/dnb-eufemia/src/components/global-status/__tests__/__image_snapshots__/globalstatus-for-sbanken-have-to-match-the-custom-icon.snap.png b/packages/dnb-eufemia/src/components/global-status/__tests__/__image_snapshots__/globalstatus-for-sbanken-have-to-match-the-custom-icon.snap.png new file mode 100644 index 0000000000000000000000000000000000000000..eb32d1948a172e87524e04bb3f689109a5e17101 GIT binary patch literal 4730 zcmd^@_fu0_*T(}$3(`T5dJ$1TO6byp6a@sO8#;(mrK6MpCb>!xX(AGeG^Hd+6(qEf zP=k^yy2Jh;;hA}#ndg@?XVyORUEj6WKIiPueqnyskQFEZ1ONc6#zqE~ z002!j_1=q#fqG4~E+zp0T)M^vy4K;&Yg3U?W@J&uO=Oy5NY&*=84Oxxak|T0EG&B> zbx{?0@xCunLa{JL!A&WwzA-Uaw$acu1RZ~*hui+pqe7IRM8d!#GaCW8{fq$5nX1}a z=h8Txx{&?1%j&IF8_PQp`L#mcAK@(Yk~e68=6E++b$-_i&2GMOX8|u72L6C^Kyx@P zPjwN1j+ll_N-6>T9%A6%pg#X!6GsSn6P_+^(9K8xE(1=R!R-pSTTiQL<0+upEjY*T z+QR;)!Zh0RAQNX*)oj||L)19q|7v_s1;6rS{qK9M0Ne_D$u7Y}ry^Bo@k z5${mLCFth)EemM&C`R;jM@d8~Rg|8Er$AZs&^0!c{>(5bDv541yb-)bFnRwaP>q16Y*`Uw2bZ&eM9~$Fl4;#24A`9mvmh z1)CCVmSKQ}`)~RUGHtLzHCLHxB+i2?ZFas-@qu$%3~dgS zRQy_*=jGp=Ht-#<;hrhK+n!*i>b72%5_Ev6H8Q?UOtXcJpp=%siox=MGk8fX=R`o`VPP1}c6nF=tY?a8O?sajS zflo3``l39)fRA>2tJwDT9g7oCKP|p)Zq;wxX*&!Gd1AizYDb83cs1`Z8(9Y=``k0k zN|OjW?q*H*?zi33nDH4}sr2J3w@>QtF}{8>0bTfiF+5?$4;AMR+M?0u@tOe9uX#8WY`v`5 z;g^vwsZmfuw0Vfd6o(32BQ?d$>0?F48TYprZW)}qrl(dvy$n0Sk;XP4m9tq(ejL-9 z9Eeu%gfzbObT1{mc*r4B1$sGq3^r1YR+?KNYQzfSS*g7?VQNspr{fDBoDOyg*j$6? z$<8GAToa`TE_cpyGuN(n7>ZG*!cuiyoQa7p3G!@0ove0aJKpT&*&@aIeY`J$)!aI{ z?MwSXmE*b5qGb@s6oEa6^Fjaa2r_pX1N50Re-OHYm}q_eFqD5JD=^qYxKy4AQUsuv zk(#Xs9L}fc%&K`Yg_isr1Mz;z&^7xJ6h4MdGJJVKH$SZV+ve7WMWdto6pMQJf@aIW zFO|C2I*YyOp;9GEG^zoMhA8_&+TcYflOT9TN*f*JwK~OdlZ7vIJY$NnwY|x2lC#;D zp4J}IX=SE`SO@!&DJ!HfQLXrj&r*_>)qmOF4p{iim9lzFYI?#_%Y~eO!{^eU!gXEH z)o>IhLzqOsjO7W>4!3#a3Vt4KNvTK6D+q?IjmnW9jo?`nP2WDX(8i1mI^Zs~O&55umh+A!>c@XA zpC8PL)qD@2|JVYbr1d40Ph{2yDtIIU30=Jqko$?=ku<$Pp?1^U8vJgPVG&U6U)Akf zjh1FjykD{ak(G;r(0B`SS>9gx^XNESSJ#aEgPoQMHaZpWc~2w9W|~Qysc!Bv_VRb2 zn2)Vwu+C9+Wu3K{t;^Sfz{MJ8OHeOYyc7V$l~KVa$Fn#>6>`aZm6)<3x zzfyT>K7S&x+t45)@tA~q7TF{JeB3? z_XiSx&u7v)U(qsgSTSN+k7%0)?|20H%y_IHpO{k`Df`ItW-o`I^VHCm&k$R#Lp?rK z#A$6AsTn3$o}(h%1=N(QHnMGYhQzMhYPs~8loS^iKD`0q>B(XL;CbuR1?>|5);=}+Y)y|ak9`e zjYg#;CUiURGi)TZvv|gFC^3xZ=*NnY4^(7QvKOr3XpxtMM|WO9*RngvuaRP~@Y$U7t!m1~I%?Rs9}Odb0DNSLz0W$V4snz%UWX_y*InY1VR*`bgS+$tM`LKWy9- zt}iQHuyPRAZDl#0%A}1DNq3S;8%M5DdMNkhQy*fg3ShN-uhl|H${HQh$`&i^ zm%`GZfsehDmmo1XWOsc_TPhV*6vs(tnA|Ou)pw|=4Vp|Uiq#J5sSPpCe;R8OEc9BGT0d{2^nkHr_eCpaV*U z@nq}L8VKL|TD>;1;&m1bK+2$Qpex}fbfz{5SdzjGod#dQ1WoACxqlhh;RmB4I^KVu z=bn!6vW=c)%jQ7O^t{pua6Z1<`Bu>pd0l<*F33FpAk>!;%0kZa1VAQkBnqrr1Om;`WhIg_?Pm$E|MRvu|wsRdDf14jZ`C^ zs+IU%ynYfLB}Y>x@-Wmx7&3L0#7-~iDocZ8$P@^kl|j9?e7;ldgeto0fzw)v`gd8S-Ym@*?JERJL3 zD@Fpwj6*zo!t1<2X@!b-*IR!R)*-)MuV@g*Tbp?zwLl^g`ZZ-~in_5}Ba`6a9B`|V zV^QD4z`lEDbu>EsYC}r;$yf(^sC2-SX!}D7+3(k9dkc0CYR>8o?zLLt8UYmmal1D0 zxLln$q()23$L+k{*}KBCHjS4?{iYD#CAP*DVQfIYC<8tUD&k|07@d3aAh!eM^6H@; zwyo=4=NwUE^uUuaY6+*U7_Llen8HsX!6p3Q}BV7@k!o*B^t8wg3F&7G%_!!sdP(6 z^G!CCmDQc8w%YoagdeHMMYk9TdP^OqTztt55(jg7$~*QKulb0`)I~ysx0CbetmxIi z$BdUu%Y;Xw?79`9Hg%keTXYd`XXBS3+BHn*8x@fEO()GONijxdQN<*_6$Kjknljs_ zx|TL~oHqWg9bsyZW6z8R_)wb$NuMeG1RPCyd(vGM`$bW1#U1l{Nv@y0yiCFxym^$Z zS_3I{*UlzljL0{(O_-a+Q-~YK>)I%_C{k0d2W<1Y62W}zW2V+*##8=X=fjX)p-ewP zn}YXW7Hz6}2ZB||WzP@uPaXZ{i5h!8;;_5aq@py)bD7ZhQOLp$1v}|p-@e15)7b>) zmU9XwkXf1^Psl&M$2qS>KQ5#}{*)+i&i*+BORW_8hscsviv;$?(((i%Z)C7Ro~-%fO0`{adN+WPcU0yv@+af zu0#IQd}z=BJEP`bp3Dz5@WaoVR~9iC|EbYplFcHYL2{bQidUrCtj^2Vo+f7;yJzy( z5RGbB*KL%VE(&A%{BzDy&Pe3q(@(jC!rvT@fK^i~|0(KAuqr*{a&9WpUir4ue}+!3 zj&K?eQnCYeu~Mn$1(kfcw<5gG41nbD040Bd>=~Ki4QLoLU;NTiK7%@bK3Dif%zVZl ziFZ`&H;5AXgUg95AbjFvkjWnj3#$4hF#DY|r`^=V-D?dJXA+r`RCUh$xIbqF^VG!i zp`^4k;x^@Rg=fY0>06z73Zf$v_OkEUWtqs5Wrk6*8$Q{IvX3>{qA4XQ)F6XwV{9pr zWZ$!v7+dz?tNMO_f5Pwg{hjlid+vGeec$^$_dMsGdtXVG=7!7+JPcG+RLsUkdRM8a zsPU)$TXeLiZHD#p2`Vbckg=YQb%^uoBl@@I*91CGYAESt4z+$pW}SVK|Bx;*`nM??2pR(x?b5p)!K~D$mL3hfLZbuJ(+27hjR9=3Vhv^n z*grFkh!<&pyS0IZB7)R`RpV}N|E>YNg8t+F-xr9iPS=>;VYxE*@BLLy173P}h4yd2 z8K&f7j)nEjVv$Hh~cE5y~5OxSI1TmR${cn_t_Yxb{PKl2J1Us`xwZF0R#$j>mq9v#!)ti#i+y!D8J#{l zX6syeBXDtqQPOs7LLckh)%%FBobRq1>8|Ogp$fY$8TD(>*;yKAV6nNNG_^kEb#hD@ zYB6LnH8Dy4o}+cLiFF_UblzmdSnQEoft;+n)^6co^NMuC`d!ASQ*%j`=+(PheYHP0 zBs;fLdg>Ato$_QJU#qYb8tn3k*maVzc3l33y{zyZ6Z@Re>7UGN@8;4jlJ_YrS|jb> z2P$Bi2d=qD{;{pw!r(-MX!et}rtpfu?)x49o}(XSIa~8tQrugXgd>J!(Zc%2SWChH#b~tkn;n%<^5cu75u(3H3%2)RHY|%+dgP+vybF#g1=6BirXqiNuH3EMzm+eKcoXVB zD8UP)4ln-4V_1=_xT{(w)h;5wM{EJ;pXizub~H`cqI!oH&zd+$@-q1%jtEr!*!2;b80AeRw)O_R5fUY|uGl z;;}Z*97F7Of#dB#7ona=quY>kO1%$ZS9KAoP~F0#UNr-9t}q@JAw9yr)=4E+rt$Of-W}6SuJ#lU-r?M+H zB$}WE<^A>+hLPhSE+Z*&%LyJom+&~k)uIpQLxv$f#!=TwU-bL>o^hZmGHE184bs1D zZd|#PBVChQ33GE!9o8ePss)V{#xXV(F5sr4s-Lp{B#6J;2rS4HYDez=+-UTX=bLD# zUFgYdyAHc~XG&#>kAb`EL*j5`RcLiB2uXb{@%mi#TsP4r()yi65AdoiG+}0Zvn>{qXwI4u19rAAS5m(pjVP^R z=0ZL4tDD7t!#3^hU)ZY`7)pWQ7yK0^o+%bdFTPANI+0~&yMSuSSo~h=`^b#gqC`>( zdFwy|_VlgFijlfg=>LJ<@(*Yf$m&(R^3D`dhY|bMR<3=;4XaOFL#LhiDsl>ahAdh2@MeX}ScqIQl-$51M^+LF+x{8> zo}+~8Tzrwnb0e2S0#Y4~iOHCIJJ9HkOVL|XpPy|$SmZ5;kC@iq-{ST0+9T4~aP7Lq z(2Q<9T-`fiq7xMhikGRcKXP^KYyALfAJoV4v(?QUD*8-MH6QFac+tu{>??>T6|f_y zFHzTP9s61mZ#j(nPB!Xy=LuatyShIIWq^jMPrZ2@ykf`Q-+PlzP2tFno2;82$)O2r z{;1ffaM;xslPSk(sN^z>AXr}*r)>2R-1Ya>-H%A}#vfB}Ubwr)+-sBIRiQuc{-Fx)SHh`gIe_32we#YxCVf$DvE- z!dQHTXLe$;#U*Ab@)D?qI|tp+yZOlV&E7*YuYL()bqptF(C042)~-a_dJ_D;)3eKF zouK;UM=7+?1xXZd_TaGLQ}!p?WdyPuR4_M6D~7opZZMtf>rk&~gy3y66aZySl30k! z_F5(YD*5aHpNsbRvOYUy=laY{19OJ~%E<(gmA`O@++>q1*@x3$;rN38qEPRoHom!# z%Z9gX7M~Pp0xs8V>zGkFSisA}Q2p8cP(pM?yW=?PmVhKUSMXc_8;mdN^tzNM#bvXa z{fPA|6G%2s%g$k?=8acg`G=0vp-j}QoLZEl1yR~*p8sw2mqY^P43kS!dbXV88rRMF zSA|6aVeH3xyfSbkQMq!!Yu!Pye4g^d2uXyMoh*CEws+Cv*dyOU&4|k1zgR8DysAYV z&djB2u*AH`l$FUtP=91}5KZO6EDu58Hv5>(v~u7?a#HvT!3Q_2=6Vz&^I)8RXCJVL zb-rGBp%G#nhT8V!qCKmKByQ@Ii}mHr#_5GR;q@8p;3>UUEd&i?Qx z&*)I2oom=*y(Z?;!)M1(bFQv*jgHrZIu9>>-yJZ0!V9&hsm$|E=nxraw} zTx;{yOF~)%s(PvTQ7%$3WjkdpPB5cM1~oZTzpsnq@d{JwS(0Pw_CzFGF~aFa{JbHu zizQsI)VXI>!%Ta=KNm$+%6~Z1z^ys2roT4474QHRaz+#ohGKtVR*hK}Ta#4RaPMuy zebPq+!bL?;8&PHllx@~T8+@kCZCCiN=Kbi(L}5v2_v%LK*;e(RwNggz5F#IEK8<8- z$1QnA&5SbMI6CXV`5}q{bphjnqxikYsgX@9_{1jF?2ZWdPC}^JGN5z4AL}(V za`(k!f)<#+Q2+-u9X1s1o5ON}UWuBl^l2P=^I|AWs84>^5bovm1okOBCKl;7fD>mLyV<1iGxk=d$l^aG<#Y&)%f>&;{h|2mp z`bM(y+3v${qMF^gb|Ux7HbHro{H?Jhb`>s9G*=qC(3a@2PK!%_w>L{l5KHVCU6xz? z>OH8lDWRG+A_p&YL`;Zv+z$sc)vG@Gl^eOy$qDn1j29@{_mkpSLLa00kdk z{Z@?7Ny~p6o&fF_dD7e2A+y)ApfNF}7k|d5_GCI+t_Q2S6e&T@aBg=26wcG|=D!fS z!5aF&TF6?5vvU^Qofj_ZQq&!=*RbyNTDxQSVnmzzx!4y`{(-mMIKJ^KLm{QeI-@x4 zjgXh`CE9o8g0r>zuU~+mr3{)WbzByFCSPySUn44e1Uz+!Rk!tL;oP!^^N&<|QMLj1!JZ>BPYb=wZHuCiVwwxFm{Vx;svpFkjj zDZh%s2 z%hz;j2-9cYzMBToEnXfX-Dcvg>cPR}xm`(xTpB*{E3l8toZsvzc!upV^Zm2GFKpqo z3%=cT47&Dbu81cx#RY^@l|7f%sZ2-4Eib|Ci*;pEIz%0fGE_7!(%U^PVVhJkLIF1& z)uK)&!vtpY-X+MT1-4~*M9OMG8TF@Wut61MCkkDW=Uq$-Yc>%c@{aGRBJH%VLNS#@ zk-W;o=#8j?^@?dKa07q+&l{!CH&~GCJmPAW|A^k^{qsZT#;roqzV&+-(dQvSoc3>L zTlhY9U$84Q8davpbZCNcCQl1wQ2n^enKMyO@t~BxZ}Z(BOwEaELR0nTT9e}~UpW8^ z2}3YY%DvRV*uuhl*Wz`?(O^6>iZW*XGiOv%YOzCd5^{Wx(&DRzzVC+{U0Wjb9+ppR6;pACb%*m)oa&rf7KvaaDnJ z2bGqs&kvFIwO@{v0`-`h5ZNd--;cZ4sN)ZJT)>~UpeIRE7pdn&`yZ{VF1EELZU@k% zvF`yzMpa^}YP8qxhKDJ0Q+RpwMXK#0Vxz?82&c^%5G@SDe zdk>9?HGE1Y+|I^W0#nM4q{R!; z2=6BE;b*MgH}w#k#o}>4gtctW@GeyG)6f%pbjn}s0VQBJd)2RV8Tux@`VCv!rBIHu z3f;Y(5jl_DmiqOlkVu1U9*t<85xQY6xxtjzM}yba*iab#GuE_GN2BgLvdI_r#LT|_ z{(TEN12BwvkIUU_xUn?ix6~8T4l#Bqedon&X=wE1qOoI_$5rc{0Nt0e{ZZ=M9CKaKG@ zMssSRRMf3rxsPc+=B>I+x|n|He{jLm-&sWMtLz5IS-;O}{U0*W5Bvtj>+zNz{-rmG z+80l0PAcPHEG0&7L@fNN$bA~mZ#Y3c!1ae#f&K4C1ADrCv-O630V8Q6>nHGN*zr0BE1BO)jsn!h4*wv1?% kQ6Ks1e=y#EiC|CWeFW$^8}kF3Q_x9etZ%OOS{HTqU$56}y8r+H literal 0 HcmV?d00001 From 33ca39c3bb9ff47e7c735bb9fe9dd7d94ab38546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Wed, 15 Jan 2025 13:09:34 +0100 Subject: [PATCH 13/61] fix(ToggleButton): add type for the `size` property (#4459) --- .../uilib/components/button/properties.mdx | 30 +--- .../components/toggle-button/properties.mdx | 41 +----- .../src/components/button/ButtonDocs.ts | 129 ++++++++++++++++++ .../button/__tests__/Button.test.tsx | 50 +++++-- .../toggle-button/ToggleButton.d.ts | 8 +- .../components/toggle-button/ToggleButton.js | 2 + .../toggle-button/ToggleButtonDocs.ts | 81 +++++++++++ .../toggle-button/ToggleButtonGroupDocs.ts | 84 ++++++++++++ .../__tests__/ToggleButton.test.tsx | 39 ++++++ 9 files changed, 387 insertions(+), 77 deletions(-) create mode 100644 packages/dnb-eufemia/src/components/button/ButtonDocs.ts create mode 100644 packages/dnb-eufemia/src/components/toggle-button/ToggleButtonDocs.ts create mode 100644 packages/dnb-eufemia/src/components/toggle-button/ToggleButtonGroupDocs.ts diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/button/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/button/properties.mdx index d43f477b42f..f0592b56ccc 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/button/properties.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/button/properties.mdx @@ -2,34 +2,12 @@ showTabs: true --- +import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable' +import { ButtonProperties } from '@dnb/eufemia/src/components/button/ButtonDocs' + ## Properties -| Properties | Description | -| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | _(optional)_ `button`, `reset` or `submit` for the `type` HTML attribute. Defaults to `button` for legacy reasons. | -| `text` or `children` | _(optional)_ the content of the button can be a string or a React Element. | -| `aria-label` or `title` | _(optional)_ required if there is no text in the button. If `text` and `children` are undefined, setting the `title` property will automatically set `aria-label` with the same value. | -| `variant` | _(optional)_ defines the kind of button. Possible values are `primary`, `secondary`, `tertiary` and `signal`. Defaults to `primary` (or `secondary` if icon only). | -| `size` | _(optional)_ the size of the button. For now there is `small`, `medium`, `default` and `large`. | -| `icon` | _(optional)_ to be included in the button. [Primary Icons](/icons/primary) can be set as a string (e.g. `icon="chevron_right"`), other icons should be set as React elements. | -| `icon_position` | _(optional)_ position of icon inside the button. Set to `left` or `right`. Tertiary button variant also supports `top`. Defaults to `right` if not set. | -| `icon_size` | _(optional)_ define icon width and height. Defaults to 16px. | -| `href` | _(optional)_ if you want the button to behave as a link. Use with caution! A link should normally visually be a link and not a button. | -| `target` | _(optional)_ When button behaves as a link. Used to specify where to open the linked document, specified by `href`. Possible values are `_self`, `_blank`, `_parent` and `_top`. | -| `rel` | _(optional)_ When button behaves as a link. Used to specify the relationship between a linked resource and the current document. Examples(non-exhaustive list) of values are `nofollow`, `search`, and `tag`. | -| `to` | _(optional)_ use this property only if you are using a router Link component as the `element` that uses the `to` property to declare the navigation url. | -| `wrap` | _(optional)_ if set to `true` the button text will wrap in to new lines if the overflow point is reached. Defaults to `false`. | -| `stretch` | _(optional)_ set it to `true` in order to stretch the button to the available space. Defaults to false. | -| `bounding` | _(optional)_ set it to `true` in order to extend the bounding box (above the visual button background). You may also look into the HTML class `dnb-button__bounding` if it needs some CSS customization in order to get the particular button right for your use-case. | -| `element` | _(optional)_ only meant to be used for special use cases. Defaults to `button` or `a` depending if href is set or not. | -| `custom_content` | _(optional)_ if you need to inject completely custom markup (React Element) into the button component. You have then to handle alignment and styling by yourself. | -| `skeleton` | _(optional)_ if set to `true`, an overlaying skeleton with animation will be shown. | -| `tooltip` | _(optional)_ Provide a string or a React Element to be shown as the tooltip content. | -| `status` | _(optional)_ set it to either `status="error"` or a text with a status message. The style defaults to an error message. You can use `true` to only get the status color, without a message. | -| `status_state` | _(optional)_ defines the state of the status. Currently there are two statuses `[error, info]`. Defaults to `error`. | -| `status_props` | _(optional)_ use an object to define additional FormStatus properties. | -| `globalStatus` | _(optional)_ the [configuration](/uilib/components/global-status/properties/#configuration-object) used for the target [GlobalStatus](/uilib/components/global-status). | -| [Space](/uilib/layout/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | + ### Unstyled variant diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/toggle-button/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/toggle-button/properties.mdx index 14042d0d7de..e923246d68d 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/toggle-button/properties.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/toggle-button/properties.mdx @@ -2,43 +2,14 @@ showTabs: true --- +import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable' +import { ToggleButtonProperties } from '@dnb/eufemia/src/components/toggle-button/ToggleButtonDocs' +import { ToggleButtonGroupProperties } from '@dnb/eufemia/src/components/toggle-button/ToggleButtonGroupDocs' + ## `ToggleButton` properties -| Properties | Description | -| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `value` | _(required)_ defines the `value` as a string. Use it to get the value during the `on_change` event listener callback in the **ToggleButtonGroup**. | -| `text` | _(required)_ the text shown in the ToggleButton. | -| `checked` | _(optional)_ determine whether the ToggleButton is checked or not. The default will be `false`. | -| `title` | _(optional)_ the `title` of the input - describing it a bit further for accessibility reasons. | -| `label` | _(optional)_ use either the `label` property or provide a custom one. | -| `icon` | _(optional)_ icon to be included in the toggle button. | -| `icon_position` | _(optional)_ position of the icon inside the toggle button. Set to `left` or `right`. Defaults to `right` if not set. | -| `icon_size` | _(optional)_ define icon width and height. Defaults to 16px. | -| `status` | _(optional)_ text with a status message. The style defaults to an error message. You can use `true` to only get the status color, without a message. | -| `status_state` | _(optional)_ defines the state of the status. Currently, there are two statuses `[error, info]`. Defaults to `error`. | -| `status_props` | _(optional)_ use an object to define additional FormStatus properties. | -| `globalStatus` | _(optional)_ the [configuration](/uilib/components/global-status/properties/#configuration-object) used for the target [GlobalStatus](/uilib/components/global-status). | -| `suffix` | _(optional)_ text describing the content of the ToggleButton more than the label. You can also send in a React component, so it gets wrapped inside the ToggleButton component. | -| `skeleton` | _(optional)_ if set to `true`, an overlaying skeleton with animation will be shown. | -| [Space](/uilib/layout/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | + ## `ToggleButton.Group` properties -| Properties | Description | -| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `value` | _(optional)_ defines the pre-selected ToggleButton button. The value has to match the one provided in the ToggleButton button. Use a string value. | -| `values` | _(optional)_ defines the pre-selected ToggleButton buttons in `multiselect` mode. The values have to match the one provided in the ToggleButton buttons. Use array, either as JS or JSON string. | -| `multiselect` | _(optional)_ defines if the ToggleButton's should act as a multi-selectable list of toggle buttons. Defaults to `false`. | -| `layout_direction` | _(optional)_ Define the layout direction of the ToggleButton buttons. Can be either `column` or `row`. Defaults to `column`. | -| `title` | _(optional)_ the `title` of group, describing it a bit further for accessibility reasons. | -| `status` | _(optional)_ uses the `form-status` component to show failure messages. | -| `status_state` | _(optional)_ defines the state of the status. Currently, there are two statuses `[error, info]`. Defaults to `error`. | -| `status_props` | _(optional)_ use an object to define additional FormStatus properties. | -| `globalStatus` | _(optional)_ the [configuration](/uilib/components/global-status/properties/#configuration-object) used for the target [GlobalStatus](/uilib/components/global-status). | -| `label` | _(optional)_ use either the `label` property or provide a custom one. | -| `label_direction` | _(optional)_ to define the `label` layout direction on how the next element should be placed on. Can be either `vertical` or `horizontal`. Defaults to `horizontal`. | -| `label_sr_only` | _(optional)_ use `true` to make the label only readable by screen readers. | -| `vertical` | _(optional)_ will force both `direction` and `label_direction` to be **vertical** if set to `true`. | -| `suffix` | _(optional)_ text describing the content of the ToggleButtonGroup more than the label. You can also send in a React component, so it gets wrapped inside the ToggleButtonGroup component. | -| `skeleton` | _(optional)_ if set to `true`, an overlaying skeleton with animation will be shown. | -| [Space](/uilib/layout/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | + diff --git a/packages/dnb-eufemia/src/components/button/ButtonDocs.ts b/packages/dnb-eufemia/src/components/button/ButtonDocs.ts new file mode 100644 index 00000000000..a9bc2daa38f --- /dev/null +++ b/packages/dnb-eufemia/src/components/button/ButtonDocs.ts @@ -0,0 +1,129 @@ +import { PropertiesTableProps } from '../../shared/types' + +export const ButtonProperties: PropertiesTableProps = { + type: { + doc: 'The type HTML attribute. Defaults to `button` for legacy reasons.', + type: ['button', 'reset', 'submit'], + status: 'optional', + }, + text: { + doc: 'The content of the button can be a string or a React Element.', + type: ['string', 'React.ReactNode'], + status: 'optional', + }, + 'aria-label': { + doc: 'Required if there is no text in the button. If `text` and `children` are undefined, setting the `title` property will automatically set `aria-label` with the same value.', + type: 'string', + status: 'optional', + }, + title: { + doc: 'Required if there is no text in the button. If `text` and `children` are undefined, setting the `title` property will automatically set `aria-label` with the same value.', + type: 'string', + status: 'optional', + }, + variant: { + doc: 'Defines the kind of button. Possible values are `primary`, `secondary`, `tertiary` and `signal`. Defaults to `primary` (or `secondary` if icon only).', + type: ['primary', 'secondary', 'tertiary', 'signal'], + status: 'optional', + }, + size: { + doc: 'The size of the button. For now there is `small`, `medium`, `default` and `large`.', + type: ['small', 'medium', 'default', 'large'], + status: 'optional', + }, + icon: { + doc: 'To be included in the button. [Primary Icons](/icons/primary) can be set as a string (e.g. `icon="chevron_right"`), other icons should be set as React elements.', + type: ['string', 'React.ReactNode'], + status: 'optional', + }, + icon_position: { + doc: 'Position of icon inside the button. Set to `left` or `right`. Tertiary button variant also supports `top`. Defaults to `right` if not set.', + type: ['left', 'right', 'top'], + status: 'optional', + }, + icon_size: { + doc: 'Define icon width and height. Defaults to 16px.', + type: 'string', + status: 'optional', + }, + href: { + doc: 'If you want the button to behave as a link. Use with caution! A link should normally visually be a link and not a button.', + type: 'string', + status: 'optional', + }, + target: { + doc: 'When button behaves as a link. Used to specify where to open the linked document, specified by `href`. Possible values are `_self`, `_blank`, `_parent` and `_top`.', + type: ['_self', '_blank', '_parent', '_top'], + status: 'optional', + }, + rel: { + doc: 'When button behaves as a link. Used to specify the relationship between a linked resource and the current document. Examples(non-exhaustive list) of values are `nofollow`, `search`, and `tag`.', + type: 'string', + status: 'optional', + }, + to: { + doc: 'Use this property only if you are using a router Link component as the `element` that uses the `to` property to declare the navigation url.', + type: 'string', + status: 'optional', + }, + wrap: { + doc: 'If set to `true` the button text will wrap in to new lines if the overflow point is reached. Defaults to `false`.', + type: 'boolean', + status: 'optional', + }, + stretch: { + doc: 'Set it to `true` in order to stretch the button to the available space. Defaults to false.', + type: 'boolean', + status: 'optional', + }, + bounding: { + doc: 'Set it to `true` in order to extend the bounding box (above the visual button background). You may also look into the HTML class `dnb-button__bounding` if it needs some CSS customization in order to get the particular button right for your use-case.', + type: 'boolean', + status: 'optional', + }, + element: { + doc: 'Only meant to be used for special use cases. Defaults to `button` or `a` depending if href is set or not.', + type: 'string', + status: 'optional', + }, + custom_content: { + doc: 'If you need to inject completely custom markup (React Element) into the button component. You have then to handle alignment and styling by yourself.', + type: 'React.ReactNode', + status: 'optional', + }, + skeleton: { + doc: 'If set to `true`, an overlaying skeleton with animation will be shown.', + type: 'boolean', + status: 'optional', + }, + tooltip: { + doc: 'Provide a string or a React Element to be shown as the tooltip content.', + type: ['string', 'React.ReactNode'], + status: 'optional', + }, + status: { + doc: 'Set it to either `status="error"` or a text with a status message. The style defaults to an error message. You can use `true` to only get the status color, without a message.', + type: ['error', 'info', 'boolean'], + status: 'optional', + }, + status_state: { + doc: 'Defines the state of the status. Currently there are two statuses `[error, info]`. Defaults to `error`.', + type: ['error', 'info'], + status: 'optional', + }, + status_props: { + doc: 'Use an object to define additional FormStatus properties.', + type: 'object', + status: 'optional', + }, + globalStatus: { + doc: 'The [configuration](/uilib/components/global-status/properties/#configuration-object) used for the target [GlobalStatus](/uilib/components/global-status).', + type: 'object', + status: 'optional', + }, + '[Space](/uilib/layout/space/properties)': { + doc: 'Spacing properties like `top` or `bottom` are supported.', + type: ['string', 'object'], + status: 'optional', + }, +} diff --git a/packages/dnb-eufemia/src/components/button/__tests__/Button.test.tsx b/packages/dnb-eufemia/src/components/button/__tests__/Button.test.tsx index 9a57ccbda43..9a536bc2817 100644 --- a/packages/dnb-eufemia/src/components/button/__tests__/Button.test.tsx +++ b/packages/dnb-eufemia/src/components/button/__tests__/Button.test.tsx @@ -56,21 +56,43 @@ describe('Button component', () => { expect(button.classList).not.toContain('dnb-button--has-text') }) - it('has size set to medium when button size is default', () => { - render( diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/IterateItemContext.ts b/packages/dnb-eufemia/src/extensions/forms/Iterate/IterateItemContext.ts index d5d042b7f1e..ff923449848 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/IterateItemContext.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/IterateItemContext.ts @@ -14,7 +14,7 @@ export interface IterateItemContextState { isNew?: boolean path?: Path itemPath?: Path - nestedIteratePath?: Path + absolutePath?: Path arrayValue?: Array containerMode?: ContainerMode previousContainerMode?: ContainerMode diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/PushButton.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/PushButton.tsx index 5152e7fb294..3b4629308e3 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/PushButton.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/PushButton.tsx @@ -3,7 +3,11 @@ import classnames from 'classnames' import { Button } from '../../../../components' import { ButtonProps } from '../../../../components/Button' import IterateItemContext from '../IterateItemContext' -import { useArrayLimit, useSwitchContainerMode } from '../hooks' +import { + useArrayLimit, + useItemPath, + useSwitchContainerMode, +} from '../hooks' import { omitDataValueReadWriteProps, Path } from '../../types' import { add } from '../../../../icons' import DataContext from '../../DataContext/Context' @@ -12,6 +16,7 @@ import { convertJsxToString } from '../../../../shared/component-helper' export type Props = ButtonProps & { path?: Path + itemPath?: Path pushValue: unknown | ((value: unknown) => void) /** @@ -25,10 +30,20 @@ function PushButton(props: Props) { const iterateItemContext = useContext(IterateItemContext) const { handlePush } = iterateItemContext ?? {} - const { pushValue, className, path, text, children, ...restProps } = - props + const { + pushValue, + className, + path, + itemPath, + text, + children, + ...restProps + } = props const buttonProps = omitDataValueReadWriteProps(restProps) - const arrayValue = useDataValue().getValueByPath(path) + + const { absolutePath } = useItemPath(itemPath) + + const arrayValue = useDataValue().getValueByPath(path || absolutePath) const { hasReachedLimit, setShowStatus } = useArrayLimit(path) @@ -47,19 +62,25 @@ function PushButton(props: Props) { const newValue = typeof pushValue === 'function' ? pushValue(arrayValue) : pushValue - if (handlePush) { + if (handlePush && !absolutePath) { // Inside an Iterate element - make the change through the Iterate component handlePush(newValue) } else { // If not inside an iterate, it could still manipulate a source data set through useFieldProps - await handlePathChange?.(path, [...(arrayValue ?? []), newValue]) + await handlePathChange?.(path || absolutePath, [ + ...(arrayValue ?? []), + newValue, + ]) } - setTimeout(() => { - setLastItemContainerMode('view') - }, 100) // UX improvement because of the "openDelay" + if (!absolutePath) { + setTimeout(() => { + setLastItemContainerMode('view') + }, 100) // UX improvement because of the "openDelay" + } }, [ arrayValue, + absolutePath, handlePathChange, handlePush, hasReachedLimit, diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/PushButtonDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/PushButtonDocs.ts index d6863aa80df..37cf7d87e7f 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/PushButtonDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/PushButtonDocs.ts @@ -6,6 +6,11 @@ export const PushButtonProperties: PropertiesTableProps = { type: 'string', status: 'required', }, + itemPath: { + doc: 'The path to the item in a nested array, to add the new item to.', + type: 'string', + status: 'optional', + }, pushValue: { doc: 'The element to add to the array when the button is clicked. Can be a function to returns the push value.', type: 'unknown', diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/__tests__/PushButton.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/__tests__/PushButton.test.tsx index e0856d8ae5c..42a049ed233 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/__tests__/PushButton.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushButton/__tests__/PushButton.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { render, fireEvent, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import IterateItemContext from '../../IterateItemContext' -import { Field, Form, Iterate } from '../../..' +import { DataContext, Field, Form, Iterate } from '../../..' describe('PushButton', () => { it('should call handlePush when clicked inside an Iterate element', () => { @@ -254,4 +254,232 @@ describe('PushButton', () => { expect(document.querySelector('.dnb-form-status')).toBeNull() }) }) + + describe('itemPath', () => { + it('should add item to the correct array', async () => { + let collectedData = null + + render( + + + + + + + + + + + + {(context) => { + collectedData = context.data + return null + }} + + + ) + + expect(collectedData).toEqual({ + outer: [{ inner: ['foo'] }], + }) + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-push-button') + ) + + expect(collectedData).toEqual({ + outer: [{ inner: ['foo', 'bar'] }], + }) + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-remove-element-button') + ) + + expect(collectedData).toEqual({ + outer: [{ inner: ['bar'] }], + }) + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-remove-element-button') + ) + + expect(collectedData).toEqual({ + outer: [{ inner: [] }], + }) + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-push-button') + ) + + expect(collectedData).toEqual({ + outer: [{ inner: ['bar'] }], + }) + }) + + it('should add item within PushContainer to the correct array', async () => { + let outerData = null + let pushContainerData = null + + render( + + + + + + + + + + + + + + + + + + {(context) => { + pushContainerData = context.data + return null + }} + + + + + {(context) => { + outerData = context.data + return null + }} + + + ) + + expect(outerData).toEqual(undefined) + expect(pushContainerData).toEqual({ + pushContainerItems: [{}], + }) + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-push-button') + ) + + expect(outerData).toEqual(undefined) + expect(pushContainerData).toEqual({ + pushContainerItems: [ + { + inner: ['new value'], + }, + ], + }) + + await userEvent.click( + document.querySelector('.dnb-push-container__done-button') + ) + + expect(outerData).toEqual({ outer: [{ inner: ['new value'] }] }) + expect(pushContainerData).toEqual({ + pushContainerItems: [{}], + }) + }) + + it('should stay in edit mode when pushing new item (with changed items beforehand)', async () => { + let outerData = null + + let containerModeOfFirstItem = null + + const ContainerModeConsumer = () => { + const context = React.useContext(IterateItemContext) + if (context.index === 0) { + containerModeOfFirstItem = context.containerMode + } + + return null + } + + render( + + + + content + + + + + + + + + + + + + + + content + + + + {(context) => { + outerData = context.data + return null + }} + + + ) + + expect(containerModeOfFirstItem).toEqual('view') + expect(outerData).toEqual({ + outer: [ + { + inner: ['new value'], + }, + ], + }) + + await userEvent.click( + document.querySelectorAll('.dnb-push-container__done-button')[1] + ) + + expect(outerData).toEqual({ + outer: [ + { + inner: ['new value'], + }, + {}, + ], + }) + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-push-button') + ) + + expect(containerModeOfFirstItem).toEqual('view') + + await userEvent.click( + document.querySelector('.dnb-push-container__edit-button') + ) + + expect(containerModeOfFirstItem).toEqual('edit') + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-push-button') + ) + + await expect(() => { + expect(containerModeOfFirstItem).toEqual('view') + }).toNeverResolve() + expect(containerModeOfFirstItem).toEqual('edit') + }) + }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx index 5e712114798..4ce1de5912b 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx @@ -17,18 +17,43 @@ import OpenButton from './OpenButton' import { Flex, HeightAnimation } from '../../../../components' import { OnCommit, Path } from '../../types' import { SpacingProps } from '../../../../shared/types' -import { useArrayLimit, useSwitchContainerMode } from '../hooks' +import { + useArrayLimit, + useItemPath, + useSwitchContainerMode, +} from '../hooks' import Toolbar from '../Toolbar' import { useTranslation } from '../../hooks' import { ArrayItemAreaProps } from '../Array/ArrayItemArea' import { clearedData } from '../../DataContext/Provider' -export type Props = { +/** + * Deprecated, as it is supported by all major browsers and Node.js >=v18 + * So it's a question of time, when we will remove this polyfill + */ +import structuredClone from '@ungap/structured-clone' + +type OnlyPathRequired = { /** * The path to the array to add the new item to. */ path: Path + /** The sub path to the array to add the new item to. */ + itemPath?: Path +} + +type OnlyItemPathRequired = { + /** + * The path to the array to add the new item to. + */ + path?: Path + + /** The sub path to the array to add the new item to. */ + itemPath: Path +} + +export type Props = (OnlyPathRequired | OnlyItemPathRequired) & { /** * The title of the container. */ @@ -98,6 +123,7 @@ function PushContainer(props: AllProps) { isolatedData, bubbleValidation, path, + itemPath, title, required = requiredInherited, children, @@ -107,14 +133,22 @@ function PushContainer(props: AllProps) { ...rest } = props + const { absolutePath } = useItemPath(itemPath) const commitHandleRef = useRef<() => void>() const switchContainerModeRef = useRef<(mode: ContainerMode) => void>() const containerModeRef = useRef() - const { value: entries = [], moveValueToPath } = - useDataValue>(path) + const { + value: entries = [], + moveValueToPath, + getValueByPath, + } = useDataValue>(path || itemPath) - const { setNextContainerMode } = useSwitchContainerMode(path) - const { hasReachedLimit, setShowStatus } = useArrayLimit(path) + const { setNextContainerMode } = useSwitchContainerMode( + path || absolutePath + ) + const { hasReachedLimit, setShowStatus } = useArrayLimit( + path || absolutePath + ) const cancelHandler = useCallback(() => { if (hasReachedLimit) { setShowStatus(false) @@ -124,6 +158,7 @@ function PushContainer(props: AllProps) { const showOpenButton = showOpenButtonWhen?.(entries) const newItemContextProps: PushContainerContext = { path, + itemPath, entries, commitHandleRef, switchContainerMode: switchContainerModeRef.current, @@ -171,7 +206,11 @@ function PushContainer(props: AllProps) { } commitHandleRef={commitHandleRef} transformOnCommit={({ pushContainerItems }) => { - return moveValueToPath(path, [...entries, ...pushContainerItems]) + return moveValueToPath( + path || absolutePath, + [...entries, ...pushContainerItems], + absolutePath ? structuredClone(getValueByPath('/')) : {} + ) }} onCommit={(data, options) => { const { clearData, preventCommit } = options diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerContext.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerContext.tsx index ece6653f592..60c7b57f908 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerContext.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerContext.tsx @@ -4,6 +4,7 @@ import { ContainerMode } from '../Array' type PushContainerContext = { path: Path + itemPath: Path entries?: Array commitHandleRef: React.MutableRefObject<() => void> switchContainerMode?: (mode: ContainerMode) => void diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerDocs.ts index 2e85d22ebf9..c252d5697a9 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerDocs.ts @@ -7,6 +7,11 @@ export const PushContainerProperties: PropertiesTableProps = { type: 'string', status: 'required', }, + itemPath: { + doc: 'The path to the item in a nested array, to add the new item to.', + type: 'string', + status: 'optional', + }, title: { doc: 'The title of the container.', type: 'React.Node', diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/__tests__/PushContainer.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/__tests__/PushContainer.test.tsx index 5c83d3520dc..b5246a0fd17 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/__tests__/PushContainer.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/__tests__/PushContainer.test.tsx @@ -1059,4 +1059,142 @@ describe('PushContainer', () => { expect(document.querySelector('.dnb-form-status')).toBeNull() }) }) + + describe('itemPath', () => { + it('should add item to the correct array', async () => { + let collectedData = null + + render( + + + + + + + + + + + + + + {(context) => { + collectedData = context.data + return null + }} + + + ) + + expect(collectedData).toEqual({ + outer: [{ inner: [] }], + }) + + await userEvent.click( + document.querySelector('.dnb-push-container__done-button') + ) + + expect(collectedData).toEqual({ + outer: [{ inner: ['bar'] }], + }) + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-remove-element-button') + ) + + expect(collectedData).toEqual({ + outer: [{ inner: [] }], + }) + + await userEvent.click( + document.querySelector('.dnb-push-container__done-button') + ) + + expect(collectedData).toEqual({ + outer: [{ inner: ['bar'] }], + }) + }) + + it('should use itemPath to determine the initial amount of items when "showOpenButtonWhen" is used', async () => { + render( + + + + + + + } + showOpenButtonWhen={() => true} + > + + + + + ) + + expect( + document.querySelector('.dnb-forms-iterate-open-button') + ).toBeInTheDocument() + expect( + document.querySelector('.dnb-forms-section-block') + ).toHaveClass('dnb-height-animation--hidden') + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-open-button') + ) + + expect( + document.querySelector('.dnb-forms-section-block') + ).toHaveClass('dnb-height-animation--is-visible') + }) + + it('should show PushContainer based on the amount of items', async () => { + render( + + + + + + + + } + showOpenButtonWhen={(list) => list.length > 0} + > + + + + + ) + + expect( + document.querySelector('.dnb-forms-section-block') + ).toHaveClass('dnb-height-animation--hidden') + + await userEvent.click( + document.querySelector('.dnb-forms-iterate-remove-element-button') + ) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + expect( + document.querySelector('.dnb-forms-section-block') + ).toHaveClass('dnb-height-animation--is-visible') + }) + }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/EditButton.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/EditButton.tsx index 68fbfe65242..afd83526848 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/EditButton.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/EditButton.tsx @@ -16,6 +16,7 @@ export default function EditButton() { return (