diff --git a/package.json b/package.json index fd1d73ce..37566562 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.33", + "version": "0.18.34", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", @@ -10,6 +10,7 @@ "ios": "npm run build && cap sync && NODE_ENV=development cap run ios", "bundle:lit-html": "esbuild ./clientLibs/litHtml.js --bundle --format=esm --outfile=src/js/lib/lit-html.js", "format": "pretty-quick --staged", + "format:all": "prettier --write .", "test": "npm run test:unit", "test:unit": "node --import ./tests/unit/env.js --test-isolation=none --test-force-exit --test 'tests/unit/specs/**/*.test.js'", "test:unit:coverage": "node --import ./tests/unit/env.js --test-isolation=none --test-force-exit --experimental-test-coverage --test-coverage-include='src/js/**' --test 'tests/unit/specs/**/*.test.js'", diff --git a/src/css/style.css b/src/css/style.css index 1b183304..3201d625 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1044,6 +1044,8 @@ tab-bar:not([full-width]) { .sidebar::backdrop { background: var(--overlay-color); + opacity: 0; + transition: opacity 0.3s ease; } .sidebar { @@ -1131,6 +1133,19 @@ tab-bar:not([full-width]) { transform: translateX(0); } +.sidebar[open]::backdrop { + opacity: 1; +} + +.sidebar[open][data-closing] { + pointer-events: none; + transform: translateX(-100%); +} + +.sidebar[open][data-closing]::backdrop { + opacity: 0; +} + .sidebar-profile { padding: 20px 20px 16px 20px; } @@ -5974,6 +5989,26 @@ emoji-picker-dialog emoji-picker, } } +@keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes slide-down { + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(100%); + opacity: 0; + } +} + .bottom-sheet { border: var(--hair) solid var(--post-border-color); border-bottom: none; @@ -5997,6 +6032,26 @@ emoji-picker-dialog emoji-picker, padding-bottom: var(--safe-area-inset-bottom); } +/* Shared scroll region for a bottom sheet's flex-fill body: fills the space + between a fixed header/footer and scrolls its own overflow. Requires a + flex-column ancestor (the dialog or content wrapper) to give it height. */ +.sheet-scroll-region { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; +} + +.bottom-sheet[data-closing] { + pointer-events: none; +} + +/* Only mobile bottom sheets animate out; on desktop they close instantly. */ +@media (max-width: 799px) { + .bottom-sheet[data-closing] { + animation: slide-down 0.15s ease-in forwards; + } +} + @media (min-width: 800px) { .bottom-sheet { padding-bottom: 0; @@ -6042,6 +6097,10 @@ emoji-picker-dialog emoji-picker, animation: fade-in 0.2s ease-out; } +.bottom-sheet[data-closing]::backdrop { + animation: fade-out 0.15s ease-in forwards; +} + @media (max-width: 799px) { .bottom-sheet-fullscreen { top: var(--sheet-top); @@ -6668,6 +6727,10 @@ context-menu-item-group:not(:last-child) .context-menu-item-group { animation: fade-in 0.2s ease-out; } +.modal-dialog[data-closing] { + pointer-events: none; +} + .modal-dialog-title { font-size: 24px; font-weight: 600; @@ -6787,6 +6850,11 @@ context-menu-item-group:not(:last-child) .context-menu-item-group { } } +.image-alt-text-dialog { + display: flex; + flex-direction: column; +} + .image-alt-text-dialog-content { padding: 20px; display: flex; @@ -6796,6 +6864,12 @@ context-menu-item-group:not(:last-child) .context-menu-item-group { min-height: 0; } +@media (min-width: 800px) { + .image-alt-text-dialog-content { + max-height: 80vh; + } +} + .image-alt-text-dialog-header { display: flex; justify-content: space-between; @@ -7657,6 +7731,7 @@ context-menu-item-group:not(:last-child) .context-menu-item-group { word-break: break-word; display: -webkit-box; -webkit-line-clamp: 8; + line-clamp: 8; -webkit-box-orient: vertical; overflow: hidden; } @@ -9221,9 +9296,6 @@ tab-bar[full-width] .tab-bar-button { display: flex; flex-direction: column; gap: 16px; - flex: 1 1 auto; - min-height: 0; - overflow-y: auto; } @media (min-width: 800px) { diff --git a/src/index.html b/src/index.html index 4fc8285d..9be5a1b1 100644 --- a/src/index.html +++ b/src/index.html @@ -157,6 +157,7 @@ import { IdentityResolver } from "/js/atproto.js"; import { Router } from "/js/router.js"; import { scrollLocks } from "/js/scrollLocks.js"; + import { closeWithAnimation } from "/js/dialogHelpers.js"; import { Api } from "/js/api.js"; import { auth } from "/js/auth.js"; import { NotificationService } from "/js/notificationService.js"; @@ -514,7 +515,7 @@ if (wrapper && typeof wrapper.close === "function") { wrapper.close(); } else { - dialog.close(); + closeWithAnimation(dialog); } }); }); diff --git a/src/js/components/account-switcher-dialog.js b/src/js/components/account-switcher-dialog.js index e31d50e9..d7642e6e 100644 --- a/src/js/components/account-switcher-dialog.js +++ b/src/js/components/account-switcher-dialog.js @@ -1,7 +1,7 @@ import { html, render } from "/js/lib/lit-html.js"; import { Component } from "/js/components/component.js"; import { scrollLocks } from "/js/scrollLocks.js"; -import { enableDragToDismiss } from "/js/utils.js"; +import { closeWithAnimation, enableDragToDismiss } from "/js/dialogHelpers.js"; import { auth, getLoginErrorMessage } from "/js/auth.js"; import { Signal, ReactiveStore, effect } from "/js/signals.js"; import { showToast } from "/js/toasts.js"; @@ -101,6 +101,11 @@ class AccountSwitcherDialog extends Component { this.close(); } }} + @close=${() => { + this.scrollLock?.release(); + this.scrollLock = null; + this.dispatchEvent(new CustomEvent("dialog-closed")); + }} >
@@ -280,6 +285,7 @@ class AccountSwitcherDialog extends Component { open() { this.scrollLock ??= scrollLocks.acquire({ target: this }); const dialog = this.querySelector("dialog"); + if (dialog?.open) return; dialog.showModal(); enableDragToDismiss(dialog, { onClose: () => this.close(), @@ -290,13 +296,7 @@ class AccountSwitcherDialog extends Component { } close() { - this.scrollLock?.release(); - this.scrollLock = null; - const dialog = this.querySelector("dialog"); - if (dialog?.open) { - dialog.close(); - } - this.dispatchEvent(new CustomEvent("dialog-closed")); + return closeWithAnimation(this.querySelector("dialog")); } } diff --git a/src/js/components/add-to-lists-dialog.js b/src/js/components/add-to-lists-dialog.js index 17d6e2ad..41b86a2c 100644 --- a/src/js/components/add-to-lists-dialog.js +++ b/src/js/components/add-to-lists-dialog.js @@ -1,7 +1,7 @@ import { html, render } from "/js/lib/lit-html.js"; import { Component } from "/js/components/component.js"; import { scrollLocks } from "/js/scrollLocks.js"; -import { enableDragToDismiss } from "/js/utils.js"; +import { closeWithAnimation, enableDragToDismiss } from "/js/dialogHelpers.js"; import { Signal, SignalSet, ReactiveStore, effect } from "/js/signals.js"; import { isModerationList } from "/js/dataHelpers.js"; import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js"; @@ -110,6 +110,11 @@ class AddToListsDialog extends Component { event.preventDefault(); this.close(); }} + @close=${() => { + this.scrollLock?.release(); + this.scrollLock = null; + this.dispatchEvent(new CustomEvent("dialog-closed")); + }} >
` - : html`
+ : html`
-
+
${this.imageUrl ? html`
Preview @@ -121,6 +127,7 @@ class ImageAltTextDialog extends Component { open() { this.scrollLock ??= scrollLocks.acquire({ target: this }); const dialog = this.querySelector(".image-alt-text-dialog"); + if (dialog?.open) return; dialog.showModal(); this.querySelector(".image-alt-text-dialog-textarea")?.focus({ preventScroll: true, @@ -134,16 +141,12 @@ class ImageAltTextDialog extends Component { resetScrollOnBlur( dialog, - this.querySelector(".image-alt-text-dialog-content"), + this.querySelector(".image-alt-text-dialog-body"), ); } close() { - this.scrollLock?.release(); - this.scrollLock = null; - const dialog = this.querySelector(".image-alt-text-dialog"); - dialog.close(); - this.dispatchEvent(new CustomEvent("alt-text-dialog-closed")); + return closeWithAnimation(this.querySelector(".image-alt-text-dialog")); } save() { diff --git a/src/js/components/new-chat-dialog.js b/src/js/components/new-chat-dialog.js index 994859ed..3c99419b 100644 --- a/src/js/components/new-chat-dialog.js +++ b/src/js/components/new-chat-dialog.js @@ -1,7 +1,11 @@ import { html, render } from "/js/lib/lit-html.js"; import { Component } from "/js/components/component.js"; import { scrollLocks } from "/js/scrollLocks.js"; -import { enableDragToDismiss } from "/js/utils.js"; +import { + closeWithAnimation, + enableDragToDismiss, + resetScrollOnBlur, +} from "/js/dialogHelpers.js"; import { Signal, ReactiveStore, effect } from "/js/signals.js"; import { getDisplayName, MISSING_HANDLE } from "/js/dataHelpers.js"; import { avatarTemplate } from "/js/templates/avatar.template.js"; @@ -227,6 +231,11 @@ class NewChatDialog extends Component { event.preventDefault(); this.close(); }} + @close=${() => { + this.scrollLock?.release(); + this.scrollLock = null; + this.dispatchEvent(new CustomEvent("dialog-closed")); + }} >
@@ -298,6 +307,7 @@ class NewChatDialog extends Component { open() { this.scrollLock ??= scrollLocks.acquire({ target: this }); const dialog = this.querySelector(".new-chat-dialog"); + if (dialog?.open) return; dialog.showModal(); this.querySelector(".new-chat-search-input")?.focus({ preventScroll: true, @@ -307,16 +317,11 @@ class NewChatDialog extends Component { scrollContainer: this.querySelector(".new-chat-results"), ignoreTouchTarget: (element) => element.closest("button, input") !== null, }); + resetScrollOnBlur(dialog, this.querySelector(".new-chat-results")); } close() { - this.scrollLock?.release(); - this.scrollLock = null; - const dialog = this.querySelector(".new-chat-dialog"); - if (dialog?.open) { - dialog.close(); - } - this.dispatchEvent(new CustomEvent("dialog-closed")); + return closeWithAnimation(this.querySelector(".new-chat-dialog")); } } diff --git a/src/js/components/post-composer.js b/src/js/components/post-composer.js index 36143c38..071b63e4 100644 --- a/src/js/components/post-composer.js +++ b/src/js/components/post-composer.js @@ -5,15 +5,18 @@ import { postHeaderTextTemplate } from "/js/templates/postHeaderText.template.js import { richTextTemplate } from "/js/templates/richText.template.js"; import { classnames, - enableDragToDismiss, graphemeCount, readFileAsDataUrl, - resetScrollOnBlur, sanitizeUri, } from "/js/utils.js"; import { externalLinkTemplate } from "/js/templates/externalLink.template.js"; import { confirmModal } from "/js/modals/confirm.modal.js"; import { scrollLocks } from "/js/scrollLocks.js"; +import { + closeWithAnimation, + enableDragToDismiss, + resetScrollOnBlur, +} from "/js/dialogHelpers.js"; import { imageIconTemplate } from "/js/templates/icons/imageIcon.template.js"; import { emojiIconTemplate } from "/js/templates/icons/emojiIcon.template.js"; import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js"; @@ -541,11 +544,17 @@ class PostComposer extends Component { } } }} - @cancel=${async () => { + @cancel=${async (event) => { + event.preventDefault(); if (await this.confirmClose()) { this.close(); } }} + @close=${() => { + this.scrollLock?.release(); + this.scrollLock = null; + this.dispatchEvent(new CustomEvent("post-composer-closed")); + }} @keydown=${(e) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); @@ -1236,6 +1245,7 @@ class PostComposer extends Component { open() { this.scrollLock ??= scrollLocks.acquire({ target: this }); const dialog = this.querySelector(".post-composer"); + if (dialog?.open) return; dialog.showModal(); this.querySelector("rich-text-input")?.focus({ preventScroll: true }); @@ -1269,11 +1279,7 @@ class PostComposer extends Component { } close() { - this.scrollLock?.release(); - this.scrollLock = null; - const dialog = this.querySelector(".post-composer"); - dialog.close(); - this.dispatchEvent(new CustomEvent("post-composer-closed")); + return closeWithAnimation(this.querySelector(".post-composer")); } // Drop trailing empty posts and confirm mid-thread empty posts diff --git a/src/js/components/post-notifications-dialog.js b/src/js/components/post-notifications-dialog.js index 15ffcf51..2258a3c9 100644 --- a/src/js/components/post-notifications-dialog.js +++ b/src/js/components/post-notifications-dialog.js @@ -1,7 +1,7 @@ import { html, render } from "/js/lib/lit-html.js"; import { Component } from "/js/components/component.js"; import { scrollLocks } from "/js/scrollLocks.js"; -import { enableDragToDismiss } from "/js/utils.js"; +import { closeWithAnimation, enableDragToDismiss } from "/js/dialogHelpers.js"; import "/js/components/toggle-switch.js"; import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js"; @@ -42,6 +42,11 @@ class PostNotificationsDialog extends Component { event.preventDefault(); this.close(); }} + @close=${() => { + this.scrollLock?.release(); + this.scrollLock = null; + this.dispatchEvent(new CustomEvent("dialog-closed")); + }} >