-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding A Modal Component #1369
base: dev
Are you sure you want to change the base?
Adding A Modal Component #1369
Changes from 22 commits
0159f23
7dd67bc
2b4d893
89f94f7
4e98cab
d1337d2
e9e2c30
e1875a5
b66ec29
fefe3d3
eea1802
dc5b15c
d766ecb
3e4e8f2
6adf4be
5cdf594
36c646e
a25f093
d9391d0
8511b8f
313d06f
84c19ca
365021c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,57 @@ | ||||||
import { h, Component, VNode } from 'preact'; | ||||||
import Modal from '..'; | ||||||
import { InfoIcon } from 'client/lazy-app/icons'; | ||||||
import { linkRef } from 'shared/prerendered-app/util'; | ||||||
|
||||||
import * as style from './style.css'; | ||||||
import 'add-css:./style.css'; | ||||||
|
||||||
interface Props { | ||||||
modalTitle: string; | ||||||
text?: string; | ||||||
} | ||||||
|
||||||
interface State {} | ||||||
|
||||||
export default class ModalHint extends Component<Props, State> { | ||||||
private modalComponent?: Modal; | ||||||
|
||||||
private onclick = (event: Event) => { | ||||||
if (!this.modalComponent) | ||||||
throw new Error('ModalHint is missing a modalComponent'); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can probably be skipped, as it's guaranteed to be there. |
||||||
|
||||||
// Stop bubbled events from triggering the modal | ||||||
if (!(event.currentTarget as Element).matches('button')) return; | ||||||
|
||||||
this.modalComponent.showModal(); | ||||||
}; | ||||||
|
||||||
render({ modalTitle, text }: Props) { | ||||||
return ( | ||||||
<span | ||||||
class={style.modalHint} | ||||||
onClick={(event) => { | ||||||
// When the button is clicked, the event starts bubbling up | ||||||
// which might cause unexpected behaviour | ||||||
event.preventDefault(); | ||||||
event.stopImmediatePropagation(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think |
||||||
}} | ||||||
> | ||||||
<button | ||||||
class={style.modalButton} | ||||||
onClick={this.onclick} | ||||||
title="Learn more" | ||||||
> | ||||||
<InfoIcon></InfoIcon> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
{text} | ||||||
</button> | ||||||
<Modal | ||||||
ref={linkRef(this, 'modalComponent')} | ||||||
icon={<InfoIcon></InfoIcon>} | ||||||
title={modalTitle} | ||||||
content={this.props.children as any} | ||||||
></Modal> | ||||||
</span> | ||||||
); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
.modal-hint { | ||
display: inline-block; | ||
vertical-align: bottom; | ||
} | ||
|
||
.modal-button { | ||
composes: unbutton from global; | ||
|
||
color: inherit; | ||
|
||
display: flex; | ||
align-items: center; | ||
gap: 0.5rem; | ||
|
||
border-radius: 2px; | ||
} | ||
|
||
.modal-button:hover { | ||
text-decoration: underline solid 1px currentColor; | ||
text-underline-offset: 0.1em; | ||
} | ||
|
||
.modal-button:focus { | ||
outline: white solid 1px; | ||
outline-offset: 0.25em; | ||
} | ||
|
||
.modal-button > svg { | ||
width: 1em; | ||
height: 1em; | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,120 @@ | ||||||
import { h, Component, VNode, Fragment } from 'preact'; | ||||||
import * as style from './style.css'; | ||||||
import 'add-css:./style.css'; | ||||||
import { linkRef } from 'shared/prerendered-app/util'; | ||||||
import { cleanSet } from '../util/clean-modify'; | ||||||
import { animateFrom, animateTo } from '../util'; | ||||||
import { RoundedCrossIcon } from '../icons'; | ||||||
|
||||||
interface Props { | ||||||
icon: VNode; | ||||||
title: string; | ||||||
content: VNode; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we decided that the content of the model should be the children of the modal? |
||||||
} | ||||||
|
||||||
interface State {} | ||||||
|
||||||
export default class Modal extends Component<Props, State> { | ||||||
private dialogElement?: HTMLDialogElement; | ||||||
|
||||||
componentDidMount() { | ||||||
if (!this.dialogElement) throw new Error('Modal missing'); | ||||||
} | ||||||
|
||||||
private _closeOnTransitionEnd = () => { | ||||||
// If modal does not exist | ||||||
if (!this.dialogElement) return; | ||||||
|
||||||
this.dialogElement.close(); | ||||||
}; | ||||||
|
||||||
showModal() { | ||||||
if (!this.dialogElement) throw Error('Modal missing'); | ||||||
if (this.dialogElement.open) return; | ||||||
|
||||||
this.dialogElement.showModal(); | ||||||
// animate modal opening | ||||||
animateTo( | ||||||
this.dialogElement, | ||||||
[ | ||||||
{ opacity: 0, transform: 'translateY(50px)' }, | ||||||
{ opacity: 1, transform: 'translateY(0px)' }, | ||||||
], | ||||||
{ duration: 250, easing: 'ease' }, | ||||||
); | ||||||
// animate modal::backdrop | ||||||
// some browsers don't support ::backdrop, catch those errors | ||||||
try { | ||||||
animateFrom( | ||||||
this.dialogElement, | ||||||
{ opacity: 0 }, | ||||||
{ | ||||||
duration: 250, | ||||||
easing: 'ease', | ||||||
pseudoElement: '::backdrop', | ||||||
}, | ||||||
); | ||||||
} catch (e) {} | ||||||
} | ||||||
|
||||||
private _hideModal() { | ||||||
if (!this.dialogElement) throw Error('Modal missing / hidden'); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's ok to assume |
||||||
if (!this.dialogElement.open) return; | ||||||
|
||||||
// animate modal closing | ||||||
const anim = animateTo( | ||||||
this.dialogElement, | ||||||
{ opacity: 0, transform: 'translateY(50px)' }, | ||||||
{ duration: 250, easing: 'ease' }, | ||||||
); | ||||||
// animate modal::backdrop | ||||||
// some browsers don't support ::backdrop, catch those errors | ||||||
try { | ||||||
animateTo(this.dialogElement, [{ opacity: 0 }], { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
duration: 250, | ||||||
easing: 'ease', | ||||||
pseudoElement: '::backdrop', | ||||||
}); | ||||||
} catch (e) {} | ||||||
anim.onfinish = this._closeOnTransitionEnd; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Using the promise makes it clearer that we only expect this to happen once. Also, it avoids passing an event into a function that doesn't expect it. https://jakearchibald.com/2021/function-callback-risks/ |
||||||
} | ||||||
|
||||||
private _onKeyDown = (event: KeyboardEvent) => { | ||||||
// Default behaviour of <dialog> closes it instantly when you press Esc | ||||||
// So we hijack it to smoothly hide the modal | ||||||
if (event.key === 'Escape') { | ||||||
this._hideModal(); | ||||||
event.preventDefault(); | ||||||
event.stopImmediatePropagation(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think |
||||||
} | ||||||
}; | ||||||
|
||||||
render({ title, icon, content }: Props, {}: State) { | ||||||
return ( | ||||||
<dialog | ||||||
class={style['modalDialog']} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
ref={linkRef(this, 'dialogElement')} | ||||||
onKeyDown={this._onKeyDown} | ||||||
onClick={(event) => { | ||||||
// Prevent clicks from leaking out of the dialog | ||||||
event.preventDefault(); | ||||||
event.stopImmediatePropagation(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it needs to stop immediate propogation? |
||||||
}} | ||||||
> | ||||||
<article class={style.article}> | ||||||
<header class={style.header}> | ||||||
<span class={style.modalIcon}>{icon}</span> | ||||||
<span class={style.modalTitle}>{title}</span> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a heading element. |
||||||
<button class={style.closeButton} onClick={() => this._hideModal()}> | ||||||
<RoundedCrossIcon></RoundedCrossIcon> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
</button> | ||||||
</header> | ||||||
<div class={style.contentContainer}> | ||||||
<article class={style.content}>{content}</article> | ||||||
</div> | ||||||
<footer class={style.footer}></footer> | ||||||
</article> | ||||||
</dialog> | ||||||
); | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change this to "label" or something that better describes its usage.