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:
@@ -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;
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
106
src/discussions/posts/PostsList.jsx
Normal file
106
src/discussions/posts/PostsList.jsx
Normal 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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user