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 {