@takaro/lib-components 0.0.13 → 0.0.14

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/Dockerfile.dev CHANGED
@@ -1,4 +1,4 @@
1
- FROM node:20.17.0-alpine AS build
1
+ FROM node:20.18.0-alpine AS build
2
2
 
3
3
  ENV NODE_ENV=development
4
4
  WORKDIR /app
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takaro/lib-components",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "private": false,
5
5
  "description": "Takaro UI is a simple and customizable component library to build React apps faster within the Takaro eco system",
6
6
  "license": "AGPL-3.0-or-later",
@@ -22,6 +22,7 @@ export default {
22
22
  args: {
23
23
  exclusive: true,
24
24
  orientation: 'horizontal',
25
+ defaultValue: 'left',
25
26
  fullWidth: false,
26
27
  },
27
28
  } as Meta<ToggleButtonGroupProps>;
@@ -17,7 +17,9 @@ export const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(fun
17
17
  { selected = false, disabled = false, onClick = undefined, value, parentClickEvent = () => {}, children, tooltip },
18
18
  ref,
19
19
  ) {
20
- const handleOnClick = () => {
20
+ const handleOnClick = (m: MouseEvent) => {
21
+ m.preventDefault();
22
+ m.stopPropagation();
21
23
  if (disabled) return;
22
24
  parentClickEvent(value);
23
25
  if (onClick) {
@@ -47,7 +47,12 @@ export const ToggleButtonGroup: FC<ToggleButtonGroupProps> & SubComponents = ({
47
47
  const m = new Map<string, boolean>();
48
48
  Children.forEach(children, (child) => {
49
49
  if (isValidElement(child)) {
50
- m.set(child.props.value, false);
50
+ console.log(child.props.value, defaultValue);
51
+ if (child.props.value === defaultValue) {
52
+ m.set(child.props.value, true);
53
+ } else {
54
+ m.set(child.props.value, false);
55
+ }
51
56
  }
52
57
  });
53
58
  return m;
@@ -65,7 +70,16 @@ export const ToggleButtonGroup: FC<ToggleButtonGroupProps> & SubComponents = ({
65
70
  }
66
71
  return setSelected(value);
67
72
  }
68
- // handle case that each button has a seperate state
73
+
74
+ // In case there always has to be one value selected, the clicked value is true and there is currently only one value selected then we don't do anything
75
+ if (
76
+ !canSelectNone &&
77
+ (selected as Map<string, boolean>).get(value) === true &&
78
+ Array.from((selected as Map<string, boolean>).values()).filter(Boolean).length === 1
79
+ ) {
80
+ return;
81
+ }
82
+
69
83
  setSelected(new Map((selected as Map<string, boolean>).set(value, !(selected as Map<string, boolean>).get(value))));
70
84
  };
71
85
 
@@ -5,6 +5,7 @@ import { styled } from '../../../styled';
5
5
  import { SubmitHandler, useForm } from 'react-hook-form';
6
6
  import { z } from 'zod';
7
7
  import { zodResolver } from '@hookform/resolvers/zod';
8
+ import { DrawerOptions } from './useDrawer';
8
9
 
9
10
  const ButtonContainer = styled.div`
10
11
  display: flex;
@@ -18,7 +19,10 @@ const Status = styled.div`
18
19
  export default {
19
20
  title: 'data/Drawer',
20
21
  component: Drawer,
21
- } as Meta;
22
+ args: {
23
+ promptCloseConfirmation: false,
24
+ },
25
+ } as Meta<DrawerOptions>;
22
26
 
23
27
  export const Loading: StoryFn = () => {
24
28
  return <DrawerSkeleton />;
@@ -33,7 +37,7 @@ interface FormFields {
33
37
  onStoreFront: boolean;
34
38
  }
35
39
 
36
- export const Default: StoryFn = () => {
40
+ export const Default: StoryFn<DrawerOptions> = ({ promptCloseConfirmation }) => {
37
41
  const [open, setOpen] = useState<boolean>(false);
38
42
 
39
43
  const validationSchema = useMemo(
@@ -57,7 +61,7 @@ export const Default: StoryFn = () => {
57
61
  return (
58
62
  <>
59
63
  <Button onClick={() => setOpen(true)} text="Open drawer" />
60
- <Drawer open={open} onOpenChange={setOpen}>
64
+ <Drawer open={open} onOpenChange={setOpen} promptCloseConfirmation={promptCloseConfirmation}>
61
65
  <Drawer.Content>
62
66
  <Drawer.Heading>Product Details</Drawer.Heading>
63
67
  <Drawer.Body>
@@ -85,11 +89,8 @@ export const Default: StoryFn = () => {
85
89
  />
86
90
  </CollapseList.Item>
87
91
  <CollapseList.Item title="Pricing">
88
- <RadioGroup
89
- control={control}
90
- label="Selection a price type"
91
- name="priceType"
92
- options={[
92
+ <RadioGroup control={control} label="Selection a price type" name="priceType">
93
+ {[
93
94
  {
94
95
  label: 'Fixed',
95
96
  labelPosition: 'left',
@@ -100,8 +101,16 @@ export const Default: StoryFn = () => {
100
101
  labelPosition: 'left',
101
102
  value: 'variable',
102
103
  },
103
- ]}
104
- />
104
+ ].map(({ value, label }) => (
105
+ <div
106
+ key={`gender-${value}`}
107
+ style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}
108
+ >
109
+ <RadioGroup.Item value={value} id={value} />
110
+ <label htmlFor={value}>{label}</label>
111
+ </div>
112
+ ))}
113
+ </RadioGroup>
105
114
  </CollapseList.Item>
106
115
  </CollapseList>
107
116
  <CollapseList.Item title="Settings">
@@ -1,5 +1,6 @@
1
1
  import { forwardRef, HTMLProps, useState } from 'react';
2
2
  import { styled } from '../../../styled';
3
+ import { Button } from '../../../components';
3
4
  import { AnimatePresence, motion, PanInfo } from 'framer-motion';
4
5
  import SimpleBar from 'simplebar-react';
5
6
 
@@ -15,6 +16,39 @@ const Container = styled(motion.div)`
15
16
  border-left: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
16
17
  `;
17
18
 
19
+ const ButtonContainer = styled.div`
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: flex-start;
23
+ gap: ${({ theme }) => theme.spacing[1]};
24
+ `;
25
+
26
+ const CloseConfirmationWrapper = styled.div`
27
+ position: absolute;
28
+ top: 0;
29
+ left: 0;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ width: 100%;
34
+ height: 100%;
35
+ background: rgba(0, 0, 0, 0.8);
36
+ `;
37
+
38
+ const CloseConfirmationContainer = styled.div`
39
+ height: 150px;
40
+ padding: ${({ theme }) => theme.spacing[2]};
41
+ width: calc(100% - 200px);
42
+ background-color: ${({ theme }) => theme.colors.background};
43
+ border: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
44
+ border-radius: ${({ theme }) => theme.borderRadius.medium};
45
+
46
+ h2,
47
+ p {
48
+ margin-bottom: ${({ theme }) => theme.spacing[1]};
49
+ }
50
+ `;
51
+
18
52
  export const HandleContainer = styled.div`
19
53
  height: 80vh;
20
54
  top: 10vh;
@@ -35,7 +69,16 @@ export const HandleContainer = styled.div`
35
69
  `;
36
70
 
37
71
  export const DrawerContent = forwardRef<HTMLElement, HTMLProps<HTMLDivElement>>(function DrawerContent(props, propRef) {
38
- const { context, labelId, descriptionId, getFloatingProps, setOpen, canDrag } = useDrawerContext();
72
+ const {
73
+ context,
74
+ labelId,
75
+ descriptionId,
76
+ getFloatingProps,
77
+ setOpen,
78
+ canDrag,
79
+ showConfirmDialog,
80
+ setShowConfirmDialog,
81
+ } = useDrawerContext();
39
82
 
40
83
  const ref = useMergeRefs([context.refs.setFloating, propRef]);
41
84
  const [dragPosition, setDragPosition] = useState<number>(0);
@@ -100,6 +143,18 @@ export const DrawerContent = forwardRef<HTMLElement, HTMLProps<HTMLDivElement>>(
100
143
  </HandleContainer>
101
144
  )}
102
145
  <SimpleBar style={{ maxHeight: '92vh' }}>{props.children}</SimpleBar>
146
+ {showConfirmDialog && (
147
+ <CloseConfirmationWrapper>
148
+ <CloseConfirmationContainer>
149
+ <h2>Your progress will be lost</h2>
150
+ <p>Are you sure you want to exit? Your progress will not be saved.</p>
151
+ <ButtonContainer>
152
+ <Button text="Cancel" color="secondary" onClick={() => setShowConfirmDialog(false)} />
153
+ <Button text="Discard changes" onClick={() => setOpen(false)} color="error" />
154
+ </ButtonContainer>
155
+ </CloseConfirmationContainer>
156
+ </CloseConfirmationWrapper>
157
+ )}
103
158
  </Container>
104
159
  </FloatingFocusManager>
105
160
  </FloatingOverlay>
@@ -6,17 +6,20 @@ export interface DrawerOptions {
6
6
  initialOpen?: boolean;
7
7
  onOpenChange?: (open: boolean) => void;
8
8
  canDrag?: boolean;
9
+ promptCloseConfirmation?: boolean;
9
10
  }
10
11
 
11
12
  export function useDrawer({
12
13
  initialOpen = false,
13
14
  canDrag = false,
14
15
  open: controlledOpen,
16
+ promptCloseConfirmation = false,
15
17
  onOpenChange: setControlledOpen,
16
18
  }: DrawerOptions) {
17
19
  const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
18
20
  const [labelId, setLabelId] = useState<string | undefined>();
19
21
  const [descriptionId, setDescriptionId] = useState<string | undefined>();
22
+ const [showConfirmDialog, setShowConfirmDialog] = useState<boolean>(false);
20
23
 
21
24
  const open = controlledOpen ?? uncontrolledOpen;
22
25
  const setOpen = setControlledOpen ?? setUncontrolledOpen;
@@ -32,13 +35,24 @@ export function useDrawer({
32
35
  useClick(context, {
33
36
  enabled: controlledOpen == null,
34
37
  }),
35
- useDismiss(context, { outsidePressEvent: 'mousedown' }),
38
+ useDismiss(context, {
39
+ outsidePressEvent: 'mousedown',
40
+ outsidePress: (_mouseEvent) => {
41
+ if (promptCloseConfirmation === true) {
42
+ setShowConfirmDialog(true);
43
+ return false;
44
+ }
45
+ return true;
46
+ },
47
+ }),
36
48
  ]);
37
49
 
38
50
  return useMemo(
39
51
  () => ({
40
52
  open,
41
53
  setOpen,
54
+ showConfirmDialog,
55
+ setShowConfirmDialog,
42
56
  ...interactions,
43
57
  ...data,
44
58
  labelId,
@@ -47,6 +61,6 @@ export function useDrawer({
47
61
  setDescriptionId,
48
62
  canDrag,
49
63
  }),
50
- [open, setOpen, interactions, data, labelId, descriptionId],
64
+ [open, setOpen, interactions, data, labelId, descriptionId, showConfirmDialog],
51
65
  );
52
66
  }
@@ -17,11 +17,18 @@ import {
17
17
  VisibilityState,
18
18
  ColumnPinningState,
19
19
  RowSelectionState,
20
+ ExpandedState,
21
+ getExpandedRowModel,
22
+ Row,
20
23
  } from '@tanstack/react-table';
21
24
  import { Wrapper, StyledTable, Toolbar, Flex, TableWrapper } from './style';
22
- import { Button, Empty, Spinner, ToggleButtonGroup } from '../../../components';
23
- import { AiOutlinePicCenter as RelaxedDensityIcon, AiOutlinePicRight as TightDensityIcon } from 'react-icons/ai';
24
-
25
+ import { Button, Empty, IconButton, Spinner, ToggleButtonGroup } from '../../../components';
26
+ import {
27
+ AiOutlinePicCenter as RelaxedDensityIcon,
28
+ AiOutlinePicRight as TightDensityIcon,
29
+ AiOutlineRight as ExpandIcon,
30
+ AiOutlineUp as CollapseIcon,
31
+ } from 'react-icons/ai';
25
32
  import { ColumnHeader } from './subcomponents/ColumnHeader';
26
33
  import { ColumnVisibility } from './subcomponents/ColumnVisibility';
27
34
  import { Filter } from './subcomponents/Filter';
@@ -37,8 +44,12 @@ export interface TableProps<DataType extends object> {
37
44
  data: DataType[];
38
45
  isLoading?: boolean;
39
46
 
40
- // currently not possible to type this properly: https://github.com/TanStack/table/issues/4241
47
+ /// Condition for row to be expandable
48
+ canExpand?: (row: Row<DataType>) => boolean;
49
+ /// What to render when row can be expanded
50
+ renderDetailPanel?: (row: Row<DataType>) => JSX.Element;
41
51
 
52
+ /// currently not possible to type this properly: https://github.com/TanStack/table/issues/4241
42
53
  columns: ColumnDef<DataType, any>[];
43
54
 
44
55
  /// Renders actions that are always visible
@@ -83,7 +94,9 @@ export function Table<DataType extends object>({
83
94
  title,
84
95
  rowSelection,
85
96
  columnSearch,
97
+ renderDetailPanel,
86
98
  renderToolbar,
99
+ canExpand = () => false,
87
100
  renderRowSelectionActions,
88
101
  isLoading = false,
89
102
  }: TableProps<DataType>) {
@@ -111,9 +124,13 @@ export function Table<DataType extends object>({
111
124
  {} as Record<string, boolean>,
112
125
  );
113
126
  });
127
+
114
128
  const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({});
115
129
  const { storedValue: density, setValue: setDensity } = useLocalStorage<Density>(`table-density-${id}`, 'tight');
116
130
 
131
+ // Might because potentially none fullfil the canExpand condtion.
132
+ const rowsMightExpand = renderDetailPanel ? true : false;
133
+ const [expanded, setExpanded] = useState<ExpandedState>({});
117
134
  const [openColumnVisibilityTooltip, setOpenColumnVisibilityTooltip] = useState<boolean>(false);
118
135
  const [hasShownColumnVisibilityTooltip, setHasShownColumnVisibilityTooltip] = useState<boolean>(false);
119
136
  const [columnOrder, setColumnOrder] = useState<ColumnOrderState>(
@@ -126,6 +143,7 @@ export function Table<DataType extends object>({
126
143
  );
127
144
 
128
145
  const ROW_SELECTION_COL_SPAN = rowSelection ? 1 : 0;
146
+ const EXPAND_ROW_COL_SPAN = rowsMightExpand ? 1 : 0;
129
147
  const MINIMUM_ROW_COUNT_FOR_PAGINATION = 5;
130
148
 
131
149
  // handles the column visibility tooltip (shows tooltip when the first column is hidden)
@@ -154,6 +172,7 @@ export function Table<DataType extends object>({
154
172
  data,
155
173
  columns,
156
174
  getCoreRowModel: getCoreRowModel(),
175
+ getExpandedRowModel: getExpandedRowModel(),
157
176
  pageCount: pagination?.pageOptions.pageCount ?? -1,
158
177
  manualPagination: true,
159
178
  paginateExpandedRows: true, // Expanded rows will be paginated this means that rows that take up more space will be shown on next page.
@@ -169,6 +188,7 @@ export function Table<DataType extends object>({
169
188
  enablePinning: true,
170
189
  enableHiding: !!columnVisibility,
171
190
  enableRowSelection: !!rowSelection,
191
+ getRowCanExpand: canExpand,
172
192
  autoResetPageIndex: false,
173
193
 
174
194
  columnResizeMode: 'onChange',
@@ -180,6 +200,7 @@ export function Table<DataType extends object>({
180
200
  onColumnOrderChange: setColumnOrder,
181
201
  onColumnPinningChange: setColumnPinning,
182
202
  onRowSelectionChange: rowSelection ? rowSelection?.setRowSelectionState : undefined,
203
+ onExpandedChange: setExpanded,
183
204
 
184
205
  initialState: {
185
206
  columnVisibility,
@@ -194,6 +215,7 @@ export function Table<DataType extends object>({
194
215
  state: {
195
216
  columnVisibility,
196
217
  columnOrder,
218
+ expanded,
197
219
  sorting: sorting.sortingState,
198
220
  columnFilters: columnFiltering.columnFiltersState,
199
221
  globalFilter: columnSearch.columnSearchState,
@@ -204,6 +226,7 @@ export function Table<DataType extends object>({
204
226
  });
205
227
 
206
228
  const tableHasNoData = isLoading === false && table.getRowModel().rows.length === 0;
229
+ const tableHasData = isLoading === false && table.getRowModel().rows.length !== 0;
207
230
 
208
231
  // rowSelection.rowSelectionState has the following shape: { [rowId: string]: boolean }
209
232
  const hasRowSelection = useMemo(() => {
@@ -254,7 +277,7 @@ export function Table<DataType extends object>({
254
277
  <thead>
255
278
  {table.getHeaderGroups().map((headerGroup) => (
256
279
  <tr key={headerGroup.id}>
257
- {rowSelection && table.getRowModel().rows.length !== 0 && !isLoading && (
280
+ {rowSelection && tableHasData && (
258
281
  <Th
259
282
  isActive={false}
260
283
  isRight={false}
@@ -276,6 +299,17 @@ export function Table<DataType extends object>({
276
299
  </div>
277
300
  </Th>
278
301
  )}
302
+ {rowsMightExpand && tableHasData && (
303
+ <Th
304
+ isActive={false}
305
+ isRight={false}
306
+ isDragging={false}
307
+ canDrag={false}
308
+ isRowSelection={true}
309
+ width={15}
310
+ />
311
+ )}
312
+
279
313
  {headerGroup.headers.map((header) => (
280
314
  <ColumnHeader
281
315
  header={header}
@@ -291,7 +325,7 @@ export function Table<DataType extends object>({
291
325
  {/* loading state */}
292
326
  {isLoading && (
293
327
  <tr>
294
- <td colSpan={table.getAllColumns().length + ROW_SELECTION_COL_SPAN}>
328
+ <td colSpan={table.getAllColumns().length + ROW_SELECTION_COL_SPAN + EXPAND_ROW_COL_SPAN}>
295
329
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '15px' }}>
296
330
  <Spinner size="small" />
297
331
  </div>
@@ -302,7 +336,7 @@ export function Table<DataType extends object>({
302
336
  {/* empty state */}
303
337
  {tableHasNoData && (
304
338
  <tr>
305
- <td colSpan={table.getAllColumns().length + ROW_SELECTION_COL_SPAN}>
339
+ <td colSpan={table.getAllColumns().length + ROW_SELECTION_COL_SPAN + EXPAND_ROW_COL_SPAN}>
306
340
  <div style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
307
341
  <Empty
308
342
  header=""
@@ -323,30 +357,51 @@ export function Table<DataType extends object>({
323
357
 
324
358
  {!isLoading &&
325
359
  table.getRowModel().rows.map((row) => (
326
- <tr key={row.id}>
327
- {row.getCanSelect() && (
328
- <td style={{ paddingRight: '10px', width: '15px' }}>
329
- <CheckBox
330
- value={row.getIsSelected()}
331
- id={row.id}
332
- name={row.id}
333
- hasError={false}
334
- disabled={!row.getCanSelect()}
335
- onChange={() => row.toggleSelected()}
336
- hasDescription={false}
337
- size="small"
338
- />
339
- </td>
340
- )}
341
- {row.getVisibleCells().map(({ column, id, getContext }) => (
342
- <td key={id}>{flexRender(column.columnDef.cell, getContext())}</td>
343
- ))}
344
- {row.getIsExpanded() && (
345
- <tr>
346
- <td colSpan={table.getVisibleLeafColumns().length} />
347
- </tr>
348
- )}
349
- </tr>
360
+ <>
361
+ <tr key={row.id}>
362
+ {row.getCanExpand() ? (
363
+ <td style={{ paddingRight: '10px', width: '15px' }}>
364
+ {row.getIsExpanded() ? (
365
+ <IconButton
366
+ size="tiny"
367
+ icon={<CollapseIcon />}
368
+ ariaLabel="Collapse expanded row"
369
+ onClick={() => row.toggleExpanded(false)}
370
+ />
371
+ ) : (
372
+ <IconButton
373
+ size="tiny"
374
+ icon={<ExpandIcon />}
375
+ ariaLabel="expand row"
376
+ onClick={() => row.toggleExpanded(true)}
377
+ />
378
+ )}
379
+ </td>
380
+ ) : rowsMightExpand ? (
381
+ <td />
382
+ ) : (
383
+ <></>
384
+ )}
385
+ {row.getCanSelect() && (
386
+ <td style={{ paddingRight: '10px', width: '15px' }}>
387
+ <CheckBox
388
+ value={row.getIsSelected()}
389
+ id={row.id}
390
+ name={row.id}
391
+ hasError={false}
392
+ disabled={!row.getCanSelect()}
393
+ onChange={() => row.toggleSelected()}
394
+ hasDescription={false}
395
+ size="small"
396
+ />
397
+ </td>
398
+ )}
399
+ {row.getVisibleCells().map(({ column, id, getContext }) => (
400
+ <td key={id}>{flexRender(column.columnDef.cell, getContext())}</td>
401
+ ))}
402
+ </tr>
403
+ {row.getIsExpanded() && renderDetailPanel!(row)}
404
+ </>
350
405
  ))}
351
406
  </tbody>
352
407
 
@@ -355,9 +410,15 @@ export function Table<DataType extends object>({
355
410
  <tr>
356
411
  {/* This is the row selection */}
357
412
  {ROW_SELECTION_COL_SPAN ? <td colSpan={1} /> : null}
413
+ {/* This is for the row expansion icon */}
414
+ {EXPAND_ROW_COL_SPAN ? <td colSpan={1} /> : null}
358
415
  {pagination && (
359
416
  <>
360
- <td colSpan={table.getVisibleLeafColumns().length - 3 - ROW_SELECTION_COL_SPAN} />
417
+ <td
418
+ colSpan={
419
+ table.getVisibleLeafColumns().length - 3 - ROW_SELECTION_COL_SPAN - EXPAND_ROW_COL_SPAN
420
+ }
421
+ />
361
422
  <td colSpan={1}>
362
423
  <span>
363
424
  showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}-
@@ -46,6 +46,27 @@ export const StyledTable = styled.table<{ density: Density }>`
46
46
  border-bottom: none;
47
47
  }
48
48
 
49
+ tr.subrow {
50
+ border-bottom: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
51
+
52
+ &:nth-child(even) {
53
+ background-color: ${({ theme }) => theme.colors.background};
54
+ }
55
+
56
+ &:nth-child(odd) {
57
+ background-color: ${({ theme }) => theme.colors.backgroundAlt};
58
+ }
59
+
60
+ td {
61
+ &:first-child {
62
+ padding-left: 0;
63
+ padding-bottom: 0;
64
+ }
65
+ padding: ${({ theme, density }) =>
66
+ density === 'tight' ? `${theme.spacing['0_5']} 0` : `${theme.spacing['0_5']} 0`};
67
+ }
68
+ }
69
+
49
70
  td {
50
71
  border-right: none;
51
72
  border-bottom: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
@@ -14,6 +14,7 @@ export const PageSizeSelect: FC<PageSizeSelectProps> = ({ onPageSizeChange, page
14
14
  id="page-size"
15
15
  multiple={false}
16
16
  name="pageSize"
17
+ inPortal={true}
17
18
  value={pageSize.toString() || '10'}
18
19
  onChange={onPageSizeChange}
19
20
  render={(selectedItems) => {
@@ -128,7 +128,7 @@ const defaultListItems = [
128
128
  icon: <AiOutlineMenu />,
129
129
  title: 'Api reference',
130
130
  description: 'A complete API reference for our libraries',
131
- to: 'https://api.stg.takaro.dev/api.html',
131
+ to: 'https://api.takaro.io/api.html',
132
132
  },
133
133
  /*
134
134
  {
@@ -29,6 +29,7 @@ export const ControlledDatePicker: FC<ControlledDatePickerProps> = (props) => {
29
29
  description,
30
30
  allowFutureDates = true,
31
31
  allowPastDates = true,
32
+ canClear,
32
33
  customDateFilter,
33
34
  } = defaultsApplier(props);
34
35
 
@@ -91,6 +92,7 @@ export const ControlledDatePicker: FC<ControlledDatePickerProps> = (props) => {
91
92
  format={format}
92
93
  placeholder={placeholder}
93
94
  mode={mode}
95
+ canClear={canClear}
94
96
  />
95
97
  )}
96
98
  {showError && error?.message && <ErrorMessage message={error.message} />}
@@ -253,7 +253,7 @@ export const AbsoluteSubmit = () => {
253
253
  name="date"
254
254
  required={false}
255
255
  loading={false}
256
- description={'The role will be automatically removed after this date'}
256
+ description={'The role will be automatically removed after this date.'}
257
257
  popOverPlacement={'bottom'}
258
258
  timePickerOptions={{ interval: 30 }}
259
259
  allowPastDates={false}
@@ -1,5 +1,5 @@
1
- import { FC, useLayoutEffect, useMemo, useState } from 'react';
2
- import { Button, Popover } from '../../../../components';
1
+ import { FC, MouseEvent, useLayoutEffect, useMemo, useState } from 'react';
2
+ import { Button, IconButton, Popover } from '../../../../components';
3
3
  import { DateTime, DateTimeFormatOptions, Settings } from 'luxon';
4
4
  import { dateFormats, timeFormats } from './formats';
5
5
  import { GenericInputProps } from '../../InputProps';
@@ -8,6 +8,7 @@ import { TimePicker } from '../subcomponents/TimePicker';
8
8
  import { Calendar } from '../subcomponents/Calendar';
9
9
  import { RelativePicker, timeDirection } from '../subcomponents/RelativePicker';
10
10
  import { Placement } from '@floating-ui/react';
11
+ import { AiOutlineClose as ClearIcon } from 'react-icons/ai';
11
12
 
12
13
  interface TimePickerOptions {
13
14
  /// Determines the interval between time options
@@ -51,6 +52,9 @@ export interface DatePickerProps {
51
52
 
52
53
  /// Placeholder text for the input
53
54
  placeholder?: string;
55
+
56
+ /// Can set field back to undefined
57
+ canClear?: boolean;
54
58
  }
55
59
 
56
60
  export type GenericDatePickerProps = GenericInputProps<string, HTMLInputElement> & DatePickerProps;
@@ -71,6 +75,7 @@ export const GenericDatePicker: FC<GenericDatePickerProps> = ({
71
75
  format = DateTime.DATE_SHORT,
72
76
  allowPastDates = true,
73
77
  allowFutureDates = true,
78
+ canClear = false,
74
79
  customDateFilter,
75
80
  mode,
76
81
  }) => {
@@ -172,11 +177,22 @@ export const GenericDatePicker: FC<GenericDatePickerProps> = ({
172
177
  return renderPlaceholder();
173
178
  };
174
179
 
180
+ const handleClear = (e: MouseEvent) => {
181
+ e.preventDefault();
182
+ e.stopPropagation();
183
+ if (onChange) {
184
+ onChange(null as any);
185
+ }
186
+ };
187
+
175
188
  return (
176
189
  <Popover placement={popOverPlacement} open={open} onOpenChange={setOpen}>
177
- <Popover.Trigger asChild>
190
+ <Popover.Trigger asChild readOnly={readOnly}>
178
191
  <ResultContainer readOnly={readOnly} hasError={hasError} onClick={() => setOpen(!open)}>
179
- {renderResult()}
192
+ <span>{renderResult()}</span>
193
+ {!readOnly && canClear && value && !open && (
194
+ <IconButton size="tiny" icon={<ClearIcon />} ariaLabel="clear" onClick={handleClear} />
195
+ )}
180
196
  </ResultContainer>
181
197
  </Popover.Trigger>
182
198
  <Popover.Content>
@@ -18,10 +18,9 @@ export const ResultContainer = styled.div<{ readOnly: boolean; hasError: boolean
18
18
  z-index: ${({ theme }) => theme.zIndex.dropdown};
19
19
  cursor: ${({ readOnly }) => (readOnly ? 'not-allowed' : 'pointer')};
20
20
  user-select: none;
21
-
22
- span {
23
- color: ${({ theme }) => theme.colors.primary};
24
- }
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: space-between;
25
24
  `;
26
25
 
27
26
  export const ContentContainer = styled.div`
@@ -1,3 +1,10 @@
1
+ export interface PaginationProps {
2
+ isFetching: boolean;
3
+ hasNextPage: boolean;
4
+ isFetchingNextPage: boolean;
5
+ fetchNextPage: () => void;
6
+ }
7
+
1
8
  export { ControlledCheckBox as CheckBox } from './CheckBox';
2
9
  export type { ControlledCheckBoxProps as CheckBoxProps } from './CheckBox';
3
10
  export { GenericCheckBox as UnControlledCheckBox } from './CheckBox/Generic';
@@ -29,6 +29,11 @@ export const ControlledSelectQueryField: FC<ControlledSelectQueryFieldProps> & S
29
29
  inPortal,
30
30
  debounce,
31
31
  isLoadingData,
32
+ hasNextPage,
33
+ fetchNextPage,
34
+ isFetching,
35
+ isFetchingNextPage,
36
+ optionCount,
32
37
  handleInputValueChange,
33
38
  } = defaultsApplier(props);
34
39
 
@@ -85,7 +90,12 @@ export const ControlledSelectQueryField: FC<ControlledSelectQueryFieldProps> & S
85
90
  onFocus={handleOnFocus}
86
91
  value={field.value}
87
92
  debounce={debounce}
93
+ optionCount={optionCount}
88
94
  handleInputValueChange={handleInputValueChange}
95
+ isFetching={isFetching}
96
+ isFetchingNextPage={isFetchingNextPage}
97
+ hasNextPage={hasNextPage}
98
+ fetchNextPage={fetchNextPage}
89
99
  >
90
100
  {children}
91
101
  </GenericSelectQueryField>
@@ -28,14 +28,15 @@ import { useDebounce } from '../../../../../hooks';
28
28
  import { setAriaDescribedBy } from '../../../layout';
29
29
  import { FeedBackContainer } from '../style';
30
30
  import { SelectItem, SelectContext, getLabelFromChildren } from '../../';
31
+ import { PaginationProps } from '../../../';
31
32
 
32
33
  /* The SearchField depends on a few things of <Select/> */
33
34
  import { GroupLabel } from '../../SelectField/style';
34
35
  import { SelectContainer, SelectButton, StyledArrowIcon, StyledFloatingOverlay } from '../../sharedStyle';
35
- import { IconButton, Spinner } from '../../../../../components';
36
+ import { IconButton, InfiniteScroll, Spinner } from '../../../../../components';
36
37
  import { GenericTextField } from '../../../TextField/Generic';
37
38
 
38
- interface SharedSelectQueryFieldProps {
39
+ interface SharedSelectQueryFieldProps extends PaginationProps {
39
40
  // Enables loading data feedback for user
40
41
  isLoadingData?: boolean;
41
42
  /// The placeholder text to show when the input is empty
@@ -53,6 +54,9 @@ interface SharedSelectQueryFieldProps {
53
54
 
54
55
  /// The selected items shown in the select field
55
56
  render?: (selectedItems: SelectItem[]) => React.ReactNode;
57
+
58
+ /// The total options that will be visible when fully loaded
59
+ optionCount?: number;
56
60
  }
57
61
 
58
62
  interface SingleSelectQueryFieldProps extends SharedSelectQueryFieldProps {
@@ -100,12 +104,17 @@ export const GenericSelectQueryField = forwardRef<HTMLInputElement, GenericSelec
100
104
  hasError,
101
105
  children,
102
106
  readOnly,
107
+ isFetchingNextPage,
108
+ isFetching,
109
+ fetchNextPage,
110
+ hasNextPage,
103
111
  render,
104
112
  multiple = false,
105
113
  canClear = false,
106
114
  debounce = 250,
107
115
  isLoadingData: isLoading = false,
108
116
  handleInputValueChange,
117
+ optionCount,
109
118
  } = defaultsApplier(props);
110
119
 
111
120
  const [open, setOpen] = useState<boolean>(false);
@@ -201,6 +210,7 @@ export const GenericSelectQueryField = forwardRef<HTMLInputElement, GenericSelec
201
210
  name={`${name}-input`}
202
211
  hasDescription={false}
203
212
  icon={<SearchIcon />}
213
+ suffix={isLoading ? 'Loading' : optionCount !== undefined ? `Result: ${optionCount}` : undefined}
204
214
  hasError={hasError}
205
215
  value={inputValue.value}
206
216
  onChange={onInputChange}
@@ -215,6 +225,14 @@ export const GenericSelectQueryField = forwardRef<HTMLInputElement, GenericSelec
215
225
  </FeedBackContainer>
216
226
  )}
217
227
  {hasOptions && options}
228
+ {hasOptions && !isLoading && (
229
+ <InfiniteScroll
230
+ isFetching={isFetching}
231
+ hasNextPage={hasNextPage}
232
+ fetchNextPage={fetchNextPage}
233
+ isFetchingNextPage={isFetchingNextPage}
234
+ />
235
+ )}
218
236
  {/* Basically first interaction */}
219
237
  {!hasOptions && inputValue.value === '' && <FeedBackContainer>Start typing to search</FeedBackContainer>}
220
238
  {/* When there is no result */}
@@ -69,6 +69,11 @@ export const ServerSideSubmit: StoryFn<SelectQueryFieldProps> = (args) => {
69
69
  handleInputValueChange={mockAPICall}
70
70
  isLoadingData={loading}
71
71
  required={false}
72
+ hasNextPage={false}
73
+ optionCount={10}
74
+ isFetching={false}
75
+ isFetchingNextPage={false}
76
+ fetchNextPage={() => {}}
72
77
  name="film"
73
78
  >
74
79
  {/* In this case the label is the same as the value but ofcourse that can differ*/}
@@ -124,6 +129,11 @@ export const ClientSideSubmit: StoryFn<SelectQueryFieldProps> = (args) => {
124
129
  handleInputValueChange={handleInputChange}
125
130
  required={false}
126
131
  debounce={0}
132
+ hasNextPage={false}
133
+ optionCount={10}
134
+ isFetching={false}
135
+ isFetchingNextPage={false}
136
+ fetchNextPage={() => {}}
127
137
  name="film"
128
138
  >
129
139
  {/* In this case the label is the same as the value but ofcourse that can differ*/}
@@ -186,6 +196,11 @@ export const ClientSideMultiSelectSubmit: StoryFn<SelectQueryFieldProps> = (args
186
196
  required={false}
187
197
  canClear={args.canClear}
188
198
  debounce={0}
199
+ hasNextPage={false}
200
+ optionCount={10}
201
+ isFetching={false}
202
+ isFetchingNextPage={false}
203
+ fetchNextPage={() => {}}
189
204
  multiple
190
205
  name="films"
191
206
  >
@@ -240,6 +255,11 @@ export const Generic: StoryFn<SelectQueryFieldProps> = () => {
240
255
  hasDescription={false}
241
256
  value={result}
242
257
  name="film"
258
+ hasNextPage={false}
259
+ optionCount={10}
260
+ isFetching={false}
261
+ isFetchingNextPage={false}
262
+ fetchNextPage={() => {}}
243
263
  >
244
264
  {/* In this case the label is the same as the value but ofcourse that can differ*/}
245
265
  <SelectQueryField.OptionGroup>
@@ -6,10 +6,12 @@ export interface OptionGroupProps extends PropsWithChildren {
6
6
  }
7
7
 
8
8
  export const OptionGroup: FC<OptionGroupProps> = ({ children, label }) => {
9
- /* Currently this is actually never rendered. in Select.index.tsx the OptionGroup is built based on the props*/
9
+ {
10
+ /* This is actually never rendered, the optiongroup is build in the select fields themself*/
11
+ }
10
12
  return (
11
13
  <li>
12
- {label} && <div>{label}</div>
14
+ {label && <div>{label}</div>}
13
15
  <ul>{children}</ul>
14
16
  </li>
15
17
  );
@@ -1,7 +1,7 @@
1
1
  import { FC, PropsWithChildren, ReactElement, useState } from 'react';
2
2
  import { styled } from '../../../styled';
3
3
  import { IoMdArrowDropup as ArrowUp } from 'react-icons/io';
4
- import { AnimatePresence, motion } from 'framer-motion';
4
+ import { motion } from 'framer-motion';
5
5
  import { useTheme } from '../../../hooks';
6
6
 
7
7
  const StyledList = styled.div`
@@ -78,36 +78,31 @@ const Item: FC<PropsWithChildren<ItemProps>> = ({ collapsed = false, title, chil
78
78
  <ArrowUp size={18} />
79
79
  </Header>
80
80
  {description && <p>{description}</p>}
81
- <AnimatePresence>
82
- {!isCollapsed && (
83
- <motion.div
84
- style={{ maxHeight: '100%', overflowY: 'hidden' }}
85
- key={`collapse-item-${title}`}
86
- variants={{
87
- open: { opacity: 1, height: 'auto', flexGrow: 1, minHeight: 0, overflowY: 'auto' },
88
- collapsed: { opacity: 0, height: 0 },
89
- }}
90
- initial="collapsed"
91
- animate="open"
92
- exit="collapsed"
93
- transition={{ duration: 0.125, ease: 'linear' }}
94
- >
95
- <motion.div
96
- variants={{
97
- open: { y: 0 },
98
- collapsed: { y: -6 },
99
- }}
100
- transition={{ duration: 0.125, ease: 'linear' }}
101
- style={{
102
- transformOrigin: 'top center',
103
- padding: `${theme.spacing['0_75']} ${theme.spacing['0_5']} ${theme.spacing['1_5']} ${theme.spacing['1']}`,
104
- }}
105
- >
106
- {children}
107
- </motion.div>
108
- </motion.div>
109
- )}
110
- </AnimatePresence>
81
+ <motion.div
82
+ style={{ maxHeight: '100%', overflowY: 'hidden' }}
83
+ key={`collapse-item-${title}`}
84
+ variants={{
85
+ open: { opacity: 1, height: 'auto', flexGrow: 1, minHeight: 0, overflowY: 'auto', visibility: 'visible' },
86
+ collapsed: { opacity: 0, height: 0, visibility: 'hidden' },
87
+ }}
88
+ initial="collapsed"
89
+ animate={isCollapsed ? 'collapsed' : 'open'}
90
+ transition={{ duration: 0.125, ease: 'linear' }}
91
+ >
92
+ <motion.div
93
+ variants={{
94
+ open: { y: 0 },
95
+ collapsed: { y: -6 },
96
+ }}
97
+ transition={{ duration: 0.125, ease: 'linear' }}
98
+ style={{
99
+ transformOrigin: 'top center',
100
+ padding: `${theme.spacing['0_75']} ${theme.spacing['0_5']} ${theme.spacing['1_5']} ${theme.spacing['1']}`,
101
+ }}
102
+ >
103
+ {children}
104
+ </motion.div>
105
+ </motion.div>
111
106
  </div>
112
107
  );
113
108
  };