forked from patternfly/patternfly-react
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPaginationOptionsMenu.tsx
226 lines (209 loc) · 7.33 KB
/
PaginationOptionsMenu.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import { Menu, MenuContent, MenuList, MenuItem } from '../Menu';
import { MenuToggle } from '../MenuToggle';
import { Popper } from '../../helpers/Popper/Popper';
import { PaginationToggleTemplateProps, ToggleTemplate } from './ToggleTemplate';
import { PerPageOptions, OnPerPageSelect } from './Pagination';
import { fillTemplate } from '../../helpers';
export interface PaginationOptionsMenuProps extends React.HTMLProps<HTMLDivElement> {
/** Custom class name added to the pagination options menu. */
className?: string;
/** Id added to the title of the pagination options menu. */
widgetId?: string;
/** Flag indicating if pagination options menu is disabled. */
isDisabled?: boolean;
/** Menu will open up or open down from the options menu toggle. */
dropDirection?: 'up' | 'down';
/** Minimum width of the pagination options menu. If set to "trigger", the minimum width will be set to the toggle width. */
minWidth?: string | 'trigger';
/** Array of titles and values which will be the options on the options menu dropdown. */
perPageOptions?: PerPageOptions[];
/** The title of the pagination options menu. */
itemsPerPageTitle?: string;
/** Current page number. */
page?: number;
/** The suffix to be displayed after each option on the options menu dropdown. */
perPageSuffix?: string;
/** The type or title of the items being paginated. */
itemsTitle?: string;
/** Accessible label for the options toggle. */
optionsToggleAriaLabel?: string;
/** The total number of items being paginated. */
itemCount?: number;
/** The first index of the items being paginated. */
firstIndex?: number;
/** The last index of the items being paginated. */
lastIndex?: number;
/** Flag to indicate whether to show last full page of results when user selects perPage
* value that is greater than remaining rows.
*/
isLastFullPageShown?: boolean;
/** The number of items to be displayed per page. */
perPage?: number;
/** The number of the last page. */
lastPage?: number;
/** This will be shown in pagination toggle span. You can use firstIndex, lastIndex,
* itemCount, and/or itemsTitle props.
*/
toggleTemplate: ((props: PaginationToggleTemplateProps) => React.ReactElement) | string;
/** Function called when user selects number of items per page. */
onPerPageSelect?: OnPerPageSelect;
/** Label for the English word "of". */
ofWord?: string;
}
export const PaginationOptionsMenu: React.FunctionComponent<PaginationOptionsMenuProps> = ({
className,
widgetId,
page: pageProp,
itemCount,
isDisabled = false,
minWidth,
dropDirection = 'down',
perPageOptions = [] as PerPageOptions[],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
itemsPerPageTitle = 'Items per page',
perPageSuffix = 'per page',
optionsToggleAriaLabel,
ofWord = 'of',
perPage = 0,
firstIndex = 0,
lastIndex = 0,
isLastFullPageShown = false,
itemsTitle = 'items',
toggleTemplate,
onPerPageSelect = () => null as any
}: PaginationOptionsMenuProps) => {
const [isOpen, setIsOpen] = React.useState(false);
const toggleRef = React.useRef<HTMLButtonElement>(null);
const menuRef = React.useRef<HTMLDivElement>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const onToggle = () => {
setIsOpen((prevState) => !prevState);
};
const onSelect = () => {
setIsOpen((prevState) => !prevState);
toggleRef.current?.focus();
};
const handleNewPerPage = (_evt: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPerPage: number) => {
let newPage = pageProp;
while (Math.ceil(itemCount / newPerPage) < newPage) {
newPage--;
}
if (isLastFullPageShown) {
if (itemCount / newPerPage !== newPage) {
while (newPage > 1 && itemCount - newPerPage * newPage < 0) {
newPage--;
}
}
}
const startIdx = (newPage - 1) * newPerPage;
const endIdx = newPage * newPerPage;
return onPerPageSelect(_evt, newPerPage, newPage, startIdx, endIdx);
};
React.useEffect(() => {
const handleMenuKeys = (event: KeyboardEvent) => {
// Close the menu on tab or escape
if (
(isOpen && menuRef.current?.contains(event.target as Node)) ||
toggleRef.current?.contains(event.target as Node)
) {
if (event.key === 'Escape' || event.key === 'Tab') {
setIsOpen(false);
toggleRef.current?.focus();
}
}
};
const handleClick = (event: MouseEvent) => {
// Focus the first non-disabled menu item on toggle 'click'
if (isOpen && toggleRef.current?.contains(event.target as Node)) {
setTimeout(() => {
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled)');
firstElement && (firstElement as HTMLElement).focus();
}, 0);
}
// If the event is not on the toggle, close the menu
if (
isOpen &&
!toggleRef?.current?.contains(event.target as Node) &&
!menuRef.current?.contains(event.target as Node)
) {
setIsOpen(false);
}
};
window.addEventListener('keydown', handleMenuKeys);
window.addEventListener('click', handleClick);
return () => {
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
}, [isOpen, menuRef]);
const renderItems = () =>
perPageOptions.map(({ value, title }) => (
<MenuItem
key={value}
data-action={`per-page-${value}`}
isSelected={perPage === value}
onClick={(event) => handleNewPerPage(event, value)}
>
{title}
{` ${perPageSuffix}`}
</MenuItem>
));
const toggle = (
<MenuToggle
ref={toggleRef}
onClick={onToggle}
{...(optionsToggleAriaLabel && { 'aria-label': optionsToggleAriaLabel })}
isDisabled={isDisabled || (itemCount && itemCount <= 0)}
isExpanded={isOpen}
{...(widgetId && { id: `${widgetId}-toggle` })}
variant="plainText"
aria-haspopup="listbox"
>
{toggleTemplate &&
typeof toggleTemplate === 'string' &&
fillTemplate(toggleTemplate, { firstIndex, lastIndex, ofWord, itemCount, itemsTitle })}
{toggleTemplate &&
typeof toggleTemplate !== 'string' &&
(toggleTemplate as (props: PaginationToggleTemplateProps) => React.ReactElement)({
firstIndex,
lastIndex,
ofWord,
itemCount,
itemsTitle
})}
{!toggleTemplate && (
<ToggleTemplate
firstIndex={firstIndex}
lastIndex={lastIndex}
ofWord={ofWord}
itemCount={itemCount}
itemsTitle={itemsTitle}
/>
)}
</MenuToggle>
);
const menu = (
<Menu className={css(className)} onSelect={onSelect} ref={menuRef}>
<MenuContent>
<MenuList>{renderItems()}</MenuList>
</MenuContent>
</Menu>
);
return (
<div ref={containerRef}>
<Popper
trigger={toggle}
triggerRef={toggleRef}
popper={menu}
popperRef={menuRef}
isVisible={isOpen}
direction={dropDirection}
appendTo={containerRef.current || undefined}
minWidth={minWidth !== undefined ? minWidth : 'revert'}
/>
</div>
);
};
PaginationOptionsMenu.displayName = 'PaginationOptionsMenu';