Merge pull request #1701 from Human-Connection/1273-fix-post-page-nav

fix the bug with scrolling post comments into view
This commit is contained in:
Robert Schäfer 2019-10-01 23:17:53 +02:00 committed by GitHub
commit b31126c391
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 137 additions and 90 deletions

View File

@ -0,0 +1,10 @@
export default function(to, from, savedPosition) {
if (savedPosition) return savedPosition
// Edge case: If you click on a notification from a comment and then on the
// post page you click on 'comments', we avoid a "jumping" scroll behavior,
// ie. jump to the top and scroll back from there
if (to.path === from.path && to.hash !== from.hash) return false
return { x: 0, y: 0 }
}

View File

@ -32,6 +32,7 @@ describe('Comment.vue', () => {
truncate: a => a,
removeHtml: a => a,
},
$scrollTo: jest.fn(),
$apollo: {
mutate: jest.fn().mockResolvedValue({
data: {
@ -51,6 +52,8 @@ describe('Comment.vue', () => {
})
describe('shallowMount', () => {
beforeEach(jest.useFakeTimers)
Wrapper = () => {
const store = new Vuex.Store({
getters,
@ -117,7 +120,35 @@ describe('Comment.vue', () => {
})
})
beforeEach(jest.useFakeTimers)
describe('scrollToAnchor mixin', () => {
describe('$route.hash !== comment.id', () => {
beforeEach(() => {
mocks.$route = {
hash: '',
}
})
it('skips $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).not.toHaveBeenCalled()
})
})
describe('$route.hash === comment.id', () => {
beforeEach(() => {
mocks.$route = {
hash: '#commentId-2',
}
})
it('calls $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).toHaveBeenCalledWith('#commentId-2')
})
})
})
describe('test callbacks', () => {
beforeEach(() => {

View File

@ -10,7 +10,7 @@
</ds-card>
</div>
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
<ds-card :id="`commentId-${comment.id}`">
<ds-card :id="anchor">
<ds-space margin-bottom="small" margin-top="small">
<hc-user :user="author" :date-time="comment.createdAt" />
<!-- Content Menu (can open Modals) -->
@ -80,8 +80,10 @@ import ContentMenu from '~/components/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
import HcCommentForm from '~/components/CommentForm/CommentForm'
import CommentMutations from '~/graphql/CommentMutations'
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
export default {
mixins: [scrollToAnchor],
data: function() {
return {
isCollapsed: true,
@ -109,6 +111,9 @@ export default {
user: 'auth/user',
isModerator: 'auth/isModerator',
}),
anchor() {
return `commentId-${this.comment.id}`
},
displaysComment() {
return !this.unavailable || this.isModerator
},
@ -142,6 +147,9 @@ export default {
},
},
methods: {
checkAnchor(anchor) {
return `#${this.anchor}` === anchor
},
isAuthor(id) {
return this.user.id === id
},

View File

@ -42,6 +42,7 @@ describe('CommentList.vue', () => {
truncate: a => a,
removeHtml: a => a,
},
$scrollTo: jest.fn(),
$apollo: {
queries: {
Post: {
@ -65,12 +66,46 @@ describe('CommentList.vue', () => {
})
}
beforeEach(() => {
it('displays a comments counter', () => {
wrapper = Wrapper()
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
})
it('displays a comments counter', () => {
wrapper = Wrapper()
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
})
describe('scrollToAnchor mixin', () => {
beforeEach(jest.useFakeTimers)
describe('$route.hash !== `#comments`', () => {
beforeEach(() => {
mocks.$route = {
hash: '',
}
})
it('skips $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).not.toHaveBeenCalled()
})
})
describe('$route.hash === `#comments`', () => {
beforeEach(() => {
mocks.$route = {
hash: '#comments',
}
})
it('calls $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).toHaveBeenCalledWith('#comments')
})
})
})
})
})

View File

@ -30,8 +30,10 @@
</template>
<script>
import Comment from '~/components/Comment/Comment'
import scrollToAnchor from '~/mixins/scrollToAnchor'
export default {
mixins: [scrollToAnchor],
components: {
Comment,
},
@ -39,6 +41,9 @@ export default {
post: { type: Object, default: () => {} },
},
methods: {
checkAnchor(anchor) {
return anchor === '#comments'
},
updateCommentList(updatedComment) {
this.post.comments = this.post.comments.map(comment => {
return comment.id === updatedComment.id ? updatedComment : comment

View File

@ -75,7 +75,7 @@ export default {
const followedUser = follow ? data.followUser : data.unfollowUser
this.$emit('update', followedUser)
} catch {
} catch (err) {
optimisticResult.followedByCurrentUser = !follow
this.$emit('optimistic', optimisticResult)
}

View File

@ -0,0 +1,23 @@
function scrollToAnchor(anchor, { checkAnchor, $scrollTo }) {
if (typeof checkAnchor !== 'function')
throw new Error(
'You must define `checkAnchor` on the component if you use scrollToAnchor mixin!',
)
if (!checkAnchor(anchor)) return
setTimeout(() => {
$scrollTo(anchor)
}, 250)
}
export default {
watch: {
$route(to, from) {
const anchor = to && to.hash
scrollToAnchor(anchor, this)
},
},
mounted() {
const anchor = this.$route && this.$route.hash
scrollToAnchor(anchor, this)
},
}

View File

@ -124,80 +124,6 @@ export default {
middleware: ['authenticated', 'termsAndConditions'],
linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active',
scrollBehavior: (to, _from, savedPosition) => {
let position = false
// if no children detected and scrollToTop is not explicitly disabled
if (
to.matched.length < 2 &&
to.matched.every(r => r.components.default.options.scrollToTop !== false)
) {
// scroll to the top of the page
position = {
x: 0,
y: 0,
}
} else if (to.matched.some(r => r.components.default.options.scrollToTop)) {
// if one of the children has scrollToTop option set to true
position = {
x: 0,
y: 0,
}
}
// savedPosition is only available for popstate navigations (back button)
if (savedPosition) {
position = savedPosition
}
return new Promise(resolve => {
// wait for the out transition to complete (if necessary)
window.$nuxt.$once('triggerScroll', () => {
let processInterval = null
let processTime = 0
const callInterval = 100
const callIntervalLimit = 2000
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {
let hash = to.hash
// CSS.escape() is not supported with IE and Edge.
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
hash = '#' + window.CSS.escape(hash.substr(1))
}
try {
processInterval = setInterval(() => {
const hashIsFound = document.querySelector(hash)
if (hashIsFound) {
position = {
selector: hash,
offset: { x: 0, y: -500 },
}
}
processTime += callInterval
if (hashIsFound || processTime >= callIntervalLimit) {
clearInterval(processInterval)
processInterval = null
}
}, callInterval)
} catch (e) {
/* eslint-disable-next-line no-console */
console.warn(
'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).',
)
}
}
let resolveInterval = setInterval(() => {
if (!processInterval) {
clearInterval(resolveInterval)
resolve(position)
}
}, callInterval)
})
})
},
},
/*
@ -216,6 +142,13 @@ export default {
keys: envWhitelist,
},
],
[
'vue-scrollto/nuxt',
{
offset: -100, // to compensate fixed navbar height
duration: 1000,
},
],
'cookie-universal-nuxt',
'@nuxtjs/apollo',
'@nuxtjs/axios',

View File

@ -83,6 +83,7 @@
"vue-count-to": "~1.0.13",
"vue-infinite-scroll": "^2.0.2",
"vue-izitoast": "^1.2.1",
"vue-scrollto": "^2.17.1",
"vue-sweetalert-icons": "~4.2.0",
"vuex-i18n": "~1.13.1",
"xregexp": "^4.2.4",

View File

@ -77,17 +77,6 @@ export default {
]
},
},
watch: {
$route(to, from) {
if (to.hash === '#comments') {
window.scroll({
top: document.getElementById('comments').offsetTop,
left: 0,
behavior: 'smooth',
})
}
},
},
}
</script>

View File

@ -197,7 +197,7 @@ export default {
.ds-card-image {
img {
max-height: 300px;
height: 300px;
object-fit: cover;
object-position: center;
}

View File

@ -4235,6 +4235,11 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
bezier-easing@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86"
integrity sha1-wE3+i5JtbsrKGBPWn/F5t8ICXYY=
bfj@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.1.tgz#05a3b7784fbd72cfa3c22e56002ef99336516c48"
@ -15307,6 +15312,13 @@ vue-router@~3.0.7:
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.7.tgz#b36ca107b4acb8ff5bc4ff824584059c23fcb87b"
integrity sha512-utJ+QR3YlIC/6x6xq17UMXeAfxEvXA0VKD3PiSio7hBOZNusA1jXcbxZxVEfJunLp48oonjTepY8ORoIlRx/EQ==
vue-scrollto@^2.17.1:
version "2.17.1"
resolved "https://registry.yarnpkg.com/vue-scrollto/-/vue-scrollto-2.17.1.tgz#cd62ee0b98cf7e2ba9fd94f029addcd093978a48"
integrity sha512-uxOJXg6cZL88B+hTXRHDJMR+gHGiaS70ZTNk55fE5Z2TdwyIx9K/IHoNeTrtBrM6u3FASAIymKjZaQLmDf8Ykg==
dependencies:
bezier-easing "2.1.0"
vue-server-renderer@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.6.10.tgz#cb2558842ead360ae2ec1f3719b75564a805b375"