Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Add support for sprite object in setting modal
- Set the correct map view when opening a new style on an empty map
- Allow root-relative urls in the stylefile
- Add ability to change the size of the panels
- _...Add new stuff here..._

### 🐞 Bug fixes
Expand All @@ -29,6 +30,7 @@
- Fixed headers in left panes (Layers list and Layer editor) to remain visible when scrolling
- Fix error when using a source from localhost
- Fix an issue with scrolling when using the code editor
- Fix Cypress E2E test failures caused by resizable panel sizes persisting in localStorage.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to add this, this is part of the feature...

- _...Add new stuff here..._

## 3.0.0
Expand Down
39 changes: 39 additions & 0 deletions cypress/e2e/maputnik-cypress-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,46 @@ export default class MaputnikCypressHelper {
force: true,
});
},

pointerDrag: (testId: string, deltaX: number) => {
cy.get(`[data-wd-key="${testId}"]`).then(($el) => {
const rect = $el[0].getBoundingClientRect();
const startX = rect.left + rect.width / 2;
const startY = rect.top + rect.height / 2;

cy.document().trigger("pointerdown", {
clientX: startX,
clientY: startY,
pointerId: 1,
button: 0,
buttons: 1,
pointerType: "mouse",
bubbles: true,
});
cy.document().trigger("pointermove", {
clientX: startX + deltaX,
clientY: startY,
pointerId: 1,
button: 0,
buttons: 1,
pointerType: "mouse",
bubbles: true,
});
cy.document().trigger("pointerup", {
clientX: startX + deltaX,
clientY: startY,
pointerId: 1,
button: 0,
buttons: 0,
pointerType: "mouse",
bubbles: true,
});
});
},
...this.helper.when,
visit: (url: string, options?: Partial<Cypress.VisitOptions>) => {
cy.visit(url, options);
},
};

public beforeAndAfter = this.helper.beforeAndAfter;
Expand Down
17 changes: 16 additions & 1 deletion cypress/e2e/maputnik-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ export class MaputnikDriver {
private helper = new MaputnikCypressHelper();
private modalDriver = new ModalDriver();

private resetLayoutStorage = (win: Window) => {
const keysToRemove: string[] = [];
for (let i = 0; i < win.localStorage.length; i++) {
const key = win.localStorage.key(i);
if (key && (key.startsWith("react-resizable-panels:maputnik") || key.startsWith("maputnik:sidebar"))) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => win.localStorage.removeItem(key));
};

public beforeAndAfter = () => {
beforeEach(() => {
this.given.setupMockBackedResponses();
Expand Down Expand Up @@ -183,7 +194,11 @@ export class MaputnikDriver {
if (zoom) {
url.hash = `${zoom}/41.3805/2.1635`;
}
this.helper.when.visit(url.toString());
this.helper.when.visit(url.toString(), {
onBeforeLoad: (win) => {
this.resetLayoutStorage(win);
},
});
if (styleProperties) {
this.helper.when.acceptConfirm();
}
Expand Down
44 changes: 44 additions & 0 deletions cypress/e2e/sidebar-resize.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MaputnikDriver } from "./maputnik-driver";

describe("sidebar resize", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();

beforeEach(() => {
when.setStyle("both");
});

it("resize handle is visible", () => {
then(get.elementByTestId("sidebar-resize-handle")).shouldBeVisible();
});

it("inner resize handle is visible", () => {
then(get.elementByTestId("inner-resize-handle")).shouldBeVisible();
});

it("dragging the handle changes sidebar width", () => {
get.element(".maputnik-layout-list").then(($list) => {
const initialWidth = $list[0].getBoundingClientRect().width;

when.pointerDrag("sidebar-resize-handle", 100);

get.element(".maputnik-layout-list").should(($listAfter) => {
const newWidth = $listAfter[0].getBoundingClientRect().width;
expect(newWidth).to.be.greaterThan(initialWidth);
});
});
});

it("dragging inner handle changes list/drawer split", () => {
get.element(".maputnik-layout-list").then(($list) => {
const initialWidth = $list[0].getBoundingClientRect().width;

when.pointerDrag("inner-resize-handle", 80);

get.element(".maputnik-layout-list").should(($listAfter) => {
const newWidth = $listAfter[0].getBoundingClientRect().width;
expect(newWidth).to.be.greaterThan(initialWidth);
});
});
});
});
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"react-i18next": "^17.0.7",
"react-icons": "^5.6.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.11.0",
"reconnecting-websocket": "^4.4.0",
"slugify": "^1.6.9",
"string-hash": "^1.1.3",
Expand Down
176 changes: 133 additions & 43 deletions src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,144 @@
import React from "react";
import React, {useEffect, useState} from "react";
import {Group, Panel, Separator, useDefaultLayout} from "react-resizable-panels";
import ScrollContainer from "./ScrollContainer";
import { type WithTranslation, withTranslation } from "react-i18next";
import { IconContext } from "react-icons";
import {useTranslation} from "react-i18next";
import {IconContext} from "react-icons";

type AppLayoutInternalProps = {
const DEFAULT_LIST_WIDTH = 200;
const DEFAULT_DRAWER_WIDTH = 370;
const DEFAULT_SIDEBAR_WIDTH = DEFAULT_LIST_WIDTH + DEFAULT_DRAWER_WIDTH;
const DEFAULT_LIST_RATIO = DEFAULT_LIST_WIDTH / DEFAULT_SIDEBAR_WIDTH;

const SIDEBAR_LAYOUT_STORAGE_ID = "maputnik:sidebar-layout";
const SIDEBAR_INNER_LAYOUT_STORAGE_ID = "maputnik:sidebar-inner-layout";
const SIDEBAR_PANEL_ID = "sidebar";
const MAP_PANEL_ID = "map";
const LIST_PANEL_ID = "list";
const DRAWER_PANEL_ID = "drawer";

const getDefaultSidebarPercent = () => {
const viewportWidth = window.innerWidth || DEFAULT_SIDEBAR_WIDTH;
return Math.min(100, (DEFAULT_SIDEBAR_WIDTH / viewportWidth) * 100);
};

const getDefaultListPercent = () => DEFAULT_LIST_RATIO * 100;

type AppLayoutProps = {
toolbar: React.ReactElement
layerList: React.ReactElement
layerEditor?: React.ReactElement
codeEditor?: React.ReactElement
map: React.ReactElement
bottom?: React.ReactElement
modals?: React.ReactNode
} & WithTranslation;

class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {

render() {
document.body.dir = this.props.i18n.dir();

return <IconContext.Provider value={{size: "14px"}}>
<div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-main">
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
<ScrollContainer>
{this.props.codeEditor}
</ScrollContainer>
</div>
}
{!this.props.codeEditor && <>
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
</>}
{this.props.map}
</div>
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
};

export default function AppLayout(props: AppLayoutProps) {
const {t, i18n} = useTranslation();

const sidebarLayout = useDefaultLayout({
id: SIDEBAR_LAYOUT_STORAGE_ID,
panelIds: [SIDEBAR_PANEL_ID, MAP_PANEL_ID],
});
const sidebarInnerLayout = useDefaultLayout({
id: SIDEBAR_INNER_LAYOUT_STORAGE_ID,
panelIds: [LIST_PANEL_ID, DRAWER_PANEL_ID],
});

useEffect(() => {
document.body.dir = i18n.dir();
}, [i18n]);

const [sidebarSize, setSidebarSize] = useState<number>(
() => sidebarLayout.defaultLayout?.[SIDEBAR_PANEL_ID] ?? getDefaultSidebarPercent()
);
const [listSize, setListSize] = useState<number>(
() => sidebarInnerLayout.defaultLayout?.[LIST_PANEL_ID] ?? getDefaultListPercent()
);

return <IconContext.Provider value={{size: "14px"}}>
<div className="maputnik-layout" style={{
"--sidebar-list-width": `${listSize}%`,
"--sidebar-drawer-width": `${100 - listSize}%`,
"--sidebar-total-width": `${sidebarSize}%`,
} as React.CSSProperties}>
{props.toolbar}
<div className="maputnik-layout-main">
<Group
className="maputnik-layout-panels"
orientation="horizontal"
id={SIDEBAR_LAYOUT_STORAGE_ID}
defaultLayout={sidebarLayout.defaultLayout}
onLayoutChanged={(layout) => {
setSidebarSize(layout[SIDEBAR_PANEL_ID] ?? sidebarSize);
sidebarLayout.onLayoutChanged(layout);
}}
>
<Panel
id={SIDEBAR_PANEL_ID}
className={props.codeEditor ? "maputnik-layout-code-editor" : "maputnik-layout-sidebar"}
defaultSize="570px"
minSize="280px"
>
{props.codeEditor ? <ScrollContainer>
{props.codeEditor}
</ScrollContainer> : <Group
className="maputnik-layout-sidebar-panels"
orientation="horizontal"
id={SIDEBAR_INNER_LAYOUT_STORAGE_ID}
defaultLayout={sidebarInnerLayout.defaultLayout}
onLayoutChanged={(layout) => {
setListSize(layout[LIST_PANEL_ID] ?? listSize);
sidebarInnerLayout.onLayoutChanged(layout);
}}
>
<Panel
id={LIST_PANEL_ID}
className="maputnik-layout-list"
defaultSize={`${getDefaultListPercent()}%`}
groupResizeBehavior="preserve-relative-size"
minSize="100px"
>
{props.layerList}
</Panel>
<Separator
className="maputnik-layout-resize-handle maputnik-layout-resize-handle--inner"
data-wd-key="inner-resize-handle"
title={t("Drag to resize list / editor split")}
aria-label={t("Drag to resize list / editor split")}
/>
<Panel
id={DRAWER_PANEL_ID}
className="maputnik-layout-drawer"
defaultSize={`${100 - getDefaultListPercent()}%`}
minSize="150px"
>
<ScrollContainer>
{props.layerEditor}
</ScrollContainer>
</Panel>
</Group>}
</Panel>
<Separator
className="maputnik-layout-resize-handle"
data-wd-key="sidebar-resize-handle"
title={t("Drag to resize sidebar")}
aria-label={t("Drag to resize sidebar")}
/>
<Panel
id={MAP_PANEL_ID}
className="maputnik-layout-map"
minSize={0}
>
{props.map}
</Panel>
</Group>
</div>
</IconContext.Provider>;
}
{props.bottom && <div className="maputnik-layout-bottom">
{props.bottom}
</div>
}
{props.modals}
</div>
</IconContext.Provider>;
}

const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;
11 changes: 10 additions & 1 deletion src/components/LayerListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@ type DraggableLabelProps = {
layerType: string
dragAttributes?: React.HTMLAttributes<HTMLElement>
dragListeners?: React.HTMLAttributes<HTMLElement>
onSelect: () => void
};

const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
const { dragAttributes, dragListeners } = props;
return <div className="maputnik-layer-list-item-handle" {...dragAttributes} {...dragListeners}>

const handleClick = (e: React.MouseEvent) => {
// Ensure layer selection fires even when dnd-kit captures the pointer
e.stopPropagation();
props.onSelect();
};

return <div className="maputnik-layer-list-item-handle" {...dragAttributes} {...dragListeners} onClick={handleClick}>
<IconLayer
className="layer-handle__icon"
type={props.layerType}
Expand Down Expand Up @@ -138,6 +146,7 @@ const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props
layerType={props.layerType}
dragAttributes={attributes}
dragListeners={listeners}
onSelect={() => props.onLayerSelect(props.layerIndex)}
/>
<span style={{ flexGrow: 1 }} />
<IconAction
Expand Down
2 changes: 2 additions & 0 deletions src/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
"Delete expression": "Ausdruck löschen",
"Delete filter block": "Filterblock löschen",
"Deuteranopia filter": "Deuteranopie-Filter",
"Drag to resize list / editor split": "Ziehen zum Ändern der Größe der Liste / des Editors",
"Drag to resize sidebar": "Ziehen zum Ändern der Größe der Seitenleiste",
"Duplicate": "Duplizieren",
"Encoding": "Kodierung",
"Enter URL...": "URL eingeben...",
Expand Down
Loading
Loading