@loadsmart/loadsmart-ui 5.1.1 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Dropdown/Dropdown.types.d.ts +2 -2
- package/dist/components/Dropdown/useDropdown.d.ts +1 -5
- package/dist/components/Icon/Icon.d.ts +1 -0
- package/dist/components/Select/Select.types.d.ts +4 -4
- package/dist/components/TablePagination/RowsPerPage.d.ts +4 -0
- package/dist/components/TablePagination/TablePagination.d.ts +4 -0
- package/dist/components/TablePagination/TablePagination.stories.d.ts +5 -0
- package/dist/components/TablePagination/TablePagination.styles.d.ts +5 -0
- package/dist/components/TablePagination/TablePagination.test.d.ts +1 -0
- package/dist/components/TablePagination/TablePagination.types.d.ts +50 -0
- package/dist/components/TablePagination/TablePaginationActions.d.ts +11 -0
- package/dist/components/TablePagination/index.d.ts +2 -0
- package/dist/hooks/useClickOutside/useClickOutside.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +445 -437
- package/dist/index.js.map +1 -1
- package/dist/loadsmart.theme-63c13988.js +2 -0
- package/dist/{loadsmart.theme-37a60d56.js.map → loadsmart.theme-63c13988.js.map} +1 -1
- package/dist/{prop-06c02f6d.js → prop-0c635ee9.js} +2 -2
- package/dist/{prop-06c02f6d.js.map → prop-0c635ee9.js.map} +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/theming/index.js +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/utils/toolset/keyboard.d.ts +4 -0
- package/package.json +1 -1
- package/src/components/Dropdown/Dropdown.tsx +11 -8
- package/src/components/Dropdown/Dropdown.types.ts +2 -2
- package/src/components/Dropdown/useDropdown.ts +1 -6
- package/src/components/Icon/Icon.tsx +2 -0
- package/src/components/Icon/assets/caret-right-last.svg +4 -0
- package/src/components/Select/Select.test.tsx +23 -1
- package/src/components/Select/Select.types.ts +4 -4
- package/src/components/Select/useSelect.ts +11 -9
- package/src/components/TablePagination/RowsPerPage.tsx +76 -0
- package/src/components/TablePagination/TablePagination.stories.tsx +37 -0
- package/src/components/TablePagination/TablePagination.styles.ts +13 -0
- package/src/components/TablePagination/TablePagination.test.tsx +112 -0
- package/src/components/TablePagination/TablePagination.tsx +51 -0
- package/src/components/TablePagination/TablePagination.types.ts +59 -0
- package/src/components/TablePagination/TablePaginationActions.tsx +144 -0
- package/src/components/TablePagination/index.ts +2 -0
- package/src/components/Tag/Tag.tsx +1 -1
- package/src/hooks/useClickOutside/useClickOutside.ts +6 -6
- package/src/index.ts +3 -0
- package/src/testing/SelectEvent/SelectEvent.ts +8 -7
- package/src/utils/toolset/keyboard.ts +4 -0
- package/dist/loadsmart.theme-37a60d56.js +0 -2
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
export interface DropdownProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onBlur'> {
|
|
6
|
-
disabled?: boolean
|
|
7
|
-
onBlur?: () => void
|
|
8
|
-
}
|
|
3
|
+
import type { DropdownProps } from './Dropdown.types'
|
|
9
4
|
|
|
10
5
|
export interface useDropdownProps {
|
|
11
6
|
expanded: boolean
|
|
@@ -20,6 +20,7 @@ import SortIcon from './assets/sort.svg'
|
|
|
20
20
|
import UploadIcon from './assets/upload.svg'
|
|
21
21
|
import WarningIcon from './assets/warning.svg'
|
|
22
22
|
import DotsHorizontalIcon from './assets/dots-horizontal.svg'
|
|
23
|
+
import CaretRightLastIcon from './assets/caret-right-last.svg'
|
|
23
24
|
|
|
24
25
|
import type { IconProps as GenericIconProps } from '../IconFactory'
|
|
25
26
|
|
|
@@ -44,6 +45,7 @@ const icons = {
|
|
|
44
45
|
upload: UploadIcon as JSX.Element,
|
|
45
46
|
warning: WarningIcon as JSX.Element,
|
|
46
47
|
'dots-horizontal': DotsHorizontalIcon as JSX.Element,
|
|
48
|
+
'caret-right-last': CaretRightLastIcon as JSX.Element,
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const Icon = IconFactory(icons)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg viewBox="0 0 48 48" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2501 6.38126L14.1842 10.4471L27.7371 24L14.1842 37.5529L18.2501 41.6187L31.803 28.0659L35.8688 24L31.803 19.9341L18.2501 6.38126Z"/>
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2922 38.375L44.1256 38.375L44.1256 9.625L40.2922 9.625L40.2922 38.375Z"/>
|
|
4
|
+
</svg>
|
|
@@ -11,6 +11,7 @@ import selectEvent from '../../testing/SelectEvent'
|
|
|
11
11
|
|
|
12
12
|
import type { SelectProps, Option, GenericOption } from './Select.types'
|
|
13
13
|
import Select from './Select'
|
|
14
|
+
import userEvent from '@testing-library/user-event'
|
|
14
15
|
|
|
15
16
|
const {
|
|
16
17
|
Playground,
|
|
@@ -159,6 +160,27 @@ describe('Select', () => {
|
|
|
159
160
|
expect(items.length).toBeGreaterThanOrEqual(1)
|
|
160
161
|
expect(items[0]).toHaveTextContent(FRUITS[indexToSearch].label)
|
|
161
162
|
})
|
|
163
|
+
|
|
164
|
+
it('calls blur when focus is lost', async () => {
|
|
165
|
+
const onBlur = jest.fn((event?: MouseEvent | TouchEvent | KeyboardEvent) => event)
|
|
166
|
+
|
|
167
|
+
setup({ onBlur })
|
|
168
|
+
|
|
169
|
+
const selectInput = screen.getByLabelText('Select your favorite fruit')
|
|
170
|
+
|
|
171
|
+
await selectEvent.expand(selectInput)
|
|
172
|
+
|
|
173
|
+
userEvent.click(document.body)
|
|
174
|
+
|
|
175
|
+
await selectEvent.expand(selectInput)
|
|
176
|
+
|
|
177
|
+
userEvent.keyboard('{Escape}{/Escape}')
|
|
178
|
+
|
|
179
|
+
expect(onBlur).toHaveBeenCalledTimes(2)
|
|
180
|
+
|
|
181
|
+
expect(onBlur).toHaveBeenNthCalledWith(1, new MouseEvent('mousedown'))
|
|
182
|
+
expect(onBlur).toHaveBeenNthCalledWith(2, new KeyboardEvent('keyup'))
|
|
183
|
+
})
|
|
162
184
|
})
|
|
163
185
|
|
|
164
186
|
describe('Single selection', () => {
|
|
@@ -839,7 +861,7 @@ describe('Select', () => {
|
|
|
839
861
|
const setup = (overrides: Partial<SelectProps>) =>
|
|
840
862
|
renderer(<Playground {...overrides} options={SelectableKeyTypeOptions} />).render()
|
|
841
863
|
|
|
842
|
-
const expectedValue = (value:
|
|
864
|
+
const expectedValue = (value: unknown) => ({
|
|
843
865
|
target: {
|
|
844
866
|
id: 'select-playground',
|
|
845
867
|
name: 'select-playground',
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import EventLike from 'utils/types/EventLike'
|
|
3
3
|
|
|
4
|
-
import type {
|
|
5
|
-
import type { DropdownProps, DropdownMenuItemProps } from 'components/Dropdown'
|
|
4
|
+
import type { DropdownMenuItemProps, DropdownProps } from 'components/Dropdown'
|
|
6
5
|
import type { TextFieldProps } from 'components/TextField'
|
|
7
6
|
import type {
|
|
8
|
-
SelectableAdapter,
|
|
9
7
|
Selectable,
|
|
8
|
+
SelectableAdapter,
|
|
10
9
|
SelectableKeyType,
|
|
11
10
|
SelectableState,
|
|
12
11
|
} from 'hooks/useSelectable'
|
|
13
12
|
import { useSelectableReturn } from 'hooks/useSelectable/useSelectable.types'
|
|
13
|
+
import type { ChangeEvent, ComponentType, FocusEvent, HTMLAttributes } from 'react'
|
|
14
14
|
|
|
15
15
|
export type Option = Selectable
|
|
16
16
|
export interface GenericOption {
|
|
@@ -119,7 +119,7 @@ export type useSelectReturn = {
|
|
|
119
119
|
getDropdownProps: () => {
|
|
120
120
|
toggle: () => void
|
|
121
121
|
expanded: boolean
|
|
122
|
-
onBlur: () => void
|
|
122
|
+
onBlur: (event?: MouseEvent | TouchEvent | KeyboardEvent) => void
|
|
123
123
|
}
|
|
124
124
|
getCreatebleProps: () => CreatableProps
|
|
125
125
|
isCreatable: () => boolean
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
3
2
|
import { isFunction } from '@loadsmart/utils-function'
|
|
4
3
|
import { isNil } from '@loadsmart/utils-object'
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
5
5
|
|
|
6
|
-
import { GenericAdapter } from './Select.constants'
|
|
7
|
-
import { getValue, getDisplayValue, escapeRegExp, getAdapter } from './useSelect.helpers'
|
|
8
6
|
import { useDropdown } from 'components/Dropdown'
|
|
7
|
+
import { useDidMount } from 'hooks/useDidMount'
|
|
9
8
|
import { useFocusTrap } from 'hooks/useFocusTrap'
|
|
10
9
|
import { SelectableKeyType } from 'hooks/useSelectable'
|
|
11
|
-
import { useSelectable } from './Select.context'
|
|
12
10
|
import to from 'utils/toolset/awaitTo'
|
|
13
|
-
import toArray from 'utils/toolset/toArray'
|
|
14
11
|
import { isThenable } from 'utils/toolset/isThenable'
|
|
15
|
-
import
|
|
12
|
+
import toArray from 'utils/toolset/toArray'
|
|
13
|
+
import { GenericAdapter } from './Select.constants'
|
|
14
|
+
import { useSelectable } from './Select.context'
|
|
15
|
+
import { escapeRegExp, getAdapter, getDisplayValue, getValue } from './useSelect.helpers'
|
|
16
16
|
|
|
17
17
|
import type { ChangeEvent, FocusEvent } from 'react'
|
|
18
18
|
import type {
|
|
@@ -191,7 +191,7 @@ function useOptions<T = any>(props: { datasources: SelectDatasource<T>[]; adapte
|
|
|
191
191
|
*/
|
|
192
192
|
function useSelect(props: SelectProps): useSelectReturn {
|
|
193
193
|
const didMount = useDidMount()
|
|
194
|
-
const { multiple, onQueryChange, onChange, onCreate, id, name, disabled = false } = props
|
|
194
|
+
const { multiple, onQueryChange, onChange, onCreate, id, name, disabled = false, onBlur } = props
|
|
195
195
|
const dropdown = useDropdown(props)
|
|
196
196
|
|
|
197
197
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -264,17 +264,19 @@ function useSelect(props: SelectProps): useSelectReturn {
|
|
|
264
264
|
return {
|
|
265
265
|
toggle: dropdown.toggle,
|
|
266
266
|
expanded: dropdown.expanded,
|
|
267
|
-
onBlur() {
|
|
267
|
+
onBlur(event?: MouseEvent | TouchEvent | KeyboardEvent) {
|
|
268
268
|
if (!multiple) {
|
|
269
269
|
setQuery(getDisplayValue(adapters, selectable.selected, multiple))
|
|
270
270
|
} else {
|
|
271
271
|
setQuery('')
|
|
272
272
|
}
|
|
273
273
|
options.reset()
|
|
274
|
+
|
|
275
|
+
onBlur?.(event)
|
|
274
276
|
},
|
|
275
277
|
}
|
|
276
278
|
},
|
|
277
|
-
[adapters, dropdown.expanded, dropdown.toggle, multiple, options, selectable.selected]
|
|
279
|
+
[adapters, dropdown.expanded, dropdown.toggle, multiple, options, selectable.selected, onBlur]
|
|
278
280
|
)
|
|
279
281
|
|
|
280
282
|
const getTriggerProps = useCallback(
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text } from 'components/Text'
|
|
3
|
+
import { Dropdown, DropdownContext } from 'components/Dropdown'
|
|
4
|
+
import { Layout } from 'components/Layout'
|
|
5
|
+
import { Icon } from 'components/Icon'
|
|
6
|
+
import { RowsPerPageProps } from './TablePagination.types'
|
|
7
|
+
import { ButtonProps } from 'components/Button'
|
|
8
|
+
import { NoPaddingButton } from './TablePagination.styles'
|
|
9
|
+
|
|
10
|
+
const TriggerButton = (props: Omit<ButtonProps, 'scale' | 'variant'>) => {
|
|
11
|
+
const { toggle } = React.useContext(DropdownContext)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<NoPaddingButton data-testid="rows-per-page-button" onClick={toggle} {...props}>
|
|
15
|
+
<Icon name="caret-down" size={16} color="neutral-darker" />
|
|
16
|
+
</NoPaddingButton>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function RowsPerPage({
|
|
21
|
+
page,
|
|
22
|
+
rowsPerPage,
|
|
23
|
+
onRowsPerPageChange,
|
|
24
|
+
labelRowsPerPage,
|
|
25
|
+
count,
|
|
26
|
+
rowsPerPageOptions,
|
|
27
|
+
disabled = false,
|
|
28
|
+
}: RowsPerPageProps): JSX.Element {
|
|
29
|
+
const getItemsRange = () => {
|
|
30
|
+
if (!count) {
|
|
31
|
+
return 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const from = page * rowsPerPage + 1
|
|
35
|
+
let to = (page + 1) * rowsPerPage
|
|
36
|
+
|
|
37
|
+
if (to > count) {
|
|
38
|
+
to = count
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return `${from}-${to}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Layout.Group space="s" align="center">
|
|
46
|
+
<Text variant="caption" color={disabled ? 'color-neutral' : 'color-neutral-dark'}>
|
|
47
|
+
{labelRowsPerPage}
|
|
48
|
+
</Text>
|
|
49
|
+
<Text variant="body" color={disabled ? 'color-neutral' : 'color-neutral-dark'}>
|
|
50
|
+
<Text variant="body-bold" color={disabled ? 'color-neutral' : 'color-neutral-dark'}>
|
|
51
|
+
{getItemsRange()}
|
|
52
|
+
</Text>{' '}
|
|
53
|
+
of{' '}
|
|
54
|
+
<Text variant="body-bold" color={disabled ? 'color-neutral' : 'color-neutral-dark'}>
|
|
55
|
+
{count}
|
|
56
|
+
</Text>
|
|
57
|
+
</Text>
|
|
58
|
+
<Dropdown>
|
|
59
|
+
<TriggerButton disabled={disabled} />
|
|
60
|
+
<Dropdown.Menu>
|
|
61
|
+
{rowsPerPageOptions.map((option) => (
|
|
62
|
+
<Dropdown.Item
|
|
63
|
+
key={option}
|
|
64
|
+
onClick={() => onRowsPerPageChange(option)}
|
|
65
|
+
trailing={option === rowsPerPage && <Icon name="check" size={20} color="accent" />}
|
|
66
|
+
>
|
|
67
|
+
{option} per page
|
|
68
|
+
</Dropdown.Item>
|
|
69
|
+
))}
|
|
70
|
+
</Dropdown.Menu>
|
|
71
|
+
</Dropdown>
|
|
72
|
+
</Layout.Group>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default RowsPerPage
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import TablePagination from './TablePagination'
|
|
3
|
+
import { Meta, Story } from '@storybook/react/types-6-0'
|
|
4
|
+
|
|
5
|
+
import type { TablePaginationProps } from './TablePagination.types'
|
|
6
|
+
import { Layout } from 'components/Layout'
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: 'Components/TablePagination',
|
|
10
|
+
component: TablePagination,
|
|
11
|
+
argTypes: {},
|
|
12
|
+
} as Meta
|
|
13
|
+
|
|
14
|
+
export const Playground: Story<TablePaginationProps> = (args: TablePaginationProps) => {
|
|
15
|
+
const [page, setPage] = useState(args.page)
|
|
16
|
+
const [rowsPerPage, setRowsPerPage] = useState(args.rowsPerPage)
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Layout.Group>
|
|
20
|
+
<TablePagination
|
|
21
|
+
{...args}
|
|
22
|
+
onPageChange={(page) => setPage(page)}
|
|
23
|
+
page={page}
|
|
24
|
+
rowsPerPage={rowsPerPage}
|
|
25
|
+
onRowsPerPageChange={setRowsPerPage}
|
|
26
|
+
/>
|
|
27
|
+
</Layout.Group>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Playground.args = {
|
|
32
|
+
page: 0,
|
|
33
|
+
count: 1004,
|
|
34
|
+
rowsPerPage: 100,
|
|
35
|
+
disabled: false,
|
|
36
|
+
variant: 'default',
|
|
37
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import userEvent from '@testing-library/user-event'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import renderer, { screen } from '../../../tests/renderer'
|
|
5
|
+
|
|
6
|
+
import TablePagination from './TablePagination'
|
|
7
|
+
import { TablePaginationProps } from './TablePagination.types'
|
|
8
|
+
|
|
9
|
+
describe('TablePagination', () => {
|
|
10
|
+
const setup = ({ ...overrides }: Partial<TablePaginationProps> = {}) => {
|
|
11
|
+
const defaultProps: TablePaginationProps = {
|
|
12
|
+
variant: 'default',
|
|
13
|
+
count: 1000,
|
|
14
|
+
onPageChange: jest.fn(),
|
|
15
|
+
onRowsPerPageChange: jest.fn(),
|
|
16
|
+
rowsPerPage: 100,
|
|
17
|
+
page: 0,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
renderer(<TablePagination {...defaultProps} {...overrides} />).render()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
it('disables preview page buttons if page = 0', () => {
|
|
24
|
+
setup()
|
|
25
|
+
|
|
26
|
+
expect(screen.getByTitle(/previous page/i)).toBeDisabled()
|
|
27
|
+
expect(screen.getByTitle(/first page/i)).toBeDisabled()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('disables next and last page buttons if the page is the last one', () => {
|
|
31
|
+
setup({
|
|
32
|
+
page: 9,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
expect(screen.getByTitle(/next page/i)).toBeDisabled()
|
|
36
|
+
expect(screen.getByTitle(/last page/i)).toBeDisabled()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('calls the onChangePage action after the pagination actions are clicked', () => {
|
|
40
|
+
const onPageChange = jest.fn()
|
|
41
|
+
setup({
|
|
42
|
+
page: 2,
|
|
43
|
+
onPageChange,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
userEvent.click(screen.getByTitle(/previous page/i))
|
|
47
|
+
expect(onPageChange).toHaveBeenLastCalledWith(1)
|
|
48
|
+
userEvent.click(screen.getByTitle(/first page/i))
|
|
49
|
+
expect(onPageChange).toHaveBeenLastCalledWith(0)
|
|
50
|
+
userEvent.click(screen.getByTitle(/next page/i))
|
|
51
|
+
expect(onPageChange).toHaveBeenLastCalledWith(3)
|
|
52
|
+
userEvent.click(screen.getByTitle(/last page/i))
|
|
53
|
+
expect(onPageChange).toHaveBeenLastCalledWith(9)
|
|
54
|
+
|
|
55
|
+
userEvent.clear(screen.getByTitle('Page'))
|
|
56
|
+
userEvent.type(screen.getByTitle('Page'), '5')
|
|
57
|
+
userEvent.tab()
|
|
58
|
+
expect(onPageChange).toHaveBeenLastCalledWith(4)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('calls the onRowsPerPageChange with the selected option', () => {
|
|
62
|
+
const onPageChange = jest.fn()
|
|
63
|
+
const onRowsPerPageChange = jest.fn()
|
|
64
|
+
setup({
|
|
65
|
+
page: 2,
|
|
66
|
+
onPageChange,
|
|
67
|
+
onRowsPerPageChange,
|
|
68
|
+
})
|
|
69
|
+
userEvent.click(screen.getByTestId('rows-per-page-button'))
|
|
70
|
+
userEvent.click(screen.getByText(/50 per page/i))
|
|
71
|
+
|
|
72
|
+
expect(onRowsPerPageChange).toHaveBeenLastCalledWith(50)
|
|
73
|
+
expect(onPageChange).toHaveBeenLastCalledWith(0)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('hides some actions for the compact variant', () => {
|
|
77
|
+
setup({
|
|
78
|
+
variant: 'compact',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(screen.queryByTitle(/first page/i)).not.toBeInTheDocument()
|
|
82
|
+
expect(screen.queryByTitle(/last page/i)).not.toBeInTheDocument()
|
|
83
|
+
expect(screen.queryByTitle('Page')).not.toBeInTheDocument()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('disables all the buttons when the component is disabled', () => {
|
|
87
|
+
setup({
|
|
88
|
+
disabled: true,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(screen.getByTitle('Page')).toBeDisabled()
|
|
92
|
+
expect(screen.getByTitle(/previous page/i)).toBeDisabled()
|
|
93
|
+
expect(screen.getByTitle(/first page/i)).toBeDisabled()
|
|
94
|
+
expect(screen.getByTitle(/next page/i)).toBeDisabled()
|
|
95
|
+
expect(screen.getByTitle(/last page/i)).toBeDisabled()
|
|
96
|
+
expect(screen.getByTestId('rows-per-page-button')).toBeDisabled()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('displays no pages state when no items available', () => {
|
|
100
|
+
setup({
|
|
101
|
+
count: 0,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
expect(screen.getByTitle('Page')).toHaveValue(0)
|
|
105
|
+
expect(screen.getByTitle('Page')).toBeDisabled()
|
|
106
|
+
expect(screen.getByTitle(/previous page/i)).toBeDisabled()
|
|
107
|
+
expect(screen.getByTitle(/first page/i)).toBeDisabled()
|
|
108
|
+
expect(screen.getByTitle(/next page/i)).toBeDisabled()
|
|
109
|
+
expect(screen.getByTitle(/last page/i)).toBeDisabled()
|
|
110
|
+
expect(screen.getByTestId('rows-per-page-button')).toBeDisabled()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Layout } from 'components/Layout'
|
|
4
|
+
|
|
5
|
+
import type { TablePaginationProps } from './TablePagination.types'
|
|
6
|
+
import TablePaginationActions from 'components/TablePagination/TablePaginationActions'
|
|
7
|
+
import RowsPerPage from 'components/TablePagination/RowsPerPage'
|
|
8
|
+
|
|
9
|
+
function TablePagination(props: TablePaginationProps): JSX.Element {
|
|
10
|
+
const {
|
|
11
|
+
variant = 'default',
|
|
12
|
+
count,
|
|
13
|
+
labelRowsPerPage = 'Rows per page:',
|
|
14
|
+
onPageChange,
|
|
15
|
+
onRowsPerPageChange,
|
|
16
|
+
page,
|
|
17
|
+
rowsPerPage = 50,
|
|
18
|
+
rowsPerPageOptions = [10, 25, 50, 100],
|
|
19
|
+
disabled = false,
|
|
20
|
+
...rest
|
|
21
|
+
} = props
|
|
22
|
+
|
|
23
|
+
const handleRowsPerPageChange = (selectedOption: number) => {
|
|
24
|
+
onRowsPerPageChange(selectedOption)
|
|
25
|
+
onPageChange(0)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Layout.Group space="xl" align="center" justify="space-between" {...rest}>
|
|
30
|
+
<RowsPerPage
|
|
31
|
+
page={page}
|
|
32
|
+
count={count}
|
|
33
|
+
onRowsPerPageChange={handleRowsPerPageChange}
|
|
34
|
+
rowsPerPage={rowsPerPage}
|
|
35
|
+
rowsPerPageOptions={rowsPerPageOptions}
|
|
36
|
+
labelRowsPerPage={labelRowsPerPage}
|
|
37
|
+
disabled={disabled || !count}
|
|
38
|
+
/>
|
|
39
|
+
<TablePaginationActions
|
|
40
|
+
variant={variant}
|
|
41
|
+
page={page}
|
|
42
|
+
onPageChange={onPageChange}
|
|
43
|
+
rowsPerPage={rowsPerPage}
|
|
44
|
+
count={count}
|
|
45
|
+
disabled={disabled || !count}
|
|
46
|
+
/>
|
|
47
|
+
</Layout.Group>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default TablePagination
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { GroupProps } from 'components/Layout/Group'
|
|
2
|
+
|
|
3
|
+
export interface TablePaginationProps extends GroupProps {
|
|
4
|
+
/**
|
|
5
|
+
* The pagination variant
|
|
6
|
+
* @default 'default'
|
|
7
|
+
*/
|
|
8
|
+
variant?: 'compact' | 'default'
|
|
9
|
+
/**
|
|
10
|
+
* The total number of paginated items
|
|
11
|
+
*/
|
|
12
|
+
count: number
|
|
13
|
+
/**
|
|
14
|
+
* Customize the rows per page label.
|
|
15
|
+
* @default 'Rows per page:'
|
|
16
|
+
*/
|
|
17
|
+
labelRowsPerPage?: string
|
|
18
|
+
/**
|
|
19
|
+
* Callback fired when the page is changed.
|
|
20
|
+
*/
|
|
21
|
+
onPageChange: (page: number) => void
|
|
22
|
+
/**
|
|
23
|
+
* Callback fired when the number of rows per page is changed.
|
|
24
|
+
*/
|
|
25
|
+
onRowsPerPageChange: (rowsPerPage: number) => void
|
|
26
|
+
/**
|
|
27
|
+
* The number of rows per page.
|
|
28
|
+
* @default 50
|
|
29
|
+
*/
|
|
30
|
+
rowsPerPage?: number
|
|
31
|
+
/**
|
|
32
|
+
* The zero-based index of the current page.
|
|
33
|
+
*/
|
|
34
|
+
page: number
|
|
35
|
+
/**
|
|
36
|
+
* Customizes the options of the rows per page select field.
|
|
37
|
+
* @default [10, 25, 50, 100]
|
|
38
|
+
*/
|
|
39
|
+
rowsPerPageOptions?: number[]
|
|
40
|
+
/**
|
|
41
|
+
* Disable all the pagination actions
|
|
42
|
+
*/
|
|
43
|
+
disabled?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TablePaginationActionsProps = Omit<
|
|
47
|
+
TablePaginationProps,
|
|
48
|
+
'labelRowsPerPage' | 'onRowsPerPageChange' | 'rowsPerPageOptions' | 'rowsPerPage'
|
|
49
|
+
> & {
|
|
50
|
+
rowsPerPage: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type RowsPerPageProps = Omit<
|
|
54
|
+
TablePaginationProps,
|
|
55
|
+
'rowsPerPageOptions' | 'onPageChange' | 'rowsPerPage'
|
|
56
|
+
> & {
|
|
57
|
+
rowsPerPageOptions: number[]
|
|
58
|
+
rowsPerPage: number
|
|
59
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { ChangeEvent, KeyboardEvent, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Layout } from 'components/Layout'
|
|
4
|
+
import { Text } from 'components/Text'
|
|
5
|
+
import { TextField } from 'components/TextField'
|
|
6
|
+
import { TablePaginationActionsProps } from 'components/TablePagination/TablePagination.types'
|
|
7
|
+
import styled from 'styled-components'
|
|
8
|
+
import { Icon, IconProps } from 'components/Icon'
|
|
9
|
+
import Keyboard from 'utils/toolset/keyboard'
|
|
10
|
+
import { NoPaddingButton } from './TablePagination.styles'
|
|
11
|
+
import { prop } from 'tools/index'
|
|
12
|
+
|
|
13
|
+
export const ActionIcon = styled(Icon).attrs({
|
|
14
|
+
color: 'neutral-darker',
|
|
15
|
+
size: '16',
|
|
16
|
+
})<IconProps & { rotate?: number }>`
|
|
17
|
+
transform: rotate(${prop('rotate', 0)}deg);
|
|
18
|
+
`
|
|
19
|
+
|
|
20
|
+
function TablePaginationActions({
|
|
21
|
+
variant = 'default',
|
|
22
|
+
disabled = false,
|
|
23
|
+
onPageChange,
|
|
24
|
+
page,
|
|
25
|
+
count,
|
|
26
|
+
rowsPerPage,
|
|
27
|
+
}: TablePaginationActionsProps): JSX.Element {
|
|
28
|
+
const totalPages = Math.ceil(count / rowsPerPage)
|
|
29
|
+
const [pageValue, setPageValue] = useState<number | ''>(page + 1)
|
|
30
|
+
const isCompact = variant === 'compact'
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
setPageValue(page + 1)
|
|
34
|
+
}, [page])
|
|
35
|
+
|
|
36
|
+
const handleFirstPageClick = () => {
|
|
37
|
+
onPageChange(0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const handlePreviousPageClick = () => {
|
|
41
|
+
onPageChange(page - 1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleNextPageClick = () => {
|
|
45
|
+
onPageChange(page + 1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleLastPageClick = () => {
|
|
49
|
+
onPageChange(totalPages - 1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const publishPageChange = () => {
|
|
53
|
+
if (pageValue && pageValue - 1 !== page) {
|
|
54
|
+
onPageChange(pageValue - 1)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
59
|
+
if (Keyboard(e).is('ENTER')) {
|
|
60
|
+
publishPageChange()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
65
|
+
if (Keyboard(e).is(['E_LOWERCASE', 'DOT', 'MINUS', 'PLUS'])) {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handlePageChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
71
|
+
if (e.target.value === '') {
|
|
72
|
+
setPageValue('')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const numberValue = Number(e.target.value)
|
|
77
|
+
|
|
78
|
+
if (!numberValue || numberValue < 1 || numberValue > totalPages) return
|
|
79
|
+
|
|
80
|
+
setPageValue(numberValue)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Layout.Group space="s" align="center">
|
|
85
|
+
{!isCompact && (
|
|
86
|
+
<NoPaddingButton
|
|
87
|
+
onClick={handleFirstPageClick}
|
|
88
|
+
disabled={page === 0 || disabled}
|
|
89
|
+
title="First page"
|
|
90
|
+
>
|
|
91
|
+
<ActionIcon name="caret-right-last" rotate={180} />
|
|
92
|
+
</NoPaddingButton>
|
|
93
|
+
)}
|
|
94
|
+
<NoPaddingButton
|
|
95
|
+
onClick={handlePreviousPageClick}
|
|
96
|
+
disabled={page === 0 || disabled}
|
|
97
|
+
title="Previous page"
|
|
98
|
+
>
|
|
99
|
+
<ActionIcon name="caret-left" />
|
|
100
|
+
</NoPaddingButton>
|
|
101
|
+
{!isCompact && (
|
|
102
|
+
<>
|
|
103
|
+
<TextField
|
|
104
|
+
type="number"
|
|
105
|
+
min={1}
|
|
106
|
+
max={totalPages}
|
|
107
|
+
disabled={disabled || totalPages === 1}
|
|
108
|
+
onChange={handlePageChange}
|
|
109
|
+
onBlur={publishPageChange}
|
|
110
|
+
onKeyUp={handleKeyUp}
|
|
111
|
+
onKeyDown={handleKeyDown}
|
|
112
|
+
scale="small"
|
|
113
|
+
value={count ? pageValue : 0}
|
|
114
|
+
title="Page"
|
|
115
|
+
/>
|
|
116
|
+
<Text variant="body" color={disabled ? 'color-neutral' : 'color-neutral-dark'}>
|
|
117
|
+
of{' '}
|
|
118
|
+
<Text variant="body-bold" color={disabled ? 'color-neutral' : 'color-neutral-dark'}>
|
|
119
|
+
{totalPages}
|
|
120
|
+
</Text>
|
|
121
|
+
</Text>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
<NoPaddingButton
|
|
125
|
+
onClick={handleNextPageClick}
|
|
126
|
+
disabled={page >= totalPages - 1 || disabled}
|
|
127
|
+
title="Next page"
|
|
128
|
+
>
|
|
129
|
+
<ActionIcon name="caret-right" />
|
|
130
|
+
</NoPaddingButton>
|
|
131
|
+
{!isCompact && (
|
|
132
|
+
<NoPaddingButton
|
|
133
|
+
onClick={handleLastPageClick}
|
|
134
|
+
disabled={page >= totalPages - 1 || disabled}
|
|
135
|
+
title="Last page"
|
|
136
|
+
>
|
|
137
|
+
<ActionIcon name="caret-right-last" />
|
|
138
|
+
</NoPaddingButton>
|
|
139
|
+
)}
|
|
140
|
+
</Layout.Group>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default TablePaginationActions
|