feat: Make it possible to close inline reply with the escape key. (#2273)

Fixes #915.

Co-authored-by: Nolan Lawson <nolan@nolanlawson.com>
This commit is contained in:
James Teh 2022-12-03 06:54:54 +10:00 committed by GitHub
parent 8fc9d5c728
commit 815438172e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 47 additions and 10 deletions

View file

@ -153,6 +153,7 @@ export default {
<li><kbd>f</kbd> to favorite</li> <li><kbd>f</kbd> to favorite</li>
<li><kbd>b</kbd> to boost</li> <li><kbd>b</kbd> to boost</li>
<li><kbd>r</kbd> to reply</li> <li><kbd>r</kbd> to reply</li>
<li><kbd>Escape</kbd> to close reply</li>
<li><kbd>i</kbd> to open images, video, or audio</li> <li><kbd>i</kbd> to open images, video, or audio</li>
<li><kbd>y</kbd> to show or hide sensitive media</li> <li><kbd>y</kbd> to show or hide sensitive media</li>
<li><kbd>m</kbd> to mention the author</li> <li><kbd>m</kbd> to mention the author</li>

View file

@ -39,7 +39,7 @@
{#if isStatusInOwnThread} {#if isStatusInOwnThread}
<StatusDetails {...params} {...timestampParams} /> <StatusDetails {...params} {...timestampParams} />
{/if} {/if}
<StatusToolbar {...params} {replyShown} on:recalculateHeight /> <StatusToolbar {...params} {replyShown} on:recalculateHeight on:focusArticle="focusArticle()" />
{#if replyShown} {#if replyShown}
<StatusComposeBox {...params} on:recalculateHeight /> <StatusComposeBox {...params} on:recalculateHeight />
{/if} {/if}
@ -133,6 +133,7 @@
import { composeNewStatusMentioning } from '../../_actions/mention.js' import { composeNewStatusMentioning } from '../../_actions/mention.js'
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid.js' import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid.js'
import { addEmojiTooltips } from '../../_utils/addEmojiTooltips.js' import { addEmojiTooltips } from '../../_utils/addEmojiTooltips.js'
import { tryToFocusElement } from '../../_utils/tryToFocusElement.js'
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea', 'label']) const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea', 'label'])
const isUserInputElement = node => INPUT_TAGS.has(node.localName) const isUserInputElement = node => INPUT_TAGS.has(node.localName)
@ -213,6 +214,10 @@
async mentionAuthor () { async mentionAuthor () {
const { accountForShortcut } = this.get() const { accountForShortcut } = this.get()
await composeNewStatusMentioning(accountForShortcut) await composeNewStatusMentioning(accountForShortcut)
},
focusArticle () {
const { elementId } = this.get()
tryToFocusElement(elementId, /* scroll */ true)
} }
}, },
computed: { computed: {

View file

@ -42,6 +42,7 @@
{#if enableShortcuts} {#if enableShortcuts}
<Shortcut scope={shortcutScope} key="f" on:pressed="toggleFavorite(true)"/> <Shortcut scope={shortcutScope} key="f" on:pressed="toggleFavorite(true)"/>
<Shortcut scope={shortcutScope} key="r" on:pressed="reply()"/> <Shortcut scope={shortcutScope} key="r" on:pressed="reply()"/>
<Shortcut scope={shortcutScope} key="escape" on:pressed="dismiss()"/>
<Shortcut scope={shortcutScope} key="b" on:pressed="reblog(true)"/> <Shortcut scope={shortcutScope} key="b" on:pressed="reblog(true)"/>
{/if} {/if}
<style> <style>
@ -146,6 +147,13 @@
this.fire('recalculateHeight') this.fire('recalculateHeight')
}) })
}, },
dismiss () {
const { replyShown } = this.get()
if (replyShown) {
this.reply()
this.fire('focusArticle')
}
},
async onOptionsClick () { async onOptionsClick () {
const { originalStatus, originalAccountId } = this.get() const { originalStatus, originalAccountId } = this.get()
const updateRelationshipPromise = updateProfileAndRelationship(originalAccountId) const updateRelationshipPromise = updateProfileAndRelationship(originalAccountId)

View file

@ -172,15 +172,22 @@ function unmapKeys (keyMap, keys, component) {
function acceptShortcutEvent (event) { function acceptShortcutEvent (event) {
const { target } = event const { target } = event
return !( if (
event.altKey || event.altKey ||
event.metaKey || event.metaKey ||
event.ctrlKey || event.ctrlKey ||
(event.shiftKey && event.key !== '?') || // '?' is a special case - it is allowed (event.shiftKey && event.key !== '?') // '?' is a special case - it is allowed
(target && ( ) {
target.isContentEditable || return false
}
if (event.key === 'Escape') {
// Allow escape everywhere.
return true
}
// Don't allow other keys in text boxes.
return !(target && (
target.isContentEditable ||
['TEXTAREA', 'SELECT'].includes(target.tagName) || ['TEXTAREA', 'SELECT'].includes(target.tagName) ||
(target.tagName === 'INPUT' && !['radio', 'checkbox'].includes(target.getAttribute('type'))) (target.tagName === 'INPUT' && !['radio', 'checkbox'].includes(target.getAttribute('type')))
)) ))
)
} }

View file

@ -4,7 +4,7 @@ import { scheduleIdleTask } from './scheduleIdleTask.js'
const RETRIES = 5 const RETRIES = 5
const TIMEOUT = 50 const TIMEOUT = 50
export async function tryToFocusElement (id) { export async function tryToFocusElement (id, scroll) {
for (let i = 0; i < RETRIES; i++) { for (let i = 0; i < RETRIES; i++) {
if (i > 0) { if (i > 0) {
await new Promise(resolve => setTimeout(resolve, TIMEOUT)) await new Promise(resolve => setTimeout(resolve, TIMEOUT))
@ -13,7 +13,7 @@ export async function tryToFocusElement (id) {
const element = document.getElementById(id) const element = document.getElementById(id)
if (element) { if (element) {
try { try {
element.focus({ preventScroll: true }) element.focus({ preventScroll: !scroll })
console.log('focused element', id) console.log('focused element', id)
return return
} catch (e) { } catch (e) {

View file

@ -17,7 +17,7 @@ import {
getFirstModalMedia, getFirstModalMedia,
getNthStatusAccountLink, getNthStatusAccountLink,
getNthStatusAccountLinkSelector, getNthStatusAccountLinkSelector,
focus focus, getNthComposeReplyInput, getActiveElementId, getActiveElementClassList
} from '../utils' } from '../utils'
import { homeTimeline } from '../fixtures' import { homeTimeline } from '../fixtures'
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
@ -234,3 +234,19 @@ test('Shortcut down makes next status active when focused inside of a status', a
.pressKey('down') .pressKey('down')
.expect(isNthStatusActive(2)()).ok() .expect(isNthStatusActive(2)()).ok()
}) })
test('Press r to reply, press Esc to close reply', async t => {
await loginAsFoobar(t)
await t
.expect(getNthStatus(1).exists).ok()
await activateStatus(t, 0)
const id = await getActiveElementId()
await t
.expect(getNthComposeReplyInput(1).exists).notOk()
.pressKey('r')
.expect(getNthComposeReplyInput(1).exists).ok()
.expect(getActiveElementClassList()).contains('compose-box-input')
.pressKey('esc')
.expect(getNthComposeReplyInput(1).exists).notOk()
.expect(getActiveElementId()).eql(id)
})