diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx index cca449b0ca8..70b7968fba1 100644 --- a/app/javascript/mastodon/components/status_list.jsx +++ b/app/javascript/mastodon/components/status_list.jsx @@ -40,6 +40,12 @@ export default class StatusList extends ImmutablePureComponent { trackScroll: true, }; + componentDidMount() { + this.columnHeaderHeight = parseFloat( + getComputedStyle(this.node.node).getPropertyValue('--column-header-height') + ) || 0; + } + getFeaturedStatusCount = () => { return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0; }; @@ -53,35 +59,68 @@ export default class StatusList extends ImmutablePureComponent { }; handleMoveUp = (id, featured) => { - const elementIndex = this.getCurrentStatusIndex(id, featured) - 1; - this._selectChild(elementIndex, true); + const index = this.getCurrentStatusIndex(id, featured); + this._selectChild(id, index, -1); }; handleMoveDown = (id, featured) => { - const elementIndex = this.getCurrentStatusIndex(id, featured) + 1; - this._selectChild(elementIndex, false); + const index = this.getCurrentStatusIndex(id, featured); + this._selectChild(id, index, 1); }; + _selectChild = (id, index, direction) => { + const listContainer = this.node.node; + let listItem = listContainer.querySelector( + // :nth-child uses 1-based indexing + `.item-list > :nth-child(${index + 1 + direction})` + ); + + if (!listItem) { + return; + } + + // If selected container element is empty, we skip it + if (listItem.matches(':empty')) { + this._selectChild(id, index + direction, direction); + return; + } + + // Check if the list item is a post + let targetElement = listItem.querySelector('.focusable'); + + // Otherwise, check if the item contains follow suggestions or + // is a 'load more' button. + if ( + !targetElement && ( + listItem.querySelector('.inline-follow-suggestions') || + listItem.matches('.load-more') + ) + ) { + targetElement = listItem; + } + + if (targetElement) { + const elementRect = targetElement.getBoundingClientRect(); + + const isFullyVisible = + elementRect.top >= this.columnHeaderHeight && + elementRect.bottom <= window.innerHeight; + + if (!isFullyVisible) { + targetElement.scrollIntoView({ + block: direction === 1 ? 'start' : 'center', + }); + } + + targetElement.focus(); + } + } + handleLoadOlder = debounce(() => { const { statusIds, lastId, onLoadMore } = this.props; onLoadMore(lastId || (statusIds.size > 0 ? statusIds.last() : undefined)); }, 300, { leading: true }); - _selectChild (index, align_top) { - const container = this.node.node; - // TODO: This breaks at the inline-follow-suggestions container - const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - if (align_top && container.scrollTop > element.offsetTop) { - element.scrollIntoView(true); - } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { - element.scrollIntoView(false); - } - element.focus(); - } - } - setRef = c => { this.node = c; }; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 40b073f68b9..0fd97fb7129 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2868,6 +2868,8 @@ a.account__display-name { } &__main { + --column-header-height: 62px; + box-sizing: border-box; width: 100%; flex: 0 1 auto; @@ -8815,6 +8817,10 @@ noscript { .conversation { position: relative; + // When scrolling these elements into view, take into account + // the column header height + scroll-margin-top: var(--column-header-height, 0); + &.unread { &::before { content: '';