Skip to content
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

Open
wants to merge 23 commits into
base: dev
Choose a base branch
from

Conversation

aryanpingle
Copy link
Contributor

This PR adds a custom Modal component so that Squoosh can communicate better with users (snackbars are SO last year).

While the snackbar popup will convey small and concise messages to users, the Modal can be used for displaying verbose information like:

  1. INFO mode - Explaining codecs and their parameters
  2. UPDATE mode - Informing users about new features
  3. ERROR mode - Expanding error messages on mobile, where users can't view the console error messages

It's keyboard accessible, has pre-formatted styles for different HTML elements, is responsive across the board and can be easily accessed by other components/elements.

I've tested the feature on the following devices:

  • Safari - iOS / Mac
  • Chrome - iOS / Android / Windows
  • Firefox - Android / Windows

DEMO

In the right-side options under the quality slider, click on any of the modal checkboxes. If you're using a keyboard, focus will be restored to that checkbox once the modal is closed.


Unnecessary PR Lore: This is complementary to PR #1366 where Surma rightly pointed out that the HTML hover title is inaccessible to mobile users. I initially planned to put small one-line descriptions for each codec param in the title attribute, then later add modal support with more detailed descriptions. But I figure if those one-liners will be overwritten anyway, might as well do them soon.

@aryanpingle aryanpingle marked this pull request as ready for review June 17, 2023 02:18
@aryanpingle
Copy link
Contributor Author

@argyleink @surma could y'all take a look at this whenever you're free?

Copy link
Collaborator

@surma surma left a comment

Choose a reason for hiding this comment

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

Great work! This is gonna be a super useful primitive to have in Squoosh going forward.

Have a couple of remarks, and a bigger rework request/idea around the <dialog> element. LMK what you think!

src/client/lazy-app/Compress/Output/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/modal-context.ts Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
Copy link
Collaborator

@jakearchibald jakearchibald left a comment

Choose a reason for hiding this comment

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

I didn't do a full review, as I think this will change pretty significantly when it moves to use <dialog>, which will hopefully reduce the complexity.

src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/modal-context.ts Outdated Show resolved Hide resolved
Copy link
Member

@argyleink argyleink left a comment

Choose a reason for hiding this comment

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

Overall pretty good! Spacing works, sizes work, but the colors were a bit off the rails, so I've made a few suggestions to bring it back into the theme.

Screenshot 2023-06-26 at 1 46 57 PM

A couple other notes!

  • I agree with others that dialog is a good choice here, it'll save a bunch of maintenance burden (like trying to trap focus, etc) and I dont think a majority of the users are on iOS making these transformations
  • The labels should probably also be keyboard focusable. It's a bit strange that focus is "restored" but not really to where I came from, rather the nearest checkbox to where I came from. Also not sure how to invoke this from keyboard, tho it's clear how to dismiss it.

Cool idea here. Hope this review is helpful!

src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Compress/Modal/style.css Outdated Show resolved Hide resolved
@aryanpingle
Copy link
Contributor Author

@argyleink That's a wayyy better design, thank you for the review!

And yep, my plan is to give an optional prop to the Range / Checkbox / etc components that turns their labels into modal-activating keyboard-navigable buttons. So far this was just a test to see if focus can be returned to the calling checkbox, which it can.

Working on a refactor using the <dialog> element as we speak 👍

@aryanpingle
Copy link
Contributor Author

aryanpingle commented Jul 1, 2023

  • Placed the Modal component directory in lazy-app instead of in Compress
  • Refactored using the <dialog> element
  • Used the improved design by @argyleink
  • Switched to using VNodes for modal content and icon
  • Switched from using Context/Provider to shared functions

Caveats:

  • Still uses Context/Provider to share showModal(). Can't use props like with showSnack because deeply nested children should be able to access it. Gonna try @surma 's suggestion of a single global showModal() function that can use the modal as soon as it's mounted.
  • The CSS for fading the modal in and out is a bit ugly, but I couldn't find an easier way considering the dialog element's nuances.

@aryanpingle
Copy link
Contributor Author

Switched from using Context/Provider to shared functions

Copy link
Collaborator

@surma surma left a comment

Choose a reason for hiding this comment

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

This looks really good!

Some minor nits, and of course you should remove the dummy checkboxes and content before merging, but otherwise LGTM! 🎉

src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/modal-context.ts Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
* Improvements to Modal UI
* Modal is now keyboard accessible
* Added new/better info, error and update icons
TS 4.8.3 onwards supports `showModal()`. Also bumped @types/node.
Copy link
Member

@argyleink argyleink left a comment

Choose a reason for hiding this comment

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

approved but with 1 change request!

  • the mobile layout has the dialog at translateY(0), doesnt look right: pop that to 20px or so it looks more intentional 👍🏻

Copy link
Collaborator

@jakearchibald jakearchibald left a comment

Choose a reason for hiding this comment

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

This is heading in the right direction, but I think trying to make the modal a singleton is unnecessary.

One of the benefits of <dialog> is that it doesn't need to be placed out-of-context in the top of the DOM, as it pops into the top-layer when it opens. It feels like the implementation will become simpler if we lean into that ("use the platform" etc etc).

So, the usage of this component would be:

<Modal shown={bool} icon={iconJSX} title="Hello!">
  Modal content goes here.
</Modal>

We'd have many of those in our JSX, in context.

And when shown changes, the component handles the animated show and hide.

I guess at some point we'd also have a tooltip component that outputs an icon and a modal, and causes the modal to show when the icon is clicked.

src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
@jakearchibald
Copy link
Collaborator

jakearchibald commented Jul 13, 2023

Thinking about the helper component:

<ModalHelpIcon title="Hello!">
  Modal content goes here.
</ModalHelpIcon>

So the above would output a ℹ️ icon (or whatever), and a <Modal />. When the icon is clicked, it shows the modal, by toggling the shown prop passed to <Modal />.

That means we can just add <ModalHelpIcon /> where we want to details codecs etc, and that component takes care of the show/hide state.

@aryanpingle
Copy link
Contributor Author

Added a ModalHint component that contains a Modal within itself and shows it when activated. Went with a subtle green accent color so that hints like in the image below stand out. @argyleink, would love your thoughts on this!

image

@argyleink
Copy link
Member

fun additions!

I've got a 2 overall comments 🙂

  1. replace the green with var(--blue) (maybe) or var(--less-light-gray) (prolly), like you've used in the dialog header here. info icons shouldn't try and steal attention, unless hovered, you can spice them up then.
    Screenshot 2023-07-26 at 7 38 22 AM

  2. there's alignment issues, here's some screenshots
    Screenshot 2023-07-26 at 7 38 06 AM Screenshot 2023-07-26 at 7 38 33 AM

@aryanpingle
Copy link
Contributor Author

  • Fixed ModalHint alignment issues
  • Switched from transitionEnd event handler to the Web Animations API

Copy link
Collaborator

@jakearchibald jakearchibald left a comment

Choose a reason for hiding this comment

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

This is coming along nicely!

Where did the icons come from? Do we have the rights to use them?

Comment on lines 166 to 168
<ModalHint modalTitle="mozJPEG - quality" content={sampleContent()}>
Quality:
</ModalHint>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it makes more sense for the content of the modal to be the child of <ModalHint>, and the 'label' to be a prop. The label could probably just be a string too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm I suppose that makes more sense. My thought process was we can just wrap existing text nodes (like for the custom input elements) in the ModalHint and get the modal functionality without much refactoring - but in hindsight that's a cop-out.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It works both ways, and in this case it isn't clear-cut which thing is naturally the "children". But "content" sounds like just another name for "children", whereas the actual child is the label. That, and I usually lean towards having the larger, more tree-like content as the child.

But yeah, it isn't clear-cut.

src/client/lazy-app/icons/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/icons/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/icons/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/style.css Outdated Show resolved Hide resolved
// animate modal::backdrop
// some browsers don't support ::backdrop, catch those errors
try {
animateTo(this.dialogElement, [{ opacity: 0 }], {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
animateTo(this.dialogElement, [{ opacity: 0 }], {
animateTo(this.dialogElement, { opacity: 0 }, {

src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/ModalHint/style.css Outdated Show resolved Hide resolved
src/client/lazy-app/Modal/ModalHint/index.tsx Outdated Show resolved Hide resolved
@jakearchibald
Copy link
Collaborator

jakearchibald commented Sep 27, 2023

@argyleink @kosamari @surma UX question: Right now the whole label becomes an activator for the modal (see "Auto sample chroma" in the preview). My hunch is that only the icon should activate the modal, and the rest of the label should remain as an activator for the checkbox. Can I get a second opinion?

Copy link
Collaborator

@jakearchibald jakearchibald left a comment

Choose a reason for hiding this comment

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

Coming along nicely!

src/client/lazy-app/icons/index.tsx Outdated Show resolved Hide resolved
src/client/lazy-app/icons/index.tsx Outdated Show resolved Hide resolved
outline: var(--medium-light-gray); /* Overwrite default black outline */
}

/* Don't show focus ring on mobile */
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/* Don't show focus ring on mobile */
/* Only show the focus ring on keyboard interaction */

interface Props {
icon: VNode;
title: string;
content: VNode;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

onClick={(event) => {
// Prevent clicks from leaking out of the dialog
event.preventDefault();
event.stopImmediatePropagation();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think it needs to stop immediate propogation?

}

private _hideModal() {
if (!this.dialogElement) throw Error('Modal missing / hidden');
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's ok to assume dialogElement is there, since this component never presents a state where it isn't. Other instances of this can be skipped too.


interface Props {
modalTitle: string;
text?: string;
Copy link
Collaborator

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.

onClick={this.onclick}
title="Learn more"
>
<InfoIcon></InfoIcon>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
<InfoIcon></InfoIcon>
<InfoIcon/>

// When the button is clicked, the event starts bubbling up
// which might cause unexpected behaviour
event.preventDefault();
event.stopImmediatePropagation();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think stopImmediatePropagation is needed.


private onclick = (event: Event) => {
if (!this.modalComponent)
throw new Error('ModalHint is missing a modalComponent');
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can probably be skipped, as it's guaranteed to be there.

@argyleink
Copy link
Member

UX question: Right now the whole label becomes an activator for the modal (see "Auto sample chroma" in the preview). My hunch is that only the icon should activate the modal, and the rest of the label should remain as an activator for the checkbox. Can I get a second opinion?

Confirm, label clicks are typically activators for an associated checkbox, plus this is also how Squoosh works today, using labels as activators.

I agree with Jake, the icon should isolate the interaction to learn more from a modal, both because users expect labels to activate checkboxes on the web, but also because that's the UX that users currently expect from the label interactions.

Same goes for the nested link in the mozJPEG quality modal, a link should be a link and a button to invoke a modal should be a button to invoke a modal.

Button was a good element choice to wrap the icon, as this keeps it in keyboard tab flow 👍🏻

* Optimized SVG paths
* Removed unnecessary attributes

Co-authored-by: Jake Archibald <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants