Skip to content

Commit

Permalink
BC 8586 - Make Visible Action Menu on Mobile View (#3487)
Browse files Browse the repository at this point in the history
BC-8586 - implement sticky action menu on mobile view
  • Loading branch information
muratmerdoglu-dp authored Jan 14, 2025
1 parent e02c986 commit 398ac8b
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 112 deletions.
25 changes: 23 additions & 2 deletions src/components/templates/DefaultWireframe.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@@/tests/test-utils/setup";
import { ComponentMountingOptions, mount } from "@vue/test-utils";
import DefaultWireframe from "../templates/DefaultWireframe.vue";
import { nextTick } from "vue";

describe("DefaultWireframe", () => {
const setup = (
Expand Down Expand Up @@ -44,6 +45,7 @@ describe("DefaultWireframe", () => {
props: {
fullWidth: true,
headline: "dummy title",
maxWidth: "full",
breadcrumbs: [
{
title: "dummy breadcrumb 1",
Expand Down Expand Up @@ -109,7 +111,7 @@ describe("DefaultWireframe", () => {

it("displays headline in slot", () => {
const wrapper = setup({
props: { headline: "property title", fullWidth: false },
props: { headline: "property title", fullWidth: false, maxWidth: "full" },
slots: {
header: [
"<h1>slot title</h1>",
Expand All @@ -125,7 +127,9 @@ describe("DefaultWireframe", () => {
});

it("should emit 'fab:clicked' after click the fab button", async () => {
const wrapper = setup();
const wrapper = setup({
props: { maxWidth: "nativ" },
});
await wrapper.setProps({
fabItems: {
icon: "mdi-close",
Expand All @@ -138,4 +142,21 @@ describe("DefaultWireframe", () => {

expect(wrapper.emitted("fab:clicked")).toHaveLength(1);
});

describe("when 'fixedHeader' prop is set", () => {
it("should have 'fixed-header' class", async () => {
const wrapper = setup({
props: { maxWidth: "nativ" },
});

const headerBefore = wrapper.find(".wireframe-header");
expect(headerBefore.classes("fixed")).toBe(false);

wrapper.setProps({ fixedHeader: true });
await nextTick();

const headerAfter = wrapper.find(".wireframe-header");
expect(headerAfter.classes("fixed")).toBe(true);
});
});
});
14 changes: 12 additions & 2 deletions src/components/templates/DefaultWireframe.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
id="notify-screen-reader-assertive"
class="d-sr-only"
/>
<div class="wireframe-header sticky">
<div class="wireframe-header sticky" :class="{ fixed: fixedHeader }">
<Breadcrumbs v-if="breadcrumbs.length" :breadcrumbs="breadcrumbs" />
<div v-else class="breadcrumbs-placeholder" />
<slot name="header">
Expand Down Expand Up @@ -108,6 +108,9 @@ const props = defineProps({
type: String as PropType<string | null>,
default: null,
},
fixedHeader: {
type: Boolean,
},
});
const emit = defineEmits({
Expand Down Expand Up @@ -184,6 +187,13 @@ const showDivider = computed(() => {
background-color: rgb(var(--v-theme-white));
}
.fixed {
position: fixed;
top: 64px;
width: 100%;
background-color: rgb(var(--v-theme-white));
}
@media #{map-get($display-breakpoints, 'lg-and-up')} {
.wireframe-fab {
position: relative;
Expand All @@ -208,7 +218,7 @@ $fab-wrapper-height: 80px;
align-items: center;
justify-content: flex-end;
height: $fab-wrapper-height;
margin-top: -#{$fab-wrapper-height}; // stylelint-disable-line sh-waqar/declaration-use-variable
margin-top: -#{$fab-wrapper-height};
pointer-events: none;
* {
Expand Down
1 change: 1 addition & 0 deletions src/components/templates/default-wireframe.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type Fab = {
icon: string;
title: string;
href?: string;
to?: string;
ariaLabel?: string;
dataTestId?: string;
};
50 changes: 50 additions & 0 deletions src/modules/feature/room/RoomMembers/ActionMenu.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
createTestingI18n,
createTestingVuetify,
} from "@@/tests/test-utils/setup";
import ActionMenu from "./ActionMenu.vue";

describe("ActionMenu", () => {
const setup = (selectedIds: string[] = ["test-id#1", "test-id#2"]) => {
const wrapper = mount(ActionMenu, {
global: {
plugins: [createTestingVuetify(), createTestingI18n()],
},
props: {
selectedIds,
},
});

return { wrapper };
};

it("should be rendered", () => {
const { wrapper } = setup();
expect(wrapper).toBeDefined();
});

it("should show selected count", async () => {
const { wrapper } = setup();
const selectedCount = wrapper.find(".selected-count");
expect(selectedCount.html()).toContain("2 pages.administration.selected");
});

it("should emit 'remove:selected' event when 'Remove' button is clicked", async () => {
const { wrapper } = setup();
await wrapper
.findComponent({ ref: "removeSelectedMembers" })
.trigger("click");
const emitted = wrapper.emitted("remove:selected");
expect(wrapper.emitted()).toHaveProperty("remove:selected");
expect(emitted![0][0]).toStrictEqual(["test-id#1", "test-id#2"]);
});

it("should emit 'reset:selected' event when 'Reset' button is clicked", async () => {
const { wrapper } = setup();
await wrapper
.findComponent({ ref: "resetSelectedMembers" })
.trigger("click");

expect(wrapper.emitted()).toHaveProperty("reset:selected");
});
});
52 changes: 52 additions & 0 deletions src/modules/feature/room/RoomMembers/ActionMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<div class="mr-2 pa-0 pl-4" data-testid="multi-action-menu">
<span class="d-inline-flex selected-count">
{{ selectedIds.length }}
{{ t("pages.administration.selected") }}
</span>
<v-btn
ref="removeSelectedMembers"
class="ml-2"
size="x-small"
variant="text"
:icon="mdiTrashCanOutline"
:aria-label="t('pages.rooms.members.multipleRemove.ariaLabel')"
@click="onRemove"
/>

<v-btn
ref="resetSelectedMembers"
class="ml-2 mr-2"
size="x-small"
variant="text"
:icon="mdiClose"
:aria-label="t('pages.rooms.members.remove.ariaLabel')"
@click="onReset"
/>
</div>
</template>

<script setup lang="ts">
import { mdiClose, mdiTrashCanOutline } from "@icons/material";
import { useI18n } from "vue-i18n";
const props = defineProps({
selectedIds: {
type: Array<string>,
required: true,
},
});
const { t } = useI18n();
const emit = defineEmits<{
(e: "remove:selected", selectedIds: string[]): void;
(e: "reset:selected"): void;
}>();
const onRemove = () => {
emit("remove:selected", props.selectedIds);
};
const onReset = () => {
emit("reset:selected");
};
</script>
92 changes: 50 additions & 42 deletions src/modules/feature/room/RoomMembers/MembersTable.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import MembersTable from "./MembersTable.vue";
import { nextTick, ref } from "vue";
import { mdiMenuDown, mdiMenuUp, mdiMagnify } from "@icons/material";
import { roomMemberFactory } from "@@/tests/test-utils";
import { DOMWrapper, VueWrapper } from "@vue/test-utils";
import { DOMWrapper, flushPromises, VueWrapper } from "@vue/test-utils";
import { VDataTable, VTextField } from "vuetify/lib/components/index.mjs";
import { useConfirmationDialog } from "@ui-confirmation-dialog";
import setupConfirmationComposableMock from "@@/tests/test-utils/composable-mocks/setupConfirmationComposableMock";
Expand Down Expand Up @@ -131,41 +131,29 @@ describe("MembersTable", () => {
expect(multiActionMenu.exists()).toBe(true);
});

it("should render selected members remove button", async () => {
it("should render ActionMenu component", async () => {
const { wrapper } = setup();

await selectCheckboxes([1], wrapper);

const removeButton = wrapper.findComponent({
ref: "removeSelectedMembers",
});

expect(removeButton.exists()).toBe(true);
});

it("should render selected members reset button", async () => {
const { wrapper } = setup();
const actionMenuBefore = wrapper.findComponent({ name: "ActionMenu" });
expect(actionMenuBefore.exists()).toBe(false);

await selectCheckboxes([1, 2], wrapper);
await selectCheckboxes([1], wrapper);

const resetButton = wrapper.findComponent({
ref: "resetSelectedMembers",
});
const actionMenuAfter = wrapper.findComponent({ name: "ActionMenu" });

expect(resetButton.exists()).toBe(true);
expect(actionMenuAfter.exists()).toBe(true);
});

it("should reset member selection when clicking reset button", async () => {
it("should reset member selection when clicking reset button on ActionMenu", async () => {
const { wrapper } = setup();

askConfirmationMock.mockResolvedValue(false);

await selectCheckboxes([0], wrapper);

const resetButton = wrapper.findComponent({
ref: "resetSelectedMembers",
});
await resetButton.trigger("click");
const actionMenu = wrapper.findComponent({ name: "ActionMenu" });
actionMenu.vm.$emit("reset:selected");
await flushPromises();

const checkboxes = wrapper
.getComponent(VDataTable)
Expand Down Expand Up @@ -200,17 +188,16 @@ describe("MembersTable", () => {
}
);

it("should emit remove:members when selected members remove button is clicked", async () => {
it("should emit remove:members when selected members remove button is clicked on Action Menu", async () => {
const { wrapper, mockMembers } = setup();

askConfirmationMock.mockResolvedValue(true);

await selectCheckboxes([1], wrapper);

const removeButton = wrapper.findComponent({
ref: "removeSelectedMembers",
});
await removeButton.trigger("click");
const actionMenu = wrapper.findComponent({ name: "ActionMenu" });
actionMenu.vm.$emit("remove:selected", [mockMembers[0].userId]);
await flushPromises();

const removeEvents = wrapper.emitted("remove:members");
expect(removeEvents).toHaveLength(1);
Expand All @@ -224,10 +211,9 @@ describe("MembersTable", () => {

await selectCheckboxes([1], wrapper);

const removeButton = wrapper.findComponent({
ref: "removeSelectedMembers",
});
await removeButton.trigger("click");
const actionMenu = wrapper.findComponent({ name: "ActionMenu" });
actionMenu.vm.$emit("reset:selected");
await flushPromises();

expect(wrapper.emitted()).not.toHaveProperty("remove:members");
});
Expand All @@ -246,16 +232,18 @@ describe("MembersTable", () => {
])(
"should render confirmation dialog with text for $description when remove button is clicked",
async ({ checkboxesToSelect, expectedMessage }) => {
const { wrapper } = setup();
const { wrapper, mockMembers } = setup();

askConfirmationMock.mockResolvedValue(true);

await selectCheckboxes(checkboxesToSelect, wrapper);

const removeButton = wrapper.findComponent({
ref: "removeSelectedMembers",
});
await removeButton.trigger("click");
const actionMenu = wrapper.findComponent({ name: "ActionMenu" });
actionMenu.vm.$emit(
"remove:selected",
checkboxesToSelect.map((i) => mockMembers[i].userId)
);
await flushPromises();

expect(wrapper.emitted()).toHaveProperty("remove:members");

Expand All @@ -267,16 +255,15 @@ describe("MembersTable", () => {
);

it("should keep selection if confirmation dialog is canceled", async () => {
const { wrapper } = setup();
const { wrapper, mockMembers } = setup();

askConfirmationMock.mockResolvedValue(false);

await selectCheckboxes([1], wrapper);

const removeButton = wrapper.getComponent({
ref: "removeSelectedMembers",
});
await removeButton.trigger("click");
const actionMenu = wrapper.findComponent({ name: "ActionMenu" });
actionMenu.vm.$emit("remove:selected", [mockMembers[0].userId]);
await flushPromises();

const checkboxes = wrapper
.getComponent(VDataTable)
Expand Down Expand Up @@ -410,4 +397,25 @@ describe("MembersTable", () => {
expect(dataTableTextContent).not.toContain(mockMembers[2].firstName);
});
});

describe("when 'fixedPosition' prop is set", () => {
it("should have 'fixed-position' class", async () => {
const { wrapper } = setup();

const elementBefore = wrapper.find(".table-title-header");
expect(elementBefore.classes("fixed-position")).toBe(false);

wrapper.setProps({
fixedPosition: {
enabled: true,
positionTop: 0,
},
});
await nextTick();
await nextTick();

const elementAfter = wrapper.find(".table-title-header");
expect(elementAfter.classes("fixed-position")).toBe(true);
});
});
});
Loading

0 comments on commit 398ac8b

Please sign in to comment.