@arbor-education/design-system.components 0.3.2 → 0.3.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/formField/fieldset/Fieldset.d.ts +1 -1
  3. package/dist/components/formField/fieldset/Fieldset.d.ts.map +1 -1
  4. package/dist/components/formField/fieldset/Fieldset.js +1 -1
  5. package/dist/components/formField/fieldset/Fieldset.js.map +1 -1
  6. package/dist/components/formField/inputs/number/NumberInput.d.ts.map +1 -1
  7. package/dist/components/formField/inputs/number/NumberInput.js +21 -12
  8. package/dist/components/formField/inputs/number/NumberInput.js.map +1 -1
  9. package/dist/components/formField/inputs/number/NumberInput.test.js +92 -3
  10. package/dist/components/formField/inputs/number/NumberInput.test.js.map +1 -1
  11. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
  12. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +2 -2
  13. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
  14. package/dist/components/pill/Pill.d.ts +2 -1
  15. package/dist/components/pill/Pill.d.ts.map +1 -1
  16. package/dist/components/pill/Pill.js +2 -2
  17. package/dist/components/pill/Pill.js.map +1 -1
  18. package/dist/components/pill/Pill.stories.d.ts +8 -0
  19. package/dist/components/pill/Pill.stories.d.ts.map +1 -1
  20. package/dist/components/pill/Pill.stories.js +8 -0
  21. package/dist/components/pill/Pill.stories.js.map +1 -1
  22. package/dist/components/pill/Pill.test.js +9 -0
  23. package/dist/components/pill/Pill.test.js.map +1 -1
  24. package/dist/components/table/DSDefaultColDef.d.ts.map +1 -1
  25. package/dist/components/table/DSDefaultColDef.js +32 -2
  26. package/dist/components/table/DSDefaultColDef.js.map +1 -1
  27. package/dist/components/table/Table.d.ts.map +1 -1
  28. package/dist/components/table/Table.js +5 -1
  29. package/dist/components/table/Table.js.map +1 -1
  30. package/dist/components/table/Table.stories.d.ts.map +1 -1
  31. package/dist/components/table/Table.stories.js +17 -16
  32. package/dist/components/table/Table.stories.js.map +1 -1
  33. package/dist/components/table/Table.test.js +15 -27
  34. package/dist/components/table/Table.test.js.map +1 -1
  35. package/dist/components/table/columnFilters/BooleanFilter/BooleanFilter.d.ts +13 -0
  36. package/dist/components/table/columnFilters/BooleanFilter/BooleanFilter.d.ts.map +1 -0
  37. package/dist/components/table/columnFilters/BooleanFilter/BooleanFilter.js +68 -0
  38. package/dist/components/table/columnFilters/BooleanFilter/BooleanFilter.js.map +1 -0
  39. package/dist/components/table/columnFilters/BooleanFilter/BooleanFilter.test.d.ts +2 -0
  40. package/dist/components/table/columnFilters/BooleanFilter/BooleanFilter.test.d.ts.map +1 -0
  41. package/dist/components/table/columnFilters/BooleanFilter/BooleanFilter.test.js +87 -0
  42. package/dist/components/table/columnFilters/BooleanFilter/BooleanFilter.test.js.map +1 -0
  43. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.d.ts +14 -0
  44. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.d.ts.map +1 -0
  45. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.js +73 -0
  46. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.js.map +1 -0
  47. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.test.d.ts +2 -0
  48. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.test.d.ts.map +1 -0
  49. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.test.js +102 -0
  50. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.test.js.map +1 -0
  51. package/dist/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.d.ts +8 -0
  52. package/dist/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.d.ts.map +1 -0
  53. package/dist/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.js +21 -0
  54. package/dist/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.js.map +1 -0
  55. package/dist/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.test.d.ts +2 -0
  56. package/dist/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.test.d.ts.map +1 -0
  57. package/dist/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.test.js +129 -0
  58. package/dist/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.test.js.map +1 -0
  59. package/dist/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.d.ts +2 -0
  60. package/dist/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.d.ts.map +1 -0
  61. package/dist/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.js +18 -0
  62. package/dist/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.js.map +1 -0
  63. package/dist/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.test.d.ts +2 -0
  64. package/dist/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.test.d.ts.map +1 -0
  65. package/dist/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.test.js +100 -0
  66. package/dist/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.test.js.map +1 -0
  67. package/dist/components/table/columnFilters/filterResetButton/FilterResetButton.d.ts +7 -0
  68. package/dist/components/table/columnFilters/filterResetButton/FilterResetButton.d.ts.map +1 -0
  69. package/dist/components/table/columnFilters/filterResetButton/FilterResetButton.js +7 -0
  70. package/dist/components/table/columnFilters/filterResetButton/FilterResetButton.js.map +1 -0
  71. package/dist/index.css +70 -1
  72. package/dist/index.css.map +1 -1
  73. package/package.json +2 -1
  74. package/src/components/formField/fieldset/Fieldset.tsx +2 -2
  75. package/src/components/formField/inputs/number/NumberInput.test.tsx +113 -3
  76. package/src/components/formField/inputs/number/NumberInput.tsx +26 -15
  77. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +14 -2
  78. package/src/components/pill/Pill.stories.tsx +9 -0
  79. package/src/components/pill/Pill.test.tsx +10 -0
  80. package/src/components/pill/Pill.tsx +3 -2
  81. package/src/components/table/DSDefaultColDef.ts +34 -4
  82. package/src/components/table/Table.stories.tsx +20 -16
  83. package/src/components/table/Table.test.tsx +12 -28
  84. package/src/components/table/Table.tsx +5 -0
  85. package/src/components/table/columnFilters/BooleanFilter/BooleanFilter.test.tsx +102 -0
  86. package/src/components/table/columnFilters/BooleanFilter/BooleanFilter.tsx +104 -0
  87. package/src/components/table/columnFilters/TimeFilter/TimeFilter.test.tsx +131 -0
  88. package/src/components/table/columnFilters/TimeFilter/TimeFilter.tsx +122 -0
  89. package/src/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.test.ts +183 -0
  90. package/src/components/table/columnFilters/TimeFilter/utils/compareTimeByFilterType.ts +30 -0
  91. package/src/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.test.ts +133 -0
  92. package/src/components/table/columnFilters/TimeFilter/utils/transformTimeStringToDate.ts +21 -0
  93. package/src/components/table/columnFilters/columnFilters.scss +71 -0
  94. package/src/components/table/columnFilters/filterResetButton/FilterResetButton.tsx +21 -0
  95. package/src/components/table/table.scss +1 -1
  96. package/src/index.scss +1 -0
@@ -0,0 +1,122 @@
1
+ import type { DoesFilterPassParams } from 'ag-grid-community';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { compareTimeByFilterType, type FilterType, FILTER_TYPES } from './utils/compareTimeByFilterType';
4
+ import { transformTimeStringToDate } from './utils/transformTimeStringToDate';
5
+ import { FilterResetButton } from '../filterResetButton/FilterResetButton';
6
+ import { SelectDropdown } from 'Components/formField/inputs/selectDropdown/SelectDropdown';
7
+ import { PopupParentContext } from 'Utils/PopupParentContext';
8
+
9
+ type TimeFilterModel = {
10
+ filterType: FilterType;
11
+ pivot: string;
12
+ };
13
+
14
+ export const doesTimeFilterPass: ({
15
+ model,
16
+ node,
17
+ handlerParams,
18
+ }: DoesFilterPassParams<TimeFilterModel>) => boolean = ({ model, node, handlerParams }) => {
19
+ if (!model || !model.pivot) {
20
+ return true;
21
+ }
22
+
23
+ const pivotDate = transformTimeStringToDate(model.pivot);
24
+ if (pivotDate === null) {
25
+ return false;
26
+ }
27
+
28
+ const value = handlerParams.getValue(node);
29
+ if (value == null) {
30
+ return false;
31
+ }
32
+
33
+ const dateToCheck = cellValueToDate(value);
34
+ if (dateToCheck === null) {
35
+ return false;
36
+ }
37
+
38
+ return compareTimeByFilterType(model.filterType, pivotDate, dateToCheck);
39
+ };
40
+
41
+ function cellValueToDate(value: unknown): Date | null {
42
+ if (value instanceof Date) {
43
+ return Number.isNaN(value.getTime()) ? null : value;
44
+ }
45
+ if (typeof value === 'string') {
46
+ const fromTime = transformTimeStringToDate(value);
47
+ if (fromTime !== null) return fromTime;
48
+ const parsed = new Date(value);
49
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ type TimeFilterProps = {
55
+ model: TimeFilterModel | null;
56
+ onModelChange: (model: TimeFilterModel) => void;
57
+ };
58
+
59
+ export const TimeFilter = (props: TimeFilterProps) => {
60
+ const { model, onModelChange } = props;
61
+
62
+ const { filterType: initialFilterType, pivot: initialPivot } = model ?? { filterType: FILTER_TYPES.BEFORE, pivot: '' };
63
+
64
+ const [filterType, setFilterType] = useState<FilterType>(initialFilterType);
65
+ const [pivot, setPivot] = useState(initialPivot);
66
+
67
+ const onModelChangeRef = useRef(onModelChange);
68
+ onModelChangeRef.current = onModelChange;
69
+
70
+ useEffect(() => {
71
+ setFilterType(initialFilterType);
72
+ setPivot(initialPivot);
73
+ }, [initialFilterType, initialPivot]);
74
+
75
+ const handleFilterTypeChange = (value: string[]) => {
76
+ setFilterType(value[0] as FilterType);
77
+ };
78
+
79
+ const handlePivotChange = (e: React.ChangeEvent<HTMLInputElement>) => {
80
+ setPivot(e.target.value);
81
+ };
82
+
83
+ const handleResetClick = () => {
84
+ setFilterType(FILTER_TYPES.BEFORE);
85
+ setPivot('');
86
+ };
87
+
88
+ useEffect(() => {
89
+ if (filterType === initialFilterType && pivot === initialPivot) {
90
+ return;
91
+ }
92
+ onModelChangeRef.current({ filterType, pivot });
93
+ }, [filterType, pivot, initialFilterType, initialPivot]);
94
+
95
+ const popupParentRef = useRef<HTMLDivElement>(null);
96
+
97
+ return (
98
+ <PopupParentContext.Provider value={popupParentRef}>
99
+ <div className="ds-column-filter__container ds-column-filter-time" ref={popupParentRef}>
100
+ <div className="ds-column-filter-time__row">
101
+ <SelectDropdown
102
+ key={filterType}
103
+ options={[
104
+ { label: 'Before', value: FILTER_TYPES.BEFORE },
105
+ { label: 'After', value: FILTER_TYPES.AFTER },
106
+ { label: 'At', value: FILTER_TYPES.EQUAL },
107
+ ]}
108
+ onSelectionChange={handleFilterTypeChange}
109
+ initialSelectedValues={[filterType]}
110
+ />
111
+ <input
112
+ className="ds-column-filter-time__input"
113
+ onChange={handlePivotChange}
114
+ type="time"
115
+ value={pivot}
116
+ />
117
+ </div>
118
+ <FilterResetButton onClick={handleResetClick}>Reset time</FilterResetButton>
119
+ </div>
120
+ </PopupParentContext.Provider>
121
+ );
122
+ };
@@ -0,0 +1,183 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { compareTimeByFilterType, FILTER_TYPES, type FilterType } from './compareTimeByFilterType';
3
+
4
+ describe('compareTimeByFilterType', () => {
5
+ const createDateWithTime = (hours: number, minutes: number): Date => {
6
+ const date = new Date();
7
+ date.setHours(hours, minutes, 0, 0);
8
+ return date;
9
+ };
10
+
11
+ describe('BEFORE filter type', () => {
12
+ test('returns true when check time is before pivot time', () => {
13
+ const pivotDate = createDateWithTime(14, 30);
14
+ const dateToCheck = createDateWithTime(10, 0);
15
+
16
+ const result = compareTimeByFilterType(FILTER_TYPES.BEFORE, pivotDate, dateToCheck);
17
+
18
+ expect(result).toBe(true);
19
+ });
20
+
21
+ test('returns false when check time is after pivot time', () => {
22
+ const pivotDate = createDateWithTime(14, 30);
23
+ const dateToCheck = createDateWithTime(16, 0);
24
+
25
+ const result = compareTimeByFilterType(FILTER_TYPES.BEFORE, pivotDate, dateToCheck);
26
+
27
+ expect(result).toBe(false);
28
+ });
29
+
30
+ test('returns false when check time equals pivot time', () => {
31
+ const pivotDate = createDateWithTime(14, 30);
32
+ const dateToCheck = createDateWithTime(14, 30);
33
+
34
+ const result = compareTimeByFilterType(FILTER_TYPES.BEFORE, pivotDate, dateToCheck);
35
+
36
+ expect(result).toBe(false);
37
+ });
38
+
39
+ test('returns true when check time is one minute before pivot', () => {
40
+ const pivotDate = createDateWithTime(14, 30);
41
+ const dateToCheck = createDateWithTime(14, 29);
42
+
43
+ const result = compareTimeByFilterType(FILTER_TYPES.BEFORE, pivotDate, dateToCheck);
44
+
45
+ expect(result).toBe(true);
46
+ });
47
+ });
48
+
49
+ describe('AFTER filter type', () => {
50
+ test('returns true when check time is after pivot time', () => {
51
+ const pivotDate = createDateWithTime(14, 30);
52
+ const dateToCheck = createDateWithTime(16, 0);
53
+
54
+ const result = compareTimeByFilterType(FILTER_TYPES.AFTER, pivotDate, dateToCheck);
55
+
56
+ expect(result).toBe(true);
57
+ });
58
+
59
+ test('returns false when check time is before pivot time', () => {
60
+ const pivotDate = createDateWithTime(14, 30);
61
+ const dateToCheck = createDateWithTime(10, 0);
62
+
63
+ const result = compareTimeByFilterType(FILTER_TYPES.AFTER, pivotDate, dateToCheck);
64
+
65
+ expect(result).toBe(false);
66
+ });
67
+
68
+ test('returns false when check time equals pivot time', () => {
69
+ const pivotDate = createDateWithTime(14, 30);
70
+ const dateToCheck = createDateWithTime(14, 30);
71
+
72
+ const result = compareTimeByFilterType(FILTER_TYPES.AFTER, pivotDate, dateToCheck);
73
+
74
+ expect(result).toBe(false);
75
+ });
76
+
77
+ test('returns true when check time is one minute after pivot', () => {
78
+ const pivotDate = createDateWithTime(14, 30);
79
+ const dateToCheck = createDateWithTime(14, 31);
80
+
81
+ const result = compareTimeByFilterType(FILTER_TYPES.AFTER, pivotDate, dateToCheck);
82
+
83
+ expect(result).toBe(true);
84
+ });
85
+ });
86
+
87
+ describe('EQUAL filter type', () => {
88
+ test('returns true when check time equals pivot time', () => {
89
+ const pivotDate = createDateWithTime(14, 30);
90
+ const dateToCheck = createDateWithTime(14, 30);
91
+
92
+ const result = compareTimeByFilterType(FILTER_TYPES.EQUAL, pivotDate, dateToCheck);
93
+
94
+ expect(result).toBe(true);
95
+ });
96
+
97
+ test('returns false when check time is before pivot time', () => {
98
+ const pivotDate = createDateWithTime(14, 30);
99
+ const dateToCheck = createDateWithTime(10, 0);
100
+
101
+ const result = compareTimeByFilterType(FILTER_TYPES.EQUAL, pivotDate, dateToCheck);
102
+
103
+ expect(result).toBe(false);
104
+ });
105
+
106
+ test('returns false when check time is after pivot time', () => {
107
+ const pivotDate = createDateWithTime(14, 30);
108
+ const dateToCheck = createDateWithTime(16, 0);
109
+
110
+ const result = compareTimeByFilterType(FILTER_TYPES.EQUAL, pivotDate, dateToCheck);
111
+
112
+ expect(result).toBe(false);
113
+ });
114
+
115
+ test('returns false when check time differs by one minute', () => {
116
+ const pivotDate = createDateWithTime(14, 30);
117
+ const dateToCheck = createDateWithTime(14, 31);
118
+
119
+ const result = compareTimeByFilterType(FILTER_TYPES.EQUAL, pivotDate, dateToCheck);
120
+
121
+ expect(result).toBe(false);
122
+ });
123
+ });
124
+
125
+ describe('edge cases', () => {
126
+ test('handles midnight times correctly', () => {
127
+ const pivotDate = createDateWithTime(0, 0);
128
+ const dateToCheck = createDateWithTime(0, 1);
129
+
130
+ const result = compareTimeByFilterType(FILTER_TYPES.AFTER, pivotDate, dateToCheck);
131
+
132
+ expect(result).toBe(true);
133
+ });
134
+
135
+ test('handles end of day times correctly', () => {
136
+ const pivotDate = createDateWithTime(23, 59);
137
+ const dateToCheck = createDateWithTime(23, 58);
138
+
139
+ const result = compareTimeByFilterType(FILTER_TYPES.BEFORE, pivotDate, dateToCheck);
140
+
141
+ expect(result).toBe(true);
142
+ });
143
+
144
+ test('ignores date component and only compares time', () => {
145
+ const pivotDate = new Date(2024, 0, 1, 14, 30);
146
+ const dateToCheck = new Date(2024, 0, 15, 14, 30);
147
+
148
+ const result = compareTimeByFilterType(FILTER_TYPES.EQUAL, pivotDate, dateToCheck);
149
+
150
+ expect(result).toBe(true);
151
+ });
152
+
153
+ test('ignores seconds and milliseconds', () => {
154
+ const pivotDate = new Date(2024, 0, 1, 14, 30, 45, 500);
155
+ const dateToCheck = new Date(2024, 0, 1, 14, 30, 10, 100);
156
+
157
+ const result = compareTimeByFilterType(FILTER_TYPES.EQUAL, pivotDate, dateToCheck);
158
+
159
+ expect(result).toBe(true);
160
+ });
161
+
162
+ test('returns false for invalid filter type', () => {
163
+ const pivotDate = createDateWithTime(14, 30);
164
+ const dateToCheck = createDateWithTime(14, 30);
165
+
166
+ const result = compareTimeByFilterType('invalid' as FilterType, pivotDate, dateToCheck);
167
+
168
+ expect(result).toBe(false);
169
+ });
170
+ });
171
+
172
+ describe('cross-midnight comparisons', () => {
173
+ test('correctly compares times across midnight boundary', () => {
174
+ const pivotDate = createDateWithTime(23, 30);
175
+ const dateToCheck = createDateWithTime(0, 30);
176
+
177
+ // 00:30 (30 minutes) is before 23:30 (1410 minutes) in terms of minutes since midnight
178
+ const result = compareTimeByFilterType(FILTER_TYPES.BEFORE, pivotDate, dateToCheck);
179
+
180
+ expect(result).toBe(true);
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,30 @@
1
+ export type FilterType = 'before' | 'after' | 'equal';
2
+
3
+ export const FILTER_TYPES = {
4
+ BEFORE: 'before' as FilterType,
5
+ AFTER: 'after' as FilterType,
6
+ EQUAL: 'equal' as FilterType,
7
+ };
8
+
9
+ const minutesSinceMidnight = (date: Date): number =>
10
+ date.getHours() * 60 + date.getMinutes();
11
+
12
+ export const compareTimeByFilterType = (
13
+ filterType: FilterType,
14
+ pivotDate: Date,
15
+ dateToCheck: Date,
16
+ ): boolean => {
17
+ const pivotMinutes = minutesSinceMidnight(pivotDate);
18
+ const checkMinutes = minutesSinceMidnight(dateToCheck);
19
+
20
+ switch (filterType) {
21
+ case FILTER_TYPES.BEFORE:
22
+ return checkMinutes < pivotMinutes;
23
+ case FILTER_TYPES.AFTER:
24
+ return checkMinutes > pivotMinutes;
25
+ case FILTER_TYPES.EQUAL:
26
+ return checkMinutes === pivotMinutes;
27
+ default:
28
+ return false;
29
+ }
30
+ };
@@ -0,0 +1,133 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { transformTimeStringToDate } from './transformTimeStringToDate';
3
+
4
+ describe('transformTimeStringToDate', () => {
5
+ describe('valid time strings', () => {
6
+ test('converts valid time string to Date object', () => {
7
+ const result = transformTimeStringToDate('14:30');
8
+
9
+ expect(result).toBeInstanceOf(Date);
10
+ expect(result?.getHours()).toBe(14);
11
+ expect(result?.getMinutes()).toBe(30);
12
+ expect(result?.getSeconds()).toBe(0);
13
+ expect(result?.getMilliseconds()).toBe(0);
14
+ });
15
+
16
+ test('handles midnight (00:00)', () => {
17
+ const result = transformTimeStringToDate('00:00');
18
+
19
+ expect(result).toBeInstanceOf(Date);
20
+ expect(result?.getHours()).toBe(0);
21
+ expect(result?.getMinutes()).toBe(0);
22
+ });
23
+
24
+ test('handles noon (12:00)', () => {
25
+ const result = transformTimeStringToDate('12:00');
26
+
27
+ expect(result).toBeInstanceOf(Date);
28
+ expect(result?.getHours()).toBe(12);
29
+ expect(result?.getMinutes()).toBe(0);
30
+ });
31
+
32
+ test('handles end of day (23:59)', () => {
33
+ const result = transformTimeStringToDate('23:59');
34
+
35
+ expect(result).toBeInstanceOf(Date);
36
+ expect(result?.getHours()).toBe(23);
37
+ expect(result?.getMinutes()).toBe(59);
38
+ });
39
+
40
+ test('handles single digit hours with leading zero', () => {
41
+ const result = transformTimeStringToDate('09:45');
42
+
43
+ expect(result?.getHours()).toBe(9);
44
+ expect(result?.getMinutes()).toBe(45);
45
+ });
46
+
47
+ test('handles single digit minutes with leading zero', () => {
48
+ const result = transformTimeStringToDate('15:05');
49
+
50
+ expect(result?.getHours()).toBe(15);
51
+ expect(result?.getMinutes()).toBe(5);
52
+ });
53
+ });
54
+
55
+ describe('invalid time strings', () => {
56
+ test('returns null for empty string', () => {
57
+ const result = transformTimeStringToDate('');
58
+
59
+ expect(result).toBeNull();
60
+ });
61
+
62
+ test('returns null for string without colon', () => {
63
+ const result = transformTimeStringToDate('1430');
64
+
65
+ expect(result).toBeNull();
66
+ });
67
+
68
+ test('returns null for string with too many parts', () => {
69
+ const result = transformTimeStringToDate('14:30:00');
70
+
71
+ expect(result).toBeNull();
72
+ });
73
+
74
+ test('returns null for string with only one part', () => {
75
+ const result = transformTimeStringToDate('14');
76
+
77
+ expect(result).toBeNull();
78
+ });
79
+
80
+ test('returns null for non-numeric hours', () => {
81
+ const result = transformTimeStringToDate('abc:30');
82
+
83
+ expect(result).toBeNull();
84
+ });
85
+
86
+ test('returns null for non-numeric minutes', () => {
87
+ const result = transformTimeStringToDate('14:xyz');
88
+
89
+ expect(result).toBeNull();
90
+ });
91
+
92
+ test('returns null for invalid time format', () => {
93
+ const result = transformTimeStringToDate('not a time');
94
+
95
+ expect(result).toBeNull();
96
+ });
97
+
98
+ test('accepts time with spaces (Number trims whitespace)', () => {
99
+ const result = transformTimeStringToDate('14: 30');
100
+
101
+ expect(result).toBeInstanceOf(Date);
102
+ expect(result?.getHours()).toBe(14);
103
+ expect(result?.getMinutes()).toBe(30);
104
+ });
105
+ });
106
+
107
+ describe('edge cases', () => {
108
+ test('accepts hours and minutes that would be out of normal range', () => {
109
+ // Date object normalizes 25:70 to next day 02:10
110
+ const result = transformTimeStringToDate('25:70');
111
+
112
+ expect(result).toBeInstanceOf(Date);
113
+ expect(result?.getHours()).toBe(2);
114
+ expect(result?.getMinutes()).toBe(10);
115
+ });
116
+
117
+ test('handles zero padded values', () => {
118
+ const result = transformTimeStringToDate('00:00');
119
+
120
+ expect(result?.getHours()).toBe(0);
121
+ expect(result?.getMinutes()).toBe(0);
122
+ });
123
+
124
+ test('preserves current date while setting time', () => {
125
+ const today = new Date();
126
+ const result = transformTimeStringToDate('10:30');
127
+
128
+ expect(result?.getFullYear()).toBe(today.getFullYear());
129
+ expect(result?.getMonth()).toBe(today.getMonth());
130
+ expect(result?.getDate()).toBe(today.getDate());
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,21 @@
1
+ export const transformTimeStringToDate = (timeString: string): Date | null => {
2
+ if (!timeString) {
3
+ return null;
4
+ }
5
+
6
+ const parts = timeString.split(':');
7
+ if (parts.length !== 2) {
8
+ return null;
9
+ }
10
+
11
+ const hours = Number(parts[0]);
12
+ const minutes = Number(parts[1]);
13
+
14
+ if (isNaN(hours) || isNaN(minutes)) {
15
+ return null;
16
+ }
17
+
18
+ const date = new Date();
19
+ date.setHours(hours, minutes, 0, 0);
20
+ return date;
21
+ };
@@ -0,0 +1,71 @@
1
+ .ds-column-filter__container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ gap: var(--spacing-small);
6
+ padding: var(--spacing-small);
7
+
8
+ // This is needed to ensure the filter popup is displayed above the table
9
+ z-index: 1;
10
+ position: relative;
11
+ }
12
+
13
+ .ds-column-filter__options {
14
+ display: flex;
15
+ flex-direction: row;
16
+ align-items: center;
17
+ gap: var(--spacing-small);
18
+ }
19
+
20
+ .ds-column-filter__reset-button {
21
+ margin-left: auto;
22
+ }
23
+
24
+ .ds-column-filter-time {
25
+ background-color: var(--color-mono-white);
26
+ border-radius: var(--border-radius-small);
27
+ padding: var(--spacing-small);
28
+ align-items: stretch;
29
+ }
30
+
31
+ .ds-column-filter-time__row {
32
+ display: flex;
33
+ flex-direction: row;
34
+ align-items: center;
35
+ gap: var(--spacing-small);
36
+ }
37
+
38
+ .ds-column-filter-time__row .ds-button {
39
+ flex: 1;
40
+ min-width: 0;
41
+ height: var(--size-control-small);
42
+ background-color: var(--color-grey-050);
43
+ border: var(--border-weight) solid var(--color-grey-200);
44
+ border-radius: var(--border-radius-small);
45
+ color: var(--color-grey-900);
46
+ font-family: var(--font-family-standard);
47
+ font-size: var(--font-size-2-13);
48
+ font-weight: var(--font-weight-regular);
49
+ padding: 0 var(--spacing-small);
50
+ justify-content: space-between;
51
+ }
52
+
53
+ .ds-column-filter-time__input {
54
+ flex: 1;
55
+ min-width: 0;
56
+ height: var(--size-control-small);
57
+ padding: 0 var(--spacing-small);
58
+ background-color: var(--color-grey-050);
59
+ border: var(--border-weight) solid var(--color-grey-200);
60
+ border-radius: var(--border-radius-small);
61
+ color: var(--color-grey-900);
62
+ font-family: var(--font-family-standard);
63
+ font-size: var(--font-size-2-13);
64
+ font-weight: var(--font-weight-regular);
65
+ box-sizing: border-box;
66
+
67
+ &::-webkit-calendar-picker-indicator {
68
+ opacity: 0.6;
69
+ cursor: pointer;
70
+ }
71
+ }
@@ -0,0 +1,21 @@
1
+ import { Button } from 'Components/button/Button';
2
+
3
+ type FilterResetButtonProps = {
4
+ onClick: () => void;
5
+ children?: React.ReactNode;
6
+ };
7
+
8
+ export function FilterResetButton(props: FilterResetButtonProps) {
9
+ const { onClick, children = 'Reset' } = props;
10
+ return (
11
+ <Button
12
+ className="ds-column-filter__reset-button"
13
+ aria-label="Clear this filter"
14
+ onClick={onClick}
15
+ variant="secondary"
16
+ size="S"
17
+ >
18
+ {children}
19
+ </Button>
20
+ );
21
+ }
@@ -12,7 +12,7 @@
12
12
  }
13
13
 
14
14
  &__container {
15
- .ag-header-cell-resize {
15
+ *[class^='ag-'], *[class*=' ag-'] {
16
16
  // AG-Grid gives these a z-index of 2 which interferes with our stacking elements
17
17
  // so we just unset it here
18
18
  z-index: unset;
package/src/index.scss CHANGED
@@ -27,4 +27,5 @@
27
27
  @use "components/separator/separator.scss";
28
28
  @use "components/modal/modal.scss";
29
29
  @use "components/modal/modalManager/modalManager.scss";
30
+ @use "components/table/columnFilters/columnFilters.scss";
30
31
  @import "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap";