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 { fade, fly, scale } from "svelte/transition";
import { cubicInOut } from "svelte/easing"; import { cubicInOut, linear } from "svelte/easing";
import { getItem } from "./storage"; import { getItem } from "./storage";
import { smallViewport } from "./stores"; 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) { export function maybeModalFade(node) {
return maybeFade(node, { duration: 175, easing: cubicInOut }); return maybeFade(node, { duration: 175, easing: cubicInOut });
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -2,12 +2,12 @@
import { overlayStore, userInfoStore, smallViewport, theme, doAnimations, OverlayType, sendTypingUpdatesItemStore } from "../../stores"; import { overlayStore, userInfoStore, smallViewport, theme, doAnimations, OverlayType, sendTypingUpdatesItemStore } from "../../stores";
import { logOut } from "../../auth"; import { logOut } from "../../auth";
import { maybeModalFade, maybeModalScale } from "../../animations"; import { maybeModalFade, maybeModalScale } from "../../animations";
import request, { methods, remoteBlobUpload, remoteCall } from "../../request"; import request, { methods, remoteBlobUpload } from "../../request";
import { apiRoute, getItem } from "../../storage"; import { apiRoute, getItem } from "../../storage";
import UserView from "../UserView.svelte"; import UserView from "../UserView.svelte";
import ChipBar from "../ChipBar.svelte"; import ChipBar from "../ChipBar.svelte";
import Switch from "../Switch.svelte";
import StoredSwitch from "../StoredSwitch.svelte"; import StoredSwitch from "../StoredSwitch.svelte";
import Modal from "./Modal.svelte";
export let close = () => {}; export let close = () => {};
let avatarFileInput; let avatarFileInput;
@ -76,12 +76,7 @@
}; };
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<style> <style>
.full-width {
width: 100%;
}
.separator { .separator {
margin-bottom: var(--space-sm); margin-bottom: var(--space-sm);
} }
@ -128,7 +123,7 @@
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.large-settings { :global(.large-settings) {
width: 600px; width: 600px;
min-height: 425px; min-height: 425px;
padding-bottom: var(--space-xs); padding-bottom: var(--space-xs);
@ -138,23 +133,19 @@
margin-left: auto; margin-left: auto;
} }
.settings-modal { :global(.settings-modal) {
background-color: var(--background-color-1); background-color: var(--background-color-1) !important;
} }
.settings-modal .modal-header { :global(.settings-modal .modal-header) {
padding-bottom: var(--space-xxs); padding-bottom: var(--space-xxs);
} }
</style> </style>
<!-- svelte-ignore a11y-no-static-element-interactions --> <Modal {close} className={`settings-modal ${$smallViewport ? "" : "large-settings"}`}>
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }"> <span class="h4" slot="header">Settings</span>
<div class="modal settings-modal" class:large-settings="{ !$smallViewport }" transition:maybeModalScale on:click|stopPropagation>
<div class="modal-header">
<span class="h4">Settings</span>
</div>
<div class="modal-content"> <svelte:fragment slot="content">
<ChipBar selectedOptionId="ACCOUNT" onSelect={ (tab) => selectedTab = tab } options={[ <ChipBar selectedOptionId="ACCOUNT" onSelect={ (tab) => selectedTab = tab } options={[
{ id: "ACCOUNT", text: "Account", icon: "person" }, { id: "ACCOUNT", text: "Account", icon: "person" },
{ id: "PRIVACY", text: "Privacy", icon: "lock" }, { id: "PRIVACY", text: "Privacy", icon: "lock" },
@ -209,6 +200,5 @@
{:else} {:else}
<span>Page not found: { selectedTab }</span> <span>Page not found: { selectedTab }</span>
{/if} {/if}
</div> </svelte:fragment>
</div> </Modal>
</div>

View file

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