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

feat(frontend): functional tabcontrol #1193

Merged
merged 5 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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