From 77ec956d920c84b9ae9782919676d824a8a466d7 Mon Sep 17 00:00:00 2001 From: Alexandre Umbelino Date: Mon, 27 May 2024 13:08:15 +0100 Subject: [PATCH 1/2] Display followed hashtags on the home header Display the followed hashtags on the home header, allowing for easier access to them. Listed from most recent to oldest, and with 2 dedicated buttons. Co-authored-by: Tiago Peralta --- .../button_scroll_list-test.jsx.snap | 258 ++++++++++++++++++ .../__tests__/button_scroll_list-test.jsx | 119 ++++++++ .../components/button_scroll_list.jsx | 87 ++++++ .../mastodon/components/column_header.tsx | 7 +- .../components/followed_tags_list.jsx | 67 +++++ .../mastodon/components/hashtag.tsx | 18 +- .../styles/mastodon/components.scss | 36 +++ 7 files changed, 584 insertions(+), 8 deletions(-) create mode 100644 app/javascript/mastodon/components/__tests__/__snapshots__/button_scroll_list-test.jsx.snap create mode 100644 app/javascript/mastodon/components/__tests__/button_scroll_list-test.jsx create mode 100644 app/javascript/mastodon/components/button_scroll_list.jsx create mode 100644 app/javascript/mastodon/components/followed_tags_list.jsx diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/button_scroll_list-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/button_scroll_list-test.jsx.snap new file mode 100644 index 00000000000..400b6ca87ec --- /dev/null +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/button_scroll_list-test.jsx.snap @@ -0,0 +1,258 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` handles a large number of children correctly 1`] = ` +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+`; + +exports[` handles a single child correctly 1`] = ` +
+ +
+
+
+
+
+ +
+`; + +exports[` renders an empty button scroll list element 1`] = `null`; + +exports[` renders the children 1`] = ` +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+`; diff --git a/app/javascript/mastodon/components/__tests__/button_scroll_list-test.jsx b/app/javascript/mastodon/components/__tests__/button_scroll_list-test.jsx new file mode 100644 index 00000000000..aaa0884831f --- /dev/null +++ b/app/javascript/mastodon/components/__tests__/button_scroll_list-test.jsx @@ -0,0 +1,119 @@ +import renderer from 'react-test-renderer'; + +import { render, screen } from 'mastodon/test_helpers'; + +import ButtonScrollList from '../button_scroll_list'; + +jest.mock('mastodon/components/icon', () => { + return { + Icon: () =>
MockIcon
, + }; +}); + +describe('', () => { + it('renders an empty button scroll list element', () => { + const children = []; + const component = renderer.create( + {children}, + ); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('renders the children', () => { + const children = Array.from({ length: 5 }, (_, i) => ( +
+ )); + const component = renderer.create( + {children}, + ); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('scrolls left', () => { + const children = Array.from({ length: 5 }, (_, i) => ( +
+ )); + const component = renderer.create( + {children}, + ); + const instance = component.getInstance(); + instance.scrollLeft(); + }); + + it('scrolls right', () => { + const children = Array.from({ length: 5 }, (_, i) => ( +
+ )); + const component = renderer.create( + {children}, + ); + const instance = component.getInstance(); + instance.scrollRight(); + }); + + it('scrolls left and right correctly', () => { + const children = Array.from({ length: 10 }, (_, i) => ( +
{i}
+ )); + const component = renderer.create( + {children}, + ); + + setTimeout(() => { + let instance = component.getInstance(); + instance.scrollRight(); + expect(instance.slide).toBe(1); + }, 1000); + + setTimeout(() => { + let instance = component.getInstance(); + instance.scrollLeft(); + expect(instance.slide).toBe(0); + }, 2000); + }); + + it('handles a single child correctly', () => { + const children = [
]; + const component = renderer.create( + {children}, + ); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('handles a large number of children correctly', () => { + const children = Array.from({ length: 50 }, (_, i) => ( +
+ )); + const component = renderer.create( + {children}, + ); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('checks if scroll buttons are accessible', () => { + const children = Array.from({ length: 5 }, (_, i) => ( +
+ )); + render({children}); + + const leftButton = screen.getByRole('button', { name: /scroll left/i }); + const rightButton = screen.getByRole('button', { name: /scroll right/i }); + + expect(leftButton).toBeTruthy(); + expect(rightButton).toBeTruthy(); + + leftButton.focus(); + expect(document.activeElement).toBe(leftButton); + + rightButton.focus(); + expect(document.activeElement).toBe(rightButton); + }); +}); diff --git a/app/javascript/mastodon/components/button_scroll_list.jsx b/app/javascript/mastodon/components/button_scroll_list.jsx new file mode 100644 index 00000000000..f30a54569c7 --- /dev/null +++ b/app/javascript/mastodon/components/button_scroll_list.jsx @@ -0,0 +1,87 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; +import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; +import { Icon } from 'mastodon/components/icon'; + +class ButtonScrollList extends Component { + static propTypes = { + children: PropTypes.node.isRequired, + }; + + constructor(props) { + super(props); + this.scrollRef = React.createRef(); + this.slide = 0; + this.childrenLength = React.Children.count(props.children); + } + + componentDidMount() { + setTimeout(() => { + if (this.scrollRef && this.scrollRef.current) { + const container = this.scrollRef.current; + container.scrollTo({ left: 0, behavior: 'auto' }); + this.slide = 0; + } + }, 500); + } + + scrollLeft = () => { + if (this.scrollRef && this.scrollRef.current) { + this.scrollRef.current.scrollBy({ left: -200, behavior: 'smooth' }); + this.slide = Math.max(0, this.slide - 1); + } + }; + + scrollRight = () => { + if (this.scrollRef && this.scrollRef.current) { + const { children } = this.props; + const container = this.scrollRef.current; + const maxScrollLeft = container.scrollWidth - container.clientWidth; + + if (container.scrollLeft < maxScrollLeft) { + container.scrollBy({ left: 200, behavior: 'smooth' }); + this.slide = Math.min( + React.Children.count(children) - 1, + this.slide + 1, + ); + } else { + } + } + }; + + render() { + const { children } = this.props; + + if (React.Children.count(children) === 0) { + return null; + } + + return ( +
+ +
+ {React.Children.map(children, (child, index) => ( +
{child}
+ ))} +
+ +
+ ); + } +} + +export default ButtonScrollList; diff --git a/app/javascript/mastodon/components/column_header.tsx b/app/javascript/mastodon/components/column_header.tsx index ec946cab3ed..8139237c8f3 100644 --- a/app/javascript/mastodon/components/column_header.tsx +++ b/app/javascript/mastodon/components/column_header.tsx @@ -10,6 +10,7 @@ import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; +import FollowedTagsList from 'mastodon/components/followed_tags_list'; import type { IconProp } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon'; import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; @@ -255,7 +256,7 @@ export const ColumnHeader: React.FC = ({ <> {backButton} -