🛠️ Modal: Closing the modal

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Modal: Closing the modal

The modal can be closed in three ways:

  1. Using the Escape key
  2. Clicking the close button
  3. Clicking outside the modal

We added event listeners for these three ways in our previous code:

// Previous version
modalCloseButton.addEventListener('click', event => {
  closeModal()
})

modalOverlay.addEventListener('click', event => {
  if (!event.target.closest('.modal')) {
    closeModal()
  }
})

document.addEventListener('keydown', event => {
  if (isModalOpen() && event.key === 'Escape') {
    closeModal()
  }
})

We need to add these event listeners to Modal as well.

We’ll do it in this order:

  1. Add an event listener for the close button
  2. Make sure we can close the modal
  3. Add other event listeners

Adding event listener for the close button is easy. First, we search for the close button. Then, we add an event listener to it.

export default function Modal (settings) {
  // Declare variables
  const closeButtonElement = modalElement.querySelector('.jsModalClose')

  const modal = {/* ... */}

  // Add event listeners
  closeButtonElement.addEventListener('click', _ => {/*...*/})
}

We need to close the modal in this event listener. Here, we can use a close method.

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods
    close () {
      console.log('Closing modal')
    }
  }

  // Add event listeners
  closeButtonElement.addEventListener('click', _ => modal.close())
}

Closing the modal

There are five steps to close the modal:

  1. Removing is-open
  2. Removing the focus trap
  3. Focusing on the button
  4. Setting aria-expanded back to false
  5. Removing aria-hidden from other elements

We can see these five steps clearly from the code we wrote previously.

// Previous code for closeModal
const closeModal = _ => {
  document.body.classList.remove('modal-is-open')

  document.removeEventListener('keydown', trapFocus)

  modalButton.focus()

  modalButton.setAttribute('aria-expanded', false)

  main.removeAttribute('aria-hidden')
}

Removing is-open

We need to remove is-open from overlayElement to close the modal.

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods

    close () {
      overlayElement.classList.remove('is-open')
    }
  }

  // Add event listeners
}

Removing Focus Trap

We can remove the focus trap by removing the event listener.

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods

    close () {
      // ...
      document.removeEventListener('keydown', trapFocus)
    }
  }

  // Add event listeners
}

Focusing on the button

Next, we focus on the button element.

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods

    close () {
      // ...
      buttonElement.focus()
    }
  }

  // Add event listeners
}
Focus returned to the button

Setting aria-expanded back to false

This should be simple for you.

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods

    close () {
      // ...
      buttonElement.setAttribute('aria-expanded', false)
    }
  }

  // Add event listeners
}
Aria-expanded set to false.

Removing aria-hidden from other elements

Finally, we need to remove aria-hidden from other elements.

Here, we need to find other elements, loop through them, and remove the aria-hidden attribute.

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods

    close () {
      // ...
			const overlaySiblingEls = [...overlayElement.parentElement.children]
        .filter(el => el !== overlayElement)
      overlaySiblingEls.forEach(element => {
        element.removeAttribute('aria-hidden')
      })
    }
  }

  // Add event listeners
}

Simplifying code

The part where we find sibling elements, loop through them, and add (or remove) aria-hidden can be quite difficult to read. We can create methods to make them easier to understand.

First, we create a hideSiblingElements method to add aria-hidden to sibling elements.

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods

    hideSiblingElements () {
      const overlaySiblingEls = [...overlayElement.parentElement.children]
        .filter(el => el !== overlayElement)
      overlaySiblingEls.forEach(element => {
        element.setAttribute('aria-hidden', true)
      })
    }
  }

  // Add event listeners
}

We use it like this:

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods

    open () {
       // ...
       modal.hideSiblingElements()
    }
  }

  // Add event listeners
}

Then we create a showSiblingElements method to remove aria-hidden from sibling elements.

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods

    showSiblingElements () {
      const overlaySiblingEls = [...overlayElement.parentElement.children]
        .filter(el => el !== overlayElement)
      overlaySiblingEls.forEach(element => {
        element.removeAttribute('aria-hidden')
      })
    },
  }

  // Add event listeners
}

We use showSiblingElements like this:

export default function Modal (settings) {
  // Declare variables

  const modal = {
    // Other methods

    close () {
      // ...
      modal.showSiblingElements()
    },
  }

  // Add event listeners
}

Since hideSiblingElements and showSiblingElements use the same code to get sibling elements, we can use a getter function to make them easier to understand.

export default function Modal (settings) {
  // Declare variables

  const modal = {
    get siblingElements () {
      return [...overlayElement.parentElement.children]
        .filter(el => el !== overlayElement)
    },

    showSiblingElements () {
      modal.siblingElements.forEach(element => {
        element.removeAttribute('aria-hidden')
      })
    },

    hideSiblingElements () {
      modal.siblingElements.forEach(element => {
        element.setAttribute('aria-hidden', true)
      })
    },

    // Other methods
  }

  // Add event listeners
}

Other event listeners

We need to add two more event listeners:

  1. One to close the modal when we click on the overlay
  2. One to close the modal when we press Escape

Here’s the previous code:

// Previous code
modalOverlay.addEventListener('click', event => {
  if (!event.target.closest('.modal')) {
    closeModal()
  }
})

document.addEventListener('keydown', event => {
  if (isModalOpen() && event.key === 'Escape') {
    closeModal()
  }
})

We’ll start with the first one.

Closing modal when clicking on the overlay element

We need to add a click event to the overlayElement. We can copy the code we wrote from our previous version. Remember to change closeModal to modal.close.

export default function Modal (settings) {
  // Declare variables
  const modal = {/* ... */}

  // Add event listeners
  overlayElement.addEventListener('click', event => {
    if (!event.target.closest('.modal')) {
      modal.close()
    }
  })
}

Closing the modal with the Escape key

We can copy-paste the event listener into Modal. Again, remember to change closeModal to modal.close.

export default function Modal (settings) {
  // Declare variables
  const modal = {/* ... */}

  // Add event listeners
	document.addEventListener('keydown', event => {
	  if (isModalOpen() && event.key === 'Escape') {
	    modal.close()
	  }
	})
}

We don’t have an isModalOpen function anymore. But we can create a getter function to check whether the modal is open.

export default function Modal (settings) {
  // Declare variables
  const modal = {
    get isOpen () {
      return overlayElement.classList.contains('is-open')
    }
  }

  // Add event listeners
}

We can use the isOpen getter function like this:

export default function Modal (settings) {
  // Declare variables
  const modal = {/* ... */}

  // Add event listeners
	document.addEventListener('keydown', event => {
	  if (modal.isOpen && event.key === 'Escape') {
	    modal.close()
	  }
	})
}