diff --git a/CHANGELOG.md b/CHANGELOG.md index 7194e6a1..f2a3232a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - _...Add new stuff here..._ ### 🐞 Bug fixes +- Preserve expanded layer groups when deleting layers, including the first layer of a group - The map's data listener now fires on tile loads again, so source and vector layer field autocompletion is populated - The `maputnik` desktop binary now opens the default browser automatically on startup (opt out with `--no-browser`) - _...Add new stuff here..._ diff --git a/e2e/fixtures/grouped-layers-style.json b/e2e/fixtures/grouped-layers-style.json new file mode 100644 index 00000000..46bb088b --- /dev/null +++ b/e2e/fixtures/grouped-layers-style.json @@ -0,0 +1,16 @@ +{ + "version": 8, + "name": "Grouped layers", + "sources": {}, + "layers": [ + { "id": "background", "type": "background" }, + { "id": "park-a", "type": "background" }, + { "id": "park-b", "type": "background" }, + { "id": "park-c", "type": "background" }, + { "id": "road", "type": "background" }, + { "id": "park-d", "type": "background" }, + { "id": "park-e", "type": "background" }, + { "id": "water-a", "type": "background" }, + { "id": "water-b", "type": "background" } + ] +} diff --git a/e2e/layers-list.spec.ts b/e2e/layers-list.spec.ts index 85cdd4a1..2cc4bccb 100644 --- a/e2e/layers-list.spec.ts +++ b/e2e/layers-list.spec.ts @@ -306,6 +306,75 @@ describe("layers list", () => { }); describe("groups", () => { + for (const control of ["menu", "trash"] as const) { + describe(`after deletion using ${control}`, () => { + beforeEach(async () => { + await when.setStyle("grouped_layers"); + }); + + test("keeps all groups expanded when an earlier layer is deleted", async () => { + await when.click("skip-target-layer-list"); + await then(get.elementByTestId("layer-list-item:park-b")).shouldBeVisible(); + await when.deleteLayer("background", control); + await then(get.elementByTestId("layer-list-item:background")).shouldNotExist(); + await then(get.elementByTestId("layer-list-group:park-0")).shouldExist(); + await expect(get.elementByTestId("layer-list-group:water-6").getByRole("button")).toHaveAttribute("aria-expanded", "true"); + await then(get.elementByTestId("layer-list-item:park-b")).shouldBeVisible(); + await then(get.elementByTestId("layer-list-item:park-e")).shouldBeVisible(); + await then(get.elementByTestId("layer-list-item:water-b")).shouldBeVisible(); + }); + + test("preserves separate states for groups with the same prefix", async () => { + await when.click("layer-list-group:park-1"); + await when.deleteLayer("background", control); + await then(get.elementByTestId("layer-list-group:park-0")).shouldExist(); + await when.click("layer-list-item:road"); + await expect(get.elementByTestId("layer-list-group:park-0").getByRole("button")).toHaveAttribute("aria-expanded", "true"); + await expect(get.elementByTestId("layer-list-group:park-4").getByRole("button")).toHaveAttribute("aria-expanded", "false"); + await then(get.elementByTestId("layer-list-item:park-b")).shouldBeVisible(); + await then(get.elementByTestId("layer-list-item:park-e")).shouldNotBeVisible(); + }); + + test("keeps a group expanded when its first layer is deleted", async () => { + await when.click("layer-list-group:park-1"); + await when.deleteLayer("park-a", control); + await then(get.elementByTestId("layer-list-item:park-a")).shouldNotExist(); + await when.click("layer-list-item:road"); + await expect(get.elementByTestId("layer-list-group:park-1").getByRole("button")).toHaveAttribute("aria-expanded", "true"); + await then(get.elementByTestId("layer-list-item:park-c")).shouldBeVisible(); + }); + + for (const expandedGroup of ["park-1", "park-5"]) { + test(`keeps a merged group expanded when ${expandedGroup} was expanded`, async () => { + await when.click("layer-list-group:" + expandedGroup); + await when.deleteLayer("road", control); + await then(get.elementByTestId("layer-list-item:road")).shouldNotExist(); + await expect(get.elementByTestId("layer-list-group:park-1").getByRole("button")).toHaveAttribute("aria-expanded", "true"); + await then(get.elementByTestId("layer-list-item:park-b")).shouldBeVisible(); + await then(get.elementByTestId("layer-list-item:park-e")).shouldBeVisible(); + }); + } + + test("keeps a merged group collapsed when both groups were collapsed", async () => { + await when.deleteLayer("road", control); + await then(get.elementByTestId("layer-list-item:road")).shouldNotExist(); + await when.click("layer-list-item:background"); + await expect(get.elementByTestId("layer-list-group:park-1").getByRole("button")).toHaveAttribute("aria-expanded", "false"); + await then(get.elementByTestId("layer-list-item:park-b")).shouldNotBeVisible(); + await then(get.elementByTestId("layer-list-item:park-e")).shouldNotBeVisible(); + }); + + test("preserves later groups after deleting an entire group", async () => { + await when.click("skip-target-layer-list"); + await when.deleteLayer("park-a", control); + await when.deleteLayer("park-b", control); + await when.deleteLayer("park-c", control); + await expect(get.elementByTestId("layer-list-group:water-4").getByRole("button")).toHaveAttribute("aria-expanded", "true"); + await then(get.elementByTestId("layer-list-item:water-b")).shouldBeVisible(); + }); + }); + } + test("simple", async () => { await when.setStyle("geojson"); diff --git a/e2e/maputnik-driver.ts b/e2e/maputnik-driver.ts index 9851a542..00733f00 100644 --- a/e2e/maputnik-driver.ts +++ b/e2e/maputnik-driver.ts @@ -39,6 +39,7 @@ export class MaputnikDriver { "example-style-with-zoom-7-and-center-0-51.json", "example-style-with-zoom-5-and-center-50-50.json", "access-token-style.json", + "grouped-layers-style.json", ]; for (const fixture of styleFixtures) { await this.helper.given.interceptAndMockResponse({ @@ -63,6 +64,18 @@ export class MaputnikDriver { modal: this.modalDriver.when, + deleteLayer: async (id: string, control: "menu" | "trash" = "menu") => { + if (control === "trash") { + await this.helper.when.hover("layer-list-item:" + id); + await this.helper.when.click("layer-list-item:" + id + ":delete"); + return; + } + + await this.helper.when.click("layer-list-item:" + id); + await this.helper.when.click("skip-target-layer-editor"); + await this.helper.when.click("menu-delete-layer"); + }, + setStyle: async ( styleProperties: | "geojson" @@ -73,6 +86,7 @@ export class MaputnikDriver { | "font" | "zoom_7_center_0_51" | "access_tokens" + | "grouped_layers" | "", zoom?: number ) => { @@ -85,6 +99,7 @@ export class MaputnikDriver { font: "example-style-with-fonts.json", zoom_7_center_0_51: "example-style-with-zoom-7-and-center-0-51.json", access_tokens: "access-token-style.json", + grouped_layers: "grouped-layers-style.json", }; const url = new URL(baseUrl); diff --git a/src/components/LayerList.tsx b/src/components/LayerList.tsx index 26a4e561..9ca6a2a6 100644 --- a/src/components/LayerList.tsx +++ b/src/components/LayerList.tsx @@ -106,13 +106,13 @@ class LayerListContainerInternal extends React.Component(); + let idx = 0; + for (const layers of this.groupedLayers(previousLayers)) { + const collapsed = this.isCollapsed(layerPrefix(layers[0].id), idx); + for (const layer of layers) { + collapsedByLayer.set(layer.id, collapsed); + } + idx += layers.length; + } + + const collapsedGroups: {[key: string]: boolean} = {}; + idx = 0; + for (const layers of this.groupedLayers()) { + const lookupKey = [layerPrefix(layers[0].id), idx].join("-"); + collapsedGroups[lookupKey] = layers.every(layer => collapsedByLayer.get(layer.id) !== false); + idx += layers.length; + } + + if (!lodash.isEqual(collapsedGroups, this.state.collapsedGroups)) { + this.setState({ collapsedGroups }); + } + } + componentDidUpdate (prevProps: LayerListContainerProps) { + this.preserveLayerGroupState(prevProps.layers); + if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) { const selectedItemNode = this.selectedItemRef.current; if (selectedItemNode && selectedItemNode.node) {