fix: load-more button added in place of infinite scroll (#199)

fix: accessibility issue resolved assigning aria-levels to UI components

fix: learner view loading changes and ui fix

fix: removed unused component

fix: lint fixes
This commit is contained in:
Mehak Nasir
2022-06-24 21:48:14 +05:00
committed by GitHub
parent ae6397ea32
commit a089235253
11 changed files with 139 additions and 160 deletions

View File

@@ -1,41 +0,0 @@
import React, {
useEffect,
useRef,
} from 'react';
import PropTypes from 'prop-types';
function ScrollThreshold({ onScroll }) {
const elementRef = useRef(null);
useEffect(() => {
if (!elementRef.current) {
return undefined;
}
// create the observer
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
onScroll();
}
},
);
observer.observe(elementRef.current);
// cleanup callback
return () => {
observer.disconnect();
};
}, [elementRef]);
return (
<div ref={elementRef} />
);
}
ScrollThreshold.propTypes = {
onScroll: PropTypes.func.isRequired,
};
export default ScrollThreshold;

View File

@@ -34,7 +34,7 @@ function AuthorLabel({
const labelContents = (
<>
<span className={`mr-1 font-size-14 font-style-normal font-family-inter ${fontWeight}`}>
<span className={`mr-1 font-size-14 font-style-normal font-family-inter ${fontWeight}`} role="heading" aria-level="2">
{capitalize(author)}
</span>
{icon && (

View File

@@ -18,7 +18,7 @@ export default function DiscussionSidebar({ displaySidebar }) {
<div
className={classNames('flex-column', {
'd-none': !displaySidebar,
'd-flex w-25 w-xs-100 w-lg-25 overflow-auto h-100 pb-2': displaySidebar,
'd-flex w-25 w-xs-100 w-lg-25 overflow-auto h-100': displaySidebar,
})}
style={{ minWidth: '30rem' }}
data-testid="sidebar"

View File

@@ -5,10 +5,11 @@ import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon, IconButton, Spinner } from '@edx/paragon';
import {
Button, Icon, IconButton, Spinner,
} from '@edx/paragon';
import { ArrowBack } from '@edx/paragon/icons';
import ScrollThreshold from '../../components/ScrollThreshold';
import { RequestStatus, Routes } from '../../data/constants';
import { DiscussionContext } from '../common/context';
import {
@@ -89,10 +90,9 @@ function LearnerPostsView({ intl }) {
</div>
) : (
nextPage && (
<ScrollThreshold onScroll={() => {
loadMorePosts();
}}
/>
<Button onClick={() => loadMorePosts()} variant="primary" size="md">
{intl.formatMessage(messages.loadMore)}
</Button>
)
)}
</div>

View File

@@ -5,9 +5,9 @@ import {
Redirect, useLocation, useParams,
} from 'react-router';
import { Spinner } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Spinner } from '@edx/paragon';
import ScrollThreshold from '../../components/ScrollThreshold';
import { RequestStatus, Routes } from '../../data/constants';
import { selectconfigLoadingStatus, selectLearnersTabEnabled } from '../data/selectors';
import {
@@ -18,8 +18,9 @@ import {
} from './data/selectors';
import { fetchLearners } from './data/thunks';
import { LearnerCard, LearnerFilterBar } from './learner';
import messages from './messages';
function LearnersView() {
function LearnersView({ intl }) {
const { courseId } = useParams();
const location = useLocation();
const dispatch = useDispatch();
@@ -66,7 +67,9 @@ function LearnersView() {
</div>
) : (
nextPage && (
<ScrollThreshold onScroll={loadPage} />
<Button onClick={() => loadPage()} variant="primary" size="md">
{intl.formatMessage(messages.loadMore)}
</Button>
)
)}
</div>
@@ -74,4 +77,8 @@ function LearnersView() {
);
}
export default LearnersView;
LearnersView.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(LearnersView);

View File

@@ -13,10 +13,10 @@ const messages = defineMessages({
id: 'discussions.learner.lastLogin',
defaultMessage: 'Last active {lastActiveTime}',
},
loadMorePosts: {
id: 'discussions.learner.loadMostPosts',
defaultMessage: 'Load more posts',
description: 'Text on button for loading more posts by a user',
loadMore: {
id: 'discussions.learner.loadMostLearners',
defaultMessage: 'Load more',
description: 'Text on button for loading more learners',
},
back: {
id: 'discussions.learner.back',

View File

@@ -143,6 +143,11 @@ const messages = defineMessages({
defaultMessage: 'TA',
description: 'A label for community TAs displayed next to their username.',
},
loadMorePosts: {
id: 'discussions.learner.loadMostPosts',
defaultMessage: 'Load more posts',
description: 'Text on button for loading more posts by a user',
},
});
export default messages;

View File

@@ -0,0 +1,106 @@
import React, { useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Button, Spinner } from '@edx/paragon';
import { RequestStatus } from '../../data/constants';
import { DiscussionContext } from '../common/context';
import { selectconfigLoadingStatus, selectUserIsPrivileged, selectUserIsStaff } from '../data/selectors';
import messages from '../messages';
import {
selectThreadFilters, selectThreadNextPage, selectThreadSorting, threadsLoadingStatus,
} from './data/selectors';
import { fetchThreads } from './data/thunks';
import NoResults from './NoResults';
import { PostLink } from './post';
function PostsList({ posts, topics, intl }) {
const dispatch = useDispatch();
const {
courseId,
page,
} = useContext(DiscussionContext);
const loadingStatus = useSelector(threadsLoadingStatus());
const { authenticatedUser } = useContext(AppContext);
const orderBy = useSelector(selectThreadSorting());
const filters = useSelector(selectThreadFilters());
const nextPage = useSelector(selectThreadNextPage());
const showOwnPosts = page === 'my-posts';
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userIsStaff = useSelector(selectUserIsStaff);
const configStatus = useSelector(selectconfigLoadingStatus);
const loadThreads = (topicIds, pageNum = undefined) => (
dispatch(fetchThreads(courseId, {
topicIds,
orderBy,
filters,
page: pageNum,
author: showOwnPosts ? authenticatedUser.username : null,
countFlagged: userIsPrivileged || userIsStaff,
}))
);
useEffect(() => {
if (topics !== undefined && configStatus === RequestStatus.SUCCESSFUL) {
loadThreads(topics);
}
}, [courseId, orderBy, filters, page, JSON.stringify(topics), configStatus]);
const checkIsSelected = (id) => window.location.pathname.includes(id);
let lastPinnedIdx = null;
const postInstances = posts && posts.map((post, idx) => {
if (post.pinned && lastPinnedIdx !== false) {
lastPinnedIdx = idx;
} else if (lastPinnedIdx != null && lastPinnedIdx !== false) {
lastPinnedIdx = false;
// Add a spacing after the group of pinned posts
return (
<React.Fragment key={post.id}>
<div className="p-1 bg-light-400" />
<PostLink post={post} key={post.id} isSelected={checkIsSelected} />
</React.Fragment>
);
}
return (<PostLink post={post} key={post.id} isSelected={checkIsSelected} />);
});
return (
<>
{postInstances}
{posts?.length === 0 && <NoResults />}
{loadingStatus === RequestStatus.IN_PROGRESS ? (
<div className="d-flex justify-content-center p-4">
<Spinner animation="border" variant="primary" size="lg" />
</div>
) : (
nextPage && (
<Button onClick={() => loadThreads(topics, nextPage)} variant="primary" size="md">
{intl.formatMessage(messages.loadMorePosts)}
</Button>
)
)}
</>
);
}
PostsList.propTypes = {
posts: PropTypes.arrayOf(PropTypes.shape({
pinned: PropTypes.bool.isRequired,
id: PropTypes.string.isRequired,
})),
topics: PropTypes.arrayOf(PropTypes.string),
intl: intlShape.isRequired,
};
PostsList.defaultProps = {
posts: [],
topics: undefined,
};
export default injectIntl(PostsList);

View File

@@ -1,113 +1,16 @@
import React, { useContext, useEffect } from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { AppContext } from '@edx/frontend-platform/react';
import { Spinner } from '@edx/paragon';
import ScrollThreshold from '../../components/ScrollThreshold';
import { RequestStatus } from '../../data/constants';
import { selectTopicsUnderCategory } from '../../data/selectors';
import { DiscussionContext } from '../common/context';
import { selectconfigLoadingStatus, selectUserIsPrivileged, selectUserIsStaff } from '../data/selectors';
import {
selectAllThreads,
selectThreadFilters,
selectThreadNextPage,
selectThreadSorting,
selectTopicThreads,
threadsLoadingStatus,
} from './data/selectors';
import { fetchThreads } from './data/thunks';
import PostFilterBar from './post-filter-bar/PostFilterBar';
import NoResults from './NoResults';
import { PostLink } from './post';
function PostsList({ posts, topics }) {
const dispatch = useDispatch();
const {
courseId,
page,
} = useContext(DiscussionContext);
const loadingStatus = useSelector(threadsLoadingStatus());
const { authenticatedUser } = useContext(AppContext);
const orderBy = useSelector(selectThreadSorting());
const filters = useSelector(selectThreadFilters());
const nextPage = useSelector(selectThreadNextPage());
const showOwnPosts = page === 'my-posts';
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userIsStaff = useSelector(selectUserIsStaff);
const configStatus = useSelector(selectconfigLoadingStatus);
const loadThreads = (topicIds, pageNum = undefined) => (
dispatch(fetchThreads(courseId, {
topicIds,
orderBy,
filters,
page: pageNum,
author: showOwnPosts ? authenticatedUser.username : null,
countFlagged: userIsPrivileged || userIsStaff,
}))
);
useEffect(() => {
if (topics !== undefined && configStatus === RequestStatus.SUCCESSFUL) {
loadThreads(topics);
}
}, [courseId, orderBy, filters, page, JSON.stringify(topics), configStatus]);
const checkIsSelected = (id) => window.location.pathname.includes(id);
let lastPinnedIdx = null;
const postInstances = posts && posts.map((post, idx) => {
if (post.pinned && lastPinnedIdx !== false) {
lastPinnedIdx = idx;
} else if (lastPinnedIdx != null && lastPinnedIdx !== false) {
lastPinnedIdx = false;
// Add a spacing after the group of pinned posts
return (
<React.Fragment key={post.id}>
<div className="p-1 bg-light-400" />
<PostLink post={post} key={post.id} isSelected={checkIsSelected} />
</React.Fragment>
);
}
return (<PostLink post={post} key={post.id} isSelected={checkIsSelected} />);
});
return (
<>
{postInstances}
{posts?.length === 0 && <NoResults />}
{loadingStatus === RequestStatus.IN_PROGRESS ? (
<div className="d-flex justify-content-center p-4">
<Spinner animation="border" variant="primary" size="lg" />
</div>
) : (
nextPage && (
<ScrollThreshold onScroll={() => {
loadThreads(topics, nextPage);
}}
/>
)
)}
</>
);
}
PostsList.propTypes = {
posts: PropTypes.arrayOf(PropTypes.shape({
pinned: PropTypes.bool.isRequired,
id: PropTypes.string.isRequired,
})),
topics: PropTypes.arrayOf(PropTypes.string),
};
PostsList.defaultProps = {
posts: [],
topics: undefined,
};
import PostsList from './PostsList';
function AllPostsList() {
const posts = useSelector(selectAllThreads);

View File

@@ -78,7 +78,7 @@ function PostHeader({
{preview
? (
<div className="h4 d-flex align-items-center pb-0 mb-0 flex-fill">
<div className="flex-fill text-truncate">
<div className="flex-fill text-truncate" role="heading" aria-level="1">
{post.title}
</div>
{showAnsweredBadge

View File

@@ -47,7 +47,6 @@ function PostLink({
onClick={() => isSelected(post.id)}
style={{ lineHeight: '21px' }}
role="listitem"
aria-level="1"
>
{post.pinned && (
<div className="d-flex flex-fill justify-content-end mr-4 text-primary-500 p-0">