Skip to content

Commit

Permalink
fix(UsaNavDropdownButton): add router link props to fix active class …
Browse files Browse the repository at this point in the history
…not being applied to button

When using `router-link` or `nuxt-link` the active class was not being applied to the button element
because the `to` prop wasn't being passed.
  • Loading branch information
patrickcate committed Feb 24, 2024
1 parent a6bdafc commit 92222e7
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import UsaNavDropdownButton from './UsaNavDropdownButton.vue'
import { ref, reactive } from 'vue'

const defaultProps = {
href: UsaNavDropdownButton.props.href.default,
to: UsaNavDropdownButton.props.to.default,
routerComponentName: UsaNavDropdownButton.props.routerComponentName.default,
}

const dropdownId = ref('test-dropdown-id')
const dropdownItems = reactive({ 'test-dropdown-id': false })

export default {
component: UsaNavDropdownButton,
title: 'Components/UsaNavDropdownButton',
argTypes: {
href: {
control: { type: 'text' },
},
to: {
control: { type: 'text' },
},
routerComponentName: {
control: { type: 'text' },
},
default: {
control: { type: 'text' },
},
},
args: {
href: defaultProps.href,
to: defaultProps.to,
routerComponentName: defaultProps.routerComponentName,
default: 'Test dropdown button',
},
decorators: [
Expand All @@ -35,7 +53,16 @@ const DefaultTemplate = (args, { argTypes }) => ({
setup() {
return { ...args }
},
template: `<div class="usa-header usa-header--basic"><ul class="usa-nav__primary usa-accordion"><li class="usa-nav__primary-item"><UsaNavDropdownButton>${args.default}</UsaNavDropdownButton></li></ul></div>`,
template: `<div class="usa-header usa-header--basic">
<ul class="usa-nav__primary usa-accordion">
<li class="usa-nav__primary-item">
<UsaNavDropdownButton
:href="href"
:to="to"
:router-component-name="routerComponentName">${args.default}</UsaNavDropdownButton>
</li>
</ul>
</div>`,
})

export const DefaultNavDropdownButton = DefaultTemplate.bind({})
Expand Down
104 changes: 88 additions & 16 deletions src/components/UsaNavDropdownButton/UsaNavDropdownButton.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import '@module/@uswds/uswds/dist/css/uswds.min.css'
import { ref, reactive } from 'vue'
import { ref, reactive, markRaw } from 'vue'
import UsaNavDropdownButton from './UsaNavDropdownButton.vue'

describe('UsaNavDropdownButton', () => {
Expand Down Expand Up @@ -27,15 +27,10 @@ describe('UsaNavDropdownButton', () => {
.should('have.class', 'usa-nav__link')
.and('have.attr', 'type', 'button')

cy.get('@button')
.should('have.attr', 'data-test')
.and('contain', 'test-attr')

cy.get('@button').should('have.attr', 'aria-expanded').and('contain', false)
cy.get('@button').should('have.attr', 'data-test', 'test-attr')

cy.get('@button')
.should('have.attr', 'aria-controls')
.and('contain', 'test-dropdown-id')
cy.get('@button').should('have.attr', 'aria-expanded', 'false')
cy.get('@button').should('have.attr', 'aria-controls', 'test-dropdown-id')

cy.get('@button').find('span').should('contain', 'Test dropdown button')

Expand Down Expand Up @@ -84,15 +79,11 @@ describe('UsaNavDropdownButton', () => {
.should('have.class', 'usa-nav__link')
.and('have.class', 'usa-current')

cy.get('@button')
.should('have.attr', 'data-test')
.and('contain', 'test-attr')

cy.get('@button').should('have.attr', 'aria-expanded').and('contain', true)
cy.get('@button').should('have.attr', 'data-test', 'test-attr')

cy.get('@button')
.should('have.attr', 'aria-controls')
.and('contain', 'test-dropdown-id')
.should('have.attr', 'aria-expanded', 'true')
.and('have.attr', 'aria-controls', 'test-dropdown-id')

cy.get('@button').find('span').should('contain', 'Test dropdown button')

Expand Down Expand Up @@ -138,3 +129,84 @@ describe('UsaNavDropdownButton', () => {
.and('have.class', 'usa-current')
})
})

it('renders button with local router component', () => {
const routerLinkStub = {
name: 'RouterLink',
data() {
return {
isActive: true,
isExactActive: false,
}
},
template:
'<div class="test-router-link"><slot :isActive="isActive" :isExactActive="isExactActive"></slot></div>',
}

const nuxtLinkStub = {
name: 'NuxtLink',
data() {
return {
isActive: false,
isExactActive: false,
}
},
template:
'<div class="test-nuxt-link"><slot :isActive="isActive" :isExactActive="isExactActive"></slot></div>',
}

cy.mount(UsaNavDropdownButton, {
props: {
routerComponentName: markRaw(nuxtLinkStub),
href: '/test-href',
to: '/test-to',
},
slots: {
default: () => 'Test dropdown button',
},
global: {
stubs: { 'router-link': routerLinkStub },
provide: {
'vueUswds.routerComponentName': 'router-link',
dropdownId: ref('test-dropdown-id'),
dropdownItems: reactive({
'test-dropdown-id': false,
}),
toggleDropdown: cy.stub().as('toggleDropdown'),
},
},
})
.its('wrapper')
.as('wrapper')

cy.get('div.test-nuxt-link')
.should('have.attr', 'to', '/test-to')
.and('not.have.attr', 'href')
cy.get('div.test-router-link').should('not.exist')

cy.get('.test-nuxt-link button.usa-accordion__button')
.should('have.class', 'usa-nav__link')
.and('not.have.class', 'usa-current')

cy.get('@wrapper').invoke('setProps', { routerComponentName: null })

cy.get('div.test-nuxt-link').should('not.exist')
cy.get('div.test-router-link')
.should('have.attr', 'to', '/test-to')
.and('not.have.attr', 'href')

cy.get('.test-router-link button.usa-accordion__button')
.should('have.class', 'usa-nav__link')
.and('have.class', 'usa-current')

cy.get('@wrapper').invoke('setProps', { to: '' })

cy.get('div.test-nuxt-link').should('not.exist')
cy.get('div.test-router-link')
.should('have.attr', 'to', '/test-href')
.and('not.have.attr', 'href')

cy.get('.test-router-link button.usa-accordion__button')
.should('have.class', 'usa-nav__link')
.and('have.class', 'usa-current')
})
36 changes: 30 additions & 6 deletions src/components/UsaNavDropdownButton/UsaNavDropdownButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,56 @@ export default {
</script>

<script setup>
import { inject, toRef } from 'vue'
import { computed, inject, toRef } from 'vue'
import { ROUTER_COMPONENT_NAME } from '@/utils/constants.js'
const routerComponentName = inject(
const globalRouterComponentName = inject(
'vueUswds.routerComponentName',
ROUTER_COMPONENT_NAME
)
const dropdownId = inject('dropdownId')
const toggleDropdown = inject('toggleDropdown')
const dropdownItems = inject('dropdownItems')
const props = defineProps({
href: {
type: String,
default: '',
},
to: {
type: [String, Object],
default: '',
},
routerComponentName: {
type: [String, Object],
default: '',
},
})
const isOpen = toRef(dropdownItems, dropdownId.value)
const localRouterComponentName = computed(
() => props.routerComponentName || globalRouterComponentName
)
const routeLocation = computed(() => props.to || props.href)
</script>

<template>
<component
:is="routerComponentName"
v-if="routerComponentName"
v-slot="{ isActive, isExactActive }"
:is="localRouterComponentName"
v-if="localRouterComponentName"
v-slot="slotProps"
:to="routeLocation"
custom
>
<button
v-bind="$attrs"
type="button"
class="usa-accordion__button usa-nav__link"
:class="[{ 'usa-current': isActive || isExactActive }]"
:class="[
{ 'usa-current': slotProps?.isActive || slotProps?.isExactActive },
]"
:aria-expanded="isOpen"
:aria-controls="dropdownId"
@click="toggleDropdown(dropdownId)"
Expand Down
14 changes: 9 additions & 5 deletions src/components/UsaNavPrimary/UsaNavPrimary.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,16 @@ onClickOutside(nav, closeAllItems)
v-if="item?.children?.length"
:key="item?.id || item.text"
>
<UsaNavDropdownButton v-bind="item?.attrs">{{
item.text
}}</UsaNavDropdownButton>
<UsaNavDropdownButton
v-bind="item?.attrs"
:href="item?.href"
:to="item?.to"
:router-component-name="item?.routerComponentName"
>{{ item.text }}</UsaNavDropdownButton
>
<UsaNavSubmenu
v-if="item?.cols > 1 && isMegamenu"
:key="item.children.map(item => item?.id || item.text)"
:key="item.children.map(item => item?.id || item.text).join('-')"
:cols="item.cols"
>
<template v-for="n in item.cols" :key="n" #[`col-${n}`]>
Expand All @@ -89,7 +93,7 @@ onClickOutside(nav, closeAllItems)
</UsaNavSubmenu>
<template v-else>
<UsaNavSubmenu
:key="item.children.map(item => item?.id || item.text)"
:key="item.children.map(item => item?.id || item.text).join('-')"
>
<UsaNavSubmenuItem
v-for="submenuItem in item.children"
Expand Down
4 changes: 2 additions & 2 deletions src/utils/unique-id.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { getCurrentInstance } from 'vue'
import { kebabCase } from '@/utils/common.js'

const idPrefix = 'vuswds-id-'
const idRegistry = {}

// Adapted from:
// https://github.com/wearebraid/vue-formulate/blob/master/src/Formulate.js
export function nextId(componentName = '') {
const idPrefix = 'vuswds-id-'
const vm = getCurrentInstance()
const route = vm.appContext.config.globalProperties?.$route
const route = vm?.appContext?.config?.globalProperties?.$route

const pathPrefix =
route?.path && kebabCase(route.path) ? kebabCase(route.path) : 'global'
Expand Down

0 comments on commit 92222e7

Please sign in to comment.