Skip to content

Commit

Permalink
Merge pull request #1193 from dreammall-earth/functional-tabcontrol
Browse files Browse the repository at this point in the history
feat(frontend): functional tabcontrol
  • Loading branch information
Bettelstab authored Jun 21, 2024
2 parents a66bea1 + 2717435 commit 0b985a9
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 2,296 deletions.
5 changes: 5 additions & 0 deletions frontend/src/assets/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ $font-family-default: 'Helvetica', sans-serif;
:root {
--menu-icon-height: 44px;
}

a {
color: inherit;
text-decoration: none;
}
56 changes: 49 additions & 7 deletions frontend/src/components/menu/TabControl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
import { Component, h } from 'vue'
import { VApp } from 'vuetify/components'

import { usePageContext } from '#root/renderer/context/usePageContext'

import TabControl from './TabControl.vue'

vi.mock('#root/renderer/context/usePageContext')
const mockedUsePageContext = vi.mocked(usePageContext)

describe('TabControl', () => {
const Wrapper = () => {
return mount(VApp, {
Expand All @@ -18,6 +23,11 @@ describe('TabControl', () => {

beforeEach(() => {
vi.useFakeTimers()
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mockedUsePageContext.mockReturnValue({
urlPathname: '/',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
wrapper = Wrapper()
vi.runAllTimers()
})
Expand All @@ -26,33 +36,65 @@ describe('TabControl', () => {
expect(wrapper.element).toMatchSnapshot()
})

describe('set active item by route', () => {
it('sets first item active for /', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mockedUsePageContext.mockReturnValue({
urlPathname: '/',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
wrapper = Wrapper()
expect(wrapper.find('button.tab-control').findAll('a.item')[0].classes('active')).toBe(true)
})

it('sets second item active for /cockpit', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mockedUsePageContext.mockReturnValue({
urlPathname: '/cockpit',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
wrapper = Wrapper()
expect(wrapper.find('button.tab-control').findAll('a.item')[1].classes('active')).toBe(true)
})

it('sets first item active for /somerandomroute', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mockedUsePageContext.mockReturnValue({
urlPathname: '/somerandomroute',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
wrapper = Wrapper()
expect(wrapper.find('button.tab-control').findAll('a.item')[0].classes('active')).toBe(true)
})
})

describe('click tab control button', () => {
beforeEach(async () => {
await wrapper.find('button.tab-control').trigger('click')
})

it('has three menu buttons', () => {
expect(wrapper.find('button.tab-control').findAll('button')).toHaveLength(3)
it('has two menu items', () => {
expect(wrapper.find('button.tab-control').findAll('a.item')).toHaveLength(2)
})

describe('set item', () => {
beforeEach(async () => {
await wrapper.findAll('button.item')[1].trigger('click')
await wrapper.findAll('a.item')[1].trigger('click')
})

it('changes active item', () => {
expect(wrapper.findAll('button.item')[1].classes('active')).toBe(true)
expect(wrapper.findAll('a.item')[1].classes('active')).toBe(true)
})
})

describe('set item with menu closed', () => {
beforeEach(async () => {
vi.runAllTimers()
await wrapper.findAll('button.item')[1].trigger('click')
await wrapper.findAll('a.item')[1].trigger('click')
})

it('does not change the active item', () => {
expect(wrapper.findAll('button.item')[0].classes('active')).toBe(true)
expect(wrapper.findAll('a.item')[0].classes('active')).toBe(true)
})
})
})
Expand All @@ -64,7 +106,7 @@ describe('TabControl', () => {
wrapper.unmount()
})

it('clears timouts', () => {
it('clears timeouts', () => {
expect(timeOutSpy).toBeCalled()
})
})
Expand Down
47 changes: 38 additions & 9 deletions frontend/src/components/menu/TabControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,68 @@
>
<div v-show="isSliding" ref="marker" class="marker"></div>
<div class="d-flex align-center justify-center h-100 w-100">
<button
<a
v-for="(item, index) in items"
:key="index"
:key="item.text"
ref="itemRefs"
class="item"
:class="{ active: activeItem === index }"
@click="() => setItem(index)"
@click.prevent="() => setItem(index)"
>
<div class="icon d-flex justify-center align-center">
<v-icon :icon="item.icon" class="w-100" :class="item.class"></v-icon>
</div>
{{ $t(item.text) }}
</button>
</a>
</div>
</button>
</template>

<script lang="ts" setup>
import { navigate } from 'vike/client/router'
import { onMounted, onUnmounted, ref } from 'vue'
import { usePageContext } from '#root/renderer/context/usePageContext'
import type { Ref } from 'vue'
const isOpen = ref(true)
const isSliding = ref(false)
const activeItem = ref(0)
const pageContext = usePageContext()
const tabControl: Ref<HTMLElement | null> = ref(null)
const marker: Ref<HTMLElement | null> = ref(null)
const { urlPathname } = pageContext
const items = [
{
class: 'world-cafe',
icon: '$world-cafe',
text: 'menu.worldCafe',
link: '/',
},
/*
{
class: 'mall',
icon: '$mall',
text: 'menu.mall',
},
*/
{
class: 'cockpit',
icon: '$cockpit',
text: 'menu.cockpit',
link: '/cockpit',
},
]
const isOpen = ref(true)
const isSliding = ref(false)
let defaultItem = items.findIndex((i) =>
i.link === '/' ? urlPathname === '/' : urlPathname.startsWith(i.link),
)
defaultItem = defaultItem < 0 ? 0 : defaultItem
const activeItem = ref(defaultItem)
const tabControl: Ref<HTMLElement | null> = ref(null)
const marker: Ref<HTMLElement | null> = ref(null)
const itemRefs = ref([] as HTMLElement[])
let timer: ReturnType<typeof setTimeout>
Expand Down Expand Up @@ -81,6 +96,9 @@ function closeWithDelay() {
* @param item
*/
function moveMarker() {
// For some reason, this function is called before the refs are set
if (activeItem.value < 0) return
const itemRef = itemRefs.value[activeItem.value]
marker.value?.style.setProperty('width', `${itemRef.clientWidth}px`)
Expand All @@ -100,6 +118,15 @@ function setItem(item: number) {
activeItem.value = item
// After the animation is done, navigate to the new route if necessary
const itemRef = itemRefs.value[activeItem.value]
const listener = (event: TransitionEvent) => {
if (event.propertyName !== 'background-color') return
itemRef.removeEventListener('transitionend', listener)
navigate(items[activeItem.value].link)
}
itemRef.addEventListener('transitionend', listener)
requestAnimationFrame(() => {
// Move the marker to the active item
moveMarker()
Expand Down Expand Up @@ -134,9 +161,11 @@ onUnmounted(() => {
transform: scale(0.7);
}
/*
.mall {
transform: scale(1.1);
}
*/
.marker {
position: absolute;
Expand Down
Loading

0 comments on commit 0b985a9

Please sign in to comment.