change scheduling to focus on requestIdleCallback

This commit is contained in:
Nolan Lawson 2018-03-20 20:28:53 -07:00
parent 98b704f465
commit 0c9992c0e1
11 changed files with 76 additions and 39 deletions

View file

@ -1,5 +1,5 @@
{{#if notification.type === 'mention' || notification.type === 'reblog' || notification.type === 'favourite'}}
<Status :index :length :timelineType :timelineValue
<Status :index :length :timelineType :timelineValue :focusSelector
status="{{notification.status}}"
:notification
on:recalculateHeight
@ -8,7 +8,7 @@
<article class="notification-article"
tabindex="0"
aria-posinset="{{index}}" aria-setsize="{{length}}"
>
ref:node >
<div class="follow-notification-offset">
<StatusHeader :notification :notificationId :status :statusId :timelineType
:account :accountId :uuid isStatusInNotification="true" />
@ -37,8 +37,15 @@
import Status from './Status.html'
import StatusHeader from './StatusHeader.html'
import { store } from '../../_store/store'
import { restoreFocus } from '../../_utils/restoreFocus'
export default {
oncreate() {
let focusSelector = this.get('focusSelector')
if (this.refs.node && focusSelector) {
restoreFocus(this.refs.node, focusSelector)
}
},
components: {
Status,
StatusHeader

View file

@ -5,7 +5,8 @@
aria-posinset="{{index}}"
aria-setsize="{{length}}"
aria-label="{{ariaLabel}}"
on:recalculateHeight>
on:recalculateHeight
ref:node >
{{#if showHeader}}
<StatusHeader :notification :notificationId :status :statusId :timelineType
:account :accountId :uuid :isStatusInNotification />
@ -102,6 +103,7 @@
import { goto } from 'sapper/runtime.js'
import { registerClickDelegate, unregisterClickDelegate } from '../../_utils/delegate'
import { classname } from '../../_utils/classname'
import { restoreFocus } from '../../_utils/restoreFocus'
export default {
oncreate() {
@ -110,6 +112,10 @@
// the whole <article> is clickable in this case
registerClickDelegate(delegateKey, (e) => this.onClickOrKeydown(e))
}
let focusSelector = this.get('focusSelector')
if (this.refs.node && focusSelector) {
restoreFocus(this.refs.node, focusSelector)
}
},
ondestroy() {
let delegateKey = this.get('delegateKey')

View file

@ -2,6 +2,7 @@
notification="{{virtualProps.notification}}"
timelineType="{{virtualProps.timelineType}}"
timelineValue="{{virtualProps.timelineValue}}"
focusSelector="{{virtualProps.focusSelector}}"
index="{{virtualIndex}}"
length="{{virtualLength}}"
on:recalculateHeight />

View file

@ -1,6 +1,7 @@
<Status status="{{virtualProps.status}}"
timelineType="{{virtualProps.timelineType}}"
timelineValue="{{virtualProps.timelineValue}}"
focusSelector="{{virtualProps.focusSelector}}"
index="{{virtualIndex}}"
length="{{virtualLength}}"
on:recalculateHeight />

View file

@ -79,9 +79,6 @@
console.log('timeline oncreate()')
this.setupFocus()
setupTimeline()
if (this.store.get('initialized')) {
this.restoreFocus()
}
this.setupStreaming()
},
ondestroy() {
@ -101,13 +98,22 @@
VirtualListComponent: (timelineType) => {
return timelineType === 'notifications' ? NotificationVirtualListItem : StatusVirtualListItem
},
makeProps: ($currentInstance, timelineType, timelineValue) => async (itemId) => {
let res = { timelineType, timelineValue }
makeProps: ($currentInstance, timelineType, timelineValue, $lastFocusedElementSelector) => async (itemId) => {
let res = {
timelineType,
timelineValue
}
if (timelineType === 'notifications') {
res.notification = await database.getNotification($currentInstance, itemId)
} else {
res.status = await database.getStatus($currentInstance, itemId)
}
if ($lastFocusedElementSelector && $lastFocusedElementSelector.includes(itemId)) {
// this selector is guaranteed to contain the statusId. false positives
// (e.g. notification id "1" matches notification id "11") are okay
// because Status.html won't be able to find the selector which is fine.
res.focusSelector = $lastFocusedElementSelector
}
return res
},
label: (timeline, $currentInstance, timelineType, timelineValue) => {
@ -262,22 +268,7 @@
this.store.setForTimeline(instanceName, timelineName, {
lastFocusedElementSelector: null
})
},
restoreFocus() {
let lastFocusedElementSelector = this.store.get('lastFocusedElementSelector')
console.log('lastFocused', lastFocusedElementSelector)
if (lastFocusedElementSelector) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
let element = document.querySelector(lastFocusedElementSelector)
console.log('el', element)
if (element) {
element.focus()
}
})
})
}
},
}
}
}
</script>

View file

@ -32,7 +32,7 @@
import { mark, stop } from '../../_utils/marks'
import isEqual from 'lodash/isEqual'
const DISTANCE_FROM_BOTTOM_TO_FIRE = 400
const DISTANCE_FROM_BOTTOM_TO_FIRE = 800
const SCROLL_EVENT_THROTTLE = 1000
export default {

View file

@ -4,6 +4,7 @@
import throttle from 'lodash/throttle'
import { isFullscreen, attachFullscreenListener, detachFullscreenListener } from '../../_utils/fullscreen'
import { mark, stop } from '../../_utils/marks'
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
const SCROLL_EVENT_DELAY = 300
@ -20,20 +21,16 @@
console.log('allVisibleItemsHaveHeight', allVisibleItemsHaveHeight)
if (!this.get('initializedScrollTop') && allVisibleItemsHaveHeight && node) {
this.set({'initializedScrollTop': true})
requestAnimationFrame(() => {
mark('set scrollTop')
console.log('forcing scroll top to ', scrollTop)
node.scrollTop = scrollTop
stop('set scrollTop')
})
mark('set scrollTop')
console.log('forcing scroll top to ', scrollTop)
node.scrollTop = scrollTop
stop('set scrollTop')
}
})
} else {
requestAnimationFrame(() => {
this.store.setForRealm({
scrollHeight: node.scrollHeight,
offsetHeight: node.offsetHeight
})
this.store.setForRealm({
scrollHeight: node.scrollHeight,
offsetHeight: node.offsetHeight
})
}
stop('onCreate VirtualListContainer')
@ -75,7 +72,7 @@
},
onScroll(event) {
let { scrollTop, scrollHeight } = event.target
requestAnimationFrame(() => { // delay slightly to improve scroll perf
scheduleIdleTask(() => { // delay slightly to improve scroll perf
mark('onScroll -> setForRealm()')
this.store.setForRealm({scrollTop, scrollHeight})
stop('onScroll -> setForRealm()')

View file

@ -9,6 +9,7 @@
<script>
import VirtualListItem from './VirtualListItem'
import { mark, stop } from '../../_utils/marks'
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
export default {
async oncreate() {
@ -18,7 +19,7 @@
let key = this.get('key')
if (makeProps) {
let props = await makeProps(key)
requestAnimationFrame(() => { // delay slightly to avoid slow scrolling
scheduleIdleTask(() => { // delay slightly to avoid slow scrolling
mark('VirtualListLazyItem set props')
this.set({props: props})
stop('VirtualListLazyItem set props')

View file

@ -2,7 +2,7 @@ import { mark, stop } from '../../_utils/marks'
import { RealmStore } from '../../_utils/RealmStore'
import { reselect } from '../../_utils/reselect'
const VIEWPORT_RENDER_FACTOR = 1.5
const VIEWPORT_RENDER_FACTOR = 5
class VirtualListStore extends RealmStore {
constructor (state) {

View file

@ -0,0 +1,9 @@
export function restoreFocus (element, selector) {
// Have to check from the parent because otherwise this element itself wouldn't match.
// This is fine for <article class=status> elements because they already have a div wrapper.
let elementToFocus = element.parentElement.querySelector(selector)
console.log('restoreFocus', selector, elementToFocus)
if (elementToFocus) {
elementToFocus.focus()
}
}

View file

@ -34,6 +34,7 @@ test('timeline link preserves focus', async t => {
.expect(getUrl()).contains('/accounts/')
.click(goBackButton)
.expect(getUrl()).eql('http://localhost:4002/')
.expect(getNthStatus(0).exists).ok()
.expect(getActiveElementInnerText()).eql('admin')
.click(getNthStatus(0).find('.status-sidebar'))
.expect(getUrl()).contains('/accounts/')
@ -51,10 +52,32 @@ test('notification timeline preserves focus', async t => {
.expect(getUrl()).contains('/accounts/')
.click(goBackButton)
.expect(getUrl()).eql('http://localhost:4002/notifications')
.expect(getNthStatus(0).exists).ok()
.expect(getActiveElementInnerText()).eql('quux')
.expect(getActiveElementInsideNthStatus()).eql('5')
})
test('thread preserves focus', async t => {
await t.useRole(foobarRole)
.navigateTo('/accounts/3')
await scrollToStatus(t, 2)
await t.click(getNthStatus(2))
.expect(getUrl()).contains('/statuses/')
.click(getNthStatus(24).find('.status-sidebar'))
.expect(getUrl()).contains('/accounts/')
.click(goBackButton)
.expect(getUrl()).contains('/statuses/')
.expect(getNthStatus(24).exists).ok()
.expect(getActiveElementClass()).contains('status-sidebar')
.expect(getActiveElementInsideNthStatus()).eql('24')
.click(getNthStatus(23))
.expect(getNthStatus(23).find('.status-absolute-date').exists).ok()
await goBack()
await t.expect(getNthStatus(24).find('.status-absolute-date').exists).ok()
.expect(getActiveElementClass()).contains('status-article status-in-timeline')
.expect(getActiveElementInsideNthStatus()).eql('23')
})
test('reply preserves focus and moves focus to the text input', async t => {
await t.useRole(foobarRole)
.click(getNthReplyButton(1))
@ -62,6 +85,7 @@ test('reply preserves focus and moves focus to the text input', async t => {
.expect(getActiveElementClass()).contains('compose-box-input')
.click(goBackButton)
.expect(getUrl()).eql('http://localhost:4002/')
.expect(getNthStatus(0).exists).ok()
.expect(getActiveElementClass()).contains('icon-button')
.expect(getActiveElementAriaLabel()).eql('Reply')
.expect(getActiveElementInsideNthStatus()).eql('1')