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

BC 8586 - Make Visible Action Menu on Mobile View #3487

Merged
merged 28 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
caffa8e
create new component for the actions menu
muratmerdoglu-dp Dec 18, 2024
6876ad7
fix MembersTable unit tests
muratmerdoglu-dp Dec 18, 2024
3d00f35
chore: remove unnecessary v-if
muratmerdoglu-dp Dec 18, 2024
26fb134
chore: fix Fab type
muratmerdoglu-dp Dec 20, 2024
e61fb91
add fixedHeader props to DefaultWireframe
muratmerdoglu-dp Dec 20, 2024
2583981
implement calculation of positioning
muratmerdoglu-dp Dec 20, 2024
1d72503
Merge branch 'main' into bc-8586-visible-action-menu
muratmerdoglu-dp Jan 6, 2025
de261d0
add position to ActionMenu
muratmerdoglu-dp Jan 6, 2025
0fbbf30
change props type to object
muratmerdoglu-dp Jan 7, 2025
925403c
Merge branch 'main' into bc-8586-visible-action-menu
muratmerdoglu-dp Jan 7, 2025
5b3e921
chore: remove duplicated importing
muratmerdoglu-dp Jan 7, 2025
8e158ee
chore: remove unnecessary ref value
muratmerdoglu-dp Jan 7, 2025
b38940a
add unit tests for ActionMenu.vue
muratmerdoglu-dp Jan 8, 2025
80c5c93
chore: rename variable name
muratmerdoglu-dp Jan 9, 2025
85f2575
chore: remove top value
muratmerdoglu-dp Jan 9, 2025
03bd5e6
resize the action buttons
muratmerdoglu-dp Jan 10, 2025
09cdf0e
Merge branch 'main' into bc-8586-visible-action-menu
muratmerdoglu-dp Jan 10, 2025
a9ea74a
chore: use v-else for the spacer element
muratmerdoglu-dp Jan 10, 2025
4a282bb
add unit test for DefaultWireframe
muratmerdoglu-dp Jan 10, 2025
b86e675
add unit test for MembersTable component
muratmerdoglu-dp Jan 10, 2025
79e2217
Merge branch 'main' into bc-8586-visible-action-menu
muratmerdoglu-dp Jan 13, 2025
528e40f
Merge branch 'main' into bc-8586-visible-action-menu
muratmerdoglu-dp Jan 13, 2025
71fc8d6
change the review comments
muratmerdoglu-dp Jan 13, 2025
78d4518
Merge remote-tracking branch 'refs/remotes/origin/bc-8586-visible-act…
muratmerdoglu-dp Jan 13, 2025
add4ea4
Merge branch 'main' into bc-8586-visible-action-menu
muratmerdoglu-dp Jan 13, 2025
a27b1dd
chore: remove y value watching
muratmerdoglu-dp Jan 13, 2025
2804b63
chore: add class for ActionMenu
muratmerdoglu-dp Jan 13, 2025
bdd1b3b
update generated api
muratmerdoglu-dp Jan 14, 2025
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
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,
NFriedo marked this conversation as resolved.
Show resolved Hide resolved
},
});

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
Loading