Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: Desktop-View - Added new components CommonSimpleTable and Co…
…mmonActionMenu. Co-authored-by: Dusan Vuckovic <dv@zammad.com> Co-authored-by: Martin Gruner <mg@zammad.com> Co-authored-by: Dominik Klein <dk@zammad.com> Co-authored-by: Benjamin Scharf <bs@zammad.com>
- Loading branch information
1 parent
dff64d2
commit fa07a4c
Showing
16 changed files
with
766 additions
and
48 deletions.
There are no files selected for viewing
100 changes: 100 additions & 0 deletions
100
app/frontend/apps/desktop/components/CommonActionMenu/CommonActionMenu.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ --> | ||
|
||
<script setup lang="ts"> | ||
import { computed, ref, toRefs } from 'vue' | ||
import getUuid from '#shared/utils/getUuid.ts' | ||
import type { ObjectLike } from '#shared/types/utils.ts' | ||
import CommonPopover from '#desktop/components/CommonPopover/CommonPopover.vue' | ||
import CommonButton from '#desktop/components/CommonButton/CommonButton.vue' | ||
import CommonPopoverMenu from '#desktop/components/CommonPopover/CommonPopoverMenu.vue' | ||
import { usePopover } from '#desktop/components/CommonPopover/usePopover.ts' | ||
import type { | ||
ButtonSize, | ||
ButtonVariant, | ||
} from '#desktop/components/CommonButton/types.ts' | ||
import type { | ||
MenuItem, | ||
Orientation, | ||
Placement, | ||
} from '#desktop/components/CommonPopover/types.ts' | ||
import { usePopoverMenu } from '#desktop/components/CommonPopover/usePopoverMenu.ts' | ||
interface Props { | ||
actions: MenuItem[] | ||
entity?: ObjectLike | ||
buttonSize?: ButtonSize | ||
buttonVariant?: ButtonVariant | ||
placement?: Placement | ||
orientation?: Orientation | ||
noSingleActionMode?: boolean | ||
} | ||
const props = withDefaults(defineProps<Props>(), { | ||
buttonSize: 'large', | ||
buttonVariant: 'neutral', | ||
placement: 'start', | ||
orientation: 'autoVertical', | ||
}) | ||
const popoverMenu = ref<InstanceType<typeof CommonPopoverMenu>>() | ||
const { popover, isOpen: popoverIsOpen, popoverTarget, toggle } = usePopover() | ||
const { actions, entity } = toRefs(props) | ||
const { filteredMenuItems, singleMenuItemPresent, singleMenuItem } = | ||
usePopoverMenu(actions, entity, { provides: true }) | ||
const entityId = computed(() => props.entity?.id || getUuid()) | ||
const menuId = computed(() => `popover-${entityId.value}`) | ||
const singleActionMode = computed(() => { | ||
if (props.noSingleActionMode) return false | ||
return singleMenuItemPresent.value | ||
}) | ||
</script> | ||
|
||
<template> | ||
<div v-if="filteredMenuItems" class="inline-block"> | ||
<CommonButton | ||
v-if="singleActionMode" | ||
:size="buttonSize" | ||
:variant="buttonVariant" | ||
:aria-label="$t(singleMenuItem?.label)" | ||
:icon="singleMenuItem?.icon" | ||
@click="singleMenuItem?.onClick?.(entity as ObjectLike)" | ||
/> | ||
<CommonButton | ||
v-else | ||
:id="entity?.id || entityId" | ||
ref="popoverTarget" | ||
:aria-label="$t('Action menu button')" | ||
aria-haspopup="true" | ||
:aria-controls="menuId" | ||
:class="{ | ||
'outline outline-1 outline-offset-1 outline-blue-800': popoverIsOpen, | ||
}" | ||
:size="buttonSize" | ||
:variant="buttonVariant" | ||
icon="three-dots-vertical" | ||
@click="toggle" | ||
/> | ||
<CommonPopover | ||
v-if="!singleActionMode" | ||
:id="menuId" | ||
ref="popover" | ||
:placement="placement" | ||
:orientation="orientation" | ||
:owner="popoverTarget" | ||
> | ||
<CommonPopoverMenu | ||
ref="popoverMenu" | ||
:entity="entity" | ||
:popover="popover" | ||
/> | ||
</CommonPopover> | ||
</div> | ||
</template> |
104 changes: 104 additions & 0 deletions
104
app/frontend/apps/desktop/components/CommonActionMenu/__tests__/CommonActionMenu.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ | ||
|
||
import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue' | ||
import renderComponent from '#tests/support/components/renderComponent.ts' | ||
|
||
const fn = vi.fn() | ||
describe('CommonActionMenu', () => { | ||
let view: ReturnType<typeof renderComponent> | ||
|
||
const actions = [ | ||
{ | ||
key: 'delete-foo', | ||
label: 'Delete Foo', | ||
icon: 'trash3', | ||
onClick: ({ id }: { id: string }) => { | ||
fn(id) | ||
}, | ||
}, | ||
{ | ||
key: 'change-foo', | ||
label: 'Change Foo', | ||
icon: 'person-gear', | ||
onClick: ({ id }: { id: string }) => { | ||
fn(id) | ||
}, | ||
}, | ||
] | ||
|
||
beforeEach(() => { | ||
view = renderComponent(CommonActionMenu, { | ||
props: { | ||
entity: { | ||
id: 'foo-test-action', | ||
}, | ||
actions, | ||
}, | ||
}) | ||
}) | ||
|
||
it('shows action menu button by default', () => { | ||
expect(view.getByIconName('three-dots-vertical')).toBeInTheDocument() | ||
}) | ||
|
||
it('calls onClick handler when action is clicked', async () => { | ||
await view.events.click(view.getByIconName('three-dots-vertical')) | ||
|
||
expect(view.getByIconName('trash3')).toBeInTheDocument() | ||
expect(view.getByIconName('person-gear')).toBeInTheDocument() | ||
|
||
await view.events.click(view.getByText('Change Foo')) | ||
|
||
expect(fn).toHaveBeenCalledWith('foo-test-action') | ||
}) | ||
|
||
it('finds corresponding a11y controls', async () => { | ||
const id = view | ||
.getByLabelText('Action menu button') | ||
.getAttribute('aria-controls') | ||
|
||
await view.events.click(view.getByIconName('three-dots-vertical')) | ||
|
||
const popover = document.getElementById(id as string) | ||
|
||
expect(popover?.getAttribute('id')).toEqual(id) | ||
}) | ||
|
||
describe('single action mode', () => { | ||
beforeEach(async () => { | ||
await view.rerender({ | ||
actions: [actions[0]], | ||
}) | ||
}) | ||
|
||
it('adds aria label on single action button', () => { | ||
expect(view.getByLabelText('Delete Foo')).toBeInTheDocument() | ||
}) | ||
|
||
it('supports single action mode', () => { | ||
expect( | ||
view.queryByIconName('three-dots-vertical'), | ||
).not.toBeInTheDocument() | ||
|
||
expect(view.getByIconName('trash3')).toBeInTheDocument() | ||
}) | ||
|
||
it('calls onClick handler when action is clicked', async () => { | ||
await view.events.click(view.getByIconName('trash3')) | ||
|
||
expect(fn).toHaveBeenCalledWith('foo-test-action') | ||
}) | ||
|
||
it('renders single action if prop is set', async () => { | ||
await view.rerender({ | ||
noSingleActionMode: true, | ||
}) | ||
|
||
expect(view.queryByIconName('trash3')).not.toBeInTheDocument() | ||
|
||
await view.events.click(view.getByIconName('three-dots-vertical')) | ||
|
||
expect(view.getByIconName('trash3')).toBeInTheDocument() | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.