Skip to content

Commit

Permalink
fix(UsaNav): move mobile menu functionality to UsaHeader component
Browse files Browse the repository at this point in the history
Previously the `UsaNav` component needed to be nested under a `UsaNavbar` component. This was not
consistent with USWDS styling however. This change makes it so the `UsaNav` can just be a child of a
`UsaHeader` component.
  • Loading branch information
patrickcate committed Feb 22, 2024
1 parent 9f4e7f1 commit 9e6a103
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 100 deletions.
110 changes: 110 additions & 0 deletions src/components/UsaHeader/UsaHeader.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import '@module/@uswds/uswds/dist/css/uswds.min.css'
import { h } from 'vue'
import UsaHeader from './UsaHeader.vue'
import UsaNavbar from '@/components/UsaNavbar'
import UsaNav from '@/components/UsaNav'

describe('UsaHeader', () => {
it('renders the component', () => {
Expand Down Expand Up @@ -88,6 +90,114 @@ describe('UsaHeader', () => {
cy.get('span').should('contain', 'Megamenu: false')
})

it('provide reactive values to `UsaNav` and `UsaNavbar` components', () => {
const childComponent = {
components: { UsaNavbar, UsaNav },
template: `
<UsaNavbar>Test Navbar</UsaNavbar>
<UsaNav>
<template #primary><div>Primary slot</div></template>
<template #secondary><div>Secondary slot</div></template>
</UsaNav>
`,
}

// eslint-disable-next-line cypress/no-assigning-return-values
const wrapper = cy
.mount(UsaHeader, {
slots: {
default: () => h(childComponent),
},
})
.its('wrapper')
.as('wrapper')

cy.get('div.usa-overlay')
.as('overlay')
.should('not.have.class', 'is-visible')
.and('not.be.visible')

cy.get('.usa-navbar').should('contain', 'Test Navbar')

cy.get('button.usa-menu-btn')
.as('menuButton')
.should(
'have.attr',
'aria-controls',
'__vuswds-id-global-mobile-header-menu'
)

cy.get('nav.usa-nav')
.as('nav')
.should('have.id', '__vuswds-id-global-mobile-header-menu')

cy.get('@nav').should('not.have.class', 'is-visible').and('not.be.visible')
cy.get('@nav').find('> div.usa-nav__inner').should('not.exist')

cy.get('@nav').find('> button.usa-nav__close').as('closeButton')

cy.get('body > .usa-overlay').should('not.exist')
cy.get('body > nav').should('not.exist')
cy.get('body > :not(nav)').should('not.have.attr', 'aria-hidden')
cy.get('body > :not(.usa-overlay)').should('not.have.attr', 'aria-hidden')

wrapper.vue().then(vm => {
const usaHeaderComponent = vm.findComponent(UsaHeader)
expect(usaHeaderComponent.emitted()).to.not.have.property(
'mobileMenuOpen'
)

// Click mobile menu button.
cy.get('@menuButton').click()

wrapper.vue().then(vm => {
const usaHeaderComponent = vm.findComponent(UsaHeader)

expect(usaHeaderComponent.emitted()).to.have.property('mobileMenuOpen')

const currentEvent = usaHeaderComponent.emitted('mobileMenuOpen')
expect(currentEvent).to.have.length(1)
expect(currentEvent[currentEvent.length - 1]).to.contain(true)
})
})

cy.get('body > .usa-overlay').should('exist')
cy.get('body > nav').should('exist')
cy.get('body > :not(nav)')
.should('have.attr', 'aria-hidden')
.and('contain', true)
cy.get('body > :not(.usa-overlay)').should('have.attr', 'aria-hidden')

cy.get('@overlay').should('have.class', 'is-visible').and('be.visible')
cy.get('@nav').should('have.class', 'is-visible').and('be.visible')

cy.get('@closeButton').should('have.focus')

// Click mobile menu close button.
cy.get('@closeButton').click()

wrapper.vue().then(vm => {
const usaHeaderComponent = vm.findComponent(UsaHeader)
expect(usaHeaderComponent.emitted()).to.have.property('mobileMenuOpen')

const currentEvent = usaHeaderComponent.emitted('mobileMenuOpen')
expect(currentEvent).to.have.length(2)
expect(currentEvent[currentEvent.length - 1]).to.contain(false)
})

cy.get('@menuButton').should('have.focus')

cy.get('body > .usa-overlay').should('not.exist')
cy.get('body > nav').should('not.exist')
cy.get('body > :not(nav)').should('not.have.attr', 'aria-hidden')
cy.get('body > :not(.usa-overlay)').should('not.have.attr', 'aria-hidden')

cy.get('@overlay')
.should('not.have.class', 'is-visible')
.and('not.be.visible')
cy.get('@nav').should('not.have.class', 'is-visible').and('not.be.visible')
})

it('adds custom CSS classes', () => {
cy.mount(UsaHeader, {
props: {
Expand Down
16 changes: 16 additions & 0 deletions src/components/UsaHeader/UsaHeader.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<script setup>
import { computed, provide } from 'vue'
import { useMobileMenu } from '@/composables/useMobileMenu.js'
const emit = defineEmits(['mobileMenuOpen'])
const props = defineProps({
variant: {
Expand Down Expand Up @@ -29,6 +32,14 @@ const props = defineProps({
},
})
const {
isMobileMenuOpen,
mobileMenuId,
closeMobileMenu,
openMobileMenu,
toggleMobileMenu,
} = useMobileMenu(emit)
const classes = computed(() => [
{
'usa-header--basic': props.variant === 'basic',
Expand All @@ -45,6 +56,11 @@ provide(
'isMegamenu',
computed(() => props.megamenu)
)
provide('isMobileMenuOpen', isMobileMenuOpen)
provide('mobileMenuId', mobileMenuId)
provide('closeMobileMenu', closeMobileMenu)
provide('openMobileMenu', openMobileMenu)
provide('toggleMobileMenu', toggleMobileMenu)
</script>

<template>
Expand Down
127 changes: 44 additions & 83 deletions src/components/UsaNav/UsaNav.test.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,14 @@
import '@module/@uswds/uswds/dist/css/uswds.min.css'
import { h, ref } from 'vue'
import UsaNav from './UsaNav.vue'
import UsaNavbar from '@/components/UsaNavbar'

describe('UsaNav', () => {
const wrapperComponent = {
components: { UsaNavbar, UsaNav },
props: {
ariaLabel: {
type: String,
default: 'Primary navigation',
},
closeButtonLabel: {
type: String,
default: 'Close',
},
customClasses: {
type: Object,
default: () => {
return {
button: [],
}
},
},
},
template: `
<UsaNavbar>
<UsaNav
:aria-label="ariaLabel"
:close-button-label="closeButtonLabel"
:custom-classes="customClasses"
>
<template #primary><slot name="primary"></slot></template>
<template #secondary><slot name="secondary"></slot></template>
</UsaNav>
</UsaNavbar>`,
}

it('renders the component', () => {
const isOpen = ref(false)

cy.viewport('iphone-6')

// eslint-disable-next-line cypress/no-assigning-return-values
const wrapper = cy.mount(wrapperComponent, {
cy.mount(UsaNav, {
props: {
customClasses: {
button: ['test-button-class'],
Expand All @@ -65,14 +32,21 @@ describe('UsaNav', () => {
'Test secondary slot'
),
},
global: {
provide: {
isMobileMenuOpen: isOpen,
mobileMenuId: 'test-id',
closeMobileMenu: () => (isOpen.value = false),
},
},
})

cy.get('div.usa-overlay')
.as('overlay')
.should('not.have.class', 'is-visible')
.and('not.be.visible')

cy.get('nav.usa-nav').as('nav').should('have.attr', 'id')
cy.get('nav.usa-nav').as('nav').should('have.id', 'test-id')
cy.get('@nav')
.should('have.attr', 'aria-label')
.and('contain', 'Primary navigation')
Expand Down Expand Up @@ -107,34 +81,14 @@ describe('UsaNav', () => {

cy.get('body > .usa-overlay').should('not.exist')
cy.get('body > nav').should('not.exist')
cy.get('body > :not(nav)').should('not.have.attr', 'aria-hidden')
cy.get('body > :not(.usa-overlay)').should('not.have.attr', 'aria-hidden')

wrapper.vue().then(vm => {
const usaNavbarComponent = vm.findComponent(UsaNavbar)
expect(usaNavbarComponent.emitted()).to.not.have.property(
'mobileMenuOpen'
)
})

// Click mobile menu button.
cy.get('.usa-menu-btn').as('menuButton').click()

wrapper.vue().then(vm => {
const usaNavbarComponent = vm.findComponent(UsaNavbar)
expect(usaNavbarComponent.emitted()).to.have.property('mobileMenuOpen')

const currentEvent = usaNavbarComponent.emitted('mobileMenuOpen')
expect(currentEvent).to.have.length(1)
expect(currentEvent[currentEvent.length - 1]).to.contain(true)
// Open mobile menu.
cy.then(() => {
isOpen.value = true
})

cy.get('body > .usa-overlay').should('exist')
cy.get('body > nav').should('exist')
cy.get('body > :not(nav)')
.should('have.attr', 'aria-hidden')
.and('contain', true)
cy.get('body > :not(.usa-overlay)').should('have.attr', 'aria-hidden')

cy.get('@overlay').should('have.class', 'is-visible').and('be.visible')
cy.get('@nav').should('have.class', 'is-visible').and('be.visible')
Expand All @@ -159,29 +113,18 @@ describe('UsaNav', () => {
// Click mobile menu close button.
cy.get('@closeButton').click()

wrapper.vue().then(vm => {
const usaNavbarComponent = vm.findComponent(UsaNavbar)
expect(usaNavbarComponent.emitted()).to.have.property('mobileMenuOpen')

const currentEvent = usaNavbarComponent.emitted('mobileMenuOpen')
expect(currentEvent).to.have.length(2)
expect(currentEvent[currentEvent.length - 1]).to.contain(false)
})

cy.get('@menuButton').should('have.focus')

cy.get('body > .usa-overlay').should('not.exist')
cy.get('body > nav').should('not.exist')
cy.get('body > :not(nav)').should('not.have.attr', 'aria-hidden')
cy.get('body > :not(.usa-overlay)').should('not.have.attr', 'aria-hidden')

cy.get('@overlay')
.should('not.have.class', 'is-visible')
.and('not.be.visible')
cy.get('@nav').should('not.have.class', 'is-visible').and('not.be.visible')

// Open mobile menu again.
cy.get('@menuButton').click()
cy.then(() => {
isOpen.value = true
})

cy.get('@overlay').should('have.class', 'is-visible').and('be.visible')
cy.get('@nav').should('have.class', 'is-visible').and('be.visible')
Expand All @@ -195,29 +138,35 @@ describe('UsaNav', () => {
cy.get('@nav').should('not.have.class', 'is-visible').and('not.be.visible')

// Open mobile menu again.
cy.get('@menuButton').click()
cy.then(() => {
isOpen.value = true
})

cy.get('@overlay').should('have.class', 'is-visible').and('be.visible')
cy.get('@nav').should('have.class', 'is-visible').and('be.visible')

cy.get('html').click('bottomLeft')
cy.get('html').click('topLeft')

cy.get('@overlay')
.should('not.have.class', 'is-visible')
.and('not.be.visible')
cy.get('@nav').should('not.have.class', 'is-visible').and('not.be.visible')

// Open mobile menu again.
cy.get('@menuButton').click()
cy.then(() => {
isOpen.value = true
})

cy.get('@overlay').should('have.class', 'is-visible').and('be.visible')
cy.get('@nav').should('have.class', 'is-visible').and('be.visible')
})

it('slot positions change on mobile screens', () => {
const isOpen = ref(true)

cy.viewport('iphone-6')

cy.mount(wrapperComponent, {
cy.mount(UsaNav, {
props: {
ariaLabel: 'Custom aria label',
closeButtonLabel: 'Custom close button label',
Expand All @@ -226,10 +175,15 @@ describe('UsaNav', () => {
primary: () => h('span', {}, 'Test primary slot'),
secondary: () => h('span', {}, 'Test secondary slot'),
},
global: {
provide: {
isMobileMenuOpen: isOpen,
mobileMenuId: 'test-id',
closeMobileMenu: () => (isOpen.value = false),
},
},
})

cy.get('.usa-menu-btn').as('menuButton').click()

cy.get('div.usa-overlay')
.as('overlay')
.should('have.class', 'is-visible')
Expand Down Expand Up @@ -313,19 +267,26 @@ describe('UsaNav', () => {
})

it('uses custom global mobile breakpoint', () => {
const isOpen = ref(false)

cy.viewport('iphone-6')

cy.mount(wrapperComponent, {
cy.mount(UsaNav, {
global: {
provide: {
'vueUswds.imagePath': '/test',
'vueUswds.mobileMenuBreakpoint': '400px',
closeMobileMenu: () => {},
isMobileMenuOpen: isOpen,
mobileMenuId: 'test-mobile-menu-id',
closeMobileMenu: () => (isOpen.value = false),
},
},
})

cy.get('.usa-menu-btn').as('menuButton').click()
// Open mobile menu.
cy.then(() => {
isOpen.value = true
})

cy.get('div.usa-overlay')
.as('overlay')
Expand Down
Loading

0 comments on commit 9e6a103

Please sign in to comment.