fix: fix keyboard shortcuts for pinned toots (#1033)
* fix: fix keyboard shortcuts for pinned toots fixes #908 * fix test
This commit is contained in:
parent
eeba66567c
commit
c9ca605cfe
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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)}
|
||||||
|
<div class="pinned-status-wrapper">
|
||||||
|
<!-- empty div used because we assume the parent of the <article> gets the focus outline -->
|
||||||
<Status {status}
|
<Status {status}
|
||||||
timelineType="pinned"
|
timelineType="pinned"
|
||||||
timelineValue={accountId}
|
timelineValue={accountId}
|
||||||
{index}
|
{index}
|
||||||
length={pinnedStatuses.length}
|
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'
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
|
|
@ -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 })
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue