Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

12787 resizable layout component #13044

Merged
merged 47 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a4c422e
PoC resizable layout
Jondyr Jun 19, 2024
56ec604
Alter `FormDesigner` view to test out resizable layout
Jondyr Jun 19, 2024
ac8454e
Remove commented code
Jondyr Jun 19, 2024
ce56af3
Undo deletion of css rule
Jondyr Jun 25, 2024
9b43d1f
Move `LayoutSet` selector to own toolbar similar to `SchemaEditor`
Jondyr Jun 25, 2024
eb58be2
Refactor `ResizableLayout` implementation
Jondyr Jun 25, 2024
823884f
Make preview collapsing also collapse the resizable layout element
Jondyr Jun 25, 2024
ee9552b
Refactor css and use negative inset to increase grab area of handles
Jondyr Jun 25, 2024
727a53a
Update layout on minimumSize change
Jondyr Jun 25, 2024
8074bc2
Add storybook entry for Resizable Layout
Jondyr Jun 26, 2024
d479f5f
Refactor `StudioResizableLayout`
Jondyr Jun 27, 2024
3241dc1
Add toolbar and add state to block iframe preview
Jondyr Jun 27, 2024
84f1ec9
Update ResizableLayout storybook entry
Jondyr Jun 27, 2024
90a6060
Move localstorage hook to studio-components `hook` folder
Jondyr Jun 28, 2024
8498b7e
Remove unnecessary ref proxying with imperativehandle
Jondyr Jun 28, 2024
f0aa555
Remove layoutId in favor of just using localStorageContextKey
Jondyr Jun 28, 2024
bbaacbb
Add test for resizing and keyboard presses
Jondyr Jun 28, 2024
93ae303
Hide left side when container size is small, to avoid overlapping han…
Jondyr Jun 28, 2024
d4f9060
Remove unused code
Jondyr Jun 28, 2024
08e9b74
Revert "Make preview collapsing also collapse the resizable layout el…
Jondyr Jun 28, 2024
2bd8ab0
Revert accidental v3 form editor edits
Jondyr Jun 28, 2024
249bcfb
Remove layoutId from storybook entry and unnecessary imports
Jondyr Jun 28, 2024
ea4fbaf
Remove accordion top border for first item in list
Jondyr Jul 1, 2024
45574f9
Add maximumSize and move collapsing to css rules
Jondyr Jul 2, 2024
c13ea5c
Update preview test
Jondyr Jul 2, 2024
96f57e6
Make components left menu collapsible
Jondyr Jul 2, 2024
27777b6
Fix export of localStorage hooks for tests
Jondyr Jul 2, 2024
30b19af
Fix elements test
Jondyr Jul 2, 2024
9509cbe
Fix preview test collapsing
Jondyr Jul 2, 2024
352e75e
Add test for collapsing elements view
Jondyr Jul 2, 2024
6cf2aec
Use exact matching to avoid potential false hits in playwright getbyrole
Jondyr Jul 2, 2024
8f7c22f
Add exact matching to other cases with false hits
Jondyr Jul 2, 2024
1275262
Add tests for resizablelayoutfunction and mousemovementhook
Jondyr Jul 2, 2024
7cd17d5
Fix event type in mouse movement test
Jondyr Jul 4, 2024
308c898
Update resizablelayout tests to cover more cases
Jondyr Jul 4, 2024
5967c71
Add test case for horizontal and vertical layout configuration
Jondyr Jul 4, 2024
c3b0792
Merge branch 'main' into 12787-resizable-layout-component
Jondyr Jul 10, 2024
c3030fb
Remove unused css file
Jondyr Jul 10, 2024
ec74e2c
Revert "Remove unused css file"
Jondyr Jul 10, 2024
388670d
Remove unused css rules and fix css toolbar issue
Jondyr Jul 10, 2024
ae4d2f0
Simplify valid child checker
Jondyr Jul 10, 2024
5b6d773
Add tests for collapsing components and preview in ux editor
Jondyr Jul 10, 2024
a0a6792
Add org and user to `localStorageContext` for datamodel `resizableLay…
Jondyr Jul 10, 2024
6f15724
Mock serviceContext for changed tests
Jondyr Jul 10, 2024
6421dc0
Merge branch 'main' into 12787-resizable-layout-component
Jondyr Jul 11, 2024
7f1b23e
Merge branch 'main' into 12787-resizable-layout-component
Jondyr Jul 11, 2024
7e00bf6
Update designsystemet import
Jondyr Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { renderWithProviders } from 'app-development/test/testUtils';
import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants';
import { TopBarMenu } from 'app-shared/enums/TopBarMenu';
import { RepositoryType } from 'app-shared/types/global';
import { typedLocalStorage } from 'app-shared/utils/webStorage';
import { typedLocalStorage } from '@studio/components/src/hooks/webStorage';
import type { TopBarMenuItem } from 'app-shared/types/TopBarMenuItem';

describe('Navigation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import type { LangCode } from '@altinn/text-editor';
import { TextEditor as TextEditorImpl, defaultLangCode } from '@altinn/text-editor';
import { StudioPageSpinner } from '@studio/components';
import { useLocalStorage } from 'app-shared/hooks/useLocalStorage';
import { useLocalStorage } from '@studio/components/src/hooks/useLocalStorage';
import { useSearchParams } from 'react-router-dom';
import type { TextResourceIdMutation } from '@altinn/text-editor/types';
import { useLanguagesQuery, useTextResourcesQuery } from '../../hooks/queries';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type UseMutateFunction, useMutation, useQueryClient } from '@tanstack/r
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
import type { LayoutSetConfig, LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
import { useLocalStorage } from 'app-shared/hooks/useLocalStorage';
import { useLocalStorage } from '@studio/components/src/hooks/useLocalStorage';
import type {
AddLayoutSetResponse,
LayoutSetsResponse,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RepositoryType } from 'app-shared/types/global';
import { getFilteredTopBarMenu, topBarMenuItem } from './appBarConfig';
import { typedLocalStorage } from 'app-shared/utils/webStorage';
import { typedLocalStorage } from '@studio/components/src/hooks/webStorage';
import { TopBarMenu } from 'app-shared/enums/TopBarMenu';
import type { TopBarMenuItem } from 'app-shared/types/TopBarMenuItem';
import { RoutePaths } from 'app-development/enums/RoutePaths';
Expand Down
2 changes: 1 addition & 1 deletion frontend/app-preview/src/views/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classes from './LandingPage.module.css';
import { useTranslation } from 'react-i18next';
import { usePreviewConnection } from 'app-shared/providers/PreviewConnectionContext';
import { useInstanceIdQuery, useRepoMetadataQuery, useUserQuery } from 'app-shared/hooks/queries';
import { useLocalStorage } from 'app-shared/hooks/useLocalStorage';
import { useLocalStorage } from '@studio/components/src/hooks/useLocalStorage';
import { AltinnHeader } from 'app-shared/components/altinnHeader';
import type { AltinnHeaderVariant } from 'app-shared/components/altinnHeader/types';
import { getRepositoryType } from 'app-shared/utils/repository';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useSelectedContext } from '../useSelectedContext';
import type { NavigateFunction } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { SelectedContextType } from 'app-shared/navigation/main-header/Header';
import { typedSessionStorage } from 'app-shared/utils/webStorage';
import { typedSessionStorage } from '@studio/components/src/hooks/webStorage';
import { userHasAccessToSelectedContext } from 'dashboard/utils/userUtils';

export type UseRedirectionGuardResult = {
Expand Down
2 changes: 2 additions & 0 deletions frontend/language/src/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,12 +450,14 @@
"language.za": "Zhuang",
"language.zh": "Chinese",
"language.zu": "Zulu",
"left_menu.close_components": "Close components",
"left_menu.components": "Components",
"left_menu.configure_layout_sets": "Convert form to handle multiple groups of pages",
"left_menu.configure_layout_sets_info": "NB, this action will change the folder structure of your application. Read more at <0 href=\"{{layoutSetsDocs}}\" >Altinn Docs</0> for details of what this involves.",
"left_menu.configure_layout_sets_name": "Name of first group:",
"left_menu.layout_sets": "Groups of pages",
"left_menu.layout_sets_add": "Add new group",
"left_menu.open_components": "Open components",
"left_menu.pages.invalid_page_data": "The page contains invalid data and cannot be shown. Please check all component IDs and references to components from groups.",
"left_menu.pages_error_empty": "Can not be empty",
"left_menu.pages_error_format": "Must consist of letters (a-z), numbers, or \"-\", \"_\" and \".\"",
Expand Down
2 changes: 2 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@
"language.za": "Zhuang",
"language.zh": "Kinesisk",
"language.zu": "Zulu",
"left_menu.close_components": "Lukk komponenter",
"left_menu.components": "Komponenter",
"left_menu.configure_layout_sets": "Konverter skjema til å håndtere sidegrupper",
"left_menu.configure_layout_sets_info": "Obs, ved å velge denne handlingen endrer du mappestrukturen i applikasjonen. Les mer om hva det vil si på <0 href=\"{{layoutSetsDocs}}\" >Altinn Docs</0>.",
Expand All @@ -613,6 +614,7 @@
"left_menu.layout_sets": "Sidegrupper",
"left_menu.layout_sets_add": "Legg til ny sidegruppe",
"left_menu.no_components_selected": "Velg en side for å se komponenter",
"left_menu.open_components": "Åpne komponenter",
"left_menu.pages.invalid_page_data": "Siden inneholder ugyldig data, og kan ikke vises. Kontroller alle komponent-ID-er og referanser til komponenter fra grupper.",
"left_menu.pages_error_empty": "Kan ikke være tom",
"left_menu.pages_error_format": "Må bestå av bokstaver (a-z), tall, eller \"-\", \"_\" og \".\"",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Canvas, Meta } from '@storybook/blocks';
import { Heading, Paragraph } from '@digdir/design-system-react';
import * as StudioResizableLayoutStories from './StudioResizableLayout.stories';

<Meta of={StudioResizableLayoutStories} />

<Heading level={1} size='small'>
StudioResizableLayout
</Heading>
<Paragraph>
StudioResizableLayout is used to create resizable layouts with mouse dragging and keyboard
support. Arrow keys can be used to resize the layout when the separator handle is focused.
</Paragraph>

<Canvas of={StudioResizableLayoutStories.Preview} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { StoryFn, Meta } from '@storybook/react/*';
import React from 'react';
import type { StudioResizableLayoutContainerProps } from './StudioResizableLayoutContainer/StudioResizableLayoutContainer';
import { StudioResizableLayoutContainer } from './StudioResizableLayoutContainer/StudioResizableLayoutContainer';
import { StudioResizableLayoutElement } from './StudioResizableLayoutElement/StudioResizableLayoutElement';

type PreviewProps = {
topContainerOrientation: StudioResizableLayoutContainerProps['orientation'];
subContainerOrientation: StudioResizableLayoutContainerProps['orientation'];
};

type Story = StoryFn<PreviewProps>;
const meta: Meta = {
title: 'Studio/StudioResizableLayoutContainer',
component: StudioResizableLayoutContainer,
};

export const Preview: Story = (args): React.ReactElement => (
<div>
<StudioResizableLayoutContainer
orientation={args.topContainerOrientation}
style={{ width: 900, height: 800 }}
>
<StudioResizableLayoutElement style={{ backgroundColor: '#A1A1A1' }}>
<div>
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua
</div>
</StudioResizableLayoutElement>
<StudioResizableLayoutElement>
<StudioResizableLayoutContainer orientation={args.subContainerOrientation}>
<StudioResizableLayoutElement style={{ backgroundColor: '#C1C1C1' }}>
<div>
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua
</div>
</StudioResizableLayoutElement>
<StudioResizableLayoutElement style={{ backgroundColor: '#D1D1D1' }}>
<div>
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua
</div>
</StudioResizableLayoutElement>
</StudioResizableLayoutContainer>
</StudioResizableLayoutElement>
<StudioResizableLayoutElement style={{ backgroundColor: '#F1F1F1' }}>
<div>
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua
</div>
</StudioResizableLayoutElement>
</StudioResizableLayoutContainer>
</div>
);

// TODO: use group story

Preview.args = {
topContainerOrientation: 'vertical',
subContainerOrientation: 'horizontal',
};
export default meta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.root {
display: flex;
flex-direction: row;
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import type { StudioResizableLayoutContainerProps } from './StudioResizableLayoutContainer';
import { StudioResizableLayoutContainer } from './StudioResizableLayoutContainer';
import { fireEvent, render, screen } from '@testing-library/react';
import { StudioResizableLayoutElement } from '../StudioResizableLayoutElement/StudioResizableLayoutElement';

describe('StudioResizableLayoutContainer', () => {
it('should render just one handle with two elements', () => {
renderStudioResizableLayoutContainer();
expect(screen.getAllByRole('separator').length).toBe(1);
});

it('should resize containers', () => {
renderStudioResizableLayoutContainer();
const handle = screen.getByRole('separator');

dragHandle(handle, { clientX: 400 }, { clientX: 200 });

expect(screen.getAllByTestId('resizablelayoutelement')[0].style.flexGrow).toBe('0.5');
expect(screen.getAllByTestId('resizablelayoutelement')[1].style.flexGrow).toBe('1.5');
});

it('should not resize containers below minimum size', () => {
// minimum flexgrow should be minimumSize/containerSize=0.25
renderStudioResizableLayoutContainer();
const handle = screen.getByRole('separator');

dragHandle(handle, { clientX: 400 }, { clientX: 0 });
expect(screen.getAllByTestId('resizablelayoutelement')[0].style.flexGrow).toBe('0.25');
expect(screen.getAllByTestId('resizablelayoutelement')[1].style.flexGrow).toBe('1.75');

dragHandle(handle, { clientX: 0 }, { clientX: 800 });
expect(screen.getAllByTestId('resizablelayoutelement')[0].style.flexGrow).toBe('1.75');
expect(screen.getAllByTestId('resizablelayoutelement')[1].style.flexGrow).toBe('0.25');
});

it('should not resize containers above maximum size', () => {
renderStudioResizableLayoutContainer(600);
const handle = screen.getByRole('separator');

dragHandle(handle, { clientX: 400 }, { clientX: 800 });
expect(screen.getAllByTestId('resizablelayoutelement')[0].style.flexGrow).toBe('1.5');
expect(screen.getAllByTestId('resizablelayoutelement')[1].style.flexGrow).toBe('0.5');
});
});

const dragHandle = (
handle: HTMLElement,
from: { clientX?: number; clientY?: number },
to: { clientX?: number; clientY?: number },
) => {
fireEvent.mouseDown(handle, from);
fireEvent.mouseMove(handle, to);
fireEvent.mouseUp(handle, to);
};

const renderStudioResizableLayoutContainer = (
maximumSize = 800,
collapsed = false,
props: Partial<StudioResizableLayoutContainerProps> = {},
) => {
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
value: 400,
});
return render(
<StudioResizableLayoutContainer
style={{ width: 800, height: 800 }}
orientation='horizontal'
{...props}
>
<StudioResizableLayoutElement
minimumSize={100}
maximumSize={maximumSize}
collapsed={collapsed}
collapsedSize={400}
>
<div>test1</div>
</StudioResizableLayoutElement>
<StudioResizableLayoutElement
minimumSize={100}
maximumSize={maximumSize}
collapsed={collapsed}
collapsedSize={400}
>
<div>test1</div>
</StudioResizableLayoutElement>
</StudioResizableLayoutContainer>,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { CSSProperties, ReactElement } from 'react';
import React, { Children, useEffect, useRef } from 'react';
import classes from './StudioResizableLayoutContainer.module.css';
import { type StudioResizableLayoutElementProps } from '../StudioResizableLayoutElement/StudioResizableLayoutElement';
import { useStudioResizableLayoutFunctions } from '../hooks/useStudioResizableFunctions';
import { useTrackContainerSizes } from '../hooks/useTrackContainerSizes';
import { StudioResizableLayoutContext } from '../context/StudioResizableLayoutContext';

export const ORIENTATIONS = ['horizontal', 'vertical'] as const;
export type StudioResizableOrientation = (typeof ORIENTATIONS)[number];
export const horizontal: StudioResizableOrientation = 'horizontal';
export const vertical: StudioResizableOrientation = 'vertical';

export type StudioResizableLayoutContainerProps = {
localStorageContext?: string;
orientation: StudioResizableOrientation;
style?: CSSProperties;

children: ReactElement<StudioResizableLayoutElementProps>[];
};

const StudioResizableLayoutContainer = ({
children,
orientation,
localStorageContext = 'default',
style,
}: StudioResizableLayoutContainerProps): ReactElement => {
const elementRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
elementRefs.current = elementRefs.current.slice(0, getValidChildren(children).length);
}, [children]);

const { containerSizes, setContainerSizes } = useTrackContainerSizes(localStorageContext);
const { resizeTo, resizeDelta } = useStudioResizableLayoutFunctions(
orientation,
elementRefs,
getValidChildren(children),
(index, size) => setContainerSizes((prev) => ({ ...prev, [index]: size })),
);

const renderChildren = () => {
return Children.map(getValidChildren(children), (child, index) => {
const hasNeighbour = index < getValidChildren(children).length - 1;
return React.cloneElement(child, {
index,
hasNeighbour,
ref: (element: HTMLDivElement) => (elementRefs.current[index] = element),
});
});
};

return (
<StudioResizableLayoutContext.Provider
value={{ resizeDelta, resizeTo, orientation, containerSizes }}
>
<div
className={classes.root}
style={{ ...style, flexDirection: orientation === 'horizontal' ? 'row' : 'column' }}
>
{renderChildren()}
</div>
</StudioResizableLayoutContext.Provider>
);
};

const getValidChildren = (
children: React.ReactElement<
StudioResizableLayoutElementProps,
string | React.JSXElementConstructor<any>
>[],
) => {
return Children.map(children, (child) => {
if (!child) {
return;

Check warning on line 74 in frontend/libs/studio-components/src/components/StudioResizableLayout/StudioResizableLayoutContainer/StudioResizableLayoutContainer.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/libs/studio-components/src/components/StudioResizableLayout/StudioResizableLayoutContainer/StudioResizableLayoutContainer.tsx#L74

Added line #L74 was not covered by tests
Jondyr marked this conversation as resolved.
Show resolved Hide resolved
}
return child;
}).filter((child) => !!child);
};

StudioResizableLayoutContainer.displayName = 'StudioResizableLayout.Container';

export { StudioResizableLayoutContainer as StudioResizableLayoutContainer };
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.container {
display: flex;
overflow: auto;
position: relative;
flex: 1;
}

.resizingOverlay {
position: absolute;
z-index: 100;
}
Loading