@liguelead/design-system 0.0.30 → 0.0.32
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/components/Alert/Alert.stories.tsx +94 -18
- package/components/Badge/Badge.stories.tsx +114 -0
- package/components/Badge/Badge.styles.ts +36 -0
- package/components/Badge/Badge.tsx +23 -0
- package/components/Badge/Badge.types.ts +11 -0
- package/components/Badge/index.ts +2 -0
- package/components/Button/Button.appearance.ts +1 -1
- package/components/Button/Button.stories.tsx +99 -18
- package/components/Checkbox/Checkbox.stories.tsx +107 -7
- package/components/DatePicker/DatePicker.styles.ts +1 -0
- package/components/DatePicker/DatePicker.tsx +9 -10
- package/components/IconButton/IconButton.sizes.ts +7 -7
- package/components/IconButton/IconButton.tsx +0 -1
- package/components/InputOpt/InputOpt.stories.tsx +30 -44
- package/components/Select/Select.stories.tsx +80 -19
- package/components/Select/Select.tsx +7 -9
- package/components/Table/Datatable.stories.tsx +186 -0
- package/components/Table/Table.stories.tsx +127 -46
- package/components/Table/Table.styles.ts +83 -8
- package/components/Table/Table.tsx +292 -142
- package/components/Table/Table.types.ts +104 -12
- package/components/Table/components/ColumnVisibility/ColumnVisibility.style.ts +46 -0
- package/components/Table/components/ColumnVisibility/ColumnVisibility.tsx +55 -0
- package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.styles.ts +120 -0
- package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.tsx +228 -0
- package/components/Table/components/DatatableColumnFilterMenu/index.ts +1 -0
- package/components/Table/components/DatatableTopBar/DatatableTopBar.styles.ts +25 -0
- package/components/Table/components/DatatableTopBar/DatatableTopBar.tsx +89 -0
- package/components/Table/components/DatatableTopBar/index.ts +1 -0
- package/components/Table/components/SearchInput/SearchInput.tsx +30 -0
- package/components/Table/components/TableHeader/TableHeader.tsx +98 -0
- package/components/Table/components/TablePagination/TablePagination.tsx +78 -0
- package/components/Table/components/index.ts +6 -0
- package/components/Table/hooks/useDatatableFilters.ts +88 -0
- package/components/Table/stories.fixtures.ts +100 -0
- package/components/Table/tanstack-table.d.ts +10 -0
- package/components/Table/utils/dateRangeFilterFn.ts +33 -0
- package/components/Table/utils/index.ts +2 -1
- package/components/Tabs/Tabs.stories.tsx +152 -0
- package/components/Tabs/Tabs.styles.ts +12 -0
- package/components/Tabs/Tabs.tsx +34 -0
- package/components/Tabs/Tabs.types.ts +15 -0
- package/components/Tabs/index.ts +2 -0
- package/components/TextField/TextField.stories.tsx +135 -12
- package/components/index.ts +3 -0
- package/package.json +3 -2
package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.styles.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import styled from 'styled-components'
|
|
2
|
+
import { spacing, fontSize, fontWeight, radius } from '@liguelead/foundation'
|
|
3
|
+
import { parseColor } from "../../../../utils";
|
|
4
|
+
import { FunnelSimpleIcon, FunnelSimpleXIcon } from '@phosphor-icons/react'
|
|
5
|
+
import IconButton from '../../../IconButton'
|
|
6
|
+
|
|
7
|
+
export const FunnelInactiveIcon = styled(FunnelSimpleIcon)`
|
|
8
|
+
fill: ${({ theme }) => parseColor(theme.colors.neutral700)};
|
|
9
|
+
`
|
|
10
|
+
|
|
11
|
+
export const FunnelActiveIcon = styled(FunnelSimpleXIcon)`
|
|
12
|
+
fill: ${({ theme }) => parseColor(theme.colors.primary)};
|
|
13
|
+
`
|
|
14
|
+
|
|
15
|
+
export const FilterTriggerArea = styled.button`
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: ${spacing.spacing8}px;
|
|
19
|
+
background: none;
|
|
20
|
+
border: none;
|
|
21
|
+
padding: 0;
|
|
22
|
+
margin: 0;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
color: inherit;
|
|
25
|
+
font: inherit;
|
|
26
|
+
flex: 1;
|
|
27
|
+
text-align: left;
|
|
28
|
+
|
|
29
|
+
&:focus-visible {
|
|
30
|
+
outline: 2px solid ${({ theme }) => parseColor(theme.colors.primary)};
|
|
31
|
+
outline-offset: 1px;
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
export const FilterIconWrapper = styled.span<{ $active?: boolean }>`
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
line-height: 0;
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
export const FilterButton = styled(IconButton)<{ $active?: boolean }>`
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
padding: 0;
|
|
47
|
+
transition: background 0.15s ease;
|
|
48
|
+
|
|
49
|
+
& svg {
|
|
50
|
+
width: 14px !important;
|
|
51
|
+
heigth: 14px !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&:focus-visible {
|
|
55
|
+
outline: 2px solid ${({ theme }) => parseColor(theme.colors.primary)};
|
|
56
|
+
outline-offset: 1px;
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
export const ClearFilterButton = styled(IconButton)`
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
padding: 0;
|
|
65
|
+
width: 12px !important;
|
|
66
|
+
heigth: 12px !important;
|
|
67
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
68
|
+
|
|
69
|
+
& svg {
|
|
70
|
+
width: 12px !important;
|
|
71
|
+
heigth: 12px !important;
|
|
72
|
+
}
|
|
73
|
+
`
|
|
74
|
+
|
|
75
|
+
export const FilterPopoverContent = styled.div`
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
gap: ${spacing.spacing8}px;
|
|
79
|
+
min-width: 240px;
|
|
80
|
+
max-width: 320px;
|
|
81
|
+
background: ${({ theme }) => parseColor(theme.colors.white)};
|
|
82
|
+
border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral200)};
|
|
83
|
+
border-radius: ${radius.radius4}px;
|
|
84
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
85
|
+
padding: ${spacing.spacing12}px;
|
|
86
|
+
`
|
|
87
|
+
|
|
88
|
+
export const FilterPopoverLabel = styled.span`
|
|
89
|
+
font-size: ${fontSize.fontSize12}px;
|
|
90
|
+
font-weight: ${fontWeight.fontWeight600};
|
|
91
|
+
color: ${({ theme }) => parseColor(theme.colors.textMedium)};
|
|
92
|
+
`
|
|
93
|
+
|
|
94
|
+
export const StyledTextInput = styled.input`
|
|
95
|
+
width: 100%;
|
|
96
|
+
height: 36px;
|
|
97
|
+
padding: 0 ${spacing.spacing12}px;
|
|
98
|
+
font-size: ${fontSize.fontSize14}px;
|
|
99
|
+
color: ${({ theme }) => parseColor(theme.colors.textDark)};
|
|
100
|
+
background: ${({ theme }) => parseColor(theme.colors.white)};
|
|
101
|
+
border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral300)};
|
|
102
|
+
border-radius: ${radius.radius4}px;
|
|
103
|
+
outline: none;
|
|
104
|
+
box-sizing: border-box;
|
|
105
|
+
transition: border-color 0.2s ease;
|
|
106
|
+
|
|
107
|
+
&::placeholder {
|
|
108
|
+
color: ${({ theme }) => parseColor(theme.colors.textLight)};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
&:focus {
|
|
112
|
+
border-color: ${({ theme }) => parseColor(theme.colors.primary)};
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
|
|
116
|
+
export const ClearFilterButtonWrapper = styled.span`
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: end;
|
|
120
|
+
`
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
|
2
|
+
import { XIcon } from '@phosphor-icons/react'
|
|
3
|
+
import { DateRange } from 'react-day-picker'
|
|
4
|
+
import DatePicker from '../../../DatePicker'
|
|
5
|
+
import Select from '../../../Select'
|
|
6
|
+
import {
|
|
7
|
+
ColumnFilterValue,
|
|
8
|
+
DatatableColumnMeta,
|
|
9
|
+
DatatableFilterDropdownLabels,
|
|
10
|
+
} from '../../Table.types'
|
|
11
|
+
|
|
12
|
+
import { SortIconWrapper, ThContent } from '../../Table.styles'
|
|
13
|
+
import {
|
|
14
|
+
ClearFilterButton,
|
|
15
|
+
ClearFilterButtonWrapper,
|
|
16
|
+
FilterIconWrapper,
|
|
17
|
+
FilterPopoverContent,
|
|
18
|
+
FilterPopoverLabel,
|
|
19
|
+
FilterTriggerArea,
|
|
20
|
+
FunnelActiveIcon,
|
|
21
|
+
FunnelInactiveIcon,
|
|
22
|
+
StyledTextInput,
|
|
23
|
+
} from './DatatableColumnFilterMenu.styles'
|
|
24
|
+
|
|
25
|
+
interface DatatableColumnFilterMenuProps {
|
|
26
|
+
columnId: string
|
|
27
|
+
title: React.ReactNode
|
|
28
|
+
meta?: DatatableColumnMeta
|
|
29
|
+
open: boolean
|
|
30
|
+
onOpenChange: (isOpen: boolean) => void
|
|
31
|
+
filterValue: ColumnFilterValue | undefined
|
|
32
|
+
onFilterValueChange: (columnId: string, value: ColumnFilterValue | undefined) => void
|
|
33
|
+
onClearFilter: (columnId: string) => void
|
|
34
|
+
labels?: DatatableFilterDropdownLabels
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SELECT_PLACEHOLDER_VALUE = '__placeholder__'
|
|
38
|
+
|
|
39
|
+
function DatatableColumnFilterMenu({
|
|
40
|
+
columnId,
|
|
41
|
+
title,
|
|
42
|
+
meta,
|
|
43
|
+
open,
|
|
44
|
+
onOpenChange,
|
|
45
|
+
filterValue,
|
|
46
|
+
onFilterValueChange,
|
|
47
|
+
onClearFilter,
|
|
48
|
+
labels = {},
|
|
49
|
+
}: DatatableColumnFilterMenuProps) {
|
|
50
|
+
const filterType = meta?.filterType
|
|
51
|
+
const filterLabel = meta?.filterLabel ?? String(title)
|
|
52
|
+
const filterPlaceholder = meta?.filterPlaceholder ?? 'Pesquisar...'
|
|
53
|
+
const selectOptions = [
|
|
54
|
+
{ label: filterPlaceholder, value: SELECT_PLACEHOLDER_VALUE },
|
|
55
|
+
...(meta?.selectOptions ?? []).map((o) => ({
|
|
56
|
+
label: o.label,
|
|
57
|
+
value: String(o.value),
|
|
58
|
+
})),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
const hasActiveFilter =
|
|
62
|
+
filterValue !== undefined &&
|
|
63
|
+
((filterValue.type === 'text' && filterValue.value !== '') ||
|
|
64
|
+
(filterValue.type === 'select' && filterValue.value != null) ||
|
|
65
|
+
(filterValue.type === 'dateRange' &&
|
|
66
|
+
(filterValue.value.from != null || filterValue.value.to != null)))
|
|
67
|
+
|
|
68
|
+
const handleTriggerClick = (e: React.MouseEvent) => {
|
|
69
|
+
e.stopPropagation()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handleClearClick = (e: React.MouseEvent) => {
|
|
73
|
+
e.stopPropagation()
|
|
74
|
+
onClearFilter(columnId)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
78
|
+
onFilterValueChange(columnId, { type: 'text', value: e.target.value })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const handleTextKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
82
|
+
if (e.key === 'Enter') {
|
|
83
|
+
onOpenChange(false)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handleRangeChange = (range: DateRange | undefined) => {
|
|
88
|
+
onFilterValueChange(columnId, {
|
|
89
|
+
type: 'dateRange',
|
|
90
|
+
value: range ? { from: range.from, to: range.to } : {},
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handleSelectChange = (value: string) => {
|
|
95
|
+
if (!value || value === SELECT_PLACEHOLDER_VALUE) {
|
|
96
|
+
onFilterValueChange(columnId, undefined)
|
|
97
|
+
} else {
|
|
98
|
+
onFilterValueChange(columnId, { type: 'select', value })
|
|
99
|
+
}
|
|
100
|
+
onOpenChange(false)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rangeValue: DateRange | undefined =
|
|
104
|
+
filterValue?.type === 'dateRange'
|
|
105
|
+
? { from: filterValue.value.from, to: filterValue.value.to }
|
|
106
|
+
: undefined
|
|
107
|
+
|
|
108
|
+
const textValue = filterValue?.type === 'text' ? filterValue.value : ''
|
|
109
|
+
const selectValue =
|
|
110
|
+
filterValue?.type === 'select' && filterValue.value != null
|
|
111
|
+
? String(filterValue.value)
|
|
112
|
+
: SELECT_PLACEHOLDER_VALUE
|
|
113
|
+
|
|
114
|
+
if (!filterType) {
|
|
115
|
+
return (
|
|
116
|
+
<ThContent>
|
|
117
|
+
{title}
|
|
118
|
+
</ThContent>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<PopoverPrimitive.Root
|
|
124
|
+
open={open}
|
|
125
|
+
onOpenChange={(isOpen) => {
|
|
126
|
+
onOpenChange(isOpen)
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<ThContent $filterable>
|
|
130
|
+
<PopoverPrimitive.Trigger asChild>
|
|
131
|
+
<FilterTriggerArea
|
|
132
|
+
aria-label={labels.filterButtonAriaLabel ?? `Abrir filtro da coluna ${filterLabel}`}
|
|
133
|
+
aria-pressed={hasActiveFilter}
|
|
134
|
+
aria-expanded={open}
|
|
135
|
+
onClick={handleTriggerClick}
|
|
136
|
+
>
|
|
137
|
+
{title}
|
|
138
|
+
<SortIconWrapper aria-hidden="true">
|
|
139
|
+
<FilterIconWrapper>
|
|
140
|
+
{hasActiveFilter
|
|
141
|
+
? <FunnelActiveIcon size={14} />
|
|
142
|
+
: <FunnelInactiveIcon size={14} />
|
|
143
|
+
}
|
|
144
|
+
</FilterIconWrapper>
|
|
145
|
+
</SortIconWrapper>
|
|
146
|
+
</FilterTriggerArea>
|
|
147
|
+
</PopoverPrimitive.Trigger>
|
|
148
|
+
|
|
149
|
+
{hasActiveFilter && (
|
|
150
|
+
<ClearFilterButtonWrapper>
|
|
151
|
+
<ClearFilterButton
|
|
152
|
+
type="button"
|
|
153
|
+
variant='neutralGhost'
|
|
154
|
+
aria-label={`Limpar filtro da coluna ${filterLabel}`}
|
|
155
|
+
onClick={handleClearClick}
|
|
156
|
+
>
|
|
157
|
+
<XIcon size={12} weight="bold" />
|
|
158
|
+
</ClearFilterButton>
|
|
159
|
+
</ClearFilterButtonWrapper>
|
|
160
|
+
)}
|
|
161
|
+
</ThContent>
|
|
162
|
+
|
|
163
|
+
<PopoverPrimitive.Portal>
|
|
164
|
+
<PopoverPrimitive.Content
|
|
165
|
+
side="bottom"
|
|
166
|
+
align="start"
|
|
167
|
+
sideOffset={10}
|
|
168
|
+
alignOffset={-5}
|
|
169
|
+
style={{ zIndex: 200 }}
|
|
170
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
171
|
+
onInteractOutside={(e) => {
|
|
172
|
+
const target = e.target as HTMLElement
|
|
173
|
+
if (target.closest('[data-radix-popper-content-wrapper]')) {
|
|
174
|
+
e.preventDefault()
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
<FilterPopoverContent>
|
|
179
|
+
<FilterPopoverLabel>Filtrar por {filterLabel}</FilterPopoverLabel>
|
|
180
|
+
|
|
181
|
+
{filterType === 'text' && (
|
|
182
|
+
<StyledTextInput
|
|
183
|
+
type="text"
|
|
184
|
+
value={textValue}
|
|
185
|
+
onChange={handleTextChange}
|
|
186
|
+
onKeyDown={handleTextKeyDown}
|
|
187
|
+
placeholder={filterPlaceholder}
|
|
188
|
+
aria-label={`Filtrar por ${filterLabel}`}
|
|
189
|
+
autoFocus
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{filterType === 'dateRange' && (
|
|
194
|
+
<DatePicker
|
|
195
|
+
mode="range"
|
|
196
|
+
label=""
|
|
197
|
+
rangeValue={rangeValue}
|
|
198
|
+
onRangeChange={handleRangeChange}
|
|
199
|
+
size="sm"
|
|
200
|
+
icon
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{filterType === 'select' && (
|
|
205
|
+
<Select
|
|
206
|
+
key={`select-${columnId}-${selectValue}`}
|
|
207
|
+
label=""
|
|
208
|
+
options={selectOptions}
|
|
209
|
+
defaultValue={selectValue}
|
|
210
|
+
size="sm"
|
|
211
|
+
register={{
|
|
212
|
+
name: `datatable-select-${columnId}`,
|
|
213
|
+
ref: () => { },
|
|
214
|
+
onBlur: async () => { },
|
|
215
|
+
onChange: async (e: { target: { value: string } }) => {
|
|
216
|
+
handleSelectChange(e.target.value)
|
|
217
|
+
},
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
</FilterPopoverContent>
|
|
222
|
+
</PopoverPrimitive.Content>
|
|
223
|
+
</PopoverPrimitive.Portal>
|
|
224
|
+
</PopoverPrimitive.Root>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export default DatatableColumnFilterMenu
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './DatatableColumnFilterMenu'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import styled from 'styled-components'
|
|
2
|
+
import { spacing } from '@liguelead/foundation'
|
|
3
|
+
|
|
4
|
+
export const TopBarWrapper = styled.div`
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
gap: ${spacing.spacing8}px;
|
|
9
|
+
margin-bottom: ${spacing.spacing16}px;
|
|
10
|
+
`
|
|
11
|
+
|
|
12
|
+
export const TopBarLeft = styled.div`
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: ${spacing.spacing8}px;
|
|
16
|
+
flex: 1;
|
|
17
|
+
min-width: 0;
|
|
18
|
+
`
|
|
19
|
+
|
|
20
|
+
export const TopBarRight = styled.div`
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: ${spacing.spacing8}px;
|
|
24
|
+
flex-shrink: 0;
|
|
25
|
+
`
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Table } from '@tanstack/react-table'
|
|
2
|
+
import { DownloadSimpleIcon, FunnelSimpleXIcon } from '@phosphor-icons/react'
|
|
3
|
+
import Button from '../../../Button'
|
|
4
|
+
import { ExportButtonConfig } from '../../Table.types'
|
|
5
|
+
import { ColumnVisibility } from '../index'
|
|
6
|
+
import SearchInput from '../SearchInput/SearchInput'
|
|
7
|
+
import { TopBarLeft, TopBarRight, TopBarWrapper } from './DatatableTopBar.styles'
|
|
8
|
+
|
|
9
|
+
interface DatatableTopBarProps<TData> {
|
|
10
|
+
table: Table<TData>
|
|
11
|
+
globalFilter: string
|
|
12
|
+
setGlobalFilter: (v: string) => void
|
|
13
|
+
enableGlobalSearch: boolean
|
|
14
|
+
enableColumnVisibility: boolean
|
|
15
|
+
enableClearFilters: boolean
|
|
16
|
+
hasActiveFilters: boolean
|
|
17
|
+
onClearAll: () => void
|
|
18
|
+
searchPlaceholder?: string
|
|
19
|
+
searchWidth?: string
|
|
20
|
+
clearFiltersLabel?: string
|
|
21
|
+
showExportButton?: boolean
|
|
22
|
+
onExport?: () => void
|
|
23
|
+
exportButton?: ExportButtonConfig
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function DatatableTopBar<TData>({
|
|
27
|
+
table,
|
|
28
|
+
globalFilter,
|
|
29
|
+
setGlobalFilter,
|
|
30
|
+
enableGlobalSearch,
|
|
31
|
+
enableColumnVisibility,
|
|
32
|
+
enableClearFilters,
|
|
33
|
+
hasActiveFilters,
|
|
34
|
+
onClearAll,
|
|
35
|
+
searchPlaceholder = 'Pesquisar',
|
|
36
|
+
searchWidth = '320px',
|
|
37
|
+
clearFiltersLabel = 'Limpar filtros',
|
|
38
|
+
showExportButton = false,
|
|
39
|
+
onExport,
|
|
40
|
+
exportButton = {},
|
|
41
|
+
}: DatatableTopBarProps<TData>) {
|
|
42
|
+
const showBar = enableGlobalSearch || enableColumnVisibility || (enableClearFilters && hasActiveFilters) || showExportButton
|
|
43
|
+
|
|
44
|
+
if (!showBar) return null
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<TopBarWrapper>
|
|
48
|
+
<TopBarLeft>
|
|
49
|
+
{enableGlobalSearch && (
|
|
50
|
+
<SearchInput
|
|
51
|
+
globalFilter={globalFilter}
|
|
52
|
+
setGlobalFilter={setGlobalFilter}
|
|
53
|
+
placeholder={searchPlaceholder}
|
|
54
|
+
width={searchWidth}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
</TopBarLeft>
|
|
58
|
+
|
|
59
|
+
<TopBarRight>
|
|
60
|
+
{enableClearFilters && hasActiveFilters && (
|
|
61
|
+
<Button
|
|
62
|
+
variant="solid"
|
|
63
|
+
color="danger200"
|
|
64
|
+
size="sm"
|
|
65
|
+
onClick={onClearAll}
|
|
66
|
+
aria-label={clearFiltersLabel}
|
|
67
|
+
>
|
|
68
|
+
<FunnelSimpleXIcon size={16} />
|
|
69
|
+
{clearFiltersLabel}
|
|
70
|
+
</Button>
|
|
71
|
+
)}
|
|
72
|
+
{showExportButton && (
|
|
73
|
+
<Button
|
|
74
|
+
variant={exportButton?.variant ?? 'neutralOutline'}
|
|
75
|
+
onClick={onExport}
|
|
76
|
+
disabled={exportButton?.disabled}
|
|
77
|
+
aria-label={exportButton?.ariaLabel ?? 'Exportar'}
|
|
78
|
+
>
|
|
79
|
+
<DownloadSimpleIcon size={16} />
|
|
80
|
+
{exportButton?.label ?? 'Exportar'}
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
{enableColumnVisibility && <ColumnVisibility table={table} />}
|
|
84
|
+
</TopBarRight>
|
|
85
|
+
</TopBarWrapper>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default DatatableTopBar
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './DatatableTopBar'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import TextField from "../../../TextField";
|
|
2
|
+
import { MagnifyingGlassIcon } from "@phosphor-icons/react";
|
|
3
|
+
|
|
4
|
+
export type SearchInputProps = {
|
|
5
|
+
globalFilter: string;
|
|
6
|
+
setGlobalFilter: (value: string) => void;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
width?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const SearchInput = ({
|
|
12
|
+
globalFilter,
|
|
13
|
+
setGlobalFilter,
|
|
14
|
+
placeholder = "Pesquisar",
|
|
15
|
+
width = '373px',
|
|
16
|
+
}: SearchInputProps) => {
|
|
17
|
+
return (
|
|
18
|
+
<div style={{ width }}>
|
|
19
|
+
<TextField
|
|
20
|
+
label=''
|
|
21
|
+
leftIcon={<MagnifyingGlassIcon />}
|
|
22
|
+
placeholder={placeholder}
|
|
23
|
+
value={globalFilter}
|
|
24
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setGlobalFilter(e.target.value)}
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default SearchInput;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { flexRender, Header, HeaderGroup } from '@tanstack/react-table'
|
|
2
|
+
import { SortIconWrapper, Th, ThContent } from '../../Table.styles'
|
|
3
|
+
import { DatatableColumnFilters, DatatableColumnMeta, DatatableFilterDropdownLabels } from '../../Table.types'
|
|
4
|
+
import { ArrowDownIcon, ArrowUpIcon } from '../ColumnVisibility/ColumnVisibility.style'
|
|
5
|
+
import DatatableColumnFilterMenu from '../DatatableColumnFilterMenu/DatatableColumnFilterMenu'
|
|
6
|
+
|
|
7
|
+
interface TableHeaderProps<TData> {
|
|
8
|
+
headerGroups: HeaderGroup<TData>[]
|
|
9
|
+
isDatatable: boolean
|
|
10
|
+
datatableFilters: DatatableColumnFilters
|
|
11
|
+
openFilterColumnId: string | null
|
|
12
|
+
onFilterPopoverOpenChange: (colId: string, isOpen: boolean) => void
|
|
13
|
+
onFilterValueChange: (colId: string, value: any) => void
|
|
14
|
+
onClearFilter: (colId: string) => void
|
|
15
|
+
labels?: DatatableFilterDropdownLabels
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function TableHeader<TData>({
|
|
19
|
+
headerGroups,
|
|
20
|
+
isDatatable,
|
|
21
|
+
datatableFilters,
|
|
22
|
+
openFilterColumnId,
|
|
23
|
+
onFilterPopoverOpenChange,
|
|
24
|
+
onFilterValueChange,
|
|
25
|
+
onClearFilter,
|
|
26
|
+
labels = {},
|
|
27
|
+
}: TableHeaderProps<TData>) {
|
|
28
|
+
return (
|
|
29
|
+
<thead>
|
|
30
|
+
{headerGroups.map((hg) => (
|
|
31
|
+
<tr key={hg.id}>
|
|
32
|
+
{hg.headers.map((header: Header<TData, unknown>) => {
|
|
33
|
+
const canSort = !isDatatable && header.column.getCanSort()
|
|
34
|
+
const sortDirection = header.column.getIsSorted()
|
|
35
|
+
const colMeta = header.column.columnDef.meta as DatatableColumnMeta | undefined
|
|
36
|
+
const colFilterValue = datatableFilters[header.column.id]
|
|
37
|
+
const colHasActiveFilter = isDatatable && colFilterValue !== undefined && (
|
|
38
|
+
(colFilterValue.type === 'text' && colFilterValue.value !== '') ||
|
|
39
|
+
(colFilterValue.type === 'select' && colFilterValue.value != null) ||
|
|
40
|
+
(colFilterValue.type === 'dateRange' && (
|
|
41
|
+
colFilterValue.value.from != null || colFilterValue.value.to != null
|
|
42
|
+
))
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Th
|
|
47
|
+
key={header.id}
|
|
48
|
+
$sortable={canSort}
|
|
49
|
+
$filterable={isDatatable && !!colMeta?.filterType}
|
|
50
|
+
$hasActiveSort={isDatatable ? colHasActiveFilter : !!sortDirection}
|
|
51
|
+
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
|
|
52
|
+
onKeyDown={(e) => {
|
|
53
|
+
if (canSort && (e.key === 'Enter' || e.key === ' ')) {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
header.column.getToggleSortingHandler()?.(e)
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
tabIndex={canSort ? 0 : undefined}
|
|
59
|
+
role={canSort ? 'button' : undefined}
|
|
60
|
+
aria-sort={
|
|
61
|
+
!isDatatable && sortDirection === 'asc' ? 'ascending'
|
|
62
|
+
: !isDatatable && sortDirection === 'desc' ? 'descending'
|
|
63
|
+
: undefined
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
{isDatatable ? (
|
|
67
|
+
<DatatableColumnFilterMenu
|
|
68
|
+
columnId={header.column.id}
|
|
69
|
+
title={flexRender(header.column.columnDef.header, header.getContext())}
|
|
70
|
+
meta={colMeta}
|
|
71
|
+
open={openFilterColumnId === header.column.id}
|
|
72
|
+
onOpenChange={(isOpen) => onFilterPopoverOpenChange(header.column.id, isOpen)}
|
|
73
|
+
filterValue={datatableFilters[header.column.id]}
|
|
74
|
+
onFilterValueChange={onFilterValueChange}
|
|
75
|
+
onClearFilter={onClearFilter}
|
|
76
|
+
labels={labels}
|
|
77
|
+
/>
|
|
78
|
+
) : (
|
|
79
|
+
<ThContent>
|
|
80
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
81
|
+
{canSort && (
|
|
82
|
+
<SortIconWrapper aria-hidden="true">
|
|
83
|
+
<ArrowUpIcon size={14} $selected={sortDirection === 'asc' ? 'asc' : false} />
|
|
84
|
+
<ArrowDownIcon size={14} $selected={sortDirection === 'desc' ? 'desc' : false} />
|
|
85
|
+
</SortIconWrapper>
|
|
86
|
+
)}
|
|
87
|
+
</ThContent>
|
|
88
|
+
)}
|
|
89
|
+
</Th>
|
|
90
|
+
)
|
|
91
|
+
})}
|
|
92
|
+
</tr>
|
|
93
|
+
))}
|
|
94
|
+
</thead>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default TableHeader
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Table } from '@tanstack/react-table'
|
|
2
|
+
import { CaretLeftIcon, CaretRightIcon, DotsThreeIcon } from '@phosphor-icons/react'
|
|
3
|
+
import {
|
|
4
|
+
PaginationButton,
|
|
5
|
+
PaginationContainer,
|
|
6
|
+
PaginationControls,
|
|
7
|
+
PaginationElipsis,
|
|
8
|
+
} from '../../Table.styles'
|
|
9
|
+
import IconButton from '../../../IconButton'
|
|
10
|
+
import { getPageNumbers } from '../../utils'
|
|
11
|
+
|
|
12
|
+
interface TablePaginationProps<TData> {
|
|
13
|
+
table: Table<TData>
|
|
14
|
+
currentPage: number
|
|
15
|
+
pageCount: number
|
|
16
|
+
ariaLabels?: { previous?: string; next?: string; page?: string }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function TablePagination<TData>({
|
|
20
|
+
table,
|
|
21
|
+
currentPage,
|
|
22
|
+
pageCount,
|
|
23
|
+
ariaLabels = {},
|
|
24
|
+
}: TablePaginationProps<TData>) {
|
|
25
|
+
const pageNumbersArray = getPageNumbers({ currentPage, pageCount })
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<PaginationContainer>
|
|
29
|
+
<PaginationControls>
|
|
30
|
+
<IconButton
|
|
31
|
+
variant="ghost"
|
|
32
|
+
onClick={() => table.previousPage()}
|
|
33
|
+
disabled={!table.getCanPreviousPage()}
|
|
34
|
+
aria-label={ariaLabels.previous ?? 'Página anterior'}
|
|
35
|
+
>
|
|
36
|
+
<CaretLeftIcon />
|
|
37
|
+
</IconButton>
|
|
38
|
+
|
|
39
|
+
{pageNumbersArray.map((page, index) =>
|
|
40
|
+
page === 'ellipsis' ? (
|
|
41
|
+
<PaginationElipsis key={`ellipsis-${index}`} aria-hidden="true">
|
|
42
|
+
<DotsThreeIcon size={16} />
|
|
43
|
+
</PaginationElipsis>
|
|
44
|
+
) : (
|
|
45
|
+
<PaginationButton
|
|
46
|
+
key={page}
|
|
47
|
+
size="sm"
|
|
48
|
+
$active={page === currentPage}
|
|
49
|
+
variant={page === currentPage ? 'solid' : 'ghost'}
|
|
50
|
+
onClick={() => {
|
|
51
|
+
if (typeof page === 'number') table.setPageIndex(page - 1)
|
|
52
|
+
}}
|
|
53
|
+
aria-label={
|
|
54
|
+
ariaLabels.page
|
|
55
|
+
? `${ariaLabels.page} ${page}`
|
|
56
|
+
: `Ir para página ${page}`
|
|
57
|
+
}
|
|
58
|
+
aria-current={page === currentPage ? 'page' : undefined}
|
|
59
|
+
>
|
|
60
|
+
{page}
|
|
61
|
+
</PaginationButton>
|
|
62
|
+
)
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<IconButton
|
|
66
|
+
variant="ghost"
|
|
67
|
+
onClick={() => table.nextPage()}
|
|
68
|
+
disabled={!table.getCanNextPage()}
|
|
69
|
+
aria-label={ariaLabels.next ?? 'Próxima página'}
|
|
70
|
+
>
|
|
71
|
+
<CaretRightIcon />
|
|
72
|
+
</IconButton>
|
|
73
|
+
</PaginationControls>
|
|
74
|
+
</PaginationContainer>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default TablePagination
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as ColumnVisibility } from './ColumnVisibility/ColumnVisibility'
|
|
2
|
+
export { default as DatatableColumnFilterMenu } from './DatatableColumnFilterMenu'
|
|
3
|
+
export { default as DatatableTopBar } from './DatatableTopBar'
|
|
4
|
+
export { default as SearchInput } from './SearchInput/SearchInput'
|
|
5
|
+
export { default as TableHeader } from './TableHeader/TableHeader'
|
|
6
|
+
export { default as TablePagination } from './TablePagination/TablePagination'
|