Skip to content

Commit

Permalink
Replace infinite scrolling with load more button for accessibility
Browse files Browse the repository at this point in the history
  • Loading branch information
synzen committed Dec 29, 2024
1 parent 85caaba commit 4a3a860
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 259 deletions.
327 changes: 164 additions & 163 deletions services/backend-api/client/package-lock.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion services/backend-api/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"react-hook-form": "^7.46.1",
"react-i18next": "^11.15.5",
"react-icons": "^4.10.1",
"react-intersection-observer": "^9.5.2",
"react-router-dom": "^6.2.1",
"react-select": "^5.7.4",
"react-table": "^7.8.0",
Expand Down
2 changes: 1 addition & 1 deletion services/backend-api/client/public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* tslint:disable */

/**
* Mock Service Worker (1.3.3).
* Mock Service Worker (1.3.5).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { Box, Center, Checkbox, Divider, Spinner, Stack, Text, chakra } from "@chakra-ui/react";
import { useInView } from "react-intersection-observer";
import { useCallback, useEffect } from "react";
import {
Box,
Button,
Center,
Checkbox,
Divider,
Flex,
Spinner,
Stack,
Text,
chakra,
} from "@chakra-ui/react";
import { useUserFeedsInfinite } from "../../hooks/useUserFeedsInfinite";
import { InlineErrorAlert } from "../../../../components";

Expand All @@ -10,88 +19,88 @@ interface Props {
}

export const SelectableUserFeedList = ({ selectedIds, onSelectedIdsChange }: Props) => {
const { ref: scrollRef, inView } = useInView();
const { data, status, error, fetchNextPage, hasNextPage, isFetchingNextPage } =
const { data, error, status, fetchNextPage, hasNextPage, isFetchingNextPage } =
useUserFeedsInfinite({
limit: 10,
});
const totalCount = data?.pages[0].total;

const fetchMoreOnBottomReached = useCallback(() => {
if (inView && !isFetchingNextPage && hasNextPage) {
fetchNextPage();
}
}, [fetchNextPage, inView, isFetchingNextPage, hasNextPage]);

useEffect(() => {
fetchMoreOnBottomReached();
}, [fetchMoreOnBottomReached]);

const fetchedSoFarCount = data?.pages.reduce((acc, page) => acc + page.results.length, 0) ?? 0;

return (
<Stack>
<Stack
gap={1}
bg="blackAlpha.300"
px={4}
py={3}
borderRadius="md"
maxHeight={350}
border="2px"
borderColor="gray.600"
overflow="auto"
divider={<Divider />}
bg="blackAlpha.300"
>
{data?.pages.map((page) => {
if (!page.results.length) {
return null;
}
<Stack divider={<Divider />}>
{data?.pages.map((page) => {
if (!page.results.length) {
return null;
}

return (
<Stack gap={1} divider={<Divider />}>
{page.results.map((userFeed) => (
<Box key={`feed-${userFeed.id}`}>
<Checkbox
width="100%"
onChange={(e) => {
if (e.target.checked && !selectedIds.includes(userFeed.id)) {
onSelectedIdsChange([...selectedIds, userFeed.id]);
} else if (!e.target.checked && selectedIds.includes(userFeed.id)) {
onSelectedIdsChange(selectedIds.filter((id) => id !== userFeed.id));
}
}}
isChecked={selectedIds.includes(userFeed.id)}
>
<chakra.span ml={2} display="block" fontSize="sm" fontWeight={600}>
{userFeed.title}
</chakra.span>
<chakra.span
ml={2}
display="block"
color="whiteAlpha.600"
fontSize="sm"
whiteSpace="nowrap"
return (
<Stack gap={1} divider={<Divider />}>
{page.results.map((userFeed) => (
<Box key={`feed-${userFeed.id}`}>
<Checkbox
width="100%"
onChange={(e) => {
if (e.target.checked && !selectedIds.includes(userFeed.id)) {
onSelectedIdsChange([...selectedIds, userFeed.id]);
} else if (!e.target.checked && selectedIds.includes(userFeed.id)) {
onSelectedIdsChange(selectedIds.filter((id) => id !== userFeed.id));
}
}}
isChecked={selectedIds.includes(userFeed.id)}
>
{userFeed.url}
</chakra.span>
</Checkbox>
</Box>
))}
</Stack>
);
})}
{(status === "loading" || isFetchingNextPage) && (
<chakra.span ml={2} display="block" fontSize="sm" fontWeight={600}>
{userFeed.title}
</chakra.span>
<chakra.span
ml={2}
display="block"
color="whiteAlpha.600"
fontSize="sm"
whiteSpace="nowrap"
>
{userFeed.url}
</chakra.span>
</Checkbox>
</Box>
))}
</Stack>
);
})}
</Stack>
{status === "loading" && (
<Center>
<Spinner margin={4} />
</Center>
)}
{error && <InlineErrorAlert title="Failed to list feeds" description={error.message} />}
<div ref={scrollRef} />
<Text color="whiteAlpha.600" fontSize="sm" textAlign="center" mt={6}>
Viewed {fetchedSoFarCount} of {totalCount} feeds
</Text>
<Flex width="full">
<Button
hidden={!hasNextPage}
onClick={() => fetchNextPage()}
isLoading={isFetchingNextPage}
variant="outline"
size="sm"
width="full"
>
Load more
</Button>
</Flex>
</Stack>
<Text color="whiteAlpha.600" fontSize="sm">
Showing {fetchedSoFarCount} of {totalCount} feeds
</Text>
</Stack>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ import {
SearchIcon,
} from "@chakra-ui/icons";
import dayjs from "dayjs";
import { useInView } from "react-intersection-observer";
import { FaPause, FaPlay } from "react-icons/fa6";
import { Link as RouterLink, useSearchParams } from "react-router-dom";
import { useDeleteUserFeeds, useDisableUserFeeds, useEnableUserFeeds } from "../../hooks";
Expand Down Expand Up @@ -110,7 +109,6 @@ const STATUS_FILTERS = [

export const UserFeedsTable: React.FC<Props> = ({ onSelectedFeedId }) => {
const { t } = useTranslation();
const { ref: scrollRef, inView } = useInView();
const [searchParams, setSearchParams] = useSearchParams();
const searchParamsSearch = searchParams.get("search") || "";
const [searchInput, setSearchInput] = useState(searchParamsSearch);
Expand Down Expand Up @@ -139,17 +137,6 @@ export const UserFeedsTable: React.FC<Props> = ({ onSelectedFeedId }) => {
const { mutateAsync: enableUserFeeds } = useEnableUserFeeds();
const flatData = React.useMemo(() => data?.pages?.flatMap((page) => page.results) || [], [data]);

const fetchMoreOnBottomReached = React.useCallback(() => {
if (inView && !isFetchingNextPage && hasNextPage) {
fetchNextPage();
}
}, [fetchNextPage, inView, isFetchingNextPage, hasNextPage]);

// // a check on mount and after a fetch to see if the table is already scrolled to the bottom and immediately needs to fetch more data
React.useEffect(() => {
fetchMoreOnBottomReached();
}, [fetchMoreOnBottomReached]);

const columns = useMemo(
() => [
columnHelper.display({
Expand Down Expand Up @@ -451,7 +438,7 @@ export const UserFeedsTable: React.FC<Props> = ({ onSelectedFeedId }) => {
}

return (
<Stack spacing={4} height="100%">
<Stack spacing={4}>
<form
id="user-feed-search"
onSubmit={(e) => {
Expand Down Expand Up @@ -644,7 +631,6 @@ export const UserFeedsTable: React.FC<Props> = ({ onSelectedFeedId }) => {
borderStyle="solid"
borderRadius="md"
width="100%"
mb={20}
overflowX="auto"
>
<Table
Expand Down Expand Up @@ -744,14 +730,23 @@ export const UserFeedsTable: React.FC<Props> = ({ onSelectedFeedId }) => {
})}
</tbody>
</Table>
{isFetchingNextPage && (
<Center>
<Spinner margin={4} />
</Center>
)}
<div ref={scrollRef} />
</Box>
</Stack>
<Stack>
<Center>
<Text color="whiteAlpha.600" fontSize="sm">
Viewed {flatData.length} of {data?.pages[0].total} feeds
</Text>
</Center>
<Button
disabled={!hasNextPage || isFetchingNextPage}
isLoading={isFetchingNextPage}
onClick={() => fetchNextPage()}
mb={20}
>
Load More
</Button>
</Stack>
</Stack>
);
};
26 changes: 13 additions & 13 deletions services/backend-api/client/src/mocks/data/userFeedSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,18 @@ const mockUserFeedSummary: UserFeedSummary[] = [
},
];

// for (let i = 0; i < 100; i += 1) {
// mockUserFeedSummary.push({
// id: `${i + 4}`,
// title: `Feed ${i + 4}`,
// url: `https://www.feed${i + 4}.com`,
// createdAt: new Date().toISOString(),
// healthStatus: UserFeedHealthStatus.Ok,
// disabledCode: undefined,
// computedStatus: UserFeedComputedStatus.Ok,
// isLegacyFeed: false,
// ownedByUser: true,
// });
// }
for (let i = 0; i < 100; i += 1) {
mockUserFeedSummary.push({
id: `${i + 4}`,
title: `Feed ${i + 4}`,
url: `https://www.feed${i + 4}.com`,
createdAt: new Date().toISOString(),
healthStatus: UserFeedHealthStatus.Ok,
disabledCode: undefined,
computedStatus: UserFeedComputedStatus.Ok,
isLegacyFeed: false,
ownedByUser: true,
});
}

export default mockUserFeedSummary;
1 change: 0 additions & 1 deletion services/backend-api/client/src/pages/UserFeeds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export const UserFeeds: React.FC = () => {
<ChakraLink color="blue.300" onClick={onApplyRequiresAttentionFilters}>
Apply filters to see which ones they are.
</ChakraLink>
.
{hasFailedFeedAlertsDisabled && (
<>
{" "}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ModelDefinition, Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Document, Types, Model } from "mongoose";

@Schema({
timestamps: true,
autoIndex: true,
})
export class UserFeedTag {
_id: Types.ObjectId;

@Prop({
required: true,
})
label: string;

createdAt: Date;
updatedAt: Date;
}

export type UserFeedTagDocument = UserFeedTag & Document;
export type UserFeedTagModel = Model<UserFeedTagDocument>;

export const UserFeedTagSchema = SchemaFactory.createForClass(UserFeedTag);

export const UserFeedTagFeature: ModelDefinition = {
name: UserFeedTag.name,
schema: UserFeedTagSchema,
};

0 comments on commit 4a3a860

Please sign in to comment.