-
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 13 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,56 @@ | ||||||
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; | ||||||
content: VNode; | ||||||
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, content }: Props) { | ||||||
return ( | ||||||
<span | ||||||
class={style.modalHint} | ||||||
onClick={(event) => { | ||||||
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 |
||||||
}} | ||||||
aryanpingle marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
> | ||||||
<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
|
||||||
{this.props.children} | ||||||
</button> | ||||||
<Modal | ||||||
ref={linkRef(this, 'modalComponent')} | ||||||
icon={<InfoIcon></InfoIcon>} | ||||||
title={modalTitle} | ||||||
content={content} | ||||||
></Modal> | ||||||
</span> | ||||||
); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
span.modal-hint { | ||
aryanpingle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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,136 @@ | ||||||
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 { animateTo } from '../util'; | ||||||
|
||||||
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 { | ||||||
shown: boolean; | ||||||
} | ||||||
|
||||||
export default class Modal extends Component<Props, State> { | ||||||
private dialogElement?: HTMLDialogElement; | ||||||
|
||||||
componentDidMount() { | ||||||
if (!this.dialogElement) throw new Error('Modal missing'); | ||||||
|
||||||
// Set inert by default | ||||||
this.dialogElement.inert = true; | ||||||
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. Can you talk me through the usage of 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 have a feeling it's another instance of obsolete code now that we're using the native 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 so. |
||||||
|
||||||
// Prevent events from leaking through the dialog | ||||||
this.dialogElement.onclick = (event) => { | ||||||
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. Can you talk me through the reason for this, and why the event is attached this way, rather than the usual Preact way? 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. No good reason, I'll refactor it |
||||||
} | ||||||
|
||||||
private _closeOnTransitionEnd = () => { | ||||||
// If modal does not exist | ||||||
if (!this.dialogElement) return; | ||||||
|
||||||
this.dialogElement.close(); | ||||||
this.dialogElement.inert = true; | ||||||
this.setState({ shown: false }); | ||||||
}; | ||||||
|
||||||
showModal() { | ||||||
if (!this.dialogElement || this.dialogElement.open) | ||||||
throw Error('Modal missing / already shown'); | ||||||
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. My preference is that, if the thing is already in the desired state, then just return rather than throwing. Also, try to avoid using both component state & dialog element state. Use one as the source of truth (the component state?) |
||||||
|
||||||
this.dialogElement.inert = false; | ||||||
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 { | ||||||
animateTo(this.dialogElement, [{ opacity: 0 }, { opacity: 1 }], { | ||||||
duration: 250, | ||||||
easing: 'linear', | ||||||
aryanpingle marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
pseudoElement: '::backdrop', | ||||||
}); | ||||||
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. Would it make more sense to use 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. (For browsers that support backdrop) We need fill-mode 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 I get it, but wouldn't the backdrop's default from-CSS opacity be 1, so it wouldn't need a fill-forward? |
||||||
} catch (e) {} | ||||||
this.setState({ shown: true }); | ||||||
} | ||||||
|
||||||
private _hideModal() { | ||||||
if (!this.dialogElement || !this.dialogElement.open) | ||||||
throw Error('Modal missing / hidden'); | ||||||
|
||||||
// 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: 'linear', | ||||||
aryanpingle marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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(e: KeyboardEvent) { | ||||||
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. Nit: Rename the arg to |
||||||
// Default behaviour of <dialog> closes it instantly when you press Esc | ||||||
// So we hijack it to smoothly hide the modal | ||||||
if (e.key === 'Escape') { | ||||||
this._hideModal(); | ||||||
e.preventDefault(); | ||||||
e.stopImmediatePropagation(); | ||||||
} | ||||||
} | ||||||
|
||||||
render({ title, icon, content }: Props, { shown }: State) { | ||||||
return ( | ||||||
<dialog | ||||||
ref={linkRef(this, 'dialogElement')} | ||||||
onKeyDown={(e) => this._onKeyDown(e)} | ||||||
aryanpingle marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
> | ||||||
{shown && ( | ||||||
<Fragment> | ||||||
<header class={style.header}> | ||||||
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. If you're going to use |
||||||
<span class={style.modalIcon}>{icon}</span> | ||||||
<span class={style.modalTitle}>{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> | ||||||
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. Move this to icons? |
||||||
</button> | ||||||
</header> | ||||||
<div class={style.contentContainer}> | ||||||
<article class={style.content}>{content}</article> | ||||||
</div> | ||||||
<footer class={style.footer}></footer> | ||||||
</Fragment> | ||||||
)} | ||||||
</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.