@loadsmart/loadsmart-ui 5.2.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.
Files changed (26) hide show
  1. package/dist/components/Icon/Icon.d.ts +1 -0
  2. package/dist/components/TablePagination/RowsPerPage.d.ts +4 -0
  3. package/dist/components/TablePagination/TablePagination.d.ts +4 -0
  4. package/dist/components/TablePagination/TablePagination.stories.d.ts +5 -0
  5. package/dist/components/TablePagination/TablePagination.styles.d.ts +5 -0
  6. package/dist/components/TablePagination/TablePagination.test.d.ts +1 -0
  7. package/dist/components/TablePagination/TablePagination.types.d.ts +50 -0
  8. package/dist/components/TablePagination/TablePaginationActions.d.ts +11 -0
  9. package/dist/components/TablePagination/index.d.ts +2 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +444 -436
  12. package/dist/index.js.map +1 -1
  13. package/dist/utils/toolset/keyboard.d.ts +4 -0
  14. package/package.json +1 -1
  15. package/src/components/Icon/Icon.tsx +2 -0
  16. package/src/components/Icon/assets/caret-right-last.svg +4 -0
  17. package/src/components/TablePagination/RowsPerPage.tsx +76 -0
  18. package/src/components/TablePagination/TablePagination.stories.tsx +37 -0
  19. package/src/components/TablePagination/TablePagination.styles.ts +13 -0
  20. package/src/components/TablePagination/TablePagination.test.tsx +112 -0
  21. package/src/components/TablePagination/TablePagination.tsx +51 -0
  22. package/src/components/TablePagination/TablePagination.types.ts +59 -0
  23. package/src/components/TablePagination/TablePaginationActions.tsx +144 -0
  24. package/src/components/TablePagination/index.ts +2 -0
  25. package/src/index.ts +3 -0
  26. package/src/utils/toolset/keyboard.ts +4 -0
@@ -11,6 +11,10 @@ declare const SUPPORTED_KEYS: {
11
11
  SHIFT: string;
12
12
  SPACE: string;
13
13
  TAB: string;
14
+ E_LOWERCASE: string;
15
+ DOT: string;
16
+ PLUS: string;
17
+ MINUS: string;
14
18
  };
15
19
  export declare type SupportedKey = keyof typeof SUPPORTED_KEYS;
16
20
  export declare function getKeyboardKey(e: KeyboardEvent): KeyboardEvent['key'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loadsmart/loadsmart-ui",
3
- "version": "5.2.1",
3
+ "version": "5.3.0",
4
4
  "description": "Miranda UI, a React UI library",
5
5
  "main": "dist",
6
6
  "files": [
@@ -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>
@@ -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,13 @@
1
+ import styled from 'styled-components'
2
+ import { Button } from 'components/Button'
3
+
4
+ export const NoPaddingButton = styled(Button).attrs({
5
+ variant: 'tertiary',
6
+ scale: 'small',
7
+ })`
8
+ padding: 0;
9
+
10
+ > span {
11
+ margin: 0 !important;
12
+ }
13
+ `
@@ -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
@@ -0,0 +1,2 @@
1
+ export { default as TablePagination } from './TablePagination'
2
+ export type { TablePaginationProps } from './TablePagination.types'
package/src/index.ts CHANGED
@@ -173,3 +173,6 @@ export type {
173
173
  DragDropFileProviderProps,
174
174
  DragDropFileContextValue,
175
175
  } from './components/DragDropFile/types'
176
+
177
+ export { TablePagination } from './components/TablePagination'
178
+ export type { TablePaginationProps } from './components/TablePagination'
@@ -15,6 +15,10 @@ const SUPPORTED_KEYS = {
15
15
  SHIFT: 'Shift',
16
16
  SPACE: ' ',
17
17
  TAB: 'Tab',
18
+ E_LOWERCASE: 'e',
19
+ DOT: '.',
20
+ PLUS: '+',
21
+ MINUS: '-',
18
22
  }
19
23
 
20
24
  export type SupportedKey = keyof typeof SUPPORTED_KEYS