material-react-table 2.0.0-alpha.2 → 2.0.0-alpha.4

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.
@@ -7,16 +7,20 @@ import {
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
+ import Autocomplete from '@mui/material/Autocomplete';
10
11
  import Box from '@mui/material/Box';
11
12
  import Checkbox from '@mui/material/Checkbox';
12
13
  import Chip from '@mui/material/Chip';
13
14
  import IconButton from '@mui/material/IconButton';
14
15
  import InputAdornment from '@mui/material/InputAdornment';
15
16
  import MenuItem from '@mui/material/MenuItem';
16
- import TextField from '@mui/material/TextField';
17
- import { type TextFieldProps } from '@mui/material/TextField';
17
+ import TextField, { type TextFieldProps } from '@mui/material/TextField';
18
18
  import Tooltip from '@mui/material/Tooltip';
19
19
  import { debounce } from '@mui/material/utils';
20
+ import {
21
+ DatePicker,
22
+ type DatePickerProps,
23
+ } from '@mui/x-date-pickers/DatePicker';
20
24
  import { parseFromValuesOrFunc } from '../column.utils';
21
25
  import { MRT_FilterOptionMenu } from '../menus/MRT_FilterOptionMenu';
22
26
  import { type MRT_Header, type MRT_TableInstance } from '../types';
@@ -39,6 +43,8 @@ export const MRT_FilterTextField = <TData extends Record<string, any>>({
39
43
  icons: { CloseIcon, FilterListIcon },
40
44
  localization,
41
45
  manualFiltering,
46
+ muiFilterAutocompleteProps,
47
+ muiFilterDatePickerProps,
42
48
  muiFilterTextFieldProps,
43
49
  },
44
50
  refs: { filterInputRefs },
@@ -46,6 +52,7 @@ export const MRT_FilterTextField = <TData extends Record<string, any>>({
46
52
  } = table;
47
53
  const { column } = header;
48
54
  const { columnDef } = column;
55
+ const { filterVariant } = columnDef;
49
56
 
50
57
  const textFieldProps: TextFieldProps = {
51
58
  ...parseFromValuesOrFunc(muiFilterTextFieldProps, { column, table }),
@@ -55,12 +62,30 @@ export const MRT_FilterTextField = <TData extends Record<string, any>>({
55
62
  }),
56
63
  };
57
64
 
65
+ const autocompleteProps = {
66
+ ...parseFromValuesOrFunc(muiFilterAutocompleteProps, { column, table }),
67
+ ...parseFromValuesOrFunc(columnDef.muiFilterAutocompleteProps, {
68
+ column,
69
+ table,
70
+ }),
71
+ };
72
+
73
+ const datePickerProps: DatePickerProps<any> = {
74
+ ...parseFromValuesOrFunc(muiFilterDatePickerProps, { column, table }),
75
+ ...parseFromValuesOrFunc(columnDef.muiFilterDatePickerProps, {
76
+ column,
77
+ table,
78
+ }),
79
+ };
80
+
81
+ const isDateFilter = filterVariant?.startsWith('date');
82
+ const isAutocompleteFilter = filterVariant === 'autocomplete';
58
83
  const isRangeFilter =
59
- columnDef.filterVariant === 'range' || rangeFilterIndex !== undefined;
60
- const isSelectFilter = columnDef.filterVariant === 'select';
61
- const isMultiSelectFilter = columnDef.filterVariant === 'multi-select';
84
+ filterVariant?.includes('range') || rangeFilterIndex !== undefined;
85
+ const isSelectFilter = filterVariant === 'select';
86
+ const isMultiSelectFilter = filterVariant === 'multi-select';
62
87
  const isTextboxFilter =
63
- columnDef.filterVariant === 'text' ||
88
+ ['autocomplete', 'text'].includes(filterVariant!) ||
64
89
  (!isSelectFilter && !isMultiSelectFilter);
65
90
  const currentFilterOption = columnDef._filterFn;
66
91
  const filterChipLabel = ['empty', 'notEmpty'].includes(currentFilterOption)
@@ -91,22 +116,6 @@ export const MRT_FilterTextField = <TData extends Record<string, any>>({
91
116
 
92
117
  const facetedUniqueValues = column.getFacetedUniqueValues();
93
118
 
94
- const filterSelectOptions = useMemo(
95
- () =>
96
- columnDef.filterSelectOptions ??
97
- ((isSelectFilter || isMultiSelectFilter) && facetedUniqueValues
98
- ? Array.from(facetedUniqueValues.keys())
99
- .filter((value) => value !== null && value !== undefined)
100
- .sort((a, b) => a.localeCompare(b))
101
- : undefined),
102
- [
103
- columnDef.filterSelectOptions,
104
- facetedUniqueValues,
105
- isMultiSelectFilter,
106
- isSelectFilter,
107
- ],
108
- );
109
-
110
119
  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
111
120
  const [filterValue, setFilterValue] = useState<string | string[]>(() =>
112
121
  isMultiSelectFilter
@@ -114,27 +123,21 @@ export const MRT_FilterTextField = <TData extends Record<string, any>>({
114
123
  : isRangeFilter
115
124
  ? (column.getFilterValue() as [string, string])?.[
116
125
  rangeFilterIndex as number
117
- ] || []
126
+ ] || ''
118
127
  : (column.getFilterValue() as string) ?? '',
119
128
  );
120
129
 
121
130
  const handleChangeDebounced = useCallback(
122
131
  debounce(
123
- (event: ChangeEvent<HTMLInputElement>) => {
124
- const value =
125
- textFieldProps.type === 'date'
126
- ? event.target.valueAsDate
127
- : textFieldProps.type === 'number'
128
- ? event.target.valueAsNumber
129
- : event.target.value;
132
+ (newValue: any) => {
130
133
  if (isRangeFilter) {
131
134
  column.setFilterValue((old: Array<Date | null | number | string>) => {
132
135
  const newFilterValues = old ?? ['', ''];
133
- newFilterValues[rangeFilterIndex as number] = value;
136
+ newFilterValues[rangeFilterIndex as number] = newValue;
134
137
  return newFilterValues;
135
138
  });
136
139
  } else {
137
- column.setFilterValue(value ?? undefined);
140
+ column.setFilterValue(newValue ?? undefined);
138
141
  }
139
142
  },
140
143
  isTextboxFilter ? (manualFiltering ? 400 : 200) : 1,
@@ -142,9 +145,20 @@ export const MRT_FilterTextField = <TData extends Record<string, any>>({
142
145
  [],
143
146
  );
144
147
 
145
- const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
146
- setFilterValue(event.target.value);
147
- handleChangeDebounced(event);
148
+ const handleChange = (newValue: any) => {
149
+ setFilterValue(newValue?.toString() ?? '');
150
+ handleChangeDebounced(newValue);
151
+ };
152
+
153
+ const handleTextFieldChange = (event: ChangeEvent<HTMLInputElement>) => {
154
+ const newValue =
155
+ textFieldProps.type === 'date'
156
+ ? event.target.valueAsDate
157
+ : textFieldProps.type === 'number'
158
+ ? event.target.valueAsNumber
159
+ : event.target.value;
160
+ handleChange(newValue);
161
+ textFieldProps?.onChange?.(event);
148
162
  };
149
163
 
150
164
  const handleClear = () => {
@@ -199,199 +213,264 @@ export const MRT_FilterTextField = <TData extends Record<string, any>>({
199
213
  );
200
214
  }
201
215
 
216
+ const dropdownOptions = useMemo(
217
+ () =>
218
+ columnDef.filterSelectOptions ??
219
+ ((isSelectFilter || isMultiSelectFilter || isAutocompleteFilter) &&
220
+ facetedUniqueValues
221
+ ? Array.from(facetedUniqueValues.keys())
222
+ .filter((value) => value !== null && value !== undefined)
223
+ .sort((a, b) => a.localeCompare(b))
224
+ : undefined),
225
+ [
226
+ columnDef.filterSelectOptions,
227
+ facetedUniqueValues,
228
+ isMultiSelectFilter,
229
+ isSelectFilter,
230
+ ],
231
+ );
232
+
233
+ const endAdornment =
234
+ !isAutocompleteFilter && !isDateFilter && !filterChipLabel ? (
235
+ <InputAdornment
236
+ position="end"
237
+ sx={{ mr: isSelectFilter || isMultiSelectFilter ? '20px' : undefined }}
238
+ >
239
+ <Tooltip arrow placement="right" title={localization.clearFilter ?? ''}>
240
+ <span>
241
+ <IconButton
242
+ aria-label={localization.clearFilter}
243
+ disabled={!filterValue?.toString()?.length}
244
+ onClick={handleClear}
245
+ size="small"
246
+ sx={{
247
+ height: '2rem',
248
+ transform: 'scale(0.9)',
249
+ width: '2rem'
250
+ }}
251
+ >
252
+ <CloseIcon />
253
+ </IconButton>
254
+ </span>
255
+ </Tooltip>
256
+ </InputAdornment>
257
+ ) : null;
258
+
259
+ const startAdornment = showChangeModeButton ? (
260
+ <InputAdornment position="start">
261
+ <Tooltip arrow title={localization.changeFilterMode}>
262
+ <span>
263
+ <IconButton
264
+ aria-label={localization.changeFilterMode}
265
+ onClick={handleFilterMenuOpen}
266
+ size="small"
267
+ sx={{ height: '1.75rem', width: '1.75rem' }}
268
+ >
269
+ <FilterListIcon />
270
+ </IconButton>
271
+ </span>
272
+ </Tooltip>
273
+ {filterChipLabel && (
274
+ <Chip label={filterChipLabel} onDelete={handleClearEmptyFilterChip} />
275
+ )}
276
+ </InputAdornment>
277
+ ) : null;
278
+
279
+ const commonTextFieldProps: TextFieldProps = {
280
+ FormHelperTextProps: {
281
+ sx: {
282
+ fontSize: '0.75rem',
283
+ lineHeight: '0.8rem',
284
+ whiteSpace: 'nowrap',
285
+ },
286
+ },
287
+ InputProps: endAdornment //hack because mui looks for presense of endAdornment key instead of undefined
288
+ ? { endAdornment, startAdornment }
289
+ : { startAdornment },
290
+ fullWidth: true,
291
+ helperText: showChangeModeButton ? (
292
+ <label>
293
+ {localization.filterMode.replace(
294
+ '{filterType}',
295
+ // @ts-ignore
296
+ localization[
297
+ `filter${
298
+ currentFilterOption?.charAt(0)?.toUpperCase() +
299
+ currentFilterOption?.slice(1)
300
+ }`
301
+ ],
302
+ )}
303
+ </label>
304
+ ) : null,
305
+ inputProps: {
306
+ autoComplete: 'new-password', // disable autocomplete and autofill
307
+ disabled: !!filterChipLabel,
308
+ sx: {
309
+ textOverflow: 'ellipsis',
310
+ width: filterChipLabel ? 0 : undefined,
311
+ },
312
+ title: filterPlaceholder,
313
+ },
314
+ inputRef: (inputRef) => {
315
+ filterInputRefs.current[`${column.id}-${rangeFilterIndex ?? 0}`] =
316
+ inputRef;
317
+ if (textFieldProps.inputRef) {
318
+ textFieldProps.inputRef = inputRef;
319
+ }
320
+ },
321
+ margin: 'none',
322
+ onClick: (e: MouseEvent<HTMLInputElement>) => e.stopPropagation(),
323
+ placeholder:
324
+ filterChipLabel || isSelectFilter || isMultiSelectFilter
325
+ ? undefined
326
+ : filterPlaceholder,
327
+ variant: 'standard',
328
+ ...textFieldProps,
329
+ sx: (theme) => ({
330
+ minWidth: isDateFilter
331
+ ? '160px'
332
+ : isRangeFilter
333
+ ? '100px'
334
+ : !filterChipLabel
335
+ ? '120px'
336
+ : 'auto',
337
+ mx: '-2px',
338
+ p: 0,
339
+ width: 'calc(100% + 4px)',
340
+ ...(parseFromValuesOrFunc(textFieldProps?.sx, theme) as any),
341
+ }),
342
+ };
343
+
202
344
  return (
203
345
  <>
204
- <TextField
205
- FormHelperTextProps={{
206
- sx: {
207
- fontSize: '0.75rem',
208
- lineHeight: '0.8rem',
209
- whiteSpace: 'nowrap',
210
- },
211
- }}
212
- InputProps={{
213
- endAdornment: !filterChipLabel && (
214
- <InputAdornment position="end">
215
- <Tooltip
216
- arrow
217
- placement="right"
218
- title={localization.clearFilter ?? ''}
219
- >
220
- <span>
221
- <IconButton
222
- aria-label={localization.clearFilter}
223
- disabled={!filterValue?.toString()?.length}
224
- onClick={handleClear}
225
- size="small"
226
- sx={{
227
- height: '1.75rem',
228
- width: '1.75rem',
229
- }}
230
- >
231
- <CloseIcon />
232
- </IconButton>
233
- </span>
234
- </Tooltip>
235
- </InputAdornment>
236
- ),
237
- startAdornment: showChangeModeButton ? (
238
- <InputAdornment position="start">
239
- <Tooltip arrow title={localization.changeFilterMode}>
240
- <span>
241
- <IconButton
242
- aria-label={localization.changeFilterMode}
243
- onClick={handleFilterMenuOpen}
244
- size="small"
245
- sx={{ height: '1.75rem', width: '1.75rem' }}
246
- >
247
- <FilterListIcon />
248
- </IconButton>
249
- </span>
250
- </Tooltip>
251
- {filterChipLabel && (
252
- <Chip
253
- label={filterChipLabel}
254
- onDelete={handleClearEmptyFilterChip}
255
- />
256
- )}
257
- </InputAdornment>
258
- ) : null,
259
- }}
260
- SelectProps={{
261
- displayEmpty: true,
262
- multiple: isMultiSelectFilter,
263
- renderValue: isMultiSelectFilter
264
- ? (selected: any) =>
265
- !selected?.length ? (
266
- <Box sx={{ opacity: 0.5 }}>{filterPlaceholder}</Box>
267
- ) : (
268
- <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '2px' }}>
269
- {(selected as string[])?.map((value) => {
270
- const selectedValue = filterSelectOptions?.find(
271
- (option) =>
346
+ {isDateFilter ? (
347
+ <DatePicker
348
+ onChange={(newDate) => {
349
+ handleChange(newDate);
350
+ }}
351
+ value={filterValue || null}
352
+ {...datePickerProps}
353
+ slotProps={{
354
+ field: {
355
+ clearable: true,
356
+ onClear: () => handleClear(),
357
+ ...datePickerProps?.slotProps?.field,
358
+ },
359
+ textField: {
360
+ ...commonTextFieldProps,
361
+ ...datePickerProps?.slotProps?.textField,
362
+ },
363
+ }}
364
+ />
365
+ ) : isAutocompleteFilter ? (
366
+ <Autocomplete
367
+ getOptionLabel={(option) => option}
368
+ onChange={(_e, newValue) => handleChange(newValue)}
369
+ options={dropdownOptions ?? []}
370
+ {...autocompleteProps}
371
+ renderInput={(builtinTextFieldProps) => (
372
+ <TextField
373
+ {...builtinTextFieldProps}
374
+ {...commonTextFieldProps}
375
+ InputProps={{
376
+ ...builtinTextFieldProps.InputProps,
377
+ startAdornment:
378
+ commonTextFieldProps?.InputProps?.startAdornment,
379
+ }}
380
+ inputProps={{
381
+ ...builtinTextFieldProps.inputProps,
382
+ ...commonTextFieldProps?.inputProps,
383
+ }}
384
+ onChange={handleTextFieldChange}
385
+ />
386
+ )}
387
+ value={filterValue}
388
+ />
389
+ ) : (
390
+ <TextField
391
+ SelectProps={{
392
+ displayEmpty: true,
393
+ multiple: isMultiSelectFilter,
394
+ renderValue: isMultiSelectFilter
395
+ ? (selected: any) =>
396
+ !selected?.length ? (
397
+ <Box sx={{ opacity: 0.5 }}>{filterPlaceholder}</Box>
398
+ ) : (
399
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '2px' }}>
400
+ {(selected as string[])?.map((value) => {
401
+ const selectedValue = dropdownOptions?.find((option) =>
272
402
  option instanceof Object
273
403
  ? option.value === value
274
404
  : option === value,
275
- );
276
- return (
277
- <Chip
278
- key={value}
279
- label={
280
- selectedValue instanceof Object
281
- ? selectedValue.text
282
- : selectedValue
283
- }
284
- />
285
- );
286
- })}
287
- </Box>
288
- )
289
- : undefined,
290
- }}
291
- fullWidth
292
- helperText={
293
- showChangeModeButton ? (
294
- <label>
295
- {localization.filterMode.replace(
296
- '{filterType}',
297
- // @ts-ignore
298
- localization[
299
- `filter${
300
- currentFilterOption?.charAt(0)?.toUpperCase() +
301
- currentFilterOption?.slice(1)
302
- }`
303
- ],
304
- )}
305
- </label>
306
- ) : null
307
- }
308
- inputProps={{
309
- disabled: !!filterChipLabel,
310
- sx: {
311
- textOverflow: 'ellipsis',
312
- width: filterChipLabel ? 0 : undefined,
313
- },
314
- title: filterPlaceholder,
315
- }}
316
- margin="none"
317
- onChange={handleChange}
318
- onClick={(e: MouseEvent<HTMLInputElement>) => e.stopPropagation()}
319
- placeholder={
320
- filterChipLabel || isSelectFilter || isMultiSelectFilter
321
- ? undefined
322
- : filterPlaceholder
323
- }
324
- select={isSelectFilter || isMultiSelectFilter}
325
- value={filterValue ?? ''}
326
- variant="standard"
327
- {...textFieldProps}
328
- inputRef={(inputRef) => {
329
- filterInputRefs.current[`${column.id}-${rangeFilterIndex ?? 0}`] =
330
- inputRef;
331
- if (textFieldProps.inputRef) {
332
- textFieldProps.inputRef = inputRef;
333
- }
334
- }}
335
- sx={(theme) => ({
336
- '& .MuiSelect-icon': {
337
- mr: '1.5rem',
338
- },
339
- minWidth: isRangeFilter
340
- ? '100px'
341
- : !filterChipLabel
342
- ? '120px'
343
- : 'auto',
344
- mx: '-2px',
345
- p: 0,
346
- width: 'calc(100% + 4px)',
347
- ...(parseFromValuesOrFunc(textFieldProps?.sx, theme) as any),
348
- })}
349
- >
350
- {(isSelectFilter || isMultiSelectFilter) && (
351
- <MenuItem disabled divider hidden value="">
352
- <Box sx={{ opacity: 0.5 }}>{filterPlaceholder}</Box>
353
- </MenuItem>
354
- )}
355
- {textFieldProps.children ??
356
- filterSelectOptions?.map(
357
- (option: { text: string; value: string } | string) => {
358
- if (!option) return '';
359
- let value: string;
360
- let text: string;
361
- if (typeof option !== 'object') {
362
- value = option;
363
- text = option;
364
- } else {
365
- value = option.value;
366
- text = option.text;
367
- }
368
- return (
369
- <MenuItem
370
- key={value}
371
- sx={{
372
- alignItems: 'center',
373
- display: 'flex',
374
- gap: '0.5rem',
375
- m: 0,
376
- }}
377
- value={value}
378
- >
379
- {isMultiSelectFilter && (
380
- <Checkbox
381
- checked={(
382
- (column.getFilterValue() ?? []) as string[]
383
- ).includes(value)}
384
- sx={{ mr: '0.5rem' }}
385
- />
386
- )}
387
- {text}{' '}
388
- {!columnDef.filterSelectOptions &&
389
- `(${facetedUniqueValues.get(value)})`}
390
- </MenuItem>
391
- );
392
- },
393
- )}
394
- </TextField>
405
+ );
406
+ return (
407
+ <Chip
408
+ key={value}
409
+ label={
410
+ selectedValue instanceof Object
411
+ ? selectedValue.text
412
+ : selectedValue
413
+ }
414
+ />
415
+ );
416
+ })}
417
+ </Box>
418
+ )
419
+ : undefined,
420
+ }}
421
+ onChange={handleTextFieldChange}
422
+ select={isSelectFilter || isMultiSelectFilter}
423
+ {...commonTextFieldProps}
424
+ value={filterValue ?? ''}
425
+ >
426
+ {(isSelectFilter || isMultiSelectFilter) && [
427
+ <MenuItem disabled divider hidden key="p" value="">
428
+ <Box sx={{ opacity: 0.5 }}>{filterPlaceholder}</Box>
429
+ </MenuItem>,
430
+ ...[
431
+ textFieldProps.children ??
432
+ dropdownOptions?.map(
433
+ (option: { text: string; value: string } | string, index) => {
434
+ if (!option) return '';
435
+ let value: string;
436
+ let text: string;
437
+ if (typeof option !== 'object') {
438
+ value = option;
439
+ text = option;
440
+ } else {
441
+ value = option.value;
442
+ text = option.text;
443
+ }
444
+ return (
445
+ <MenuItem
446
+ key={`${index}-${value}`}
447
+ sx={{
448
+ alignItems: 'center',
449
+ display: 'flex',
450
+ gap: '0.5rem',
451
+ m: 0,
452
+ }}
453
+ value={value}
454
+ >
455
+ {isMultiSelectFilter && (
456
+ <Checkbox
457
+ checked={(
458
+ (column.getFilterValue() ?? []) as string[]
459
+ ).includes(value)}
460
+ sx={{ mr: '0.5rem' }}
461
+ />
462
+ )}
463
+ {text}{' '}
464
+ {!columnDef.filterSelectOptions &&
465
+ `(${facetedUniqueValues.get(value)})`}
466
+ </MenuItem>
467
+ );
468
+ },
469
+ ),
470
+ ],
471
+ ]}
472
+ </TextField>
473
+ )}
395
474
  <MRT_FilterOptionMenu
396
475
  anchorEl={anchorEl}
397
476
  header={header}