@arbor-education/design-system.components 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/CHANGELOG.md +22 -0
  3. package/dist/components/button/Button.d.ts.map +1 -1
  4. package/dist/components/button/Button.js +2 -2
  5. package/dist/components/button/Button.js.map +1 -1
  6. package/dist/components/combobox/Combobox.d.ts.map +1 -1
  7. package/dist/components/combobox/Combobox.js +10 -8
  8. package/dist/components/combobox/Combobox.js.map +1 -1
  9. package/dist/components/combobox/Combobox.stories.d.ts +1 -0
  10. package/dist/components/combobox/Combobox.stories.d.ts.map +1 -1
  11. package/dist/components/combobox/Combobox.stories.js +16 -0
  12. package/dist/components/combobox/Combobox.stories.js.map +1 -1
  13. package/dist/components/combobox/Combobox.test.js +107 -61
  14. package/dist/components/combobox/Combobox.test.js.map +1 -1
  15. package/dist/components/combobox/ComboboxButtonTrigger.d.ts +4 -2
  16. package/dist/components/combobox/ComboboxButtonTrigger.d.ts.map +1 -1
  17. package/dist/components/combobox/ComboboxButtonTrigger.js +11 -4
  18. package/dist/components/combobox/ComboboxButtonTrigger.js.map +1 -1
  19. package/dist/components/combobox/ComboboxTrigger.d.ts +3 -1
  20. package/dist/components/combobox/ComboboxTrigger.d.ts.map +1 -1
  21. package/dist/components/combobox/ComboboxTrigger.js +10 -2
  22. package/dist/components/combobox/ComboboxTrigger.js.map +1 -1
  23. package/dist/components/combobox/types.d.ts +3 -0
  24. package/dist/components/combobox/types.d.ts.map +1 -1
  25. package/dist/components/combobox/useComboboxPopoverBehavior.d.ts +3 -1
  26. package/dist/components/combobox/useComboboxPopoverBehavior.d.ts.map +1 -1
  27. package/dist/components/combobox/useComboboxPopoverBehavior.js +7 -6
  28. package/dist/components/combobox/useComboboxPopoverBehavior.js.map +1 -1
  29. package/dist/components/combobox/useComboboxState.d.ts.map +1 -1
  30. package/dist/components/combobox/useComboboxState.js +4 -1
  31. package/dist/components/combobox/useComboboxState.js.map +1 -1
  32. package/dist/components/datePicker/DatePicker.d.ts +4 -1
  33. package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
  34. package/dist/components/datePicker/DatePicker.js +77 -37
  35. package/dist/components/datePicker/DatePicker.js.map +1 -1
  36. package/dist/components/datePicker/DatePicker.stories.d.ts +28 -3
  37. package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
  38. package/dist/components/datePicker/DatePicker.stories.js +62 -9
  39. package/dist/components/datePicker/DatePicker.stories.js.map +1 -1
  40. package/dist/components/datePicker/DatePicker.test.js +133 -66
  41. package/dist/components/datePicker/DatePicker.test.js.map +1 -1
  42. package/dist/components/datePicker/DatePickerCalendarHeader.d.ts +8 -0
  43. package/dist/components/datePicker/DatePickerCalendarHeader.d.ts.map +1 -0
  44. package/dist/components/datePicker/DatePickerCalendarHeader.js +36 -0
  45. package/dist/components/datePicker/DatePickerCalendarHeader.js.map +1 -0
  46. package/dist/components/datePicker/dateInputUtils.d.ts +25 -0
  47. package/dist/components/datePicker/dateInputUtils.d.ts.map +1 -0
  48. package/dist/components/datePicker/dateInputUtils.js +60 -0
  49. package/dist/components/datePicker/dateInputUtils.js.map +1 -0
  50. package/dist/components/datePicker/datePickerTestUtils.test-helpers.d.ts +2 -0
  51. package/dist/components/datePicker/datePickerTestUtils.test-helpers.d.ts.map +1 -0
  52. package/dist/components/datePicker/datePickerTestUtils.test-helpers.js +4 -0
  53. package/dist/components/datePicker/datePickerTestUtils.test-helpers.js.map +1 -0
  54. package/dist/components/dateTimePicker/DateTimePicker.d.ts +22 -0
  55. package/dist/components/dateTimePicker/DateTimePicker.d.ts.map +1 -0
  56. package/dist/components/dateTimePicker/DateTimePicker.js +132 -0
  57. package/dist/components/dateTimePicker/DateTimePicker.js.map +1 -0
  58. package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts +77 -0
  59. package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts.map +1 -0
  60. package/dist/components/dateTimePicker/DateTimePicker.stories.js +163 -0
  61. package/dist/components/dateTimePicker/DateTimePicker.stories.js.map +1 -0
  62. package/dist/components/dateTimePicker/DateTimePicker.test.d.ts +2 -0
  63. package/dist/components/dateTimePicker/DateTimePicker.test.d.ts.map +1 -0
  64. package/dist/components/dateTimePicker/DateTimePicker.test.js +235 -0
  65. package/dist/components/dateTimePicker/DateTimePicker.test.js.map +1 -0
  66. package/dist/components/formField/FormField.d.ts +4 -0
  67. package/dist/components/formField/FormField.d.ts.map +1 -1
  68. package/dist/components/formField/FormField.js +2 -1
  69. package/dist/components/formField/FormField.js.map +1 -1
  70. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  71. package/dist/components/formField/FormField.stories.js +4 -1
  72. package/dist/components/formField/FormField.stories.js.map +1 -1
  73. package/dist/components/formField/FormField.test.d.ts.map +1 -1
  74. package/dist/components/formField/FormField.test.js +10 -5
  75. package/dist/components/formField/FormField.test.js.map +1 -1
  76. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts +1 -0
  77. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
  78. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +7 -3
  79. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
  80. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js +12 -0
  81. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js.map +1 -1
  82. package/dist/components/formField/inputs/text/TextInput.d.ts +4 -1
  83. package/dist/components/formField/inputs/text/TextInput.d.ts.map +1 -1
  84. package/dist/components/formField/inputs/text/TextInput.js +5 -4
  85. package/dist/components/formField/inputs/text/TextInput.js.map +1 -1
  86. package/dist/components/formField/inputs/text/TextInput.stories.d.ts +4 -1
  87. package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
  88. package/dist/components/formField/inputs/time/TimeInput.d.ts +29 -0
  89. package/dist/components/formField/inputs/time/TimeInput.d.ts.map +1 -0
  90. package/dist/components/formField/inputs/time/TimeInput.js +67 -0
  91. package/dist/components/formField/inputs/time/TimeInput.js.map +1 -0
  92. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +60 -0
  93. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts.map +1 -0
  94. package/dist/components/formField/inputs/time/TimeInput.stories.js +132 -0
  95. package/dist/components/formField/inputs/time/TimeInput.stories.js.map +1 -0
  96. package/dist/components/formField/inputs/time/TimeInput.test.d.ts +2 -0
  97. package/dist/components/formField/inputs/time/TimeInput.test.d.ts.map +1 -0
  98. package/dist/components/formField/inputs/time/TimeInput.test.js +58 -0
  99. package/dist/components/formField/inputs/time/TimeInput.test.js.map +1 -0
  100. package/dist/components/table/Table.d.ts.map +1 -1
  101. package/dist/components/table/Table.js +2 -0
  102. package/dist/components/table/Table.js.map +1 -1
  103. package/dist/components/table/Table.stories.d.ts +1 -0
  104. package/dist/components/table/Table.stories.d.ts.map +1 -1
  105. package/dist/components/table/Table.stories.js +37 -0
  106. package/dist/components/table/Table.stories.js.map +1 -1
  107. package/dist/components/table/cellRenderers/BooleanCellRenderer.d.ts +3 -0
  108. package/dist/components/table/cellRenderers/BooleanCellRenderer.d.ts.map +1 -0
  109. package/dist/components/table/cellRenderers/BooleanCellRenderer.js +15 -0
  110. package/dist/components/table/cellRenderers/BooleanCellRenderer.js.map +1 -0
  111. package/dist/components/table/cellRenderers/BooleanCellRenderer.test.d.ts +2 -0
  112. package/dist/components/table/cellRenderers/BooleanCellRenderer.test.d.ts.map +1 -0
  113. package/dist/components/table/cellRenderers/BooleanCellRenderer.test.js +31 -0
  114. package/dist/components/table/cellRenderers/BooleanCellRenderer.test.js.map +1 -0
  115. package/dist/index.css +309 -4
  116. package/dist/index.css.map +1 -1
  117. package/dist/index.d.ts +5 -0
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +3 -0
  120. package/dist/index.js.map +1 -1
  121. package/package.json +1 -1
  122. package/src/components/button/Button.tsx +2 -1
  123. package/src/components/combobox/Combobox.stories.tsx +18 -0
  124. package/src/components/combobox/Combobox.test.tsx +131 -61
  125. package/src/components/combobox/Combobox.tsx +15 -6
  126. package/src/components/combobox/ComboboxButtonTrigger.tsx +54 -25
  127. package/src/components/combobox/ComboboxTrigger.tsx +39 -15
  128. package/src/components/combobox/combobox.scss +18 -0
  129. package/src/components/combobox/types.ts +3 -0
  130. package/src/components/combobox/useComboboxPopoverBehavior.ts +10 -5
  131. package/src/components/combobox/useComboboxState.ts +4 -1
  132. package/src/components/datePicker/DatePicker.stories.tsx +67 -9
  133. package/src/components/datePicker/DatePicker.test.tsx +157 -72
  134. package/src/components/datePicker/DatePicker.tsx +163 -69
  135. package/src/components/datePicker/DatePickerCalendarHeader.tsx +82 -0
  136. package/src/components/datePicker/date-field-hint.scss +152 -0
  137. package/src/components/datePicker/dateInputUtils.ts +117 -0
  138. package/src/components/datePicker/datePicker.scss +53 -29
  139. package/src/components/datePicker/datePickerTestUtils.test-helpers.ts +6 -0
  140. package/src/components/dateTimePicker/DateTimePicker.stories.tsx +202 -0
  141. package/src/components/dateTimePicker/DateTimePicker.test.tsx +295 -0
  142. package/src/components/dateTimePicker/DateTimePicker.tsx +293 -0
  143. package/src/components/dateTimePicker/dateTimePicker.scss +17 -0
  144. package/src/components/formField/FormField.stories.tsx +10 -1
  145. package/src/components/formField/FormField.test.tsx +11 -5
  146. package/src/components/formField/FormField.tsx +5 -0
  147. package/src/components/formField/inputs/selectDropdown/SelectDropdown.test.tsx +28 -0
  148. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +8 -2
  149. package/src/components/formField/inputs/text/TextInput.tsx +6 -3
  150. package/src/components/formField/inputs/time/TimeInput.stories.tsx +170 -0
  151. package/src/components/formField/inputs/time/TimeInput.test.tsx +86 -0
  152. package/src/components/formField/inputs/time/TimeInput.tsx +168 -0
  153. package/src/components/formField/inputs/time/timeInput.scss +33 -0
  154. package/src/components/row/row.scss +2 -2
  155. package/src/components/table/Table.stories.tsx +48 -0
  156. package/src/components/table/Table.tsx +2 -0
  157. package/src/components/table/cellRenderers/BooleanCellRenderer.test.tsx +37 -0
  158. package/src/components/table/cellRenderers/BooleanCellRenderer.tsx +34 -0
  159. package/src/components/table/cellRenderers/booleanCellRenderer.scss +7 -0
  160. package/src/index.scss +3 -0
  161. package/src/index.ts +5 -0
@@ -0,0 +1,86 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
5
+ import { TimeInput } from './TimeInput';
6
+
7
+ describe('TimeInput', () => {
8
+ beforeEach(() => {
9
+ globalThis.ResizeObserver = class {
10
+ observe() {}
11
+ unobserve() {}
12
+ disconnect() {}
13
+ } as unknown as typeof ResizeObserver;
14
+ });
15
+
16
+ afterEach(() => {
17
+ vi.restoreAllMocks();
18
+ });
19
+
20
+ test('renders a native time input by default', () => {
21
+ render(<TimeInput aria-label="Start time" />);
22
+ const input = screen.getByLabelText('Start time');
23
+
24
+ expect(input).toHaveAttribute('type', 'time');
25
+ expect(input).toHaveAttribute('step', '60');
26
+ });
27
+
28
+ test('uses one-second stepping when granularity is second', () => {
29
+ render(<TimeInput aria-label="Start time" granularity="second" />);
30
+ expect(screen.getByLabelText('Start time')).toHaveAttribute('step', '1');
31
+ });
32
+
33
+ test('native mode emits the string value when changed', () => {
34
+ const onValueChange = vi.fn();
35
+ render(<TimeInput aria-label="Start time" onValueChange={onValueChange} />);
36
+
37
+ fireEvent.change(screen.getByLabelText('Start time'), {
38
+ target: { value: '14:30' },
39
+ });
40
+
41
+ expect(onValueChange).toHaveBeenCalledWith('14:30');
42
+ });
43
+
44
+ test('clicking the clock icon focuses the input and selects the hour segment when supported', async () => {
45
+ const user = userEvent.setup();
46
+ const setSelectionRange = vi.spyOn(HTMLInputElement.prototype, 'setSelectionRange').mockImplementation(() => {});
47
+
48
+ render(<TimeInput aria-label="Start time" />);
49
+
50
+ await user.click(screen.getByRole('button', { name: 'Select time' }));
51
+
52
+ expect(screen.getByLabelText('Start time')).toHaveFocus();
53
+ expect(setSelectionRange).toHaveBeenCalledWith(0, 2);
54
+ });
55
+
56
+ test('options mode renders the combobox-backed time list with plain text selection', () => {
57
+ render(
58
+ <TimeInput
59
+ aria-label="Start time"
60
+ options={['13:00', '13:30', '14:00']}
61
+ value="13:30"
62
+ />,
63
+ );
64
+
65
+ expect(screen.getByRole('button', { name: 'Start time' })).toHaveTextContent('13:30');
66
+ expect(document.querySelector('.ds-tag')).not.toBeInTheDocument();
67
+ });
68
+
69
+ test('options mode emits the selected allowed time', async () => {
70
+ const user = userEvent.setup();
71
+ const onValueChange = vi.fn();
72
+
73
+ render(
74
+ <TimeInput
75
+ aria-label="Start time"
76
+ options={['13:00', '13:30', '14:00']}
77
+ onValueChange={onValueChange}
78
+ />,
79
+ );
80
+
81
+ await user.click(screen.getByRole('button', { name: 'Start time' }));
82
+ await user.click(screen.getByText('13:30'));
83
+
84
+ expect(onValueChange).toHaveBeenCalledWith('13:30');
85
+ });
86
+ });
@@ -0,0 +1,168 @@
1
+ import classNames from 'classnames';
2
+ import { Combobox } from 'Components/combobox/Combobox';
3
+ import type { ComboboxOption, ComboboxSearchType } from 'Components/combobox/types';
4
+ import { Icon } from 'Components/icon/Icon';
5
+ import { forwardRef, useCallback, useMemo, useRef, useState, type ChangeEvent, type InputHTMLAttributes } from 'react';
6
+
7
+ /** Expected string `HH:MM` or `HH:MM:SS`; format-shaped versus strict time validation. */
8
+ export type TimeValue = `${string}:${string}` | `${string}:${string}:${string}`;
9
+ export type TimeGranularity = 'minute' | 'second';
10
+
11
+ export type TimeInputProps = {
12
+ options?: TimeValue[];
13
+ granularity?: TimeGranularity;
14
+ hasError?: boolean;
15
+ onValueChange?: (value: string) => void;
16
+ searchType?: ComboboxSearchType;
17
+ highlightStringMatches?: boolean;
18
+ } & Omit<InputHTMLAttributes<HTMLInputElement>, 'defaultValue' | 'size' | 'type' | 'value'> & {
19
+ value?: TimeValue | '';
20
+ defaultValue?: TimeValue | '';
21
+ };
22
+
23
+ const clockIcon = <Icon name="clock-3" size={16} />;
24
+
25
+ /** The forwarded ref is attached in native mode but when `options` is provided, no input ref is exposed. */
26
+ export const TimeInput = forwardRef<HTMLInputElement, TimeInputProps>((props, ref) => {
27
+ const {
28
+ options,
29
+ granularity = 'minute',
30
+ hasError = false,
31
+ onValueChange,
32
+ searchType = 'prefix',
33
+ highlightStringMatches = false,
34
+ onChange,
35
+ className,
36
+ disabled = false,
37
+ id,
38
+ name,
39
+ placeholder,
40
+ value: controlledValue,
41
+ defaultValue = '',
42
+ 'aria-describedby': ariaDescribedBy,
43
+ 'aria-invalid': ariaInvalid,
44
+ 'aria-label': ariaLabel,
45
+ ...rest
46
+ } = props;
47
+
48
+ const isControlled = controlledValue !== undefined;
49
+ const [internalValue, setInternalValue] = useState<string>(defaultValue);
50
+ const currentValue = isControlled ? controlledValue ?? '' : internalValue;
51
+ const inputRef = useRef<HTMLInputElement>(null);
52
+
53
+ const setRefs = useCallback((node: HTMLInputElement | null) => {
54
+ inputRef.current = node;
55
+ if (typeof ref === 'function') {
56
+ ref(node);
57
+ }
58
+ else if (ref) {
59
+ ref.current = node;
60
+ }
61
+ }, [ref]);
62
+
63
+ const updateValue = useCallback((nextValue: string, nativeEvent?: ChangeEvent<HTMLInputElement>) => {
64
+ if (!isControlled) {
65
+ setInternalValue(nextValue);
66
+ }
67
+ onValueChange?.(nextValue);
68
+ if (nativeEvent) {
69
+ onChange?.(nativeEvent);
70
+ return;
71
+ }
72
+ // Combobox-driven updates only provide compatibility with common form handlers.
73
+ onChange?.({
74
+ currentTarget: { value: nextValue } as EventTarget & HTMLInputElement,
75
+ target: { value: nextValue } as EventTarget & HTMLInputElement,
76
+ } as ChangeEvent<HTMLInputElement>);
77
+ }, [isControlled, onChange, onValueChange]);
78
+
79
+ const handleNativeChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
80
+ updateValue(event.currentTarget.value, event);
81
+ }, [updateValue]);
82
+
83
+ const focusAndSelectHours = useCallback(() => {
84
+ const input = inputRef.current;
85
+ if (!input) return;
86
+
87
+ input.focus();
88
+
89
+ try {
90
+ input.setSelectionRange(0, 2);
91
+ }
92
+ catch {
93
+ // Some browsers do not expose selection APIs on time inputs.
94
+ }
95
+ }, []);
96
+
97
+ const timeOptions = useMemo<ComboboxOption[]>(
98
+ () => (options ?? []).map(time => ({ value: time, label: time })),
99
+ [options],
100
+ );
101
+
102
+ const selectedTimeValue = options?.includes(currentValue as TimeValue) ? [currentValue] : [];
103
+
104
+ const handleTimeListChange = useCallback((values: string[]) => {
105
+ const nextValue = values[0] ?? '';
106
+ updateValue(nextValue);
107
+ }, [updateValue]);
108
+
109
+ if (options) {
110
+ return (
111
+ <div className={classNames('ds-time-input', className)}>
112
+ {name && <input type="hidden" name={name} value={selectedTimeValue[0] ?? ''} />}
113
+ <Combobox
114
+ id={id}
115
+ options={timeOptions}
116
+ value={selectedTimeValue}
117
+ onValueChange={handleTimeListChange}
118
+ searchType={searchType}
119
+ highlightStringMatches={highlightStringMatches}
120
+ placeholder={placeholder ?? 'Select time'}
121
+ disabled={disabled}
122
+ hasError={hasError}
123
+ aria-describedby={ariaDescribedBy}
124
+ aria-invalid={ariaInvalid}
125
+ aria-label={ariaLabel}
126
+ triggerVariant="button"
127
+ selectedValueDisplay="text"
128
+ showDropdownTrigger={false}
129
+ triggerEndContent={clockIcon}
130
+ />
131
+ </div>
132
+ );
133
+ }
134
+
135
+ return (
136
+ <div className={classNames('ds-time-input', className)}>
137
+ <input
138
+ {...rest}
139
+ ref={setRefs}
140
+ id={id}
141
+ name={name}
142
+ type="time"
143
+ value={currentValue}
144
+ step={granularity === 'second' ? 1 : 60}
145
+ disabled={disabled}
146
+ aria-describedby={ariaDescribedBy}
147
+ aria-invalid={ariaInvalid}
148
+ aria-label={ariaLabel}
149
+ placeholder={placeholder}
150
+ className={classNames('ds-input', 'ds-input--M', 'ds-time-input__input', {
151
+ 'ds-input--error': hasError,
152
+ })}
153
+ onChange={handleNativeChange}
154
+ />
155
+ <button
156
+ type="button"
157
+ className="ds-time-input__icon-button"
158
+ onClick={focusAndSelectHours}
159
+ aria-label="Select time"
160
+ disabled={disabled}
161
+ >
162
+ {clockIcon}
163
+ </button>
164
+ </div>
165
+ );
166
+ });
167
+
168
+ TimeInput.displayName = 'TimeInput';
@@ -0,0 +1,33 @@
1
+ .ds-time-input {
2
+ position: relative;
3
+ width: 100%;
4
+ }
5
+
6
+ .ds-time-input__input {
7
+ appearance: none;
8
+ padding-right: calc(var(--form-field-text-medium-height) + var(--spacing-small));
9
+
10
+ &::-webkit-calendar-picker-indicator {
11
+ display: none;
12
+ appearance: none;
13
+ }
14
+ }
15
+
16
+ .ds-time-input__icon-button {
17
+ position: absolute;
18
+ inset-block: 0;
19
+ inset-inline-end: 0;
20
+ width: var(--form-field-text-medium-height);
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ border: none;
25
+ background: transparent;
26
+ color: var(--form-field-icon-default-color-icon);
27
+ cursor: pointer;
28
+
29
+ &:disabled {
30
+ color: var(--form-field-icon-disabled-color-icon);
31
+ cursor: not-allowed;
32
+ }
33
+ }
@@ -6,11 +6,11 @@
6
6
  background-color: var(--section-list-row-default-color-background);
7
7
  padding: var(--section-list-row-spacing-vertical) var(--section-list-row-spacing-horizontal);
8
8
  border-radius: var(--section-list-row-radius);
9
+ gap: var(--spacing-small);
9
10
 
10
11
  .ds-row__icon-click {
11
12
  flex-shrink: 0;
12
- margin-left: var(--spacing-small);
13
- stroke: var(--section-list-row-default-color-icon-arrow);
13
+ stroke: var(--section-list-row-default-color-icon-arrow);
14
14
 
15
15
 
16
16
  &.ds-icon-arrow-right {
@@ -1361,4 +1361,52 @@ export const TableWithSemanticColors: Story = {
1361
1361
  },
1362
1362
  };
1363
1363
 
1364
+ interface BooleanRowData {
1365
+ id: number;
1366
+ name: string;
1367
+ hasPet: boolean | null | undefined;
1368
+ isEnrolled: boolean | null | undefined;
1369
+ }
1370
+
1371
+ const booleanCellRendererData: BooleanRowData[] = [
1372
+ { id: 1, name: 'Alice Johnson', hasPet: true, isEnrolled: true },
1373
+ { id: 2, name: 'Bob Smith', hasPet: false, isEnrolled: false },
1374
+ { id: 3, name: 'Charlie Brown', hasPet: null, isEnrolled: undefined },
1375
+ { id: 4, name: 'Diana Prince', hasPet: true, isEnrolled: null },
1376
+ { id: 5, name: 'Ethan Hunt', hasPet: undefined, isEnrolled: true },
1377
+ ];
1378
+
1379
+ const booleanCellRendererColumnDefs: ColDef<BooleanRowData>[] = [
1380
+ { field: 'name', headerName: 'Name' },
1381
+ {
1382
+ field: 'hasPet',
1383
+ headerName: 'Has Pet',
1384
+ cellRenderer: 'dsBooleanCellRenderer',
1385
+ editable: false,
1386
+ },
1387
+ {
1388
+ field: 'isEnrolled',
1389
+ headerName: 'Is Enrolled',
1390
+ cellRenderer: 'dsBooleanCellRenderer',
1391
+ editable: false,
1392
+ },
1393
+ ];
1394
+
1395
+ export const WithBooleanCellRenderer: Story = {
1396
+ parameters: {
1397
+ docs: {
1398
+ description: {
1399
+ story:
1400
+ 'The BooleanCellRenderer displays a true or false icon for boolean values, and renders nothing for any other value regardless of truthiness. Only explicit true and false are accepted, anything else is treated as nullish',
1401
+ },
1402
+ },
1403
+ },
1404
+ args: {
1405
+ rowData: booleanCellRendererData,
1406
+ columnDefs: booleanCellRendererColumnDefs,
1407
+ defaultColDef,
1408
+ domLayout: 'autoHeight',
1409
+ },
1410
+ };
1411
+
1364
1412
  export default meta;
@@ -24,6 +24,7 @@ import { focusFirstFocusableElement } from 'Utils/focusFirstFocusableElement';
24
24
  import { BooleanFilter } from './columnFilters/BooleanFilter/BooleanFilter';
25
25
  import { TimeFilter } from './columnFilters/TimeFilter/TimeFilter';
26
26
  import { TableSettingsDropdown } from './TableSettingsDropdown';
27
+ import { BooleanCellRenderer } from './cellRenderers/BooleanCellRenderer';
27
28
 
28
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
30
  type TableProps<TData = any> = {
@@ -194,6 +195,7 @@ export const Table = (props: TableProps) => {
194
195
  dsSelectDropdownCellRenderer: SelectDropdownCellRenderer,
195
196
  dsBooleanFilter: BooleanFilter,
196
197
  dsTimeFilter: TimeFilter,
198
+ dsBooleanCellRenderer: BooleanCellRenderer,
197
199
  ...components,
198
200
  }}
199
201
  {...rest}
@@ -0,0 +1,37 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import { describe, expect, test } from 'vitest';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { BooleanCellRenderer } from './BooleanCellRenderer';
5
+ import type { CustomCellRendererProps } from 'ag-grid-react';
6
+
7
+ const renderWithValue = (value: unknown) =>
8
+ render(<BooleanCellRenderer {...{ value } as CustomCellRendererProps} />);
9
+
10
+ describe('BooleanCellRenderer', () => {
11
+ test('renders a check icon when value is true', () => {
12
+ renderWithValue(true);
13
+ expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
14
+ expect(screen.getByText('true')).toBeInTheDocument();
15
+ });
16
+
17
+ test('renders an x icon when value is false', () => {
18
+ renderWithValue(false);
19
+ expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
20
+ expect(screen.getByText('false')).toBeInTheDocument();
21
+ });
22
+
23
+ test('renders nothing when value is null', () => {
24
+ const { container } = renderWithValue(null);
25
+ expect(container.innerHTML).toBeFalsy();
26
+ });
27
+
28
+ test('renders nothing when value is undefined', () => {
29
+ const { container } = renderWithValue(undefined);
30
+ expect(container.innerHTML).toBeFalsy();
31
+ });
32
+
33
+ test('renders nothing when value is a non-boolean', () => {
34
+ const { container } = renderWithValue('some string');
35
+ expect(container.innerHTML).toBeFalsy();
36
+ });
37
+ });
@@ -0,0 +1,34 @@
1
+ import type { CustomCellRendererProps } from 'ag-grid-react';
2
+ import { Icon } from 'Components/icon/Icon';
3
+
4
+ export const BooleanCellRenderer = (props: CustomCellRendererProps) => {
5
+ const { value } = props;
6
+
7
+ if (value === true) {
8
+ return (
9
+ <div className="ds-boolean-cell-renderer">
10
+ <Icon
11
+ size={24}
12
+ name="check-solid"
13
+ color="var(--color-semantic-success-700)"
14
+ screenReaderText="true"
15
+ />
16
+ </div>
17
+ );
18
+ }
19
+ else if (value === false) {
20
+ return (
21
+ <div className="ds-boolean-cell-renderer">
22
+ <Icon
23
+ size={24}
24
+ name="x-solid"
25
+ color="var(--color-semantic-destructive-700)"
26
+ screenReaderText="false"
27
+ />
28
+ </div>
29
+ );
30
+ }
31
+ else {
32
+ return null;
33
+ }
34
+ };
@@ -0,0 +1,7 @@
1
+ .ds-boolean-cell-renderer {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ width: 100%;
6
+ height: 100%;
7
+ }
package/src/index.scss CHANGED
@@ -11,6 +11,7 @@
11
11
  @use "components/formField/inputs/input.scss";
12
12
  @use "components/formField/label/label.scss";
13
13
  @use "components/formField/inputs/number/numberInput.scss";
14
+ @use "components/formField/inputs/time/timeInput.scss";
14
15
  @use "components/formField/inputs/radio/radioButtonInput.scss";
15
16
  @use "components/formField/inputs/checkbox/checkboxInput.scss";
16
17
  @use "components/formField/inputs/selectDropdown/selectDropdown";
@@ -36,10 +37,12 @@
36
37
  @use "components/progress/progress.scss";
37
38
  @use "components/toast/toast.scss";
38
39
  @use "components/datePicker/datePicker.scss";
40
+ @use "components/dateTimePicker/dateTimePicker.scss";
39
41
  @use "components/avatar/avatar.scss";
40
42
  @use "components/singleUser/singleUser.scss";
41
43
  @use "components/avatarGroup/avatarGroup.scss";
42
44
  @use "components/userDropdown/userDropdown.scss";
45
+ @use "components/table/cellRenderers/booleanCellRenderer.scss";
43
46
  @use "components/row/row.scss";
44
47
  @use "components/combobox/combobox.scss";
45
48
  @use "components/toggle/toggle.scss";
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@ export { Banner, BANNER_LEVEL, type BannerProps } from 'Components/banner/Banner
10
10
  export { Button } from 'Components/button/Button';
11
11
  export { Card } from 'Components/card/Card';
12
12
  export { DatePicker } from 'Components/datePicker/DatePicker';
13
+ export { DateTimePicker } from 'Components/dateTimePicker/DateTimePicker';
14
+ export type { DateTimePickerDisplayFormat, DateTimePickerProps } from 'Components/dateTimePicker/DateTimePicker';
13
15
  export { Dropdown } from 'Components/dropdown/Dropdown';
14
16
  export { Tag } from 'Components/tag/Tag';
15
17
  export type { TagProps, TagColor } from 'Components/tag/Tag';
@@ -26,6 +28,8 @@ export { ColourPickerDropdown } from 'Components/formField/inputs/colourPickerDr
26
28
  export { RadioButtonInput } from 'Components/formField/inputs/radio/RadioButtonInput';
27
29
  export { SelectDropdown } from 'Components/formField/inputs/selectDropdown/SelectDropdown';
28
30
  export { TextInput } from 'Components/formField/inputs/text/TextInput';
31
+ export { TimeInput } from 'Components/formField/inputs/time/TimeInput';
32
+ export type { TimeGranularity, TimeInputProps, TimeValue } from 'Components/formField/inputs/time/TimeInput';
29
33
  export { TextArea } from 'Components/formField/inputs/textArea/TextArea';
30
34
  export { Heading } from 'Components/heading/Heading';
31
35
  export { Icon } from 'Components/icon/Icon';
@@ -40,6 +44,7 @@ export { SingleUser, type SingleUserProps } from 'Components/singleUser/SingleUs
40
44
  export { Slideover, type SlideoverProps } from 'Components/slideover/Slideover';
41
45
  export { SlideoverManager } from 'Components/slideoverManager/SlideoverManager';
42
46
  export { DefaultCellRenderer } from 'Components/table/cellRenderers/DefaultCellRenderer';
47
+ export { BooleanCellRenderer } from 'Components/table/cellRenderers/BooleanCellRenderer';
43
48
  export { DSDefaultColDef } from 'Components/table/DSDefaultColDef';
44
49
  export { GridApiContext } from 'Components/table/GridApiContext';
45
50
  export { Table } from 'Components/table/Table';