forked from patternfly/patternfly-react
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPaginationOptionsMenu.tsx
More file actions
239 lines (222 loc) · 8.26 KB
/
Copy pathPaginationOptionsMenu.tsx
File metadata and controls
239 lines (222 loc) · 8.26 KB
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
227
228
229
230
231
232
233
234
235
236
237
238
239
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/Pagination/pagination';
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;
/** React ref for the container to append the options menu to. This is a static ref provided by the main pagination component. */
containerRef?: React.RefObject<HTMLDivElement>;
/** @beta The container to append the pagination options menu to. Overrides the containerRef prop. */
appendTo?: HTMLElement | (() => HTMLElement) | 'inline';
/** Flag indicating if scroll on focus of the first menu item should occur. */
shouldPreventScrollOnItemFocus?: boolean;
/** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */
focusTimeoutDelay?: number;
}
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 = '',
toggleTemplate,
onPerPageSelect = () => null as any,
containerRef,
appendTo,
shouldPreventScrollOnItemFocus = true,
focusTimeoutDelay = 0
}: PaginationOptionsMenuProps) => {
const [isOpen, setIsOpen] = React.useState(false);
const toggleRef = React.useRef<HTMLButtonElement>(null);
const menuRef = 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({ preventScroll: shouldPreventScrollOnItemFocus });
}, focusTimeoutDelay);
}
// 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);
};
}, [focusTimeoutDelay, isOpen, menuRef, shouldPreventScrollOnItemFocus]);
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 = (
<div className={css(styles.paginationPageMenu)}>
<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>
</div>
);
const menu = (
<Menu className={css(className)} onSelect={onSelect} ref={menuRef}>
<MenuContent>
<MenuList>{renderItems()}</MenuList>
</MenuContent>
</Menu>
);
const containerToAppendTo = appendTo ?? (containerRef?.current || undefined);
return (
<Popper
trigger={toggle}
triggerRef={toggleRef}
popper={menu}
popperRef={menuRef}
isVisible={isOpen}
direction={dropDirection}
appendTo={containerToAppendTo}
minWidth={minWidth !== undefined ? minWidth : 'revert'}
/>
);
};
PaginationOptionsMenu.displayName = 'PaginationOptionsMenu';