baseui 10.8.0 → 10.9.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 (170) hide show
  1. package/a11y/a11y.js +2 -2
  2. package/a11y/a11y.js.flow +3 -3
  3. package/button/styled-components.js +47 -18
  4. package/button/styled-components.js.flow +25 -5
  5. package/combobox/combobox.js +6 -3
  6. package/combobox/combobox.js.flow +4 -2
  7. package/combobox/types.js.flow +2 -0
  8. package/data-table/column-categorical.js +1 -1
  9. package/data-table/column-categorical.js.flow +2 -2
  10. package/data-table/column-numerical.js +307 -355
  11. package/data-table/column-numerical.js.flow +273 -287
  12. package/data-table/constants.js +17 -11
  13. package/data-table/constants.js.flow +11 -8
  14. package/data-table/data-table.js +53 -50
  15. package/data-table/data-table.js.flow +18 -13
  16. package/data-table/filter-shell.js +27 -4
  17. package/data-table/filter-shell.js.flow +33 -9
  18. package/data-table/locale.js +4 -2
  19. package/data-table/locale.js.flow +6 -2
  20. package/data-table/measure-column-widths.js +83 -121
  21. package/data-table/measure-column-widths.js.flow +87 -109
  22. package/datepicker/styled-components.js +1 -1
  23. package/datepicker/styled-components.js.flow +4 -1
  24. package/drawer/drawer.js +3 -1
  25. package/drawer/drawer.js.flow +7 -1
  26. package/es/a11y/a11y.js +2 -2
  27. package/es/button/styled-components.js +32 -2
  28. package/es/combobox/combobox.js +6 -3
  29. package/es/data-table/column-categorical.js +2 -2
  30. package/es/data-table/column-numerical.js +245 -317
  31. package/es/data-table/constants.js +12 -8
  32. package/es/data-table/data-table.js +18 -16
  33. package/es/data-table/filter-shell.js +26 -4
  34. package/es/data-table/locale.js +4 -2
  35. package/es/data-table/measure-column-widths.js +75 -86
  36. package/es/datepicker/styled-components.js +1 -1
  37. package/es/drawer/drawer.js +3 -1
  38. package/es/index.js +1 -1
  39. package/es/map-marker/badge-enhancer.js +61 -0
  40. package/es/map-marker/constants.js +146 -2
  41. package/es/map-marker/drag-shadow.js +32 -0
  42. package/es/map-marker/fixed-marker.js +54 -48
  43. package/es/map-marker/floating-marker.js +21 -12
  44. package/es/map-marker/index.js +1 -1
  45. package/es/map-marker/label-enhancer.js +39 -0
  46. package/es/map-marker/needle.js +26 -0
  47. package/es/map-marker/pin-head.js +42 -40
  48. package/es/map-marker/styled-components.js +177 -32
  49. package/es/map-marker/types.js +1 -1
  50. package/es/menu/maybe-child-menu.js +0 -2
  51. package/es/menu/nested-menus.js +49 -3
  52. package/es/menu/stateful-container.js +13 -12
  53. package/es/modal/modal.js +3 -1
  54. package/es/popover/popover.js +3 -1
  55. package/es/progress-bar/index.js +1 -1
  56. package/es/progress-bar/progressbar.js +25 -10
  57. package/es/progress-bar/styled-components.js +9 -5
  58. package/es/select/select-component.js +2 -10
  59. package/es/spinner/styled-components.js +34 -16
  60. package/es/table/filter.js +3 -1
  61. package/es/themes/dark-theme/color-component-tokens.js +4 -0
  62. package/es/themes/light-theme/color-component-tokens.js +4 -0
  63. package/es/timezonepicker/timezone-picker.js +53 -36
  64. package/es/timezonepicker/tzdata.js +2 -0
  65. package/es/timezonepicker/update-tzdata.js +69 -0
  66. package/esm/a11y/a11y.js +3 -3
  67. package/esm/button/styled-components.js +47 -18
  68. package/esm/combobox/combobox.js +6 -3
  69. package/esm/data-table/column-categorical.js +2 -2
  70. package/esm/data-table/column-numerical.js +304 -353
  71. package/esm/data-table/constants.js +12 -8
  72. package/esm/data-table/data-table.js +53 -50
  73. package/esm/data-table/filter-shell.js +26 -4
  74. package/esm/data-table/locale.js +4 -2
  75. package/esm/data-table/measure-column-widths.js +83 -121
  76. package/esm/datepicker/styled-components.js +1 -1
  77. package/esm/drawer/drawer.js +3 -1
  78. package/esm/index.js +1 -1
  79. package/esm/map-marker/badge-enhancer.js +79 -0
  80. package/esm/map-marker/constants.js +94 -4
  81. package/esm/map-marker/drag-shadow.js +53 -0
  82. package/esm/map-marker/fixed-marker.js +84 -80
  83. package/esm/map-marker/floating-marker.js +22 -13
  84. package/esm/map-marker/index.js +1 -1
  85. package/esm/map-marker/label-enhancer.js +60 -0
  86. package/esm/map-marker/needle.js +43 -0
  87. package/esm/map-marker/pin-head.js +77 -66
  88. package/esm/map-marker/styled-components.js +182 -51
  89. package/esm/map-marker/types.js +1 -1
  90. package/esm/menu/maybe-child-menu.js +0 -2
  91. package/esm/menu/nested-menus.js +66 -5
  92. package/esm/menu/stateful-container.js +15 -13
  93. package/esm/modal/modal.js +3 -1
  94. package/esm/popover/popover.js +3 -1
  95. package/esm/progress-bar/index.js +1 -1
  96. package/esm/progress-bar/progressbar.js +32 -10
  97. package/esm/progress-bar/styled-components.js +9 -4
  98. package/esm/select/select-component.js +2 -11
  99. package/esm/spinner/styled-components.js +35 -16
  100. package/esm/table/filter.js +3 -1
  101. package/esm/themes/dark-theme/color-component-tokens.js +4 -0
  102. package/esm/themes/light-theme/color-component-tokens.js +4 -0
  103. package/esm/timezonepicker/timezone-picker.js +64 -36
  104. package/esm/timezonepicker/tzdata.js +2 -0
  105. package/esm/timezonepicker/update-tzdata.js +160 -0
  106. package/index.js +6 -0
  107. package/index.js.flow +1 -1
  108. package/map-marker/badge-enhancer.js +90 -0
  109. package/map-marker/badge-enhancer.js.flow +86 -0
  110. package/map-marker/constants.js +103 -5
  111. package/map-marker/constants.js.flow +152 -0
  112. package/map-marker/drag-shadow.js +64 -0
  113. package/map-marker/drag-shadow.js.flow +52 -0
  114. package/map-marker/fixed-marker.js +84 -78
  115. package/map-marker/fixed-marker.js.flow +78 -66
  116. package/map-marker/floating-marker.js +22 -13
  117. package/map-marker/floating-marker.js.flow +30 -17
  118. package/map-marker/index.d.ts +125 -24
  119. package/map-marker/index.js +18 -0
  120. package/map-marker/index.js.flow +3 -0
  121. package/map-marker/label-enhancer.js +71 -0
  122. package/map-marker/label-enhancer.js.flow +63 -0
  123. package/map-marker/needle.js +54 -0
  124. package/map-marker/needle.js.flow +29 -0
  125. package/map-marker/pin-head.js +80 -69
  126. package/map-marker/pin-head.js.flow +122 -84
  127. package/map-marker/styled-components.js +200 -62
  128. package/map-marker/styled-components.js.flow +172 -22
  129. package/map-marker/types.js.flow +69 -20
  130. package/menu/index.d.ts +9 -4
  131. package/menu/maybe-child-menu.js +0 -2
  132. package/menu/maybe-child-menu.js.flow +0 -2
  133. package/menu/nested-menus.js +66 -5
  134. package/menu/nested-menus.js.flow +50 -5
  135. package/menu/stateful-container.js +15 -13
  136. package/menu/stateful-container.js.flow +19 -13
  137. package/menu/types.js.flow +7 -1
  138. package/modal/modal.js +3 -1
  139. package/modal/modal.js.flow +2 -0
  140. package/package.json +5 -4
  141. package/popover/popover.js +3 -1
  142. package/popover/popover.js.flow +2 -0
  143. package/progress-bar/index.d.ts +2 -0
  144. package/progress-bar/index.js +6 -0
  145. package/progress-bar/index.js.flow +1 -0
  146. package/progress-bar/progressbar.js +32 -10
  147. package/progress-bar/progressbar.js.flow +35 -9
  148. package/progress-bar/styled-components.js +9 -4
  149. package/progress-bar/styled-components.js.flow +15 -4
  150. package/progress-bar/types.js.flow +12 -2
  151. package/select/select-component.js +2 -11
  152. package/select/select-component.js.flow +5 -7
  153. package/spinner/styled-components.js +35 -16
  154. package/spinner/styled-components.js.flow +37 -19
  155. package/spinner/types.js.flow +10 -0
  156. package/styles/index.js.flow +1 -1
  157. package/table/filter.js +3 -1
  158. package/table/filter.js.flow +5 -1
  159. package/themes/dark-theme/color-component-tokens.js +4 -0
  160. package/themes/dark-theme/color-component-tokens.js.flow +4 -0
  161. package/themes/light-theme/color-component-tokens.js +4 -0
  162. package/themes/light-theme/color-component-tokens.js.flow +4 -0
  163. package/themes/types.js.flow +4 -0
  164. package/timezonepicker/timezone-picker.js +69 -41
  165. package/timezonepicker/timezone-picker.js.flow +52 -46
  166. package/timezonepicker/types.js.flow +1 -1
  167. package/timezonepicker/tzdata.js +10 -0
  168. package/timezonepicker/tzdata.js.flow +347 -0
  169. package/timezonepicker/update-tzdata.js +164 -0
  170. package/timezonepicker/update-tzdata.js.flow +70 -0
@@ -12,26 +12,25 @@ import {Button, SIZE} from '../button/index.js';
12
12
  import {ButtonGroup, MODE} from '../button-group/index.js';
13
13
  import {Input, SIZE as INPUT_SIZE} from '../input/index.js';
14
14
  import {useStyletron} from '../styles/index.js';
15
- import {Paragraph4} from '../typography/index.js';
16
15
 
17
16
  import Column from './column.js';
18
- import {COLUMNS, NUMERICAL_FORMATS, NUMERICAL_OPERATIONS} from './constants.js';
19
- import FilterShell from './filter-shell.js';
17
+ import {
18
+ COLUMNS,
19
+ NUMERICAL_FORMATS,
20
+ MAX_BIN_COUNT,
21
+ HISTOGRAM_SIZE,
22
+ } from './constants.js';
23
+ import FilterShell, {type ExcludeKind} from './filter-shell.js';
20
24
  import type {ColumnT, SharedColumnOptionsT} from './types.js';
21
25
  import {LocaleContext} from '../locale/index.js';
26
+ import {bin, max as maxFunc, extent, scaleLinear, median, bisector} from 'd3';
27
+ import {Slider} from '../slider/index.js';
22
28
 
23
29
  type NumericalFormats =
24
30
  | typeof NUMERICAL_FORMATS.DEFAULT
25
31
  | typeof NUMERICAL_FORMATS.ACCOUNTING
26
32
  | typeof NUMERICAL_FORMATS.PERCENTAGE;
27
33
 
28
- type NumericalOperations =
29
- | typeof NUMERICAL_OPERATIONS.EQ
30
- | typeof NUMERICAL_OPERATIONS.GT
31
- | typeof NUMERICAL_OPERATIONS.GTE
32
- | typeof NUMERICAL_OPERATIONS.LT
33
- | typeof NUMERICAL_OPERATIONS.LTE;
34
-
35
34
  type OptionsT = {|
36
35
  ...SharedColumnOptionsT<number>,
37
36
  format?: NumericalFormats | ((value: number) => string),
@@ -40,12 +39,11 @@ type OptionsT = {|
40
39
  |};
41
40
 
42
41
  type FilterParametersT = {|
43
- comparisons: Array<{|
44
- value: number,
45
- operation: NumericalOperations,
46
- |}>,
42
+ lowerValue: number,
43
+ upperValue: number,
47
44
  description: string,
48
45
  exclude: boolean,
46
+ excludeKind: ExcludeKind,
49
47
  |};
50
48
 
51
49
  type NumericalColumnT = ColumnT<number, FilterParametersT>;
@@ -86,224 +84,197 @@ function validateInput(input) {
86
84
  return Boolean(parseFloat(input)) || input === '' || input === '-';
87
85
  }
88
86
 
89
- function filterParamsToInitialState(filterParams) {
90
- if (filterParams) {
91
- if (filterParams.comparisons.length > 1) {
92
- if (
93
- filterParams.comparisons[0].operation === NUMERICAL_OPERATIONS.LT &&
94
- filterParams.comparisons[1].operation === NUMERICAL_OPERATIONS.GT
95
- ) {
96
- return {
97
- exclude: !filterParams.exclude,
98
- comparatorIndex: 0,
99
- operatorIndex: 4,
100
- right: filterParams.comparisons[1].value.toString(),
101
- left: filterParams.comparisons[0].value.toString(),
102
- };
103
- }
104
- } else {
105
- const comparison = filterParams.comparisons[0];
106
- if (comparison.operation === NUMERICAL_OPERATIONS.LT) {
107
- return {
108
- exclude: filterParams.exclude,
109
- comparatorIndex: 0,
110
- operatorIndex: 0,
111
- left: '',
112
- right: comparison.value.toString(),
113
- };
114
- } else if (comparison.operation === NUMERICAL_OPERATIONS.GT) {
115
- return {
116
- exclude: filterParams.exclude,
117
- comparatorIndex: 0,
118
- operatorIndex: 1,
119
- left: comparison.value.toString(),
120
- right: '',
121
- };
122
- } else if (comparison.operation === NUMERICAL_OPERATIONS.LTE) {
123
- return {
124
- exclude: filterParams.exclude,
125
- comparatorIndex: 0,
126
- operatorIndex: 2,
127
- left: '',
128
- right: comparison.value.toString(),
129
- };
130
- } else if (comparison.operation === NUMERICAL_OPERATIONS.GTE) {
131
- return {
132
- exclude: filterParams.exclude,
133
- comparatorIndex: 0,
134
- operatorIndex: 3,
135
- left: comparison.value.toString(),
136
- right: '',
137
- };
138
- } else if (comparison.operation === NUMERICAL_OPERATIONS.EQ) {
139
- return {
140
- exclude: filterParams.exclude,
141
- comparatorIndex: 1,
142
- operatorIndex: 0,
143
- left: comparison.value.toString(),
144
- right: '',
145
- };
146
- }
87
+ const bisect = bisector(d => d.x0);
88
+
89
+ const Histogram = React.memo(function Histogram({
90
+ data,
91
+ lower,
92
+ upper,
93
+ isRange,
94
+ exclude,
95
+ precision,
96
+ }) {
97
+ const [css, theme] = useStyletron();
98
+
99
+ const {bins, xScale, yScale} = React.useMemo(() => {
100
+ const bins = bin().thresholds(Math.min(data.length, MAX_BIN_COUNT))(data);
101
+
102
+ const xScale = scaleLinear()
103
+ .domain([bins[0].x0, bins[bins.length - 1].x1])
104
+ .range([0, HISTOGRAM_SIZE.width])
105
+ .clamp(true);
106
+
107
+ const yScale = scaleLinear()
108
+ .domain([0, maxFunc(bins, d => d.length)])
109
+ .nice()
110
+ .range([HISTOGRAM_SIZE.height, 0]);
111
+ return {bins, xScale, yScale};
112
+ }, [data]);
113
+
114
+ // We need to find the index of bar which is nearest to the given single value
115
+ const singleIndexNearest = React.useMemo(() => {
116
+ if (isRange) {
117
+ return null;
147
118
  }
148
- }
119
+ return bisect.center(bins, lower);
120
+ }, [isRange, data, lower, upper]);
149
121
 
150
- return {
151
- exclude: false,
152
- comparatorIndex: 0,
153
- operatorIndex: 0,
154
- left: '',
155
- right: '',
156
- };
157
- }
122
+ return (
123
+ <div
124
+ className={css({
125
+ display: 'flex',
126
+ marginTop: theme.sizing.scale600,
127
+ marginLeft: theme.sizing.scale200,
128
+ marginRight: 0,
129
+ marginBottom: theme.sizing.scale400,
130
+ justifyContent: 'space-between',
131
+ overflow: 'visible',
132
+ })}
133
+ >
134
+ <svg {...HISTOGRAM_SIZE}>
135
+ {bins.map((d, index) => {
136
+ const x = xScale(d.x0) + 1;
137
+ const y = yScale(d.length);
138
+ const width = Math.max(0, xScale(d.x1) - xScale(d.x0) - 1);
139
+ const height = yScale(0) - yScale(d.length);
140
+
141
+ let included;
142
+ if (singleIndexNearest != null) {
143
+ included = index === singleIndexNearest;
144
+ } else {
145
+ const withinLower = d.x1 > lower;
146
+ const withinUpper = d.x0 <= upper;
147
+ included = withinLower && withinUpper;
148
+ }
149
+
150
+ if (exclude) {
151
+ included = !included;
152
+ }
153
+
154
+ return (
155
+ <rect
156
+ key={`bar-${index}`}
157
+ fill={included ? theme.colors.primary : theme.colors.mono400}
158
+ x={x}
159
+ y={y}
160
+ width={width}
161
+ height={height}
162
+ />
163
+ );
164
+ })}
165
+ </svg>
166
+ </div>
167
+ );
168
+ });
158
169
 
159
170
  function NumericalFilter(props) {
160
171
  const [css, theme] = useStyletron();
161
172
  const locale = React.useContext(LocaleContext);
162
173
 
163
- const initialState = filterParamsToInitialState(props.filterParams);
174
+ const precision = props.options.precision;
175
+
176
+ // The state handling of this component could be refactored and cleaned up if we used useReducer.
177
+ const initialState = React.useMemo(() => {
178
+ return (
179
+ props.filterParams || {
180
+ exclude: false,
181
+ excludeKind: 'range',
182
+ comparatorIndex: 0,
183
+ lowerValue: null,
184
+ upperValue: null,
185
+ }
186
+ );
187
+ }, [props.filterParams]);
188
+
164
189
  const [exclude, setExclude] = React.useState(initialState.exclude);
165
- const [comparatorIndex, setComparatorIndex] = React.useState(
166
- initialState.comparatorIndex,
190
+
191
+ // the api of our ButtonGroup forces these numerical indexes...
192
+ // TODO look into allowing semantic names, similar to the radio component. Tricky part would be backwards compat
193
+ const [comparatorIndex, setComparatorIndex] = React.useState(() => {
194
+ switch (initialState.excludeKind) {
195
+ case 'value':
196
+ return 1;
197
+ case 'range':
198
+ default:
199
+ // fallthrough
200
+ return 0;
201
+ }
202
+ });
203
+
204
+ // We use the d3 function to get the extent as it's a little more robust to null, -Infinity, etc.
205
+ const [min, max] = React.useMemo(() => extent(props.data), [props.data]);
206
+
207
+ const [lv, setLower] = React.useState<number>(() =>
208
+ roundToFixed(initialState.lowerValue || min, precision),
167
209
  );
168
- const [operatorIndex, setOperatorIndex] = React.useState(
169
- initialState.operatorIndex,
210
+ const [uv, setUpper] = React.useState<number>(() =>
211
+ roundToFixed(initialState.upperValue || max, precision),
212
+ );
213
+
214
+ // We keep a separate value for the single select, to give a user the ability to toggle between
215
+ // the range and single values without losing their previous input.
216
+ const [sv, setSingle] = React.useState<number>(() =>
217
+ roundToFixed(initialState.lowerValue || median(props.data), precision),
170
218
  );
171
- const [left, setLeft] = React.useState(initialState.left);
172
- const [right, setRight] = React.useState(initialState.right);
173
219
 
220
+ // This is the only conditional which we want to use to determine
221
+ // if we are in range or single value mode.
222
+ // Don't derive it via something else, e.g. lowerValue === upperValue, etc.
174
223
  const isRange = comparatorIndex === 0;
175
- const min = React.useMemo(() => Math.min(...props.data), [props.data]);
176
- const max = React.useMemo(() => Math.max(...props.data), [props.data]);
177
224
 
178
- React.useEffect(() => {
179
- if (!left) {
180
- setLeft(min.toString());
181
- }
182
- if (!right) {
183
- setRight(max.toString());
184
- }
185
- }, []);
186
-
187
- const [leftDisabled, rightDisabled] = React.useMemo(() => {
188
- if (!isRange) return [false, false];
189
- switch (operatorIndex) {
190
- case 4:
191
- return [false, false];
192
- case 0:
193
- case 2:
194
- return [true, false];
195
- case 1:
196
- case 3:
197
- return [false, true];
198
- default:
199
- return [true, true];
200
- }
201
- }, [operatorIndex, isRange]);
202
-
203
- const leftInputRef = React.useRef(null);
204
- const rightInputRef = React.useRef(null);
205
- React.useEffect(() => {
206
- if (!leftDisabled && leftInputRef.current) {
207
- leftInputRef.current.focus({preventScroll: true});
208
- } else if (!rightDisabled && rightInputRef.current) {
209
- rightInputRef.current.focus({preventScroll: true});
210
- }
211
- }, [leftDisabled, rightDisabled, comparatorIndex]);
225
+ const excludeKind = isRange ? 'range' : 'value';
212
226
 
213
- React.useEffect(() => {
214
- switch (operatorIndex) {
215
- case 4:
216
- default:
217
- break;
218
- case 1:
219
- case 3:
220
- setRight(max.toString());
221
- break;
222
- case 0:
223
- case 2:
224
- setLeft(min.toString());
225
- break;
227
+ // while the user is inputting values, we take their input at face value,
228
+ // if we don't do this, a user can't input partial numbers, e.g. "-", or "3."
229
+ const [focused, setFocus] = React.useState(false);
230
+ const [inputValueLower, inputValueUpper] = React.useMemo(() => {
231
+ if (focused) {
232
+ return [isRange ? lv : sv, uv];
226
233
  }
227
- }, [operatorIndex]);
234
+
235
+ // once the user is done inputting.
236
+ // we validate then format to the given precision
237
+ let l = isRange ? lv : sv;
238
+ l = validateInput(l) ? l : min;
239
+ let h = validateInput(uv) ? uv : max;
240
+
241
+ return [roundToFixed(l, precision), roundToFixed(h, precision)];
242
+ }, [isRange, focused, sv, lv, uv, precision]);
243
+
244
+ // We bound the values within our min and max even if a user enters a huge number
245
+ let sliderValue = isRange
246
+ ? [Math.max(inputValueLower, min), Math.min(inputValueUpper, max)]
247
+ : [Math.min(Math.max(inputValueLower, min), max)];
248
+
249
+ // keep the slider happy by sorting the two values
250
+ if (isRange && sliderValue[0] > sliderValue[1]) {
251
+ sliderValue = [sliderValue[1], sliderValue[0]];
252
+ }
228
253
 
229
254
  return (
230
255
  <FilterShell
231
256
  exclude={exclude}
232
257
  onExcludeChange={() => setExclude(!exclude)}
258
+ excludeKind={excludeKind}
233
259
  onApply={() => {
234
260
  if (isRange) {
235
- switch (operatorIndex) {
236
- case 0: {
237
- const value = parseFloat(right);
238
- const operation = NUMERICAL_OPERATIONS.LT;
239
- props.setFilter({
240
- comparisons: [{value, operation}],
241
- description: `< ${value}`,
242
- exclude,
243
- });
244
- break;
245
- }
246
- case 1: {
247
- const value = parseFloat(left);
248
- const operation = NUMERICAL_OPERATIONS.GT;
249
- props.setFilter({
250
- comparisons: [{value, operation}],
251
- description: `> ${value}`,
252
- exclude,
253
- });
254
- break;
255
- }
256
- case 2: {
257
- const value = parseFloat(right);
258
- const operation = NUMERICAL_OPERATIONS.LTE;
259
- props.setFilter({
260
- comparisons: [{value, operation}],
261
- description: `≤ ${value}`,
262
- exclude,
263
- });
264
- break;
265
- }
266
- case 3: {
267
- const value = parseFloat(left);
268
- const operation = NUMERICAL_OPERATIONS.GTE;
269
- props.setFilter({
270
- comparisons: [{value, operation}],
271
- description: `≥ ${value}`,
272
- exclude,
273
- });
274
- break;
275
- }
276
- case 4: {
277
- // 'between' case is interesting since if we want less than 10 plus greater than 5
278
- // comparators, the filter will include _all_ numbers.
279
- const leftValue = parseFloat(left);
280
- const rightValue = parseFloat(right);
281
- props.setFilter({
282
- comparisons: [
283
- {
284
- value: leftValue,
285
- operation: NUMERICAL_OPERATIONS.LT,
286
- },
287
- {
288
- value: rightValue,
289
- operation: NUMERICAL_OPERATIONS.GT,
290
- },
291
- ],
292
- description: `≥ ${leftValue} & ≤ ${rightValue}`,
293
- exclude: !exclude,
294
- });
295
- break;
296
- }
297
- default:
298
- break;
299
- }
261
+ const lowerValue = parseFloat(inputValueLower);
262
+ const upperValue = parseFloat(inputValueUpper);
263
+ props.setFilter({
264
+ description: `≥ ${lowerValue} and ≤ ${upperValue}`,
265
+ exclude: exclude,
266
+ lowerValue,
267
+ upperValue,
268
+ excludeKind,
269
+ });
300
270
  } else {
301
- const value = parseFloat(left);
302
- const operation = NUMERICAL_OPERATIONS.EQ;
271
+ const value = parseFloat(inputValueLower);
303
272
  props.setFilter({
304
- comparisons: [{value, operation}],
305
273
  description: `= ${value}`,
306
- exclude,
274
+ exclude: exclude,
275
+ lowerValue: inputValueLower,
276
+ upperValue: inputValueLower,
277
+ excludeKind,
307
278
  });
308
279
  }
309
280
 
@@ -311,7 +282,7 @@ function NumericalFilter(props) {
311
282
  }}
312
283
  >
313
284
  <ButtonGroup
314
- size={SIZE.compact}
285
+ size={SIZE.mini}
315
286
  mode={MODE.radio}
316
287
  selected={comparatorIndex}
317
288
  onClick={(_, index) => setComparatorIndex(index)}
@@ -324,100 +295,130 @@ function NumericalFilter(props) {
324
295
  <Button
325
296
  type="button"
326
297
  overrides={{BaseButton: {style: {width: '100%'}}}}
298
+ aria-label={locale.datatable.numericalFilterRange}
327
299
  >
328
300
  {locale.datatable.numericalFilterRange}
329
301
  </Button>
330
302
  <Button
331
303
  type="button"
332
304
  overrides={{BaseButton: {style: {width: '100%'}}}}
305
+ aria-label={locale.datatable.numericalFilterSingleValue}
333
306
  >
334
307
  {locale.datatable.numericalFilterSingleValue}
335
308
  </Button>
336
309
  </ButtonGroup>
337
310
 
338
- {isRange && (
339
- <ButtonGroup
340
- size={SIZE.compact}
341
- mode={MODE.radio}
342
- selected={operatorIndex}
343
- onClick={(_, index) => setOperatorIndex(index)}
311
+ <Histogram
312
+ data={props.data}
313
+ lower={inputValueLower}
314
+ upper={inputValueUpper}
315
+ isRange={isRange}
316
+ exclude={exclude}
317
+ precision={props.options.precision}
318
+ />
319
+
320
+ <div className={css({display: 'flex', justifyContent: 'space-between'})}>
321
+ <Slider
322
+ // The slider throws errors when switching between single and two values
323
+ // when it tries to read getThumbDistance on a thumb which is not there anymore
324
+ // if we create a new instance these errors are prevented.
325
+ key={isRange.toString()}
326
+ min={min}
327
+ max={max}
328
+ value={sliderValue}
329
+ onChange={({value}) => {
330
+ if (!value) {
331
+ return;
332
+ }
333
+ if (isRange) {
334
+ const [lowerValue, upperValue] = value;
335
+ setLower(lowerValue);
336
+ setUpper(upperValue);
337
+ } else {
338
+ const [singleValue] = value;
339
+ setSingle(singleValue);
340
+ }
341
+ }}
344
342
  overrides={{
343
+ InnerThumb: function InnerThumb({$value, $thumbIndex}) {
344
+ return <React.Fragment>{$value[$thumbIndex]}</React.Fragment>;
345
+ },
346
+ TickBar: ({$min, $max}) => null, // we don't want the ticks
347
+ ThumbValue: () => null,
345
348
  Root: {
346
- style: ({$theme}) => ({marginBottom: $theme.sizing.scale500}),
349
+ style: () => ({
350
+ // Aligns the center of the slider handles with the histogram bars
351
+ width: 'calc(100% + 14px)',
352
+ margin: '0 -7px',
353
+ }),
354
+ },
355
+ InnerTrack: {
356
+ style: ({$theme}) => {
357
+ if (!isRange) {
358
+ return {
359
+ // For range selection we use the color as is, but when selecting the single value,
360
+ // we don't want the track standing out, so mute its color
361
+ background: theme.colors.mono400,
362
+ };
363
+ }
364
+ },
365
+ },
366
+ Thumb: {
367
+ style: () => ({
368
+ // Slider handles are small enough to visually be centered within each histogram bar
369
+ height: '18px',
370
+ width: '18px',
371
+ fontSize: '0px',
372
+ }),
347
373
  },
348
374
  }}
349
- >
350
- <Button
351
- type="button"
352
- overrides={{BaseButton: {style: {width: '100%'}}}}
353
- >
354
- &#60;
355
- </Button>
356
- <Button
357
- type="button"
358
- overrides={{BaseButton: {style: {width: '100%'}}}}
359
- >
360
- &#62;
361
- </Button>
362
- <Button
363
- type="button"
364
- overrides={{BaseButton: {style: {width: '100%'}}}}
365
- >
366
- &#8804;
367
- </Button>
368
- <Button
369
- type="button"
370
- overrides={{BaseButton: {style: {width: '100%'}}}}
371
- >
372
- &#8805;
373
- </Button>
374
- <Button
375
- type="button"
376
- overrides={{BaseButton: {style: {width: '100%'}}}}
377
- >
378
- &#61;
379
- </Button>
380
- </ButtonGroup>
381
- )}
382
-
375
+ />
376
+ </div>
383
377
  <div
384
378
  className={css({
385
379
  display: 'flex',
380
+ marginTop: theme.sizing.scale400,
381
+ // This % gap is visually appealing given the filter box width
382
+ gap: '30%',
386
383
  justifyContent: 'space-between',
387
- marginLeft: theme.sizing.scale300,
388
- marginRight: theme.sizing.scale300,
389
384
  })}
390
385
  >
391
- <Paragraph4>{format(min, props.options)}</Paragraph4>{' '}
392
- <Paragraph4>{format(max, props.options)}</Paragraph4>
393
- </div>
394
-
395
- <div className={css({display: 'flex', justifyContent: 'space-between'})}>
396
386
  <Input
397
- size={INPUT_SIZE.compact}
398
- overrides={{Root: {style: {width: isRange ? '152px' : '100%'}}}}
399
- disabled={leftDisabled}
400
- inputRef={leftInputRef}
401
- value={left}
387
+ min={min}
388
+ max={max}
389
+ size={INPUT_SIZE.mini}
390
+ overrides={{Root: {style: {width: '100%'}}}}
391
+ value={inputValueLower}
402
392
  onChange={event => {
403
393
  if (validateInput(event.target.value)) {
404
- setLeft(event.target.value);
394
+ isRange
395
+ ? // $FlowFixMe - we know it is a number by now
396
+ setLower(event.target.value)
397
+ : // $FlowFixMe - we know it is a number by now
398
+ setSingle(event.target.value);
405
399
  }
406
400
  }}
401
+ onFocus={() => setFocus(true)}
402
+ onBlur={() => setFocus(false)}
407
403
  />
408
-
409
404
  {isRange && (
410
405
  <Input
411
- size={INPUT_SIZE.compact}
412
- overrides={{Root: {style: {width: '152px'}}}}
413
- disabled={rightDisabled}
414
- inputRef={rightInputRef}
415
- value={right}
406
+ min={min}
407
+ max={max}
408
+ size={INPUT_SIZE.mini}
409
+ overrides={{
410
+ Input: {style: {textAlign: 'right'}},
411
+ Root: {style: {width: '100%'}},
412
+ }}
413
+ value={inputValueUpper}
416
414
  onChange={event => {
417
415
  if (validateInput(event.target.value)) {
418
- setRight(event.target.value);
416
+ // $FlowFixMe - we know it is a number by now
417
+ setUpper(event.target.value);
419
418
  }
420
419
  }}
420
+ onFocus={() => setFocus(true)}
421
+ onBlur={() => setFocus(false)}
421
422
  />
422
423
  )}
423
424
  </div>
@@ -480,24 +481,9 @@ function NumericalColumn(options: OptionsT): NumericalColumnT {
480
481
  kind: COLUMNS.NUMERICAL,
481
482
  buildFilter: function(params) {
482
483
  return function(data) {
483
- const included = params.comparisons.some(c => {
484
- const left = roundToFixed(data, normalizedOptions.precision);
485
- const right = roundToFixed(c.value, normalizedOptions.precision);
486
- switch (c.operation) {
487
- case NUMERICAL_OPERATIONS.EQ:
488
- return left === right;
489
- case NUMERICAL_OPERATIONS.GT:
490
- return left > right;
491
- case NUMERICAL_OPERATIONS.GTE:
492
- return left >= right;
493
- case NUMERICAL_OPERATIONS.LT:
494
- return left < right;
495
- case NUMERICAL_OPERATIONS.LTE:
496
- return left <= right;
497
- default:
498
- return true;
499
- }
500
- });
484
+ const value = roundToFixed(data, normalizedOptions.precision);
485
+ const included =
486
+ value >= params.lowerValue && value <= params.upperValue;
501
487
  return params.exclude ? !included : included;
502
488
  };
503
489
  },