Skip to content
This repository has been archived by the owner on Nov 6, 2024. It is now read-only.

feat: Modal Component #35

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
6 changes: 3 additions & 3 deletions components/01-atoms/buttons/button.twig
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@

{% set button_base_class = button_base_class|default('button') %}

{% set additional_attributes = {
{% set button_attributes = button_attributes|default({
class: bem(button_base_class, button_modifiers, button_blockname),
} %}
}) %}

<button {{ add_attributes(additional_attributes) }}>
<button {{ add_attributes(button_attributes) }}>
{% block button_content %}
{{ button_content }}
{% endblock %}
Expand Down
95 changes: 95 additions & 0 deletions components/02-molecules/modal/_modal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Base modal styles. Additional classes accomodate for custom app implementations.
callinmullaney marked this conversation as resolved.
Show resolved Hide resolved
.modal__toggle {
cursor: pointer;
}

.modal__pane {
background-color: clr(background-section);
color: clr(text);
left: 0;
width: 100vw;
opacity: 0;
position: fixed;
top: 50%;
transform: translate(0%, -50%);
transition: opacity 300ms;
padding: $space;
visibility: hidden;
z-index: 1000;
callinmullaney marked this conversation as resolved.
Show resolved Hide resolved
callinmullaney marked this conversation as resolved.
Show resolved Hide resolved

@include large {
width: $small;
left: 50%;
transform: translate(-50%, -50%);
}

.modal--active & {
opacity: 1;
visibility: visible;
max-height: 100vh;
}
}

.modal__title {
@include wrapper($container-max-width: $small);

margin-bottom: $space;
}

.modal__content {
@include wrapper($container-max-width: $small);

> * {
callinmullaney marked this conversation as resolved.
Show resolved Hide resolved
width: 100%;
margin-bottom: $space;
}

*:last-child {
margin-bottom: 0;
}
}

.modal__modal-heading {
display: flex;
flex-flow: row nowrap;

// Close button
.modal__toggle {
@include button-base;
@include button-color-primary;
@include button-medium;

cursor: pointer;
position: absolute;
right: 0;
top: 0;
}

.icon {
fill: clr(text);
stroke: clr(text);
stroke-linecap: round;
position: relative;
top: $space-one-fourth;
right: 0;
width: $space;
height: $space;
}
}

.modal__overlay {
background-color: clr(background);
color: clr(text);
display: none;
left: 0;
position: fixed;
top: 0;
opacity: 0.45;

&--active {
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably want to unnest this to modal__overlay--active to match updated best practices

display: block;
width: 100vw;
height: 100vh;
z-index: 999;
}
}
118 changes: 118 additions & 0 deletions components/02-molecules/modal/modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
Drupal.behaviors.modal = {
attach(context) {
// Variables
const modals = context.querySelector('.modal');
let activeModal = context.querySelector('.modal--active');

// Check if an element is completely empty.
function isEmpty(node) {
return node.innerHTML.trim() === '';
}

// Toggle aria-[anything] attribute from true / false.
function toggleAria(el, aria) {
let x = el.getAttribute(`aria-${aria}`);
if (x === 'true') {
x = 'false';
} else {
x = 'true';
}
el.setAttribute(`aria-${aria}`, x);
}

// Toggle tabindex focus attribute.
function toggletabIndex(el) {
let x = el.getAttribute('tabindex');
if (x === '-1') {
x = '1';
} else {
x = '-1';
}
el.setAttribute('tabindex', x);
}

// Toggle Modal
function toggleModal(modal) {
const overlay = modal.querySelector('.modal__overlay');
const modalModalPane = modal.querySelector('.modal__pane');
if (overlay) {
overlay.classList.toggle('modal__overlay--active');
}
modal.classList.toggle('modal--active');
activeModal = context.querySelector('.modal--active');
toggleAria(modalModalPane, 'hidden');
toggleAria(modal, 'expanded');
toggletabIndex(modalModalPane);
}

// AJAX Request data from url + CSS selector.
function ajaxRequest(request, url, selector) {
let response = '';
request.onreadystatechange = function processResponse() {
if (request.status === 200) {
const resp = request.responseText;
const parser = new DOMParser();
const htmlDoc = parser.parseFromString(resp, 'text/html');
const remoteselector = htmlDoc.querySelectorAll(selector);

response = remoteselector[0].innerHTML;
} else {
response = `<p>Could not process request. Error code: ${request.status}</p>`;
}
};
request.open('GET', url, false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we including ajax functionality for the modal? This seems very strange to me. The modern standard is fetch, some people still prefer axios, and even fetching data from a modal seems like an edge case.

We should remove this. Kudos for trying to cover all the bases, but let's let the small amount of people doing this decide how they want to implement fetching data.

request.send();

return response;
}

if (modals) {
[modals].forEach((modal) => {
const modalToggle = modal.querySelectorAll('.modal__toggle');
const modalModalContent = modal.querySelector('.modal__content');
const modalOverlay = modal.querySelector('.modal__overlay');

modalToggle.forEach((toggle) => {
toggle.addEventListener('click', (e) => {
const toggleChild = toggle.children[0];
e.preventDefault();

// First check if the child of toggle is a link.
// Attempt to request the <main> from the link's href.
if (
typeof toggleChild !== 'undefined' &&
toggleChild.tagName === 'A' &&
toggleChild.getAttribute('href') !== null
) {
e.preventDefault();
const request = new XMLHttpRequest();
Copy link
Contributor

Choose a reason for hiding this comment

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

We should take all this out

const url = toggleChild.getAttribute('href');
const response = ajaxRequest(
request,
url,
'#storybook-preview-iframe',
);
modalModalContent.innerHTML = response;
}
// On trigger the modal if content is present.
if (isEmpty(modalModalContent) === false) {
toggleModal(modal);
}
});
});
[modalOverlay].forEach((overlay) => {
overlay.addEventListener('click', () => {
toggleModal(modal);
});
});
});
}

// Keyboard controls for closing the modal
context.addEventListener('keyup', (e) => {
if (e.key === 'Escape') {
toggleModal(activeModal);
}
});
},
};
38 changes: 38 additions & 0 deletions components/02-molecules/modal/modal.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import modalTwig from './modal.twig';

import modalData from './modal.yml';

import './modal';

/**
* Storybook Definition.
*/
export default {
title: 'Molecules/Modal',
};

export const modalButton = () => {
return modalTwig({ ...modalData });
};

export const modalAjaxLink = () => {
return modalTwig({
...modalData,
modal__toggle:
'<a href="http://localhost:6006/?path=/story/molecules-modal--modal-ajax-link">Ajax modal data</a>',
});
};

export const modalText = () => {
return modalTwig({
...modalData,
modal__toggle: '<p>Plain Ol Text</p>',
});
};

export const modalMedia = () => {
return modalTwig({
...modalData,
modal__toggle: '<img src="https://placeimg.com/320/180/any">',
callinmullaney marked this conversation as resolved.
Show resolved Hide resolved
});
};
84 changes: 84 additions & 0 deletions components/02-molecules/modal/modal.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{% set modal__base_class = 'modal' %}
{% set modal__id = modal__id|default() %}
{% set modal__modifiers = modal__modifiers|default([]) %}

{% if modal__id is empty %}
{% set modal__id = random(100000) %}
callinmullaney marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

I get why you made it random here - in case there's multiple modals right? But part of the accessibility for having the id is to correspond with the label.

Is a screenreader reading modal-35827-heading ideal? This might need a revisit

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe you could set the random id up top and use it for the id and for the label?

{% endif %}

{% set modal_attributes = modal_attributes|default({
'class': bem(modal__base_class, modal__modifiers, modal__blockname),
'aria-modal': 'true',
'aria-expanded': 'false',
}) %}

{% set modal__pane_attributes = modal__pane_attributes|default({
'class': bem('pane', modal__modifiers, modal__base_class),
'tabindex': '-1',
'aria-hidden': 'true',
'aria-labelledby': modal__base_class ~ '-' ~ modal__id ~ '-label',
'aria-describedby': modal__base_class ~ '-' ~ modal__id ~ '-heading',
'role': 'dialog',
}) %}

{% set modal__heading_attributes = modal__heading_attributes|default({
'class': bem('title', [], modal__base_class),
'id': modal__base_class ~ '-' ~ modal__id ~ '-heading',
}) %}

{% set modal__toggle_attributes = modal__toggle_attributes|default({
'class': bem('toggle', [], modal__base_class),
'id': modal__base_class ~ '-' ~ modal__id ~ '-label',
}) %}

{% set modal__close_attributes = modal__close_attributes|default({
'class': bem('toggle', [], modal__base_class),
'aria-label': 'Close Modal',
}) %}

<div {{ add_attributes(modal_attributes) }}>
<div {{ add_attributes(modal__toggle_attributes) }}>
{% block modal__toggle %}
{% if modal__toggle is empty %}
{% include "@atoms/buttons/button.twig" with {
button_content: modal__toggle__content,
} %}
{% else %}
{{ modal__toggle }}
{% endif %}
{% endblock %}
</div>

<div {{ add_attributes(modal__pane_attributes) }}>

<div {{ bem('modal-heading', modal__modifiers, modal__base_class) }}>
{% include "@atoms/text/headings/_heading.twig" with {
heading_base_class: 'title',
heading_modifiers: modal__heading_modifiers,
heading_blockname: modal__base_class,
heading_level: 2,
heading: modal__heading,
heading_attributes: modal__heading_attributes,
} %}

{% embed "@atoms/buttons/button.twig" with {
button_attributes: modal__close_attributes,
} %}
{% block button_content %}
{% include "@atoms/images/icons/_icon.twig" with {
icon_name: 'close',
} %}
{% endblock %}
{% endembed %}
</div>

<div {{ bem('content', modal__modifiers, modal__base_class) }}>
{% block modal__content %}
{% include "@atoms/text/text/01-paragraph.twig" with {
paragraph_content: modal_content,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we include modal content within a paragraph component? How do we know it will be text content? I think this should be a generic div

} %}
{% endblock %}
</div>
</div>
<div {{ bem('overlay', modal__modifiers, modal__base_class) }}></div>
</div>
4 changes: 4 additions & 0 deletions components/02-molecules/modal/modal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
modal__toggle__content: 'Show Modal'
modal__heading: 'This is an example of a modal'
modal__close__content: 'Close Modal'
modal_content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur porttitor commodo magna nec iaculis. Duis ullamcorper sed lorem vehicula vulputate. Aenean condimentum augue libero, quis finibus libero hendrerit sit amet. Integer ullamcorper aliquet nisl, in ullamcorper augue ullamcorper sit amet. Quisque interdum nunc at egestas lobortis. Phasellus ante lorem, posuere id tortor eget, venenatis sagittis nunc. Aenean luctus tincidunt consequat. Donec vulputate, elit sit amet molestie pulvinar, eros lectus faucibus metus, id rutrum libero ipsum et leo. Curabitur egestas libero sit amet risus aliquet efficitur. Etiam suscipit posuere tortor. Pellentesque tincidunt justo quis quam aliquet varius. <a href="#" title="test-focus">Example link to test modal focus</a>'
1 change: 1 addition & 0 deletions images/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.