Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitad3abe3

Browse files
authored
refactor: revamp pagination UI view logic (#10567)
* chore: revamp Page Utility tests* refactor: simplify component design for PageButton* chore: beef up isNonInitialPage and add tests* docs: clean up comments* chore: quick refactor for buildPagedList* refactor: clean up math calculations for buildPagedList* chore: rename PageButtons file* chore: revamp how nav buttons are defined* fix: remove test disabled state* chore: clean up base nav button* chore: rename props for clarity* refactor: clean up logic for isNonInitialPage* chore: add more tests and catch bugs* docs: fix confusing typo in comments* chore: add one more test case for pagination buttons* refactor: update props definition for PaginationNavButton* fix: remove possible state sync bugs
1 parent8a7f0e9 commitad3abe3

File tree

13 files changed

+548
-193
lines changed

13 files changed

+548
-193
lines changed

‎site/src/components/PaginationWidget/PageButton.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import{PropsWithChildren}from"react";
2+
importButtonfrom"@mui/material/Button";
3+
import{useTheme}from"@emotion/react";
4+
5+
typeNumberedPageButtonProps={
6+
pageNumber:number;
7+
totalPages:number;
8+
9+
onClick?:()=>void;
10+
highlighted?:boolean;
11+
disabled?:boolean;
12+
};
13+
14+
exportfunctionNumberedPageButton({
15+
pageNumber,
16+
totalPages,
17+
onClick,
18+
highlighted=false,
19+
disabled=false,
20+
}:NumberedPageButtonProps){
21+
return(
22+
<BasePageButton
23+
name="Page button"
24+
aria-label={getNumberedButtonLabel(pageNumber,totalPages,highlighted)}
25+
onClick={onClick}
26+
highlighted={highlighted}
27+
disabled={disabled}
28+
>
29+
{pageNumber}
30+
</BasePageButton>
31+
);
32+
}
33+
34+
typePlaceholderPageButtonProps=PropsWithChildren<{
35+
pagesOmitted:number;
36+
}>;
37+
38+
exportfunctionPlaceholderPageButton({
39+
pagesOmitted,
40+
children=<>&hellip;</>,
41+
}:PlaceholderPageButtonProps){
42+
return(
43+
<BasePageButton
44+
disabled
45+
name="Omitted pages"
46+
aria-label={`Omitting${pagesOmitted} pages`}
47+
>
48+
{children}
49+
</BasePageButton>
50+
);
51+
}
52+
53+
typeBasePageButtonProps=PropsWithChildren<{
54+
name:string;
55+
"aria-label":string;
56+
57+
onClick?:()=>void;
58+
highlighted?:boolean;
59+
disabled?:boolean;
60+
}>;
61+
62+
functionBasePageButton({
63+
children,
64+
onClick,
65+
name,
66+
"aria-label":ariaLabel,
67+
highlighted=false,
68+
disabled=false,
69+
}:BasePageButtonProps){
70+
consttheme=useTheme();
71+
72+
return(
73+
<Button
74+
css={
75+
highlighted&&{
76+
borderColor:`${theme.palette.info.main}`,
77+
backgroundColor:`${theme.palette.info.dark}`,
78+
}
79+
}
80+
aria-label={ariaLabel}
81+
name={name}
82+
disabled={disabled}
83+
onClick={onClick}
84+
>
85+
{children}
86+
</Button>
87+
);
88+
}
89+
90+
functiongetNumberedButtonLabel(
91+
page:number,
92+
totalPages:number,
93+
highlighted:boolean,
94+
):string{
95+
if(highlighted){
96+
return"Current Page";
97+
}
98+
99+
if(page===1){
100+
return"First Page";
101+
}
102+
103+
if(page===totalPages){
104+
return"Last Page";
105+
}
106+
107+
return`Page${page}`;
108+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import{
2+
typeButtonHTMLAttributes,
3+
typeReactNode,
4+
useEffect,
5+
useState,
6+
}from"react";
7+
import{useTheme}from"@emotion/react";
8+
9+
importButtonfrom"@mui/material/Button";
10+
importTooltipfrom"@mui/material/Tooltip";
11+
12+
typePaginationNavButtonProps=Omit<
13+
ButtonHTMLAttributes<HTMLButtonElement>,
14+
|"aria-disabled"
15+
// Need to omit color for MUI compatibility
16+
|"color"
17+
>&{
18+
// Required/narrowed versions of default props
19+
children:ReactNode;
20+
disabled:boolean;
21+
onClick:()=>void;
22+
"aria-label":string;
23+
24+
// Bespoke props
25+
disabledMessage:ReactNode;
26+
disabledMessageTimeout?:number;
27+
};
28+
29+
functionPaginationNavButtonCore({
30+
onClick,
31+
disabled,
32+
disabledMessage,
33+
disabledMessageTimeout=3000,
34+
...delegatedProps
35+
}:PaginationNavButtonProps){
36+
consttheme=useTheme();
37+
const[showDisabledMessage,setShowDisabledMessage]=useState(false);
38+
39+
// Inline state sync - this is safe/recommended by the React team in this case
40+
if(!disabled&&showDisabledMessage){
41+
setShowDisabledMessage(false);
42+
}
43+
44+
useEffect(()=>{
45+
if(!showDisabledMessage){
46+
return;
47+
}
48+
49+
consttimeoutId=setTimeout(
50+
()=>setShowDisabledMessage(false),
51+
disabledMessageTimeout,
52+
);
53+
54+
return()=>clearTimeout(timeoutId);
55+
},[showDisabledMessage,disabledMessageTimeout]);
56+
57+
return(
58+
<Tooltiptitle={disabledMessage}open={showDisabledMessage}>
59+
{/*
60+
* Going more out of the way to avoid attaching the disabled prop directly
61+
* to avoid unwanted side effects of using the prop:
62+
* - Not being focusable/keyboard-navigable
63+
* - Not being able to call functions in response to invalid actions
64+
* (mostly for giving direct UI feedback to those actions)
65+
*/}
66+
<Button
67+
aria-disabled={disabled}
68+
css={
69+
disabled&&{
70+
borderColor:theme.palette.divider,
71+
color:theme.palette.text.disabled,
72+
cursor:"default",
73+
"&:hover":{
74+
backgroundColor:theme.palette.background.default,
75+
borderColor:theme.palette.divider,
76+
},
77+
}
78+
}
79+
onClick={()=>{
80+
if(disabled){
81+
setShowDisabledMessage(true);
82+
}else{
83+
onClick();
84+
}
85+
}}
86+
{...delegatedProps}
87+
/>
88+
</Tooltip>
89+
);
90+
}
91+
92+
exportfunctionPaginationNavButton({
93+
disabledMessageTimeout=3000,
94+
...delegatedProps
95+
}:PaginationNavButtonProps){
96+
return(
97+
// Key prop ensures that if timeout changes, the component just unmounts and
98+
// remounts, avoiding a swath of possible sync issues
99+
<PaginationNavButtonCore
100+
key={disabledMessageTimeout}
101+
disabledMessageTimeout={disabledMessageTimeout}
102+
{...delegatedProps}
103+
/>
104+
);
105+
}

‎site/src/components/PaginationWidget/PaginationWidgetBase.stories.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ const meta: Meta<typeof PaginationWidgetBase> = {
55
title:"components/PaginationWidgetBase",
66
component:PaginationWidgetBase,
77
args:{
8-
page:1,
9-
limit:12,
10-
count:200,
8+
currentPage:1,
9+
pageSize:12,
10+
totalRecords:200,
1111
},
1212
};
1313

@@ -17,19 +17,17 @@ type Story = StoryObj<typeof PaginationWidgetBase>;
1717
exportconstMoreThan8Pages:Story={};
1818

1919
exportconstLessThan8Pages:Story={
20-
args:{
21-
count:84,
22-
},
20+
args:{totalRecords:84},
2321
};
2422

2523
exportconstMoreThan7PagesWithActivePageCloseToStart:Story={
26-
args:{page:2,limit:12},
24+
args:{currentPage:2,pageSize:12},
2725
};
2826

2927
exportconstMoreThan7PagesWithActivePageFarFromBoundaries:Story={
30-
args:{page:4,limit:12},
28+
args:{currentPage:4,pageSize:12},
3129
};
3230

3331
exportconstMoreThan7PagesWithActivePageCloseToEnd:Story={
34-
args:{page:17,limit:12},
32+
args:{currentPage:17,pageSize:12},
3533
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import{screen}from"@testing-library/react";
2+
import{
3+
PaginationWidgetBase,
4+
PaginationWidgetBaseProps,
5+
}from"./PaginationWidgetBase";
6+
import{renderWithAuth}from"testHelpers/renderHelpers";
7+
importuserEventfrom"@testing-library/user-event";
8+
9+
typeSampleProps=Omit<PaginationWidgetBaseProps,"onPageChange">;
10+
11+
describe(PaginationWidgetBase.name,()=>{
12+
it("Should have its previous button be aria-disabled while on page 1",async()=>{
13+
constsampleProps:SampleProps[]=[
14+
{currentPage:1,pageSize:5,totalRecords:6},
15+
{currentPage:1,pageSize:50,totalRecords:200},
16+
{currentPage:1,pageSize:20,totalRecords:3000},
17+
];
18+
19+
for(constpropsofsampleProps){
20+
constonPageChange=jest.fn();
21+
const{ unmount}=renderWithAuth(
22+
<PaginationWidgetBase{...props}onPageChange={onPageChange}/>,
23+
);
24+
25+
constprevButton=awaitscreen.findByLabelText("Previous page");
26+
expect(prevButton).not.toBeDisabled();
27+
expect(prevButton).toHaveAttribute("aria-disabled","true");
28+
29+
awaituserEvent.click(prevButton);
30+
expect(onPageChange).not.toHaveBeenCalled();
31+
unmount();
32+
}
33+
});
34+
35+
it("Should have its next button be aria-disabled while on last page",async()=>{
36+
constsampleProps:SampleProps[]=[
37+
{currentPage:2,pageSize:5,totalRecords:6},
38+
{currentPage:4,pageSize:50,totalRecords:200},
39+
{currentPage:10,pageSize:100,totalRecords:1000},
40+
];
41+
42+
for(constpropsofsampleProps){
43+
constonPageChange=jest.fn();
44+
const{ unmount}=renderWithAuth(
45+
<PaginationWidgetBase{...props}onPageChange={onPageChange}/>,
46+
);
47+
48+
constbutton=awaitscreen.findByLabelText("Next page");
49+
expect(button).not.toBeDisabled();
50+
expect(button).toHaveAttribute("aria-disabled","true");
51+
52+
awaituserEvent.click(button);
53+
expect(onPageChange).not.toHaveBeenCalled();
54+
unmount();
55+
}
56+
});
57+
58+
it("Should have neither button be disabled for all other pages",async()=>{
59+
constsampleProps:SampleProps[]=[
60+
{currentPage:11,pageSize:5,totalRecords:60},
61+
{currentPage:2,pageSize:50,totalRecords:200},
62+
{currentPage:3,pageSize:20,totalRecords:100},
63+
];
64+
65+
for(constpropsofsampleProps){
66+
constonPageChange=jest.fn();
67+
const{ unmount}=renderWithAuth(
68+
<PaginationWidgetBase{...props}onPageChange={onPageChange}/>,
69+
);
70+
71+
constprevButton=awaitscreen.findByLabelText("Previous page");
72+
constnextButton=awaitscreen.findByLabelText("Next page");
73+
74+
expect(prevButton).not.toBeDisabled();
75+
expect(prevButton).toHaveAttribute("aria-disabled","false");
76+
77+
expect(nextButton).not.toBeDisabled();
78+
expect(nextButton).toHaveAttribute("aria-disabled","false");
79+
80+
awaituserEvent.click(prevButton);
81+
awaituserEvent.click(nextButton);
82+
expect(onPageChange).toHaveBeenCalledTimes(2);
83+
unmount();
84+
}
85+
});
86+
});

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp