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:
parent
8fc9d5c728
commit
815438172e
|
@ -153,6 +153,7 @@ export default {
|
|||
<li><kbd>f</kbd> to favorite</li>
|
||||
<li><kbd>b</kbd> to boost</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>y</kbd> to show or hide sensitive media</li>
|
||||
<li><kbd>m</kbd> to mention the author</li>
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
{#if isStatusInOwnThread}
|
||||
<StatusDetails {...params} {...timestampParams} />
|
||||
{/if}
|
||||
<StatusToolbar {...params} {replyShown} on:recalculateHeight />
|
||||
<StatusToolbar {...params} {replyShown} on:recalculateHeight on:focusArticle="focusArticle()" />
|
||||
{#if replyShown}
|
||||
<StatusComposeBox {...params} on:recalculateHeight />
|
||||
{/if}
|
||||
|
@ -133,6 +133,7 @@
|
|||
import { composeNewStatusMentioning } from '../../_actions/mention.js'
|
||||
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid.js'
|
||||
import { addEmojiTooltips } from '../../_utils/addEmojiTooltips.js'
|
||||
import { tryToFocusElement } from '../../_utils/tryToFocusElement.js'
|
||||
|
||||
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea', 'label'])
|
||||
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
|
||||
|
@ -213,6 +214,10 @@
|
|||
async mentionAuthor () {
|
||||
const { accountForShortcut } = this.get()
|
||||
await composeNewStatusMentioning(accountForShortcut)
|
||||
},
|
||||
focusArticle () {
|
||||
const { elementId } = this.get()
|
||||
tryToFocusElement(elementId, /* scroll */ true)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
{#if enableShortcuts}
|
||||
<Shortcut scope={shortcutScope} key="f" on:pressed="toggleFavorite(true)"/>
|
||||
<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)"/>
|
||||
{/if}
|
||||
<style>
|
||||
|
@ -146,6 +147,13 @@
|
|||
this.fire('recalculateHeight')
|
||||
})
|
||||
},
|
||||
dismiss () {
|
||||
const { replyShown } = this.get()
|
||||
if (replyShown) {
|
||||
this.reply()
|
||||
this.fire('focusArticle')
|
||||
}
|
||||
},
|
||||
async onOptionsClick () {
|
||||
const { originalStatus, originalAccountId } = this.get()
|
||||
const updateRelationshipPromise = updateProfileAndRelationship(originalAccountId)
|
||||
|
|
|
@ -172,15 +172,22 @@ function unmapKeys (keyMap, keys, component) {
|
|||
|
||||
function acceptShortcutEvent (event) {
|
||||
const { target } = event
|
||||
return !(
|
||||
if (
|
||||
event.altKey ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
(event.shiftKey && event.key !== '?') || // '?' is a special case - it is allowed
|
||||
(target && (
|
||||
target.isContentEditable ||
|
||||
(event.shiftKey && event.key !== '?') // '?' is a special case - it is allowed
|
||||
) {
|
||||
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) ||
|
||||
(target.tagName === 'INPUT' && !['radio', 'checkbox'].includes(target.getAttribute('type')))
|
||||
))
|
||||
)
|
||||
))
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { scheduleIdleTask } from './scheduleIdleTask.js'
|
|||
const RETRIES = 5
|
||||
const TIMEOUT = 50
|
||||
|
||||
export async function tryToFocusElement (id) {
|
||||
export async function tryToFocusElement (id, scroll) {
|
||||
for (let i = 0; i < RETRIES; i++) {
|
||||
if (i > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, TIMEOUT))
|
||||
|
@ -13,7 +13,7 @@ export async function tryToFocusElement (id) {
|
|||
const element = document.getElementById(id)
|
||||
if (element) {
|
||||
try {
|
||||
element.focus({ preventScroll: true })
|
||||
element.focus({ preventScroll: !scroll })
|
||||
console.log('focused element', id)
|
||||
return
|
||||
} catch (e) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
getFirstModalMedia,
|
||||
getNthStatusAccountLink,
|
||||
getNthStatusAccountLinkSelector,
|
||||
focus
|
||||
focus, getNthComposeReplyInput, getActiveElementId, getActiveElementClassList
|
||||
} from '../utils'
|
||||
import { homeTimeline } from '../fixtures'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
|
@ -234,3 +234,19 @@ test('Shortcut down makes next status active when focused inside of a status', a
|
|||
.pressKey('down')
|
||||
.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)
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue