From 27e46032f144383d7e7a88a7300aeb627a725a82 Mon Sep 17 00:00:00 2001 From: Tomas Engebretsen Date: Mon, 4 Mar 2024 09:45:00 +0100 Subject: [PATCH] Add property button component (#12428) --- .../StudioPropertyButton.module.css | 39 +++++++++++++ .../StudioPropertyButton.test.tsx | 58 +++++++++++++++++++ .../StudioPropertyButton.tsx | 50 ++++++++++++++++ .../components/StudioPropertyButton/index.ts | 2 + .../studio-components/src/components/index.ts | 5 +- .../src/style/studio-variables.css | 1 + .../Properties/DataModelBindings.module.css | 1 - .../Properties/Properties.module.css | 3 +- .../DefinedBinding/DefinedBinding.module.css | 24 -------- .../DefinedBinding/DefinedBinding.tsx | 31 +++++----- .../EditBinding/EditBinding.module.css | 2 +- .../UndefinedBinding.module.css | 6 -- .../UndefinedBinding/UndefinedBinding.tsx | 14 +---- 13 files changed, 171 insertions(+), 65 deletions(-) create mode 100644 frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.module.css create mode 100644 frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.test.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioPropertyButton/index.ts delete mode 100644 frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/UndefinedBinding/UndefinedBinding.module.css diff --git a/frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.module.css b/frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.module.css new file mode 100644 index 00000000000..168f8ccfba0 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.module.css @@ -0,0 +1,39 @@ +.propertyButton { + border-radius: 0; + display: flex; + padding: var(--studio-property-button-vertical-spacing) var(--fds-spacing-5); + justify-content: flex-start; + overflow: hidden; +} + +.propertyButton.withValue .content { + color: var(--fds-semantic-text-neutral-default); + text-align: left; +} + +.propertyButton.withValue .property { + font-weight: bold; +} + +.content { + overflow: hidden; + white-space: nowrap; +} + +.property, +.value { + display: block; + overflow: hidden; + text-overflow: ellipsis; +} + +.editIconWrapper { + flex: 1; + text-align: right; + display: none; +} + +.propertyButton:hover .editIconWrapper, +.propertyButton:focus .editIconWrapper { + display: inline-block; +} diff --git a/frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.test.tsx b/frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.test.tsx new file mode 100644 index 00000000000..3c06f0adb76 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.test.tsx @@ -0,0 +1,58 @@ +import type { RefObject } from 'react'; +import React from 'react'; +import type { StudioPropertyButtonProps } from './StudioPropertyButton'; +import { StudioPropertyButton } from './StudioPropertyButton'; +import { act, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +// Test data: +const property = 'Test property'; +const defaultProps: StudioPropertyButtonProps = { + property, +}; + +describe('StudioPropertyButton', () => { + it('Renders a button with the property name', () => { + renderButton(); + expect(screen.getByRole('button', { name: property })).toHaveTextContent(property); + }); + + it('Renders both the property and the value when a value is given', () => { + const value = 'Test value'; + renderButton({ value }); + expect(screen.getByRole('button', { name: property })).toHaveTextContent(property); + expect(screen.getByRole('button', { name: property })).toHaveTextContent(value); + }); + + it('Overrides the icon when a custom icon is given', () => { + const iconTestId = 'custom-icon'; + const icon = ; + renderButton({ icon }); + expect(screen.getByTestId(iconTestId)).toBeInTheDocument(); + }); + + it('Appends the given class name', () => { + const className = 'test-class'; + renderButton({ className }); + expect(screen.getByRole('button', { name: property })).toHaveClass(className); + }); + + it('Forwards a ref to the button', () => { + const ref = React.createRef(); + renderButton({}, ref); + expect(ref.current).toBe(screen.getByRole('button')); + }); + + it('Calls the onClick function when the button is clicked', async () => { + const user = userEvent.setup(); + const onClick = jest.fn(); + renderButton({ onClick }); + await act(() => user.click(screen.getByRole('button'))); + expect(onClick).toHaveBeenCalled(); + }); +}); + +const renderButton = ( + props: Partial = {}, + ref?: RefObject, +) => render(); diff --git a/frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.tsx b/frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.tsx new file mode 100644 index 00000000000..fe6df1083ba --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioPropertyButton/StudioPropertyButton.tsx @@ -0,0 +1,50 @@ +import type { ReactNode } from 'react'; +import React, { forwardRef } from 'react'; +import type { StudioButtonProps } from '../StudioButton'; +import { StudioButton } from '../StudioButton'; +import classes from './StudioPropertyButton.module.css'; +import { PlusCircleIcon, PencilIcon } from '@studio/icons'; +import cn from 'classnames'; + +export type StudioPropertyButtonProps = { + property: string; + value?: ReactNode; +} & Omit; + +const StudioPropertyButton = forwardRef( + ({ property, value, icon: givenIcon, className: givenClass, ...rest }, ref) => { + const hasValue = !!value; + + const icon = hasValue || givenIcon ? givenIcon : ; + + const className = cn(classes.propertyButton, hasValue && classes.withValue, givenClass); + + return ( + + + {property} + {value} + + {hasValue && ( + + + + )} + + ); + }, +); + +StudioPropertyButton.displayName = 'StudioPropertyButton'; + +export { StudioPropertyButton }; diff --git a/frontend/libs/studio-components/src/components/StudioPropertyButton/index.ts b/frontend/libs/studio-components/src/components/StudioPropertyButton/index.ts new file mode 100644 index 00000000000..2832ac761fc --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioPropertyButton/index.ts @@ -0,0 +1,2 @@ +export { StudioPropertyButton } from './StudioPropertyButton'; +export type { StudioPropertyButtonProps } from './StudioPropertyButton'; diff --git a/frontend/libs/studio-components/src/components/index.ts b/frontend/libs/studio-components/src/components/index.ts index fea6081bec8..4a07e5ee841 100644 --- a/frontend/libs/studio-components/src/components/index.ts +++ b/frontend/libs/studio-components/src/components/index.ts @@ -8,14 +8,15 @@ export * from './StudioDeleteButton'; export * from './StudioDropdownMenu'; export * from './StudioExpression'; export * from './StudioGridSelector'; +export * from './StudioIconTextfield'; export * from './StudioLabelWrapper'; export * from './StudioModal'; export * from './StudioNotFoundPage'; export * from './StudioPageSpinner'; +export * from './StudioPropertyButton'; export * from './StudioSpinner'; export * from './StudioTextarea'; export * from './StudioTextfield'; -export * from './StudioTreeView'; export * from './StudioToggleableTextfield'; -export * from './StudioIconTextfield'; export * from './StudioToggleableTextfieldSchema'; +export * from './StudioTreeView'; diff --git a/frontend/libs/studio-components/src/style/studio-variables.css b/frontend/libs/studio-components/src/style/studio-variables.css index f540403cecf..4cc07137842 100644 --- a/frontend/libs/studio-components/src/style/studio-variables.css +++ b/frontend/libs/studio-components/src/style/studio-variables.css @@ -6,4 +6,5 @@ --studio-treeitem-vertical-line-colour: var(--fds-semantic-border-divider-default); --studio-treeitem-vertical-line-width: 2px; --studio-expression-spacing: var(--fds-spacing-3); + --studio-property-button-vertical-spacing: var(--fds-spacing-2); } diff --git a/frontend/packages/ux-editor/src/components/Properties/DataModelBindings.module.css b/frontend/packages/ux-editor/src/components/Properties/DataModelBindings.module.css index 7eb06389a66..b2dd3f2623d 100644 --- a/frontend/packages/ux-editor/src/components/Properties/DataModelBindings.module.css +++ b/frontend/packages/ux-editor/src/components/Properties/DataModelBindings.module.css @@ -1,6 +1,5 @@ .container { display: grid; - gap: 1rem; } .switch { diff --git a/frontend/packages/ux-editor/src/components/Properties/Properties.module.css b/frontend/packages/ux-editor/src/components/Properties/Properties.module.css index 27f1be26638..3c4a6431fde 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Properties.module.css +++ b/frontend/packages/ux-editor/src/components/Properties/Properties.module.css @@ -4,5 +4,6 @@ } .datamodelBindings { - padding: var(--fds-spacing-5) 0; + --vertical-padding: calc(var(--fds-spacing-5) - var(--studio-property-button-vertical-spacing)); + padding: var(--vertical-padding) 0; } diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/DefinedBinding/DefinedBinding.module.css b/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/DefinedBinding/DefinedBinding.module.css index 86b7502265f..c063698df35 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/DefinedBinding/DefinedBinding.module.css +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/DefinedBinding/DefinedBinding.module.css @@ -1,29 +1,5 @@ -.definedBinding { - border-radius: 0; - padding-left: var(--fds-spacing-5); - padding-right: var(--fds-spacing-5); -} - -.mainContent { - align-items: flex-start; - color: var(--fds-semantic-text-neutral-default); - display: flex; - flex-direction: column; -} - .selectedOption { align-items: center; display: flex; gap: var(--fds-spacing-1); } - -.pencilIcon { - visibility: hidden; - flex: 1; - text-align: right; -} - -.definedBinding:hover .pencilIcon, -.definedBinding:focus .pencilIcon { - visibility: visible; -} diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/DefinedBinding/DefinedBinding.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/DefinedBinding/DefinedBinding.tsx index fa449ebfd5a..fb6c7bf5ac9 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/DefinedBinding/DefinedBinding.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/DefinedBinding/DefinedBinding.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { StudioButton } from '@studio/components'; -import { LinkIcon, PencilIcon } from '@studio/icons'; +import { StudioPropertyButton } from '@studio/components'; +import { LinkIcon } from '@studio/icons'; import classes from './DefinedBinding.module.css'; import { useTranslation } from 'react-i18next'; @@ -13,25 +13,20 @@ export type DefinedBindingProps = { export const DefinedBinding = ({ onClick, label, selectedOption }: DefinedBindingProps) => { const { t } = useTranslation(); const title = t('right_menu.dataModelBindings_edit', { binding: label }); + + const value = ( + + {selectedOption} + + ); + return ( - - - {label} - - {selectedOption} - - - - - - + value={value} + /> ); }; diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/EditBinding/EditBinding.module.css b/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/EditBinding/EditBinding.module.css index e6b06e2c4b1..7e78a29c1b5 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/EditBinding/EditBinding.module.css +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/EditBinding/EditBinding.module.css @@ -6,7 +6,7 @@ display: flex; flex-direction: column; gap: var(--datamodel-binding-spacing); - margin: 0 var(--fds-spacing-5); + margin: var(--studio-property-button-vertical-spacing) var(--fds-spacing-5); padding: var(--datamodel-binding-spacing); } diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/UndefinedBinding/UndefinedBinding.module.css b/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/UndefinedBinding/UndefinedBinding.module.css deleted file mode 100644 index f01a0c59d3d..00000000000 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/UndefinedBinding/UndefinedBinding.module.css +++ /dev/null @@ -1,6 +0,0 @@ -.undefinedBinding { - border-radius: 0; - justify-content: flex-start; - padding-left: var(--fds-spacing-5); - padding-right: var(--fds-spacing-5); -} diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/UndefinedBinding/UndefinedBinding.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/UndefinedBinding/UndefinedBinding.tsx index 80b58718f32..b9b6d18fd2b 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/UndefinedBinding/UndefinedBinding.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditDataModelBindings/UndefinedBinding/UndefinedBinding.tsx @@ -1,6 +1,5 @@ -import classes from './UndefinedBinding.module.css'; import React from 'react'; -import { StudioButton } from '@studio/components'; +import { StudioPropertyButton } from '@studio/components'; import { LinkIcon } from '@studio/icons'; export type UndefinedBindingProps = { @@ -9,14 +8,5 @@ export type UndefinedBindingProps = { }; export const UndefinedBinding = ({ onClick, label }: UndefinedBindingProps) => ( - } - > - {label} - + } onClick={onClick} property={label} /> );