Skip to content

Commit

Permalink
feat: APPS-2943 Add initialTab setting option to TabToggle (#611)
Browse files Browse the repository at this point in the history
* wip: add prop for initialTab selection

* refactor glider animation to work with setting intial tab position

* remove margin value on glider/offset

* add story for initialTab prop

* refactor: tab glider animation

* task: update code documentation

---------

Co-authored-by: tinuola <[email protected]>
  • Loading branch information
tinuola and tinuola authored Sep 17, 2024
1 parent c9815fe commit aadedac
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 52 deletions.
4 changes: 2 additions & 2 deletions src/lib-components/TabItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const { content, icon, title } = defineProps({
},
})
const activeTab = inject('activeTab')
const activeTabTitle = inject('activeTabTitle')
const theme = useTheme()
Expand All @@ -29,7 +29,7 @@ const classes = computed(() => {
</script>
<template>
<div v-show="title === activeTab" :class="classes">
<div v-show="title === activeTabTitle" :class="classes">
<div
v-text="content"
/>
Expand Down
125 changes: 80 additions & 45 deletions src/lib-components/TabList.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
<script setup lang="ts">
import { computed, defineAsyncComponent, provide, ref, useSlots } from 'vue'
import { computed, defineAsyncComponent, onMounted, provide, ref, useSlots, watch } from 'vue'
import { useWindowSize } from '@vueuse/core'
import { useTheme } from '@/composables/useTheme'
const { alignment } = defineProps({
const { alignment, initialTab } = defineProps({
alignment: {
type: String,
default: 'left',
},
initialTab: {
type: Number,
default: 0
}
})
const SvgIconCalendar = defineAsyncComponent(() =>
Expand All @@ -24,13 +29,14 @@ const tabProps = tabSlots!.map((tabItem) => {
const tabItems = ref(tabProps)
const activeTab = ref(tabItems.value[0]?.title)
const tabRefs = ref<Array<any>>([])
const activeTabIndex = ref(0)
const activeTabTitle = ref(tabItems.value[0]?.title)
const tabGliderRef = ref()
provide('activeTab', activeTab)
provide('activeTabTitle', activeTabTitle)
const iconMapping = {
'icon-calendar': {
Expand All @@ -44,9 +50,34 @@ const iconMapping = {
},
}
onMounted(() => {
activeTabIndex.value = initialTab
activeTabTitle.value = tabItems.value[initialTab]?.title
const activeTabElem = tabRefs.value[initialTab]
/* @argument {boolean} hasInitialWidth */
// Boolean flag to disable glider's default
// full width, and set glider's starting
// position to align with initial tab.
animateTabGlider(activeTabElem, false)
const { width } = useWindowSize()
watch(width, (_newWidth) => {
// The glider width and animation/travel distance
// depend on the width and position of the tab
// buttons; when the window resizes, these button
// values change. The animation method needs to be
// called again to update the glider logic using the
// new button values.
const activeTabElem = tabRefs.value[activeTabIndex.value]
animateTabGlider(activeTabElem, true)
})
})
// Computed
const parsedAriaLabel = computed(() => {
const tabTitle = hyphenateTabName(activeTab.value)
const tabTitle = hyphenateTabName(activeTabTitle.value)
return `panel-${tabTitle}`
})
Expand All @@ -67,48 +98,14 @@ function setTabAriaControl(tabName: string) {
return `panel-${tabTitle}`
}
function switchTab(tabName: string) {
activeTab.value = tabName
const tabIndex = tabItems.value!.findIndex(tab => tab?.title === tabName)
const tabElem: HTMLElement = tabRefs.value[tabIndex]
if (tabElem)
tabElem.focus()
if (!tabElem.classList.contains('active'))
animateTabGlider(tabElem)
}
function animateTabGlider(elem: HTMLElement) {
const tabGlider = tabGliderRef.value
const scaleGliderWidth = elem.offsetWidth / tabGlider.offsetWidth
// Calculate width to scale animated glider
// Use variable in CSS
tabGlider.style.setProperty('--scale_glider_width', scaleGliderWidth)
// Calculate and set distance to animate
// Use variable in CSS
tabGlider.style.setProperty('--translate_glider_left', `${elem.offsetLeft}px`)
// Object with positional values of tab button
const tabBtn = elem.getBoundingClientRect()
// Set glider to same height as tab button
tabGlider.style.height = `${tabBtn.height}px`
}
function hyphenateTabName(str: string) {
return str.toLowerCase().replace(/\s/g, '-')
}
function keydownHandler(e: KeyboardEvent) {
const tabTitleList = tabItems.value!.map(obj => obj?.title)
const activeIndex = tabTitleList.indexOf(activeTab.value)
const activeIndex = tabTitleList.indexOf(activeTabTitle.value)
let targetTab
Expand All @@ -132,6 +129,44 @@ function keydownHandler(e: KeyboardEvent) {
default:
}
}
function switchTab(tabName: string) {
const tabIndex = tabItems.value!.findIndex(tab => tab?.title === tabName)
activeTabTitle.value = tabName
activeTabIndex.value = tabIndex
const tabElem: HTMLElement = tabRefs.value[tabIndex]
if (tabElem)
tabElem.focus()
if (!tabElem.classList.contains('active'))
animateTabGlider(tabElem, true)
}
function animateTabGlider(elem: HTMLElement, hasInitialWidth: boolean) {
const tabGlider = tabGliderRef.value
// Get positional values of tab button
// and set glider height to match tab button
const tabBtn = elem.getBoundingClientRect()
tabGlider.style.height = `${tabBtn.height}px`
if (!hasInitialWidth) {
tabGlider.style.width = '0'
}
else {
// Set glider width to match tab button
tabGlider.style.width = `${tabBtn.width}px`
// Remove tab button background; display glider background instead
elem.style.background = 'none'
}
// Calculate and set distance (CSS variable) to animate glider
tabGlider.style.setProperty('--move_glider', `${elem.offsetLeft}px`)
}
</script>

<template>
Expand All @@ -149,11 +184,11 @@ function keydownHandler(e: KeyboardEvent) {
:ref="(el) => tabRefs[index] = el"
:key="tab?.title"
class="tab-list-item"
:class="{ active: activeTab === tab?.title }"
:class="{ active: activeTabTitle === tab?.title }"
role="tab"
:tabindex="activeTab === tab?.title ? 0 : -1"
:tabindex="activeTabTitle === tab?.title ? 0 : -1"
:aria-controls="setTabAriaControl(tab?.title)"
:aria-selected="activeTab === tab?.title"
:aria-selected="activeTabTitle === tab?.title"
@keydown="keydownHandler"
@click="switchTab(tab?.title)"
>
Expand All @@ -166,7 +201,7 @@ function keydownHandler(e: KeyboardEvent) {
</div>
</div>
<!-- Slot: TabItem -->
<div :id="parsedAriaLabel" class="tab-list-body" role="tabpanel" :aria-labelledby="parsedAriaLabel" :hidden="!activeTab">
<div :id="parsedAriaLabel" class="tab-list-body" role="tabpanel" :aria-labelledby="parsedAriaLabel" :hidden="!activeTabTitle">
<slot />
</div>
</template>
Expand Down
23 changes: 23 additions & 0 deletions src/stories/TabToggle.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ export function Default() {
}
}

export function SetInitialTab() {
return {
data() {
return { ...mockContent }
},
components: { TabItem, TabList },
template: `<div class="wrapper">
<tab-list :initial-tab="1">
<tab-item title="Label 1" icon="icon-calendar" :content="text1">
</tab-item>
<tab-item title="Label 2" icon="icon-list" :content="text2">
</tab-item>
<tab-item title="Label 3" :content="text3">
</tab-item>
</tab-list>
</div>`
}
}

export function FTVACentered() {
return {
data() {
Expand Down
8 changes: 3 additions & 5 deletions src/styles/default/_tab-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,17 @@
right: 0;
bottom: 0;
margin: 8px 0;
height: auto;
border-radius: .25rem;
background-color: $white;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, .25);
scale: var(--scale_glider_width, .125) 1;
translate: var(--translate_glider_left, 0) 0;
translate: var(--move_glider, 0) 0;
transform-origin: left;
transition: scale 300ms ease-out, translate 300ms ease-out;
}

// Set background of initial active tab
.tab-glider ~ .tab-list-item:nth-of-type(1).active {
.tab-glider ~ .tab-list-item.active {
background: $white;
transition: background-color 3000ms;
}

.tab-list-item {
Expand Down

1 comment on commit aadedac

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.