diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 54fd92f0bc0..b7ca1002f38 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1393,11 +1393,13 @@ "ux_editor.component_properties.optionalIndicator": "Vis valgfri-indikator på ledetekst", "ux_editor.component_properties.options": "Alternativer", "ux_editor.component_properties.optionsId": "Kodeliste", + "ux_editor.component_properties.page": "Side", "ux_editor.component_properties.pageBreak": "PDF-innstillinger (pageBreak)", "ux_editor.component_properties.pageRef": "Navnet til siden det gjelder (pageRef)", "ux_editor.component_properties.pagination": "Sidenummerering", "ux_editor.component_properties.position": "Plassering av valuta", "ux_editor.component_properties.preselectedOptionIndex": "Angi det valget som skal være forhåndsvalgt.", + "ux_editor.component_properties.preselectedOptionIndex_button": "Plassering av forhåndsvalgt verdi (indeks)", "ux_editor.component_properties.queryParameters": "Parametere i spørringen", "ux_editor.component_properties.readOnly": "Feltet kan kun leses", "ux_editor.component_properties.receiver": "Den som mottar skjemaet", @@ -1425,7 +1427,7 @@ "ux_editor.component_properties.showIcon": "Vis ikon", "ux_editor.component_properties.showLabelsInTable": "Alternativene skal alltid vises i tabeller", "ux_editor.component_properties.showPageInAccordion": "Vis side i trekkspilliste", - "ux_editor.component_properties.showValidations": "Vis valideringer", + "ux_editor.component_properties.showValidations": "Vis valideringstyper", "ux_editor.component_properties.simplified": "Forenklet visning", "ux_editor.component_properties.size": "Størrelse", "ux_editor.component_properties.sortOrder": "Sorteringsrekkefølge", diff --git a/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/CollapsiblePropertyEditor.module.css b/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/CollapsiblePropertyEditor.module.css new file mode 100644 index 00000000000..ed1fcde3172 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/CollapsiblePropertyEditor.module.css @@ -0,0 +1,23 @@ +.collapsibleContainer { + display: flex; + flex-direction: row; + align-items: flex-end; + gap: var(--fds-spacing-4); + padding-bottom: var(--fds-spacing-4); + padding-top: var(--fds-spacing-1); +} + +.collapsibleContainerClosed { + margin-top: var(--fds-spacing-1); + margin-bottom: var(--fds-spacing-1); +} + +.editorContent { + flex: 1; +} + +.button { + display: flex; + gap: var(--fds-spacing-3); + padding-left: 0; +} diff --git a/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/CollapsiblePropertyEditor.test.tsx b/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/CollapsiblePropertyEditor.test.tsx new file mode 100644 index 00000000000..56f1f156a11 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/CollapsiblePropertyEditor.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + CollapsiblePropertyEditor, + type CollapsiblePropertyEditorProps, +} from './CollapsiblePropertyEditor'; +import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; +import { renderWithProviders } from 'app-development/test/mocks'; +import { textMock } from '@studio/testing/mocks/i18nMock'; + +// Test data +const label = 'Test label'; +const children =
Test children
; +const icon =
Test icon
; + +describe('CollapsiblePropertyEditor', () => { + it('should render the label', () => { + renderCollapsiblePropertyEditor({ label: label }); + expect(screen.getByText('Test label')).toBeInTheDocument(); + }); + + it('should render the icon', () => { + renderCollapsiblePropertyEditor({ icon:
Test icon
}); + expect(screen.getByText('Test icon')).toBeInTheDocument(); + }); + + it('should render the children', () => { + renderCollapsiblePropertyEditor(); + expect(screen.queryByText('Test children')).not.toBeInTheDocument(); + }); + + it('should render the children when the button is clicked', async () => { + const user = userEvent.setup(); + renderCollapsiblePropertyEditor(); + await user.click(screen.getByText('Test label')); + expect(screen.getByText('Test children')).toBeInTheDocument(); + }); + + it('should hide the children when the close button is clicked', async () => { + const user = userEvent.setup(); + renderCollapsiblePropertyEditor(); + await user.click(screen.getByText('Test label')); + expect(screen.getByText('Test children')).toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: textMock('general.close') })); + expect(screen.queryByText('Test children')).not.toBeInTheDocument(); + }); +}); + +const defaultProps: CollapsiblePropertyEditorProps = { + label, + children, + icon, +}; + +const renderCollapsiblePropertyEditor = (props: Partial = {}) => { + renderWithProviders()(); +}; diff --git a/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/CollapsiblePropertyEditor.tsx b/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/CollapsiblePropertyEditor.tsx new file mode 100644 index 00000000000..e4ea3c369ce --- /dev/null +++ b/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/CollapsiblePropertyEditor.tsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import { PlusCircleIcon, XMarkIcon } from '@studio/icons'; +import { StudioButton, StudioProperty } from '@studio/components'; +import classes from './CollapsiblePropertyEditor.module.css'; +import { useTranslation } from 'react-i18next'; +import cn from 'classnames'; + +export type CollapsiblePropertyEditorProps = { + label?: string; + children?: React.ReactNode; + icon?: React.ReactNode; + disabledCloseButton?: boolean; +}; + +export const CollapsiblePropertyEditor = ({ + label, + children, + disabledCloseButton = false, + icon = , +}: CollapsiblePropertyEditorProps) => { + const { t } = useTranslation(); + const [isVisible, setIsVisible] = useState(false); + + return ( +
+ {!isVisible ? ( + setIsVisible(true)} + property={label} + /> + ) : ( + <> +
{children}
+ {!disabledCloseButton && ( + } + onClick={() => setIsVisible(false)} + title={t('general.close')} + variant='secondary' + /> + )} + + )} +
+ ); +}; diff --git a/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/index.ts b/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/index.ts new file mode 100644 index 00000000000..b7790d086df --- /dev/null +++ b/frontend/packages/ux-editor/src/components/config/CollapsiblePropertyEditor/index.ts @@ -0,0 +1 @@ +export { CollapsiblePropertyEditor } from './CollapsiblePropertyEditor'; diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.module.css b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.module.css index 2cd7aa5f8af..984ad480368 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.module.css +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.module.css @@ -12,3 +12,25 @@ .downIcon { font-size: var(--fds-sizing-6); } + +.gridButton { + padding: 0; +} + +.gridHeader { + padding: 0; +} + +.flexContainer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.heading { + margin-left: var(--fds-spacing-5); +} + +.button { + margin: var(--fds-spacing-2); +} diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx index 82f84a7f5b3..bd9ba98a427 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx @@ -160,6 +160,88 @@ describe('FormComponentConfig', () => { expect(screen.queryByText('unsupportedProperty')).not.toBeInTheDocument(); }); + it('should render CollapsiblePropertyEditor for the "sortOrder" property', () => { + render({ + props: { + schema: { + ...InputSchema, + properties: { + ...InputSchema.properties, + sortOrder: { + type: 'array', + items: { + type: 'string', + enum: ['option1', 'option2'], + }, + }, + }, + }, + }, + }); + expect( + screen.getByText(textMock('ux_editor.component_properties.sortOrder')), + ).toBeInTheDocument(); + expect( + screen.getByRole('combobox', { + name: textMock('ux_editor.component_properties.sortOrder'), + }), + ).toBeInTheDocument(); + }); + + it('should render CollapsiblePropertyEditor for the "showValidations" property and EditStringValue for other properties', () => { + render({ + props: { + schema: { + ...InputSchema, + properties: { + ...InputSchema.properties, + showValidations: { + type: 'array', + items: { + type: 'string', + enum: ['true', 'false'], + }, + }, + anotherProperty: { + type: 'array', + items: { + type: 'string', + enum: ['option1', 'option2'], + }, + }, + }, + }, + }, + }); + expect( + screen.getByText(textMock('ux_editor.component_properties.showValidations')), + ).toBeInTheDocument(); + }); + + it('should render CollapsiblePropertyEditor for "preselectedOptionIndex" and EditNumberValue for other properties', () => { + render({ + props: { + schema: { + ...InputSchema, + properties: { + ...InputSchema.properties, + preselectedOptionIndex: { + type: 'number', + enum: [0, 1, 2], + }, + anotherNumberProperty: { + type: 'number', + description: 'A sample number property', + }, + }, + }, + }, + }); + expect( + screen.getByText(textMock('ux_editor.component_properties.preselectedOptionIndex_button')), + ).toBeInTheDocument(); + }); + it('should not render property if it is null', () => { render({ props: { @@ -401,6 +483,54 @@ describe('FormComponentConfig', () => { ); }); + it('should toggle close button and grid width text when the open and close buttons are clicked', async () => { + const user = userEvent.setup(); + render({ + props: { + schema: InputSchema, + }, + }); + const openGridButton = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.grid'), + }); + await user.click(openGridButton); + expect(screen.getByText(textMock('ux_editor.component_properties.grid'))).toBeInTheDocument(); + const widthText = screen.getByText(textMock('ux_editor.modal_properties_grid')); + expect(widthText).toBeInTheDocument(); + + const closeGridButton = screen.getByRole('button', { + name: textMock('general.close'), + }); + await user.click(closeGridButton); + expect(closeGridButton).not.toBeInTheDocument(); + expect(widthText).not.toBeInTheDocument(); + }); + + it('should not render grid width text if grid button is not clicked', async () => { + const user = userEvent.setup(); + render({ + props: { + schema: InputSchema, + }, + }); + expect(screen.queryByText(textMock('ux_editor.modal_properties_grid'))).not.toBeInTheDocument(); + const openGridButton = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.grid'), + }); + await user.click(openGridButton); + expect(screen.getByText(textMock('ux_editor.component_properties.grid'))).toBeInTheDocument(); + + const widthText = screen.getByText(textMock('ux_editor.modal_properties_grid')); + expect(widthText).toBeInTheDocument(); + + const closeGridButton = screen.getByRole('button', { + name: textMock('general.close'), + }); + await user.click(closeGridButton); + expect(closeGridButton).not.toBeInTheDocument(); + expect(widthText).not.toBeInTheDocument(); + }); + const render = ({ props = {}, queries = {}, diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx index 7417695bb5a..9ea2d945e28 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx @@ -16,8 +16,10 @@ import type { UpdateFormMutateOptions } from '../../containers/FormItemContext'; import { useComponentPropertyDescription } from '../../hooks/useComponentPropertyDescription'; import classes from './FormComponentConfig.module.css'; import { RedirectToLayoutSet } from './editModal/RedirectToLayoutSet'; -import { ChevronDownIcon, ChevronUpIcon } from '@studio/icons'; -import { StudioProperty } from '@studio/components'; +import { ChevronDownIcon, ChevronUpIcon, PlusCircleIcon, XMarkIcon } from '@studio/icons'; +import { StudioButton, StudioCard, StudioProperty } from '@studio/components'; +import { CollapsiblePropertyEditor } from './CollapsiblePropertyEditor'; +import type { TranslationKey } from 'language/type'; export interface IEditFormComponentProps { editFormId: string; @@ -41,6 +43,7 @@ export const FormComponentConfig = ({ const componentPropertyLabel = useComponentPropertyLabel(); const componentPropertyDescription = useComponentPropertyDescription(); const [showOtherComponents, setShowOtherComponents] = useState(false); + const [showGrid, setShowGrid] = useState(false); if (!schema?.properties) return null; @@ -115,24 +118,12 @@ export const FormComponentConfig = ({ {layoutSet && component['layoutSet'] && ( )} - {grid && ( - <> - - {t('ux_editor.component_properties.grid')} - - - - )} + {!hideUnsupported && ( {t('ux_editor.component_other_properties_title')} )} - {/** Boolean fields, incl. expression type */} {defaultDisplayedBooleanKeys.map((propertyKey) => ( ))} - {showOtherComponents && - restOfBooleanKeys.map((propertyKey) => ( - - ))} - {restOfBooleanKeys.length > 0 && ( - setShowOtherComponents((prev) => !prev)} - property={rendertext} - /> - )} {/** Custom logic for custom file endings */} {hasCustomFileEndings && ( @@ -190,43 +163,110 @@ export const FormComponentConfig = ({ )} - {/** String properties */} - {stringPropertyKeys.map((propertyKey) => { - return ( - ( + + ))} + + {restOfBooleanKeys.length > 0 && ( + setShowOtherComponents((prev) => !prev)} + property={rendertext} + /> + )} + + {grid && ( + <> + {showGrid ? ( + + +
+ + {t('ux_editor.component_properties.grid')} + + } + onClick={() => setShowGrid(false)} + title={t('general.close')} + variant='secondary' + className={classes.button} + /> +
+
+ + + +
+ ) : ( + } + onClick={() => setShowGrid(true)} + property={t('ux_editor.component_properties.grid')} + /> + )} + + )} + + {/** String properties */} + {stringPropertyKeys.map((propertyKey) => { + return ( + + + ); })} {/** Number properties (number and integer types) */} {numberPropertyKeys.map((propertyKey) => { return ( - + label={componentPropertyLabel( + `${propertyKey}${propertyKey === 'preselectedOptionIndex' ? '_button' : ''}`, + )} + > + +
); })} {/** Array properties with enum values) */} {arrayPropertyKeys.map((propertyKey) => { return ( - + + + ); })} diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditGrid/EditGrid.module.css b/frontend/packages/ux-editor/src/components/config/editModal/EditGrid/EditGrid.module.css index 1ea531e532d..b3434e95bbb 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditGrid/EditGrid.module.css +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditGrid/EditGrid.module.css @@ -1,6 +1,7 @@ .gridContainer { background-color: white; border-radius: var(--fds-border_radius-large); + display: grid; } .tab {