fix: fix keyboard shortcuts for pinned toots (#1033)

* fix: fix keyboard shortcuts for pinned toots

fixes #908

* fix test
This commit is contained in:
Nolan Lawson 2019-02-23 09:47:36 -08:00 committed by GitHub
parent eeba66567c
commit c9ca605cfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 126 additions and 92 deletions

View File

@ -10,7 +10,6 @@
/> />
{/each} {/each}
</div> </div>
<ScrollListShortcuts bind:items=safeItems/>
<style> <style>
.the-list { .the-list {
position: relative; position: relative;
@ -18,7 +17,6 @@
</style> </style>
<script> <script>
import ListLazyItem from './ListLazyItem.html' import ListLazyItem from './ListLazyItem.html'
import ScrollListShortcuts from '../shortcut/ScrollListShortcuts.html'
import { listStore } from './listStore' import { listStore } from './listStore'
import { getScrollContainer } from '../../_utils/scrollContainer' import { getScrollContainer } from '../../_utils/scrollContainer'
import { getMainTopMargin } from '../../_utils/getMainTopMargin' import { getMainTopMargin } from '../../_utils/getMainTopMargin'
@ -64,8 +62,7 @@
length: ({ safeItems }) => safeItems.length length: ({ safeItems }) => safeItems.length
}, },
components: { components: {
ListLazyItem, ListLazyItem
ScrollListShortcuts
}, },
store: () => listStore store: () => listStore
} }

View File

@ -12,24 +12,19 @@
const VISIBILITY_CHECK_DELAY_MS = 600 const VISIBILITY_CHECK_DELAY_MS = 600
const keyToElement = key => document.querySelector(`[shortcut-key=${JSON.stringify(key)}]`)
const elementToKey = element => element.getAttribute('shortcut-key')
const scope = 'global'
export default { export default {
data: () => ({ data: () => ({
scope: 'global', activeItemChangeTime: 0,
itemToKey: (item) => item, elements: document.getElementsByClassName('shortcut-list-item')
keyToElement: (key) => {
return document.querySelector(`[shortcut-key=${JSON.stringify(key)}]`)
},
activeItemChangeTime: 0
}), }),
computed: {
itemToElement: ({ keyToElement, itemToKey }) => (item) => keyToElement(itemToKey(item))
},
oncreate () { oncreate () {
let { scope } = this.get()
addShortcutFallback(scope, this) addShortcutFallback(scope, this)
}, },
ondestroy () { ondestroy () {
let { scope } = this.get()
removeShortcutFallback(scope, this) removeShortcutFallback(scope, this)
}, },
methods: { methods: {
@ -48,10 +43,10 @@
} }
let activeItemKey = this.checkActiveItem(event.timeStamp) let activeItemKey = this.checkActiveItem(event.timeStamp)
if (!activeItemKey) { if (!activeItemKey) {
let { items, itemToKey, itemToElement } = this.get() let { elements } = this.get()
let index = firstVisibleElementIndex(items, itemToElement).first let index = firstVisibleElementIndex(elements).first
if (index >= 0) { if (index >= 0) {
activeItemKey = itemToKey(items[index]) activeItemKey = elementToKey(elements[index])
} }
} }
if (activeItemKey) { if (activeItemKey) {
@ -59,18 +54,14 @@
} }
}, },
changeActiveItem (movement, timeStamp) { changeActiveItem (movement, timeStamp) {
let { let { elements } = this.get()
items,
itemToElement,
itemToKey,
keyToElement } = this.get()
let index = -1 let index = -1
let activeItemKey = this.checkActiveItem(timeStamp) let activeItemKey = this.checkActiveItem(timeStamp)
if (activeItemKey) { if (activeItemKey) {
let len = items.length let len = elements.length
let i = -1 let i = -1
while (++i < len) { while (++i < len) {
if (itemToKey(items[i]) === activeItemKey) { if (elementToKey(elements[i]) === activeItemKey) {
index = i index = i
break break
} }
@ -83,14 +74,13 @@
return return
} }
if (index === -1) { if (index === -1) {
let { first, firstComplete } = firstVisibleElementIndex( let { first, firstComplete } = firstVisibleElementIndex(elements)
items, itemToElement)
index = (movement > 0) ? firstComplete : first index = (movement > 0) ? firstComplete : first
} else { } else {
index += movement index += movement
} }
if (index >= 0 && index < items.length) { if (index >= 0 && index < elements.length) {
activeItemKey = itemToKey(items[index]) activeItemKey = elementToKey(elements[index])
this.setActiveItem(activeItemKey, timeStamp) this.setActiveItem(activeItemKey, timeStamp)
scrollIntoViewIfNeeded(keyToElement(activeItemKey)) scrollIntoViewIfNeeded(keyToElement(activeItemKey))
} }
@ -104,7 +94,7 @@
if (!activeItem) { if (!activeItem) {
return null return null
} }
let { activeItemChangeTime, keyToElement } = this.get() let { activeItemChangeTime } = this.get()
if ((timeStamp - activeItemChangeTime) > VISIBILITY_CHECK_DELAY_MS && if ((timeStamp - activeItemChangeTime) > VISIBILITY_CHECK_DELAY_MS &&
!isVisible(keyToElement(activeItem))) { !isVisible(keyToElement(activeItem))) {
this.setActiveItem(null, 0) this.setActiveItem(null, 0)
@ -114,7 +104,6 @@
}, },
setActiveItem (key, timeStamp) { setActiveItem (key, timeStamp) {
this.set({ activeItemChangeTime: timeStamp }) this.set({ activeItemChangeTime: timeStamp })
let { keyToElement } = this.get()
try { try {
keyToElement(key).focus({ keyToElement(key).focus({
preventScroll: true preventScroll: true

View File

@ -1,6 +1,6 @@
{#if status} {#if status}
<Status {index} {length} {timelineType} {timelineValue} {focusSelector} <Status {index} {length} {timelineType} {timelineValue} {focusSelector}
{status} {notification} {shortcutScope} on:recalculateHeight {status} {notification} {enableShortcuts} on:recalculateHeight
/> />
{:else} {:else}
<article class={className} <article class={className}
@ -8,17 +8,17 @@
aria-posinset={index} aria-posinset={index}
aria-setsize={length} aria-setsize={length}
aria-label={ariaLabel} aria-label={ariaLabel}
focus-key={focusKey} focus-key={uuid}
shortcut-key={shortcutScope} shortcut-key={uuid}
on:focus="onFocus()" on:focus="onFocus()"
on:blur="onBlur()" on:blur="onBlur()"
ref:article ref:article
> >
<StatusHeader {notification} {notificationId} {status} {statusId} {timelineType} <StatusHeader {notification} {notificationId} {status} {statusId} {timelineType}
{account} {accountId} {uuid} isStatusInNotification="true" /> {account} {accountId} {uuid} isStatusInNotification="true" />
{#if shortcutScope} {#if enableShortcuts}
<Shortcut scope={shortcutScope} key="p" on:pressed="openAuthorProfile()" /> <Shortcut scope={uuid} key="p" on:pressed="openAuthorProfile()" />
<Shortcut scope={shortcutScope} key="m" on:pressed="mentionAuthor()" /> <Shortcut scope={uuid} key="m" on:pressed="mentionAuthor()" />
{/if} {/if}
</article> </article>
{/if} {/if}
@ -57,7 +57,7 @@
Shortcut Shortcut
}, },
data: () => ({ data: () => ({
shortcutScope: null enableShortcuts: null
}), }),
store: () => store, store: () => store,
computed: { computed: {
@ -74,9 +74,9 @@
), ),
className: ({ $underlineLinks }) => (classname( className: ({ $underlineLinks }) => (classname(
'notification-article', 'notification-article',
'shortcut-list-item',
$underlineLinks && 'underline-links' $underlineLinks && 'underline-links'
)), ))
focusKey: ({ uuid }) => `notification-follower-${uuid}`
}, },
methods: { methods: {
openAuthorProfile () { openAuthorProfile () {

View File

@ -1,8 +1,8 @@
<article class={className} <article class={className}
tabindex="0" tabindex="0"
delegate-key={delegateKey} delegate-key={uuid}
focus-key={delegateKey} focus-key={uuid}
shortcut-key={shortcutScope} shortcut-key={uuid}
aria-posinset={index} aria-posinset={index}
aria-setsize={length} aria-setsize={length}
aria-label={ariaLabel} aria-label={ariaLabel}
@ -40,10 +40,10 @@
<StatusComposeBox {...params} on:recalculateHeight /> <StatusComposeBox {...params} on:recalculateHeight />
{/if} {/if}
</article> </article>
{#if shortcutScope} {#if enableShortcuts}
<Shortcut scope={shortcutScope} key="o" on:pressed="open()" /> <Shortcut scope={uuid} key="o" on:pressed="open()" />
<Shortcut scope={shortcutScope} key="p" on:pressed="openAuthorProfile()" /> <Shortcut scope={uuid} key="p" on:pressed="openAuthorProfile()" />
<Shortcut scope={shortcutScope} key="m" on:pressed="mentionAuthor()" /> <Shortcut scope={uuid} key="m" on:pressed="mentionAuthor()" />
{/if} {/if}
<style> <style>
@ -146,11 +146,11 @@
export default { export default {
oncreate () { oncreate () {
let { delegateKey, isStatusInOwnThread, showContent } = this.get() let { uuid, isStatusInOwnThread, showContent } = this.get()
let { disableTapOnStatus } = this.store.get() let { disableTapOnStatus } = this.store.get()
if (!isStatusInOwnThread && !disableTapOnStatus) { if (!isStatusInOwnThread && !disableTapOnStatus) {
// the whole <article> is clickable in this case // the whole <article> is clickable in this case
registerClickDelegate(this, delegateKey, (e) => this.onClickOrKeydown(e)) registerClickDelegate(this, uuid, (e) => this.onClickOrKeydown(e))
} }
if (!showContent) { if (!showContent) {
scheduleIdleTask(() => { scheduleIdleTask(() => {
@ -179,7 +179,7 @@
notification: void 0, notification: void 0,
replyVisibility: void 0, replyVisibility: void 0,
contentPreloaded: false, contentPreloaded: false,
shortcutScope: null enableShortcuts: null
}), }),
store: () => store, store: () => store,
methods: { methods: {
@ -248,7 +248,6 @@
uuid: ({ $currentInstance, timelineType, timelineValue, notificationId, statusId }) => ( uuid: ({ $currentInstance, timelineType, timelineValue, notificationId, statusId }) => (
`${$currentInstance}/${timelineType}/${timelineValue}/${notificationId || ''}/${statusId}` `${$currentInstance}/${timelineType}/${timelineValue}/${notificationId || ''}/${statusId}`
), ),
delegateKey: ({ uuid }) => `status-${uuid}`,
isStatusInOwnThread: ({ timelineType, timelineValue, originalStatusId }) => ( isStatusInOwnThread: ({ timelineType, timelineValue, originalStatusId }) => (
(timelineType === 'status' || timelineType === 'reply') && timelineValue === originalStatusId (timelineType === 'status' || timelineType === 'reply') && timelineValue === originalStatusId
), ),
@ -285,6 +284,7 @@
), ),
className: ({ visibility, timelineType, isStatusInOwnThread, $underlineLinks, $disableTapOnStatus }) => (classname( className: ({ visibility, timelineType, isStatusInOwnThread, $underlineLinks, $disableTapOnStatus }) => (classname(
'status-article', 'status-article',
'shortcut-list-item',
visibility === 'direct' && 'status-direct', visibility === 'direct' && 'status-direct',
timelineType !== 'search' && 'status-in-timeline', timelineType !== 'search' && 'status-in-timeline',
isStatusInOwnThread && 'status-in-own-thread', isStatusInOwnThread && 'status-in-own-thread',
@ -297,7 +297,7 @@
account, accountId, uuid, isStatusInNotification, isStatusInOwnThread, account, accountId, uuid, isStatusInNotification, isStatusInOwnThread,
originalAccount, originalAccountId, spoilerShown, visibility, replyShown, originalAccount, originalAccountId, spoilerShown, visibility, replyShown,
replyVisibility, spoilerText, originalStatus, originalStatusId, inReplyToId, replyVisibility, spoilerText, originalStatus, originalStatusId, inReplyToId,
createdAtDate, timeagoFormattedDate, shortcutScope, absoluteFormattedDate }) => ({ createdAtDate, timeagoFormattedDate, enableShortcuts, absoluteFormattedDate }) => ({
notification, notification,
notificationId, notificationId,
status, status,
@ -320,7 +320,7 @@
inReplyToId, inReplyToId,
createdAtDate, createdAtDate,
timeagoFormattedDate, timeagoFormattedDate,
shortcutScope, enableShortcuts,
absoluteFormattedDate absoluteFormattedDate
}) })
} }

View File

@ -31,8 +31,8 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if shortcutScope} {#if enableShortcuts}
<Shortcut scope="{shortcutScope}" key="y" on:pressed="toggleSensitiveMedia()"/> <Shortcut scope={uuid} key="y" on:pressed="toggleSensitiveMedia()"/>
{/if} {/if}
{:else} {:else}
<MediaAttachments {mediaAttachments} {sensitive} {uuid} /> <MediaAttachments {mediaAttachments} {sensitive} {uuid} />

View File

@ -6,8 +6,8 @@
{spoilerShown ? 'Show less' : 'Show more'} {spoilerShown ? 'Show less' : 'Show more'}
</button> </button>
</div> </div>
{#if shortcutScope} {#if enableShortcuts}
<Shortcut scope="{shortcutScope}" key="x" on:pressed="toggleSpoilers()"/> <Shortcut scope={uuid} key="x" on:pressed="toggleSpoilers()"/>
{/if} {/if}
<style> <style>
.status-spoiler { .status-spoiler {

View File

@ -31,10 +31,10 @@
delegateKey={optionsKey} delegateKey={optionsKey}
/> />
</div> </div>
{#if shortcutScope} {#if enableShortcuts}
<Shortcut scope="{shortcutScope}" key="f" on:pressed="toggleFavorite()"/> <Shortcut scope={uuid} key="f" on:pressed="toggleFavorite()"/>
<Shortcut scope="{shortcutScope}" key="r" on:pressed="reply()"/> <Shortcut scope={uuid} key="r" on:pressed="reply()"/>
<Shortcut scope="{shortcutScope}" key="b" on:pressed="reblog()"/> <Shortcut scope={uuid} key="b" on:pressed="reblog()"/>
{/if} {/if}
<style> <style>
.status-toolbar { .status-toolbar {

View File

@ -3,7 +3,7 @@
timelineType={virtualProps.timelineType} timelineType={virtualProps.timelineType}
timelineValue={virtualProps.timelineValue} timelineValue={virtualProps.timelineValue}
focusSelector={virtualProps.focusSelector} focusSelector={virtualProps.focusSelector}
shortcutScope={virtualKey} enableShortcuts={true}
index={virtualIndex} index={virtualIndex}
length={virtualLength} length={virtualLength}
on:recalculateHeight /> on:recalculateHeight />

View File

@ -2,15 +2,24 @@
<h1 class="sr-only">Pinned statuses</h1> <h1 class="sr-only">Pinned statuses</h1>
<div role="feed" aria-label="Pinned statuses" class="pinned-statuses"> <div role="feed" aria-label="Pinned statuses" class="pinned-statuses">
{#each pinnedStatuses as status, index (status.id)} {#each pinnedStatuses as status, index (status.id)}
<Status {status} <div class="pinned-status-wrapper">
timelineType="pinned" <!-- empty div used because we assume the parent of the <article> gets the focus outline -->
timelineValue={accountId} <Status {status}
{index} timelineType="pinned"
length={pinnedStatuses.length} timelineValue={accountId}
/> {index}
length={pinnedStatuses.length}
enableShortcuts={true}
/>
</div>
{/each} {/each}
</div> </div>
{/if} {/if}
<style>
.pinned-status-wrapper:first-child {
margin: 2px 0; /* gives room for the focus outline */
}
</style>
<script> <script>
import { store } from '../../_store/store' import { store } from '../../_store/store'
import Status from '../status/Status.html' import Status from '../status/Status.html'
@ -38,4 +47,4 @@
} }
} }
} }
</script> </script>

View File

@ -2,7 +2,7 @@
timelineType={virtualProps.timelineType} timelineType={virtualProps.timelineType}
timelineValue={virtualProps.timelineValue} timelineValue={virtualProps.timelineValue}
focusSelector={virtualProps.focusSelector} focusSelector={virtualProps.focusSelector}
shortcutScope={virtualKey} enableShortcuts={true}
index={virtualIndex} index={virtualIndex}
length={virtualLength} length={virtualLength}
on:recalculateHeight /> on:recalculateHeight />

View File

@ -35,11 +35,13 @@
<div>Error: component failed to load! Try reloading. {error}</div> <div>Error: component failed to load! Try reloading. {error}</div>
{/await} {/await}
</div> </div>
<ScrollListShortcuts />
<script> <script>
import { store } from '../../_store/store' import { store } from '../../_store/store'
import Status from '../status/Status.html' import Status from '../status/Status.html'
import LoadingFooter from './LoadingFooter.html' import LoadingFooter from './LoadingFooter.html'
import MoreHeaderVirtualWrapper from './MoreHeaderVirtualWrapper.html' import MoreHeaderVirtualWrapper from './MoreHeaderVirtualWrapper.html'
import ScrollListShortcuts from '../shortcut/ScrollListShortcuts.html'
import { import {
importVirtualList, importVirtualList,
importList, importList,
@ -293,6 +295,9 @@
console.log('timeline preinitialized') console.log('timeline preinitialized')
this.store.set({ timelinePreinitialized: true }) this.store.set({ timelinePreinitialized: true })
} }
},
components: {
ScrollListShortcuts
} }
} }
</script> </script>

View File

@ -18,7 +18,6 @@
{/if} {/if}
</div> </div>
</VirtualListContainer> </VirtualListContainer>
<ScrollListShortcuts items={visibleItemKeys} />
<style> <style>
.virtual-list { .virtual-list {
position: relative; position: relative;
@ -29,7 +28,6 @@
import VirtualListLazyItem from './VirtualListLazyItem' import VirtualListLazyItem from './VirtualListLazyItem'
import VirtualListFooter from './VirtualListFooter.html' import VirtualListFooter from './VirtualListFooter.html'
import VirtualListHeader from './VirtualListHeader.html' import VirtualListHeader from './VirtualListHeader.html'
import ScrollListShortcuts from '../shortcut/ScrollListShortcuts.html'
import { virtualListStore } from './virtualListStore' import { virtualListStore } from './virtualListStore'
import throttle from 'lodash-es/throttle' import throttle from 'lodash-es/throttle'
import { mark, stop } from '../../_utils/marks' import { mark, stop } from '../../_utils/marks'
@ -101,8 +99,7 @@
VirtualListContainer, VirtualListContainer,
VirtualListLazyItem, VirtualListLazyItem,
VirtualListFooter, VirtualListFooter,
VirtualListHeader, VirtualListHeader
ScrollListShortcuts
}, },
computed: { computed: {
distanceFromBottom: ({ $scrollHeight, $scrollTop, $offsetHeight }) => { distanceFromBottom: ({ $scrollHeight, $scrollTop, $offsetHeight }) => {

View File

@ -21,15 +21,15 @@ export function isVisible (element) {
return rect.top < offsetHeight && rect.bottom >= topOverlay return rect.top < offsetHeight && rect.bottom >= topOverlay
} }
export function firstVisibleElementIndex (items, itemElementFunction) { export function firstVisibleElementIndex (elements) {
let offsetHeight = getOffsetHeight() let offsetHeight = getOffsetHeight()
let topOverlay = getTopOverlay() let topOverlay = getTopOverlay()
let first = -1 let first = -1
let firstComplete = -1 let firstComplete = -1
let len = items.length let len = elements.length
let i = -1 let i = -1
while (++i < len) { while (++i < len) {
let element = itemElementFunction(items[i]) let element = elements[i]
if (!element) { if (!element) {
continue continue
} }

View File

@ -1,7 +1,7 @@
import { import {
getNthStatus, scrollToStatus, closeDialogButton, modalDialogContents, getActiveElementClass, goBack, getUrl, getNthStatus, scrollToStatus, closeDialogButton, modalDialogContents, goBack, getUrl,
goBackButton, getActiveElementInnerText, getNthReplyButton, getActiveElementInsideNthStatus, focus, goBackButton, getActiveElementInnerText, getNthReplyButton, getActiveElementInsideNthStatus, focus,
getNthStatusSelector, getActiveElementTagName getNthStatusSelector, getActiveElementTagName, getActiveElementClassList
} from '../utils' } from '../utils'
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe' import { Selector as $ } from 'testcafe'
@ -23,7 +23,7 @@ test('modal preserves focus', async t => {
await t.click($(`${getNthStatusSelector(idx)} .play-video-button`)) await t.click($(`${getNthStatusSelector(idx)} .play-video-button`))
.click(closeDialogButton) .click(closeDialogButton)
.expect(modalDialogContents.exists).notOk() .expect(modalDialogContents.exists).notOk()
.expect(getActiveElementClass()).contains('play-video-button') .expect(getActiveElementClassList()).contains('play-video-button')
.expect(getActiveElementInsideNthStatus()).eql(idx.toString()) .expect(getActiveElementInsideNthStatus()).eql(idx.toString())
}) })
@ -37,7 +37,8 @@ test('timeline preserves focus', async t => {
await goBack() await goBack()
await t.expect(getUrl()).eql('http://localhost:4002/') await t.expect(getUrl()).eql('http://localhost:4002/')
.expect(getActiveElementClass()).contains('status-article status-in-timeline') .expect(getActiveElementClassList()).contains('status-article')
.expect(getActiveElementClassList()).contains('status-in-timeline')
.expect(getActiveElementInsideNthStatus()).eql('0') .expect(getActiveElementInsideNthStatus()).eql('0')
}) })
@ -55,7 +56,7 @@ test('timeline link preserves focus', async t => {
.expect(getUrl()).contains('/accounts/') .expect(getUrl()).contains('/accounts/')
.click(goBackButton) .click(goBackButton)
.expect(getUrl()).eql('http://localhost:4002/') .expect(getUrl()).eql('http://localhost:4002/')
.expect(getActiveElementClass()).contains('status-sidebar') .expect(getActiveElementClassList()).contains('status-sidebar')
.expect(getActiveElementInsideNthStatus()).eql('0') .expect(getActiveElementInsideNthStatus()).eql('0')
}) })
@ -73,8 +74,7 @@ test('notification timeline preserves focus', async t => {
.expect(getActiveElementInsideNthStatus()).eql('5') .expect(getActiveElementInsideNthStatus()).eql('5')
}) })
// TODO: this test is really flakey in CI for some reason test('thread preserves focus', async t => {
test.skip('thread preserves focus', async t => {
await loginAsFoobar(t) await loginAsFoobar(t)
await t await t
.navigateTo('/accounts/3') .navigateTo('/accounts/3')
@ -87,14 +87,15 @@ test.skip('thread preserves focus', async t => {
.click(goBackButton) .click(goBackButton)
.expect(getUrl()).contains('/statuses/') .expect(getUrl()).contains('/statuses/')
.expect(getNthStatus(24).exists).ok() .expect(getNthStatus(24).exists).ok()
.expect(getActiveElementClass()).contains('status-sidebar') .expect(getActiveElementClassList()).contains('status-sidebar')
.expect(getActiveElementInsideNthStatus()).eql('24') .expect(getActiveElementInsideNthStatus()).eql('24')
.hover(getNthStatus(23)) .hover(getNthStatus(23))
.click(getNthStatus(23)) .click(getNthStatus(23))
.expect($(`${getNthStatusSelector(23)} .status-absolute-date`).exists).ok() .expect($(`${getNthStatusSelector(23)} .status-absolute-date`).exists).ok()
await goBack() await goBack()
await t.expect($(`${getNthStatusSelector(24)} .status-absolute-date`).exists).ok() await t.expect($(`${getNthStatusSelector(24)} .status-absolute-date`).exists).ok()
.expect(getActiveElementClass()).contains('status-article status-in-timeline') .expect(getActiveElementClassList()).contains('status-article')
.expect(getActiveElementClassList()).contains('status-in-timeline')
.expect(getActiveElementInsideNthStatus()).eql('23') .expect(getActiveElementInsideNthStatus()).eql('23')
}) })
@ -103,7 +104,7 @@ test('reply preserves focus and moves focus to the text input', async t => {
await t await t
.expect(getNthStatus(1).exists).ok({ timeout: 20000 }) .expect(getNthStatus(1).exists).ok({ timeout: 20000 })
.click(getNthReplyButton(1)) .click(getNthReplyButton(1))
.expect(getActiveElementClass()).contains('compose-box-input') .expect(getActiveElementClassList()).contains('compose-box-input')
}) })
test('focus main content element on index page load', async t => { test('focus main content element on index page load', async t => {

View File

@ -9,7 +9,7 @@ import {
getNthStatusSpoiler, getNthStatusSpoiler,
getUrl, modalDialog, getUrl, modalDialog,
scrollToStatus, scrollToStatus,
isNthStatusActive, getActiveElementRectTop, scrollToTop isNthStatusActive, getActiveElementRectTop, scrollToTop, isActiveStatusPinned
} from '../utils' } from '../utils'
import { homeTimeline } from '../fixtures' import { homeTimeline } from '../fixtures'
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
@ -183,3 +183,34 @@ test('Shortcut j/k change the active status on a thread', async t => {
.expect(isNthStatusActive(2)()).notOk() .expect(isNthStatusActive(2)()).notOk()
.expect(isNthStatusActive(3)()).notOk() .expect(isNthStatusActive(3)()).notOk()
}) })
test('Shortcut j/k change the active status on pinned statuses', async t => {
await loginAsFoobar(t)
await t
.click($('a').withText('quux'))
.expect(getUrl()).contains('/accounts')
await t
.expect(getNthStatus(0).exists).ok({ timeout: 30000 })
.expect(isNthStatusActive(0)()).notOk()
.pressKey('j')
.expect(isNthStatusActive(0)()).ok()
.expect(isActiveStatusPinned()).eql(true)
.pressKey('j')
.expect(isNthStatusActive(1)()).ok()
.expect(isActiveStatusPinned()).eql(true)
.pressKey('j')
.expect(isNthStatusActive(0)()).ok()
.expect(isActiveStatusPinned()).eql(false)
.pressKey('j')
.expect(isNthStatusActive(1)()).ok()
.expect(isActiveStatusPinned()).eql(false)
.pressKey('k')
.expect(isNthStatusActive(0)()).ok()
.expect(isActiveStatusPinned()).eql(false)
.pressKey('k')
.expect(isNthStatusActive(1)()).ok()
.expect(isActiveStatusPinned()).eql(true)
.pressKey('k')
.expect(isNthStatusActive(0)()).ok()
.expect(isActiveStatusPinned()).eql(true)
})

View File

@ -1,5 +1,5 @@
import { import {
composeInput, getActiveElementClass, composeInput, getActiveElementClassList,
getNthComposeReplyButton, getNthComposeReplyButton,
getNthComposeReplyInput, getNthReplyButton, getNthComposeReplyInput, getNthReplyButton,
getNthStatusSelector getNthStatusSelector
@ -19,5 +19,5 @@ test('replying to a toot returns focus to reply button', async t => {
.click(getNthReplyButton(0)) .click(getNthReplyButton(0))
.typeText(getNthComposeReplyInput(0), 'How strange was it?', { paste: true }) .typeText(getNthComposeReplyInput(0), 'How strange was it?', { paste: true })
.click(getNthComposeReplyButton(0)) .click(getNthComposeReplyButton(0))
.expect(getActiveElementClass()).contains('status-toolbar-reply-button', { timeout: 20000 }) .expect(getActiveElementClassList()).contains('status-toolbar-reply-button', { timeout: 20000 })
}) })

View File

@ -76,8 +76,8 @@ export const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeo
export const getUrl = exec(() => window.location.href) export const getUrl = exec(() => window.location.href)
export const getActiveElementClass = exec(() => export const getActiveElementClassList = exec(() =>
(document.activeElement && document.activeElement.getAttribute('class')) || '' (document.activeElement && (document.activeElement.getAttribute('class') || '').split(/\s+/)) || []
) )
export const getActiveElementTagName = exec(() => export const getActiveElementTagName = exec(() =>
@ -162,6 +162,11 @@ export const isNthStatusActive = (idx) => (exec(() => {
dependencies: { idx } dependencies: { idx }
})) }))
export const isActiveStatusPinned = exec(() => {
return document.activeElement &&
document.activeElement.getAttribute('delegate-key').includes('pinned')
})
export const scrollToBottom = exec(() => { export const scrollToBottom = exec(() => {
document.scrollingElement.scrollTop = document.scrollingElement.scrollHeight document.scrollingElement.scrollTop = document.scrollingElement.scrollHeight
}) })