Skip to content

Commit

Permalink
Improve the modal component
Browse files Browse the repository at this point in the history
* Improvements to Modal UI
* Modal is now keyboard accessible
* Added new/better info, error and update icons
  • Loading branch information
aryanpingle committed Jun 12, 2023
1 parent bf3ff30 commit 8f4cc51
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 27 deletions.
101 changes: 91 additions & 10 deletions src/client/lazy-app/Compress/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { h, Component } from 'preact';
import * as style from './style.css';
import 'add-css:./style.css';
import { shallowEqual, isSafari } from '../../util';
// import { * } from '../../icons';
import { linkRef } from 'shared/prerendered-app/util';
import { ModalInfoIcon } from 'client/lazy-app/icons';
import {
InfoIcon,
DiamondStarIcon,
ModalErrorIcon,
} from 'client/lazy-app/icons';

interface Props {}

Expand All @@ -29,29 +31,108 @@ export default class Modal extends Component<Props, State> {
shown: false,
};

private modal?: HTMLElement;
private returnFocusElement?: HTMLElement | null;

showModal(message: ModalMessage) {
if (this.state.shown) return;
if (!this.modal) return;

// Set element to return focus to after hiding
this.returnFocusElement = document.activeElement as HTMLElement;

this.modal.style.display = '';
this.setState({
message: message,
shown: true,
});
// Wait for the 'display' reset to take place, then focus
setTimeout(() => {
this.modal?.querySelector('button')?.focus();
}, 0);
}

hideModal() {
this.setState({
message: { ...this.state.message },
shown: false,
});
setTimeout(() => {
this.modal && (this.modal.style.display = 'none');
this.returnFocusElement?.focus();
}, 250);
}

private _getCloseButton() {
return this.modal!.querySelector('button')!;
}

private _getLastFocusable() {
const focusables = this.modal!.querySelectorAll('button, a');
return focusables[focusables.length - 1] as HTMLElement;
}

private _onKeyDown(e: KeyboardEvent) {
// If Escape, hide modal
if (e.key === 'Escape' || e.keyCode == 27) {
this.hideModal();
e.preventDefault();
e.stopImmediatePropagation();
return;
}

let isTabPressed = e.key === 'Tab' || e.keyCode === 9;

if (!isTabPressed) return;

if (e.shiftKey) {
// If SHIFT + TAB was pressed on the first focusable element
// Move focus to the last focusable element
if (document.activeElement === this._getCloseButton()) {
this._getLastFocusable().focus();
e.preventDefault();
e.stopImmediatePropagation();
}
} else {
// If TAB was pressed on the last focusable element
// Move focus to the first focusable element
if (document.activeElement === this._getLastFocusable()) {
this._getCloseButton().focus();
e.preventDefault();
e.stopImmediatePropagation();
}
}
}

render({}: Props, { message, shown }: State) {
return (
<div class={`${style.modalOverlay} ${shown && style.modalShown}`}>
<div class={style.modal}>
<header class={style.header} onClick={() => this.hideModal()}>
<div class={style.modalTitle}>
{message.type === 'info' ? <ModalInfoIcon /> : <ModalInfoIcon />}
<span>{message.title}</span>
</div>
<div
class={`${style.modalOverlay} ${shown && style.modalShown}`}
onKeyDown={(e) => this._onKeyDown(e)}
tabIndex={shown ? 0 : -1}
>
<div class={style.modal} ref={linkRef(this, 'modal')}>
<header class={style.header}>
<span class={style.modalTypeIcon}>
{message.type === 'info' ? (
<InfoIcon />
) : message.type === 'error' ? (
<ModalErrorIcon />
) : (
<DiamondStarIcon />
)}
</span>
<span class={style.modalTitle}>{message.title}</span>
<button class={style.closeButton} onClick={() => this.hideModal()}>
<svg viewBox="0 0 480 480" fill="currentColor">
<path
d="M119.356 120L361 361M360.644 120L119 361"
stroke="#fff"
stroke-width="37"
stroke-linecap="round"
/>
</svg>
</button>
</header>
<div class={style.contentContainer}>
<article
Expand Down
110 changes: 99 additions & 11 deletions src/client/lazy-app/Compress/Modal/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
align-items: center;

backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
z-index: 1;

user-select: none;
Expand All @@ -26,51 +27,106 @@
opacity: 1;
}

.modal-overlay > * {
box-sizing: border-box;
}

.modal {
width: 70vw;
max-width: 60ch;
max-height: 70vh;

box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.1);
border: 4px solid black;

display: grid;
grid-template-rows: auto 1fr auto;

font-size: 1.5em;
color: white;

border-radius: 10px;
overflow: hidden;

background-color: rgb(40, 40, 40);
background-color: hsl(250deg, 100%, 90%);
}

@media (max-width: 720px) {
.modal {
width: 90%;
max-width: none;
font-size: 1.25em;
}
}

@media (max-width: 480px) {
.modal {
font-size: 1em;
}
}

.header,
.footer {
max-height: 100%;
padding: 1em;

background-color: rgba(0, 0, 0, 0.5);
background-color: rgba(0, 0, 0, 6%);
}

.header {
font-weight: 700;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 0.5em;
border-bottom: 1px solid rgba(0, 0, 0, 8%);

& > * {
height: 100%;
font-size: 1.5em;
}
}

.footer {
border-top: 1px solid rgba(0, 0, 0, 8%);
}

.modal-title {
font-size: 1.5em;
text-align: center;

/* Overflowing text gets ellipse-d */
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
white-space: nowrap;
}

.modal-type-icon {
display: flex;
justify-content: center;
align-items: center;

opacity: 0.5;

svg {
font-size: 1em;
height: 1em;
width: 1em;
margin-right: 0.5em;
}
}

.close-button {
composes: unbutton from global;
flex: 0 0 auto;
height: 1em;
width: 1em;
background-color: rgb(255, 105, 118);
border-radius: 5px;

path {
stroke: black;
}

/* Don't show focus ring on mobile */
@media (min-width: 720px) {
&:focus {
outline-width: 2px;
outline-style: solid;
outline-offset: 2px;
}
}
}

Expand All @@ -81,6 +137,38 @@

.content {
font-size: 1em;
line-height: 1.5;

padding: 0 1em;

img {
max-width: 100%;
max-height: 500px;
}

figcaption {
font-size: 0.8em;
text-align: center;
font-style: italic;
}

a {
&:link {
color: hsl(220, 100%, 50%);
}
&:visited {
color: rgb(255, 0, 103);
}
}

pre,
code {
background-color: rgba(0, 0, 0, 10%);
padding: 2px 4px;
border-radius: 3px;
}

pre {
overflow-x: auto;
}
}
27 changes: 21 additions & 6 deletions src/client/lazy-app/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,29 @@ const Icon = (props: preact.JSX.HTMLAttributes) => (
/>
);

export const ModalInfoIcon = (props: preact.JSX.HTMLAttributes) => (
<svg width="160" height="160" viewBox="0 0 160 160" fill="currentColor">
<path d="m80 15c-35.88 0-65 29.12-65 65s29.12 65 65 65 65-29.12 65-65-29.12-65-65-65zm0 10c30.36 0 55 24.64 55 55s-24.64 55-55 55-55-24.64-55-55 24.64-55 55-55z" />
export const InfoIcon = (props: preact.JSX.HTMLAttributes) => (
<svg viewBox="0 0 512 512" fill="currentColor">
<path d="M255.992,0.008C114.626,0.008,0,114.626,0,256s114.626,255.992,255.992,255.992 C397.391,511.992,512,397.375,512,256S397.391,0.008,255.992,0.008z M300.942,373.528c-10.355,11.492-16.29,18.322-27.467,29.007 c-16.918,16.177-36.128,20.484-51.063,4.516c-21.467-22.959,1.048-92.804,1.597-95.449c4.032-18.564,12.08-55.667,12.08-55.667 s-17.387,10.644-27.709,14.419c-7.613,2.782-16.225-0.871-18.354-8.234c-1.984-6.822-0.404-11.161,3.774-15.822 c10.354-11.484,16.289-18.314,27.467-28.999c16.934-16.185,36.128-20.483,51.063-4.524c21.467,22.959,5.628,60.732,0.064,87.497 c-0.548,2.653-13.742,63.627-13.742,63.627s17.387-10.645,27.709-14.427c7.628-2.774,16.241,0.887,18.37,8.242 C306.716,364.537,305.12,368.875,300.942,373.528z M273.169,176.123c-23.886,2.096-44.934-15.564-47.031-39.467 c-2.08-23.878,15.58-44.934,39.467-47.014c23.87-2.097,44.934,15.58,47.015,39.458 C314.716,152.979,297.039,174.043,273.169,176.123z" />
</svg>
);

export const ModalErrorIcon = (props: preact.JSX.HTMLAttributes) => (
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="m57.373 18.231a9.3834 9.1153 0 1 1 -18.767 0 9.3834 9.1153 0 1 1 18.767 0z"
transform="matrix(1.1989 0 0 1.2342 21.214 28.75)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zm-1.5-5.009c0-.867.659-1.491 1.491-1.491.85 0 1.509.624 1.509 1.491 0 .867-.659 1.509-1.509 1.509-.832 0-1.491-.642-1.491-1.509zM11.172 6a.5.5 0 0 0-.499.522l.306 7a.5.5 0 0 0 .5.478h1.043a.5.5 0 0 0 .5-.478l.305-7a.5.5 0 0 0-.5-.522h-1.655z"
/>
<path d="m90.665 110.96c-0.069 2.73 1.211 3.5 4.327 3.82l5.008 0.1v5.12h-39.073v-5.12l5.503-0.1c3.291-0.1 4.082-1.38 4.327-3.82v-30.813c0.035-4.879-6.296-4.113-10.757-3.968v-5.074l30.665-1.105" />
</svg>
);

export const DiamondStarIcon = (props: preact.JSX.HTMLAttributes) => (
<svg
viewBox="0 0 480 480"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M240 480C240 360 120 240 0 240C120 240 240 120 240 0C240 120 360 240 480 240C360 240 240 360 240 480Z" />
</svg>
);

Expand Down

0 comments on commit 8f4cc51

Please sign in to comment.