greatly improve handling of modals

This commit is contained in:
hippoz 2023-07-22 21:24:25 +03:00
parent dc8414c050
commit 72c9650f71
Signed by: hippoz
GPG key ID: 56C4E02A85F2FBED
11 changed files with 262 additions and 297 deletions

View file

@ -1,8 +1,22 @@
import { fade, fly, scale } from "svelte/transition";
import { cubicInOut } from "svelte/easing";
import { cubicInOut, linear } from "svelte/easing";
import { getItem } from "./storage";
import { smallViewport } from "./stores";
// Function specific for the Login and CreateAccount modals, where the transition duration is relied upon
export function maybeModalFadeIf(...e) {
if (e[1] && e[1]._condition)
return maybeModalFade(e[0]);
else
return {
delay: 0,
duration: e[1].duration,
easing: e[1].easing,
css: (_t) => ""
};
}
export function maybeModalFade(node) {
return maybeFade(node, { duration: 175, easing: cubicInOut });
}

View file

@ -1,7 +1,7 @@
<script>
import { overlayStore } from "../../stores";
import { methods, remoteSignal } from "../../request";
import { maybeModalFade, maybeModalScale } from "../../animations";
import Modal from "./Modal.svelte";
let communityName = "";
let createButtonEnabled = true;
@ -15,36 +15,20 @@
}
close();
};
const onKeydown = async (e) => {
if (e.code !== "Enter")
return;
await create();
};
</script>
<style>
.full-width {
width: 100%;
}
</style>
<Modal {close} enter={create}>
<span class="h4" slot="header">Create Community</span>
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }" on:keydown="{ onKeydown }">
<div class="modal" transition:maybeModalScale on:click|stopPropagation>
<div class="modal-header">
<span class="h4">Create Community</span>
</div>
<svelte:fragment slot="content">
<label class="input-label">
Community Name
<input class="input full-width" minlength="1" maxlength="32" bind:value={ communityName } />
</label>
</svelte:fragment>
<div class="modal-content">
<label class="input-label">
Community Name
<input class="input full-width" minlength="1" maxlength="32" bind:value={ communityName } />
</label>
</div>
<div class="modal-footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !createButtonEnabled }">Create</button>
</div>
</div>
</div>
<svelte:fragment slot="footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !createButtonEnabled }">Create</button>
</svelte:fragment>
</Modal>

View file

@ -1,7 +1,7 @@
<script>
import { overlayStore, OverlayType } from "../../stores";
import { methods, remoteCall } from "../../request";
import { maybeModalScale } from "../../animations";
import Modal from "./Modal.svelte";
let username = "";
let password = "";
@ -30,47 +30,33 @@
overlayStore.push(OverlayType.Login);
}
};
const onKeydown = async (e) => {
if (e.code !== "Enter")
return;
await create();
};
</script>
<style>
.full-width {
width: 100%;
}
.separator {
margin-bottom: var(--space-md);
}
</style>
<div class="modal-backdrop modal-backdrop-opaque" on:keydown="{ onKeydown }">
<div class="modal" transition:maybeModalScale on:click|stopPropagation on:outroend="{ outroEnd }">
<div class="modal-header">
<span class="h4">Create an Account</span>
</div>
<Modal opaque close={loginInstead} {outroEnd} enter={create}>
<span class="h4" slot="header">Create an Account</span>
<div class="modal-content">
<label class="input-label">
Username
<input class="input full-width" minlength="1" maxlength="32" bind:value={ username } />
</label>
<div class="separator" />
<label class="input-label">
Password
<input class="input full-width" minlength="8" type="password" bind:value={ password } />
</label>
</div>
<svelte:fragment slot="content">
<label class="input-label">
Username
<input class="input full-width" minlength="1" maxlength="32" bind:value={ username } />
</label>
<div class="modal-footer">
<button class="button modal-secondary-action" on:click="{ loginInstead }">Log in instead</button>
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !buttonsEnabled }">Create</button>
</div>
</div>
</div>
<div class="separator" />
<label class="input-label">
Password
<input class="input full-width" minlength="8" type="password" bind:value={ password } />
</label>
</svelte:fragment>
<svelte:fragment slot="footer">
<button class="button modal-secondary-action" on:click="{ loginInstead }">Log in instead</button>
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !buttonsEnabled }">Create</button>
</svelte:fragment>
</Modal>

View file

@ -1,7 +1,7 @@
<script>
import { overlayStore } from "../../stores";
import { methods, remoteCall, remoteSignal } from "../../request";
import { maybeModalFade, maybeModalScale } from "../../animations";
import { methods, remoteSignal } from "../../request";
import Modal from "./Modal.svelte";
let channelName = "";
let createButtonEnabled = true;
@ -16,39 +16,25 @@
}
close();
};
const onKeydown = async (e) => {
if (e.code !== "Enter")
return;
await create();
};
</script>
<style>
.full-width {
width: 100%;
}
</style>
<Modal {close} enter={create}>
<svelte:fragment slot="header">
<span class="h4">Create Channel</span>
{#if community.id !== -1}
<span class="text-fg-3 text-small">in <span class="text-fg-2">{ community.name }</span></span>
{/if}
</svelte:fragment>
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }" on:keydown="{ onKeydown }">
<div class="modal" transition:maybeModalScale on:click|stopPropagation>
<div class="modal-header">
<span class="h4">Create Channel</span>
{#if community.id !== -1}
<span class="text-fg-3 text-small">in <span class="text-fg-2">{ community.name }</span></span>
{/if}
</div>
<svelte:fragment slot="content">
<label class="input-label">
Channel Name
<input class="input full-width" minlength="1" maxlength="32" bind:value={ channelName } />
</label>
</svelte:fragment>
<div class="modal-content">
<label class="input-label">
Channel Name
<input class="input full-width" minlength="1" maxlength="32" bind:value={ channelName } />
</label>
</div>
<div class="modal-footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !createButtonEnabled }">Create</button>
</div>
</div>
</div>
<svelte:fragment slot="footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !createButtonEnabled }">Create</button>
</svelte:fragment>
</Modal>

View file

@ -1,7 +1,7 @@
<script>
import { maybeModalFade, maybeModalScale } from "../../animations";
import { overlayStore } from "../../stores";
import { methods, remoteCall, remoteSignal } from "../../request";
import { methods, remoteSignal } from "../../request";
import Modal from "./Modal.svelte";
export let channel;
@ -25,41 +25,27 @@
}
close();
};
const onKeydown = async (e) => {
if (e.code !== "Enter")
return;
await save();
};
</script>
<style>
.full-width {
width: 100%;
}
.delete-button {
color: var(--red-2);
}
</style>
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }" on:keydown="{ onKeydown }">
<div class="modal" transition:maybeModalScale on:click|stopPropagation>
<div class="modal-header">
<span class="h4">Edit Channel</span>
</div>
<Modal {close} enter={save}>
<span class="h4" slot="header">Edit Channel</span>
<div class="modal-content">
<label class="input-label">
Channel Name
<input class="input full-width" minlength="1" maxlength="32" bind:value={ channelName } />
</label>
</div>
<svelte:fragment slot="content">
<label class="input-label">
Channel Name
<input class="input full-width" minlength="1" maxlength="32" bind:value={ channelName } />
</label>
</svelte:fragment>
<div class="modal-footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
<button class="button modal-secondary-action delete-button" on:click="{ deleteChannel }" disabled="{ !buttonsEnabled }">Delete</button>
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
</div>
</div>
</div>
<svelte:fragment slot="footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
<button class="button modal-secondary-action delete-button" on:click="{ deleteChannel }" disabled="{ !buttonsEnabled }">Delete</button>
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
</svelte:fragment>
</Modal>

View file

@ -1,7 +1,7 @@
<script>
import { overlayStore } from "../../stores";
import { methods, remoteCall, remoteSignal } from "../../request";
import { maybeModalFade, maybeModalScale } from "../../animations";
import { methods, remoteSignal } from "../../request";
import Modal from "./Modal.svelte";
export let message;
@ -25,41 +25,27 @@
}
close();
};
const onKeydown = async (e) => {
if (e.code !== "Enter")
return;
await save();
};
</script>
<style>
.full-width {
width: 100%;
}
.delete-button {
color: var(--red-2);
}
</style>
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }" on:keydown="{ onKeydown }">
<div class="modal" transition:maybeModalScale on:click|stopPropagation>
<div class="modal-header">
<span class="h4">Edit Message</span>
</div>
<Modal {close} enter={save}>
<span class="h4" slot="header">Edit Message</span>
<div class="modal-content">
<label class="input-label">
Content
<input class="input full-width" minlength="1" bind:value={ messageContent } />
</label>
</div>
<svelte:fragment slot="content">
<label class="input-label">
Content
<input class="input full-width" minlength="1" bind:value={ messageContent } />
</label>
</svelte:fragment>
<div class="modal-footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
<button class="button modal-secondary-action delete-button" on:click="{ deleteMessage }" disabled="{ !buttonsEnabled }">Delete</button>
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
</div>
</div>
</div>
<svelte:fragment slot="footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
<button class="button modal-secondary-action delete-button" on:click="{ deleteMessage }" disabled="{ !buttonsEnabled }">Delete</button>
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
</svelte:fragment>
</Modal>

View file

@ -2,8 +2,8 @@
import { overlayStore, OverlayType } from "../../stores";
import { remoteCall } from "../../request";
import { authWithToken } from "../../auth";
import { maybeModalScale } from "../../animations";
import { methods } from "../../request";
import Modal from "./Modal.svelte";
let username = "";
let password = "";
@ -35,47 +35,33 @@
overlayStore.push(OverlayType.CreateAccount);
}
};
const onKeydown = async (e) => {
if (e.code !== "Enter")
return;
await login();
};
</script>
<style>
.full-width {
width: 100%;
}
.separator {
margin-bottom: var(--space-md);
}
</style>
<div class="modal-backdrop modal-backdrop-opaque" on:keydown="{ onKeydown }">
<div class="modal" transition:maybeModalScale on:click|stopPropagation on:outroend="{ outroEnd }">
<div class="modal-header">
<span class="h4">Welcome back!</span>
</div>
<Modal opaque close={createAccountInstead} {outroEnd} enter={login}>
<span class="h4" slot="header">Welcome back!</span>
<div class="modal-content">
<label class="input-label">
Username
<input class="input full-width" minlength="1" maxlength="32" bind:value={ username } />
</label>
<svelte:fragment slot="content">
<label class="input-label">
Username
<input class="input full-width" minlength="1" maxlength="32" bind:value={ username } />
</label>
<div class="separator" />
<div class="separator" />
<label class="input-label">
Password
<input class="input full-width" minlength="8" type="password" bind:value={ password } />
</label>
</div>
<label class="input-label">
Password
<input class="input full-width" minlength="8" type="password" bind:value={ password } />
</label>
</svelte:fragment>
<div class="modal-footer">
<button class="button modal-secondary-action" on:click="{ createAccountInstead }">Create an account instead</button>
<button class="button button-accent modal-primary-action" on:click="{ login }" disabled="{ !buttonsEnabled }">Log In</button>
</div>
</div>
</div>
<svelte:fragment slot="footer">
<button class="button modal-secondary-action" on:click="{ createAccountInstead }">Create an account instead</button>
<button class="button button-accent modal-primary-action" on:click="{ login }" disabled="{ !buttonsEnabled }">Log In</button>
</svelte:fragment>
</Modal>

View file

@ -0,0 +1,57 @@
<script>
import { onMount } from "svelte";
import { maybeModalFade, maybeModalFadeIf, maybeModalScale } from "../../animations";
export let close = () => {};
export let enter = () => {};
export let outroEnd = () => {};
export let className = "";
export let opaque = false;
let modal;
let blur = false;
const onKeydown = ({ code }) => {
if (code === "Enter") {
enter();
} else if (code === "Escape") {
close();
}
};
const backdropIntroEnd = () => {
blur = true;
};
const backdropOutroStart = () => {
blur = false;
};
onMount(() => {
modal.focus();
});
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-backdrop" class:modal-backdrop-opaque={opaque} class:blur={blur} transition:maybeModalFadeIf="{{ _condition: !opaque }}" on:click="{ close }" on:keydown="{ onKeydown }" on:introend={backdropIntroEnd} on:outrostart={backdropOutroStart}>
<div bind:this={modal} role="alertdialog" tabindex="-1" aria-modal="true" class={className + " modal"} transition:maybeModalScale on:click|stopPropagation on:outroend={outroEnd}>
{#if $$slots.header}
<div class="modal-header">
<slot name="header" />
</div>
{/if}
{#if $$slots.content}
<div class="modal-content">
<slot name="content" />
</div>
{/if}
{#if $$slots.footer}
<div class="modal-footer">
<slot name="footer" />
</div>
{/if}
</div>
</div>

View file

@ -1,5 +1,5 @@
<script>
import { maybeModalFade, maybeModalScale } from "../../animations";
import Modal from "./Modal.svelte";
export let onSubmit = async () => {};
export let onClose = async () => {};
@ -19,36 +19,18 @@
await onSubmit(userInput);
closePrompt();
};
const onKeydown = async (e) => {
if (e.code !== "Enter")
return;
await save();
};
</script>
<style>
.full-width {
width: 100%;
}
</style>
<Modal {closePrompt} enter={save}>
<span class="h4" slot="header">{ heading }</span>
<div class="modal-backdrop" transition:maybeModalFade on:click="{ closePrompt }" on:keydown="{ onKeydown }">
<div class="modal" transition:maybeModalScale on:click|stopPropagation>
<div class="modal-header">
<span class="h4">{ heading }</span>
</div>
<label class="input-label" slot="content">
{ valueName }
<input class="input full-width" bind:value={ userInput } />
</label>
<div class="modal-content">
<label class="input-label">
{ valueName }
<input class="input full-width" bind:value={ userInput } />
</label>
</div>
<div class="modal-footer">
<button class="button modal-secondary-action" on:click="{ closePrompt }">Cancel</button>
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Submit</button>
</div>
</div>
</div>
<svelte:fragment slot="footer">
<button class="button modal-secondary-action" on:click="{ closePrompt }">Cancel</button>
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Submit</button>
</svelte:fragment>
</Modal>

View file

@ -2,12 +2,12 @@
import { overlayStore, userInfoStore, smallViewport, theme, doAnimations, OverlayType, sendTypingUpdatesItemStore } from "../../stores";
import { logOut } from "../../auth";
import { maybeModalFade, maybeModalScale } from "../../animations";
import request, { methods, remoteBlobUpload, remoteCall } from "../../request";
import request, { methods, remoteBlobUpload } from "../../request";
import { apiRoute, getItem } from "../../storage";
import UserView from "../UserView.svelte";
import ChipBar from "../ChipBar.svelte";
import Switch from "../Switch.svelte";
import StoredSwitch from "../StoredSwitch.svelte";
import Modal from "./Modal.svelte";
export let close = () => {};
let avatarFileInput;
@ -76,12 +76,7 @@
};
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<style>
.full-width {
width: 100%;
}
.separator {
margin-bottom: var(--space-sm);
}
@ -128,7 +123,7 @@
grid-template-columns: repeat(2, 1fr);
}
.large-settings {
:global(.large-settings) {
width: 600px;
min-height: 425px;
padding-bottom: var(--space-xs);
@ -138,77 +133,72 @@
margin-left: auto;
}
.settings-modal {
background-color: var(--background-color-1);
:global(.settings-modal) {
background-color: var(--background-color-1) !important;
}
.settings-modal .modal-header {
:global(.settings-modal .modal-header) {
padding-bottom: var(--space-xxs);
}
</style>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }">
<div class="modal settings-modal" class:large-settings="{ !$smallViewport }" transition:maybeModalScale on:click|stopPropagation>
<div class="modal-header">
<span class="h4">Settings</span>
</div>
<Modal {close} className={`settings-modal ${$smallViewport ? "" : "large-settings"}`}>
<span class="h4" slot="header">Settings</span>
<div class="modal-content">
<ChipBar selectedOptionId="ACCOUNT" onSelect={ (tab) => selectedTab = tab } options={[
{ id: "ACCOUNT", text: "Account", icon: "person" },
{ id: "PRIVACY", text: "Privacy", icon: "lock" },
{ id: "APPEARANCE", text: "Appearance", icon: "palette" },
]}></ChipBar>
<svelte:fragment slot="content">
<ChipBar selectedOptionId="ACCOUNT" onSelect={ (tab) => selectedTab = tab } options={[
{ id: "ACCOUNT", text: "Account", icon: "person" },
{ id: "PRIVACY", text: "Privacy", icon: "lock" },
{ id: "APPEARANCE", text: "Appearance", icon: "palette" },
]}></ChipBar>
<div class="separator" />
{#if selectedTab === "ACCOUNT"}
<div class="settings-card full-width">
<UserView user={$userInfoStore}></UserView>
<input type="file" style="display: none;" accept="image/png, image/jpeg, image/webp" name="avatar-upload" multiple={false} bind:this={avatarFileInput} on:change={onAvatarFileChange}>
<div class="left-auto">
<button class="button" on:click="{ openAvatarInput }">Update Avatar</button>
<button class="button button-danger" on:click="{ doLogout }">Log Out</button>
</div>
</div>
{:else if selectedTab == "PRIVACY"}
<div class="switch-option-card full-width">
<div class="info">
<span class="info-heading">Let others know when I'm typing</span>
<span class="text-fg-3 text-small">If this is enabled, other users will see an indicator while you're typing a message.</span>
</div>
<div class="option-switch">
<StoredSwitch store={ sendTypingUpdatesItemStore } />
</div>
</div>
<div class="switch-option-card full-width">
<div class="info">
<span class="info-heading">Make Waffle work</span>
<span class="text-fg-3 text-small">Waffle needs to store data such as your messages, created channels, your username, your profile picture and more in order to work. If you'd like to stop this, you can delete your account.</span>
</div>
</div>
{:else if selectedTab === "APPEARANCE"}
<span class="input-label">Theme</span>
<div class="horizontal-selections">
<button class="button selection-option full-width selected" class:selected="{ $theme === "dark" }" on:click="{ () => theme.set('dark') }">Dark</button>
<button class="button selection-option full-width" class:selected="{ $theme === "light" }" on:click="{ () => theme.set('light') }">Light</button>
</div>
<div class="separator" />
{#if selectedTab === "ACCOUNT"}
<div class="settings-card full-width">
<UserView user={$userInfoStore}></UserView>
<input type="file" style="display: none;" accept="image/png, image/jpeg, image/webp" name="avatar-upload" multiple={false} bind:this={avatarFileInput} on:change={onAvatarFileChange}>
<div class="left-auto">
<button class="button" on:click="{ openAvatarInput }">Update Avatar</button>
<button class="button button-danger" on:click="{ doLogout }">Log Out</button>
</div>
<div class="switch-option-card full-width">
<div class="info">
<span class="info-heading">Reduce animations</span>
<span class="text-fg-3 text-small">Reduce the amount of animations and visual effects.</span>
</div>
{:else if selectedTab == "PRIVACY"}
<div class="switch-option-card full-width">
<div class="info">
<span class="info-heading">Let others know when I'm typing</span>
<span class="text-fg-3 text-small">If this is enabled, other users will see an indicator while you're typing a message.</span>
</div>
<div class="option-switch">
<StoredSwitch store={ sendTypingUpdatesItemStore } />
</div>
<div class="option-switch">
<StoredSwitch store={ doAnimations } inverted={ true } />
</div>
<div class="switch-option-card full-width">
<div class="info">
<span class="info-heading">Make Waffle work</span>
<span class="text-fg-3 text-small">Waffle needs to store data such as your messages, created channels, your username, your profile picture and more in order to work. If you'd like to stop this, you can delete your account.</span>
</div>
</div>
{:else if selectedTab === "APPEARANCE"}
<span class="input-label">Theme</span>
<div class="horizontal-selections">
<button class="button selection-option full-width selected" class:selected="{ $theme === "dark" }" on:click="{ () => theme.set('dark') }">Dark</button>
<button class="button selection-option full-width" class:selected="{ $theme === "light" }" on:click="{ () => theme.set('light') }">Light</button>
</div>
<div class="separator" />
<div class="switch-option-card full-width">
<div class="info">
<span class="info-heading">Reduce animations</span>
<span class="text-fg-3 text-small">Reduce the amount of animations and visual effects.</span>
</div>
<div class="option-switch">
<StoredSwitch store={ doAnimations } inverted={ true } />
</div>
</div>
{:else}
<span>Page not found: { selectedTab }</span>
{/if}
</div>
</div>
</div>
</div>
{:else}
<span>Page not found: { selectedTab }</span>
{/if}
</svelte:fragment>
</Modal>

View file

@ -222,10 +222,13 @@ body {
bottom: 0;
z-index: 15;
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(1.5px);
contain: strict;
}
.modal-backdrop.blur {
backdrop-filter: blur(1.5px);
}
.modal-backdrop-opaque {
background-color: var(--background-color-1);
backdrop-filter: unset;
@ -284,7 +287,6 @@ body {
.modal-backdrop {
align-items: flex-end;
justify-content: center;
backdrop-filter: unset;
}
.modal-backdrop-opaque {
@ -327,8 +329,10 @@ body {
background: none;
text-align: center;
border: none;
padding: 0.7em;
border-radius: 1em;
padding: 0.85em;
padding-top: 0.65em;
padding-bottom: 0.65em;
border-radius: 9999px;
font: inherit;
user-select: none;
font-weight: 550;
@ -588,6 +592,10 @@ body {
border-bottom-left-radius: var(--radius-mdplus);
}
.full-width {
width: 100%;
}
/*! the tweaks below are heavily based on modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */