@visns-studio/visns-components 6.4.3 → 6.4.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.
package/package.json CHANGED
@@ -91,7 +91,7 @@
91
91
  "react-dom": "^17.0.0 || ^18.0.0"
92
92
  },
93
93
  "name": "@visns-studio/visns-components",
94
- "version": "6.4.3",
94
+ "version": "6.4.4",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -39,6 +39,7 @@ import NumberFilter from '@visns-studio/visns-datagrid-enterprise/NumberFilter';
39
39
  import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
40
40
  import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
41
41
  import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
42
+ import NativeDateFilterEditor from './columns/NativeDateFilterEditor';
42
43
  import { toast } from 'react-toastify';
43
44
  import fetchUtil from '../utils/fetchUtil';
44
45
  import { confirmDialog } from './utils/ConfirmDialog';
@@ -3725,7 +3726,21 @@ const DataGrid = forwardRef(
3725
3726
  if (column.filter && column.filter.type) {
3726
3727
  switch (column.filter.type) {
3727
3728
  case 'date':
3728
- filterEditor = DateFilter;
3729
+ // Native `<input type="date">` by default
3730
+ // — the popup calendar is clipped inside a
3731
+ // scrolling grid and its day cells are far
3732
+ // below a comfortable tap target. A column
3733
+ // can opt back into it with
3734
+ // `filter: { datePicker: 'popup' }`, the
3735
+ // same escape hatch Field uses. Both
3736
+ // editors emit the identical value
3737
+ // (`dateFormat`-formatted string, `''`
3738
+ // when cleared), so the posted `where[]`
3739
+ // entry is unchanged either way.
3740
+ filterEditor =
3741
+ column.filter.datePicker === 'popup'
3742
+ ? DateFilter
3743
+ : NativeDateFilterEditor;
3729
3744
  filterEditorProps = (props, { index }) => {
3730
3745
  return {
3731
3746
  dateFormat: 'DD-MM-YYYY',
@@ -45,7 +45,6 @@ import DatePickerPortal from './utils/DatePickerPortal';
45
45
  import {
46
46
  fromNativeInputValue,
47
47
  toNativeInputValue,
48
- useNativeDateInput,
49
48
  } from './utils/nativeDateInput';
50
49
 
51
50
  import 'react-toggle/style.css';
@@ -171,15 +170,6 @@ function Field({
171
170
  const [emailSuggestions, setEmailSuggestions] = useState([]);
172
171
  const [showEmailSuggestions, setShowEmailSuggestions] = useState(false);
173
172
 
174
- /**
175
- * On touch / narrow devices `date` and `datetime` fields swap
176
- * react-datepicker's popup calendar for the platform's own picker — the
177
- * popup is clipped by modals on a phone and its day cells are far below a
178
- * comfortable tap target. Desktop rendering is untouched.
179
- * See utils/nativeDateInput.js for the (identical) value contract.
180
- */
181
- const nativeDateInput = useNativeDateInput();
182
-
183
173
  /**
184
174
  * `colour` field popup picker: dismiss the popover when the user clicks
185
175
  * anywhere outside it or presses Escape. Picking a colour inside the
@@ -3147,16 +3137,22 @@ function Field({
3147
3137
  }
3148
3138
  autoComplete="off"
3149
3139
  />
3150
- {value && (
3151
- <button
3152
- type="button"
3153
- aria-label="Clear date"
3154
- className={styles.nativeDateClear}
3155
- onClick={() => onChangeDate(null, settings.id)}
3156
- >
3157
- <X size={16} />
3158
- </button>
3159
- )}
3140
+ {/* Always mounted: inserting this node while the input is
3141
+ focused makes mobile browsers dismiss the just-opened
3142
+ native picker (the DOM mutated around the focused
3143
+ control). Visibility is CSS-only. */}
3144
+ <button
3145
+ type="button"
3146
+ aria-label="Clear date"
3147
+ aria-hidden={value ? undefined : 'true'}
3148
+ tabIndex={value ? 0 : -1}
3149
+ className={`${styles.nativeDateClear} ${
3150
+ value ? '' : styles.nativeDateClearHidden
3151
+ }`}
3152
+ onClick={() => onChangeDate(null, settings.id)}
3153
+ >
3154
+ <X size={16} />
3155
+ </button>
3160
3156
  </div>
3161
3157
  );
3162
3158
  };
@@ -3165,7 +3161,11 @@ function Field({
3165
3161
  switch (settings.type) {
3166
3162
  case 'date':
3167
3163
  case 'datetime':
3168
- if (nativeDateInput) {
3164
+ // Native on every viewport — the popup calendar clips inside
3165
+ // modals and duplicates what browsers now do well themselves.
3166
+ // A field can opt back into the react-datepicker popup with
3167
+ // `datePicker: 'popup'` in its config.
3168
+ if (settings.datePicker !== 'popup') {
3169
3169
  return renderNativeDateInput(settings.type === 'datetime');
3170
3170
  }
3171
3171
 
@@ -0,0 +1,434 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import moment from 'moment';
11
+ import { X } from 'lucide-react';
12
+ import { toNativeInputValue } from '../utils/nativeDateInput';
13
+
14
+ /**
15
+ * Native (`<input type="date">`) replacement for the datagrid's popup
16
+ * calendar column filter.
17
+ *
18
+ * The grid ships `DateFilter` from `@visns-studio/visns-datagrid-*`, which
19
+ * renders an Inovua `DateInput` — a text box plus its own floating calendar.
20
+ * That popup is the last remaining non-native date picker in the app: it is
21
+ * clipped inside scrolling grids, its day cells are far below a comfortable
22
+ * tap target, and it duplicates something every browser now does well. This
23
+ * editor renders the platform control instead, on every viewport.
24
+ *
25
+ * ---------------------------------------------------------------------------
26
+ * The filter-editor contract (community-edition
27
+ * Layout/ColumnLayout/Cell/renderFilter/index.tsx -> `GenericFilter.render`):
28
+ *
29
+ * filterValue { name, operator, type, value, ... } — never mutate it
30
+ * onChange called with a WHOLE filterValue object, `value` replaced
31
+ * emptyValue the filter type's "no filter" value (`''` for `date`)
32
+ * filterDelay ms to debounce before calling onChange (grid default 250)
33
+ * render REQUIRED wrapper — the editor must return `render(node)`,
34
+ * which supplies the filter-row chrome and settings icon
35
+ * cellProps the column's props (may carry `dateFormat`)
36
+ * filterEditorProps object, or `(props, { value, index }) => object`
37
+ * ref receives the editor instance; the grid calls `setValue()`
38
+ * on it (e.g. DataGrid's dependent-filter reset) and reads
39
+ * `getInputRef()`
40
+ * theme / rtl / readOnly / disabled / style / i18n / cell / renderInPortal
41
+ *
42
+ * ---------------------------------------------------------------------------
43
+ * Value shape — byte-identical to `DateFilter`.
44
+ *
45
+ * `DateInput` emits `dateMoment.format(dateFormat)` and `''` for a cleared
46
+ * value (packages/Calendar/src/DateInput/index.tsx `format()`), so with the
47
+ * `dateFormat: 'DD-MM-YYYY'` that DataGrid configures, the grid — and
48
+ * therefore the `where[]` entry posted to the server — has always carried a
49
+ * string like `'02-12-2025'`, and `''` once cleared. This editor formats with
50
+ * the same resolved `dateFormat` and clears to the same `emptyValue`, so no
51
+ * server-side filtering changes.
52
+ */
53
+
54
+ // Moment tokens that mean "this format carries a time of day". Escaped
55
+ // literals (`[at]`) are stripped first so they cannot produce a false match.
56
+ const TIME_TOKENS = /[HhmsSAa]/;
57
+
58
+ const formatHasTime = (format) =>
59
+ typeof format === 'string' &&
60
+ TIME_TOKENS.test(format.replace(/\[[^\]]*\]/g, ''));
61
+
62
+ /**
63
+ * Resolve the display format the same way `DateFilter` does: an explicit
64
+ * `cellProps.dateFormat` wins, otherwise `filterEditorProps` — which DataGrid
65
+ * passes as a function — is consulted.
66
+ *
67
+ * @returns {string} '' when nothing is configured, which is what
68
+ * `DateInput.format()` falls back on (moment then uses its
69
+ * own ISO default).
70
+ */
71
+ const resolveDateFormat = (props, value, index) => {
72
+ const { cellProps } = props;
73
+
74
+ if (cellProps && cellProps.dateFormat !== undefined) {
75
+ return cellProps.dateFormat;
76
+ }
77
+
78
+ return resolveEditorProps(props, value, index).dateFormat || '';
79
+ };
80
+
81
+ /**
82
+ * `filterEditorProps` may be an object or, as DataGrid passes it for date
83
+ * filters, a `(props, { value, index }) => object` factory. DateFilter also
84
+ * falls back to the copy carried on `filterValue`, so this does too.
85
+ */
86
+ function resolveEditorProps(props, value, index) {
87
+ let { filterEditorProps } = props;
88
+
89
+ if (filterEditorProps === undefined) {
90
+ filterEditorProps = props.filterValue && props.filterValue.filterEditorProps;
91
+ }
92
+
93
+ if (typeof filterEditorProps === 'function') {
94
+ return filterEditorProps(props, { value, index }) || {};
95
+ }
96
+
97
+ return filterEditorProps || {};
98
+ }
99
+
100
+ /**
101
+ * A stored filter value (formatted with `dateFormat`) -> the string the
102
+ * native input wants ('YYYY-MM-DD' / 'YYYY-MM-DDTHH:mm').
103
+ */
104
+ const toInputValue = (value, dateFormat, withTime) => {
105
+ if (value === null || value === undefined || value === '') {
106
+ return '';
107
+ }
108
+
109
+ // Strict against the configured format first — '02-12-2025' parsed
110
+ // loosely would be read as an ISO-ish string and come back wrong.
111
+ let parsed = dateFormat ? moment(value, dateFormat, true) : null;
112
+
113
+ if (!parsed || !parsed.isValid()) {
114
+ parsed = moment(value);
115
+ }
116
+
117
+ if (!parsed.isValid()) {
118
+ return '';
119
+ }
120
+
121
+ return toNativeInputValue(parsed, withTime);
122
+ };
123
+
124
+ /**
125
+ * The native input's raw string -> the value the grid and the server expect.
126
+ * Cleared / partially-typed input yields `emptyValue`, which `loadData` in
127
+ * DataGrid.jsx drops from the posted `where[]` so the grid shows every row.
128
+ */
129
+ const fromInputValue = (raw, dateFormat, withTime, emptyValue) => {
130
+ if (!raw) {
131
+ return emptyValue;
132
+ }
133
+
134
+ // `datetime-local` normally yields 'YYYY-MM-DDTHH:mm', but a non-default
135
+ // `step` makes browsers append seconds — accept both.
136
+ const parsed = withTime
137
+ ? moment(raw, ['YYYY-MM-DD[T]HH:mm', 'YYYY-MM-DD[T]HH:mm:ss'], true)
138
+ : moment(raw, 'YYYY-MM-DD', true);
139
+
140
+ if (!parsed.isValid()) {
141
+ return emptyValue;
142
+ }
143
+
144
+ // `format()` with no argument is moment's ISO default — exactly what
145
+ // `DateInput.format()` produces when no dateFormat is configured.
146
+ return dateFormat ? parsed.format(dateFormat) : parsed.format();
147
+ };
148
+
149
+ const isRangeOperator = (operator) =>
150
+ operator === 'inrange' || operator === 'notinrange';
151
+
152
+ const WRAPPER_CLASS =
153
+ 'InovuaReactDataGrid__column-header__filter ' +
154
+ 'InovuaReactDataGrid__column-header__filter--date ' +
155
+ 'visns-native-date-filter';
156
+
157
+ /**
158
+ * One `<input type="date">` (or `datetime-local`) dressed as a grid filter,
159
+ * with the clear affordance the popup calendar used to provide — Chrome's
160
+ * date input has no clear button of its own.
161
+ */
162
+ const NativeDateInput = ({
163
+ inputValue,
164
+ withTime,
165
+ disabled,
166
+ readOnly,
167
+ placeholder,
168
+ style,
169
+ onValueChange,
170
+ inputRef,
171
+ ...rest
172
+ }) => (
173
+ <span
174
+ className={`${WRAPPER_CLASS}${
175
+ inputValue ? '' : ' visns-native-date-filter--empty'
176
+ }${disabled ? ' visns-native-date-filter--disabled' : ''}`}
177
+ style={{ minWidth: 0, ...style }}
178
+ >
179
+ <input
180
+ {...rest}
181
+ ref={inputRef}
182
+ type={withTime ? 'datetime-local' : 'date'}
183
+ className="visns-native-date-filter__input"
184
+ value={inputValue}
185
+ disabled={disabled}
186
+ readOnly={readOnly}
187
+ // `<input type="date">` has no placeholder of its own — the
188
+ // browser always shows its locale mask — so the configured
189
+ // "Select Date" text becomes the accessible name instead.
190
+ aria-label={placeholder || undefined}
191
+ title={placeholder || undefined}
192
+ autoComplete="off"
193
+ onChange={(event) => onValueChange(event.target.value)}
194
+ />
195
+ {inputValue && !disabled && !readOnly ? (
196
+ <button
197
+ type="button"
198
+ aria-label="Clear filter"
199
+ className="visns-native-date-filter__clear"
200
+ onClick={() => onValueChange('')}
201
+ >
202
+ <X size={14} />
203
+ </button>
204
+ ) : null}
205
+ </span>
206
+ );
207
+
208
+ const NativeDateFilterEditor = forwardRef((props, ref) => {
209
+ const {
210
+ filterValue,
211
+ onChange,
212
+ filterDelay,
213
+ emptyValue,
214
+ readOnly,
215
+ disabled,
216
+ style,
217
+ render,
218
+ } = props;
219
+
220
+ // `''` is the `date` filter type's emptyValue (community-edition
221
+ // filterTypes.ts) — fall back to it so a grid without the prop still
222
+ // clears to what DateFilter emitted.
223
+ const noValue = emptyValue === undefined ? '' : emptyValue;
224
+
225
+ const range = isRangeOperator(filterValue && filterValue.operator);
226
+
227
+ const [value, setValue] = useState(() =>
228
+ filterValue ? filterValue.value || (range ? {} : '') : ''
229
+ );
230
+
231
+ const inputRef = useRef(null);
232
+ const timerRef = useRef(null);
233
+ const propValueRef = useRef(filterValue && filterValue.value);
234
+
235
+ // Keep the emitter fresh for the debounce timer without re-arming it.
236
+ const emitRef = useRef(null);
237
+ emitRef.current = (nextValue) => {
238
+ if (onChange) {
239
+ onChange({ ...filterValue, value: nextValue });
240
+ }
241
+ };
242
+
243
+ /**
244
+ * The popup calendar only ever emitted on an explicit pick, so it never
245
+ * needed `filterDelay`. A native input emits on every segment edit — Chrome
246
+ * reports 0002-12-02, 0020-12-02, 0202-12-02 while a year is typed — so the
247
+ * delay the grid already provides is honoured here, exactly as StringFilter
248
+ * does. The posted value is unchanged; only the moment of the request moves.
249
+ */
250
+ const emit = useCallback(
251
+ (nextValue) => {
252
+ if (timerRef.current) {
253
+ clearTimeout(timerRef.current);
254
+ timerRef.current = null;
255
+ }
256
+
257
+ if (filterDelay && filterDelay >= 1) {
258
+ timerRef.current = setTimeout(() => {
259
+ timerRef.current = null;
260
+ emitRef.current(nextValue);
261
+ }, filterDelay);
262
+ return;
263
+ }
264
+
265
+ emitRef.current(nextValue);
266
+ },
267
+ [filterDelay]
268
+ );
269
+
270
+ useEffect(
271
+ () => () => {
272
+ if (timerRef.current) {
273
+ clearTimeout(timerRef.current);
274
+ }
275
+ },
276
+ []
277
+ );
278
+
279
+ // Mirrors DateFilter.componentDidUpdate: adopt the incoming value only
280
+ // when the grid itself changed it (a reset, a saved view, a dependent
281
+ // filter clear), not on every render.
282
+ const incoming = filterValue ? filterValue.value : undefined;
283
+
284
+ useEffect(() => {
285
+ if (incoming !== propValueRef.current) {
286
+ propValueRef.current = incoming;
287
+ setValue(incoming || (range ? {} : ''));
288
+ }
289
+ }, [incoming, range]);
290
+
291
+ useImperativeHandle(
292
+ ref,
293
+ () => ({
294
+ // GenericFilter -> cellInstance.setFilterValue() reaches the
295
+ // editor through this; like StringFilter/DateFilter it updates
296
+ // the displayed value without re-emitting.
297
+ setValue: (next) => {
298
+ propValueRef.current = next;
299
+ setValue(next || (range ? {} : ''));
300
+ },
301
+ getInputRef: () => inputRef.current,
302
+ }),
303
+ [range]
304
+ );
305
+
306
+ const startFormat = useMemo(
307
+ () =>
308
+ resolveDateFormat(
309
+ props,
310
+ range ? value && value.start : value,
311
+ 0
312
+ ),
313
+ // eslint-disable-next-line react-hooks/exhaustive-deps
314
+ [props.cellProps, props.filterEditorProps, value, range]
315
+ );
316
+
317
+ const endFormat = useMemo(
318
+ () => (range ? resolveDateFormat(props, value && value.end, 1) : ''),
319
+ // eslint-disable-next-line react-hooks/exhaustive-deps
320
+ [props.cellProps, props.filterEditorProps, value, range]
321
+ );
322
+
323
+ const startEditorProps = resolveEditorProps(
324
+ props,
325
+ range ? value && value.start : value,
326
+ 0
327
+ );
328
+ const endEditorProps = range
329
+ ? resolveEditorProps(props, value && value.end, 1)
330
+ : {};
331
+
332
+ // datetime-local only when the configured format actually carries a time
333
+ // — DataGrid's date filters use 'DD-MM-YYYY', which does not, so they stay
334
+ // date-only and keep emitting a date-only string.
335
+ const startWithTime = formatHasTime(startFormat);
336
+ const endWithTime = formatHasTime(endFormat);
337
+
338
+ const handleChange = useCallback(
339
+ (raw, key, dateFormat, withTime) => {
340
+ const next = fromInputValue(raw, dateFormat, withTime, noValue);
341
+
342
+ if (!key) {
343
+ if (next === value) {
344
+ return;
345
+ }
346
+
347
+ setValue(next);
348
+ propValueRef.current = next;
349
+ emit(next);
350
+ return;
351
+ }
352
+
353
+ const base = value && typeof value === 'object' ? { ...value } : {};
354
+
355
+ if (base[key] === next) {
356
+ return;
357
+ }
358
+
359
+ base[key] = next;
360
+ setValue(base);
361
+ propValueRef.current = base;
362
+ emit(base);
363
+ },
364
+ [emit, noValue, value]
365
+ );
366
+
367
+ if (!filterValue) {
368
+ return render ? render(null) : null;
369
+ }
370
+
371
+ if (range) {
372
+ const startValue = value && typeof value === 'object' ? value.start : '';
373
+ const endValue = value && typeof value === 'object' ? value.end : '';
374
+
375
+ const node = (
376
+ <div
377
+ className="visns-native-date-filter__range"
378
+ style={{ display: 'flex', minWidth: 0 }}
379
+ >
380
+ <NativeDateInput
381
+ inputValue={toInputValue(
382
+ startValue,
383
+ startFormat,
384
+ startWithTime
385
+ )}
386
+ withTime={startWithTime}
387
+ disabled={disabled}
388
+ readOnly={readOnly}
389
+ placeholder={startEditorProps.placeholder}
390
+ style={style}
391
+ inputRef={inputRef}
392
+ onValueChange={(raw) =>
393
+ handleChange(raw, 'start', startFormat, startWithTime)
394
+ }
395
+ />
396
+ <div className="InovuaReactDataGrid__column-header__filter__binary_operator_separator" />
397
+ <NativeDateInput
398
+ inputValue={toInputValue(endValue, endFormat, endWithTime)}
399
+ withTime={endWithTime}
400
+ disabled={disabled}
401
+ readOnly={readOnly}
402
+ placeholder={endEditorProps.placeholder}
403
+ style={style}
404
+ onValueChange={(raw) =>
405
+ handleChange(raw, 'end', endFormat, endWithTime)
406
+ }
407
+ />
408
+ </div>
409
+ );
410
+
411
+ return render ? render(node) : node;
412
+ }
413
+
414
+ const node = (
415
+ <NativeDateInput
416
+ inputValue={toInputValue(value, startFormat, startWithTime)}
417
+ withTime={startWithTime}
418
+ disabled={disabled}
419
+ readOnly={readOnly}
420
+ placeholder={startEditorProps.placeholder}
421
+ style={style}
422
+ inputRef={inputRef}
423
+ onValueChange={(raw) =>
424
+ handleChange(raw, null, startFormat, startWithTime)
425
+ }
426
+ />
427
+ );
428
+
429
+ return render ? render(node) : node;
430
+ });
431
+
432
+ NativeDateFilterEditor.displayName = 'NativeDateFilterEditor';
433
+
434
+ export default NativeDateFilterEditor;
@@ -1837,9 +1837,19 @@ input.nativeDateInput[type] {
1837
1837
  input.nativeDateFilled[type] {
1838
1838
  padding-right: 2.75rem;
1839
1839
 
1840
- &::-webkit-calendar-picker-indicator {
1841
- -webkit-appearance: none;
1842
- display: none;
1840
+ /* Phones: tapping the field itself opens the OS sheet, so the browser's
1841
+ calendar glyph is redundant next to our clear button. */
1842
+ @media (pointer: coarse) {
1843
+ &::-webkit-calendar-picker-indicator {
1844
+ -webkit-appearance: none;
1845
+ display: none;
1846
+ }
1847
+ }
1848
+
1849
+ /* Desktop: the indicator IS how the picker opens — keep it visible and
1850
+ reserve room for the clear button to sit beside it. */
1851
+ @media (pointer: fine) {
1852
+ padding-right: 4.4rem;
1843
1853
  }
1844
1854
  }
1845
1855
 
@@ -1848,6 +1858,11 @@ input.nativeDateFilled[type] {
1848
1858
  top: 50%;
1849
1859
  right: 0.4rem;
1850
1860
  transform: translateY(-50%);
1861
+
1862
+ /* Sit left of the browser's calendar indicator on desktop. */
1863
+ @media (pointer: fine) {
1864
+ right: 2.3rem;
1865
+ }
1851
1866
  display: flex;
1852
1867
  align-items: center;
1853
1868
  justify-content: center;
@@ -1867,3 +1882,10 @@ input.nativeDateFilled[type] {
1867
1882
  outline: none;
1868
1883
  }
1869
1884
  }
1885
+
1886
+ /* Present in the DOM but inert while the field is empty — see the note in
1887
+ Field.jsx's native date markup. */
1888
+ .nativeDateClearHidden {
1889
+ visibility: hidden;
1890
+ pointer-events: none;
1891
+ }
@@ -510,3 +510,144 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
510
510
  .InovuaReactDataGrid__group-toolbar {
511
511
  display: none !important;
512
512
  }
513
+
514
+ /* =========================================================================
515
+ Native date column filters — see columns/NativeDateFilterEditor.jsx
516
+
517
+ The editor replaces Inovua's popup-calendar DateFilter with the platform's
518
+ own picker. Everything here exists to make an `<input type="date">` sit in
519
+ the filter row at exactly the size and weight of its text / dropdown
520
+ neighbours, whose look comes from the toolkit's own
521
+ `.inovua-react-toolkit-text-input--theme-default-light` rule (32px tall,
522
+ 1px #cdcecd border, 14px #555e68 text).
523
+
524
+ Height itself is not set here: the wrapper carries the grid's own
525
+ `.InovuaReactDataGrid__column-header__filter` class, which the datagrid
526
+ theme already pins to min/max-height 32px alongside every other filter.
527
+
528
+ Note: `global.css` excludes `.visns-native-date-filter__input` from the
529
+ app-wide `input[type]` rule, whose 1rem padding would otherwise burst the
530
+ 32px filter row.
531
+ ========================================================================= */
532
+ .visns-native-date-filter {
533
+ position: relative;
534
+ display: flex;
535
+ /* stretch, not center: the input has to fill the 32px the theme pins the
536
+ filter to, or the clickable area is smaller than the box it draws. */
537
+ align-items: stretch;
538
+ box-sizing: border-box;
539
+ width: 100%;
540
+ min-width: 0;
541
+ overflow: hidden;
542
+ border: 1px solid #cdcecd;
543
+ border-radius: 1px;
544
+ background: #fff;
545
+ color: #555e68;
546
+ font-size: 14px;
547
+ transition: border 0.2s, box-shadow 0.2s;
548
+ }
549
+
550
+ .visns-native-date-filter:hover {
551
+ border-color: #7986cb;
552
+ }
553
+
554
+ .visns-native-date-filter:focus-within {
555
+ border-color: #7986cb;
556
+ box-shadow: 0 0 0 2px rgba(121, 134, 203, 0.4);
557
+ }
558
+
559
+ .visns-native-date-filter--disabled {
560
+ opacity: 0.5;
561
+ }
562
+
563
+ .visns-native-date-filter--disabled:hover {
564
+ border-color: #cdcecd;
565
+ }
566
+
567
+ .visns-native-date-filter__input {
568
+ flex: 1 1 auto;
569
+ box-sizing: border-box;
570
+ width: 100%;
571
+ min-width: 0;
572
+ height: auto;
573
+ margin: 0;
574
+ padding: 0 2px 0 8px;
575
+ border: none;
576
+ outline: none;
577
+ background: none;
578
+ color: inherit;
579
+ font-family: inherit;
580
+ font-size: inherit;
581
+ line-height: 1;
582
+ -webkit-appearance: none;
583
+ appearance: none;
584
+ }
585
+
586
+ /* An empty native date input renders its locale mask (dd/mm/yyyy) at full
587
+ contrast, which reads as a value; mute it to the same placeholder weight
588
+ the toolkit's text filters use. (`:invalid` cannot express this — an empty
589
+ optional date input is valid — so the editor sets the modifier itself.) */
590
+ .visns-native-date-filter--empty .visns-native-date-filter__input {
591
+ color: rgba(85, 94, 104, 0.7);
592
+ }
593
+
594
+ /* The calendar glyph IS how the picker opens on desktop — keep it, just
595
+ shrink it into the 32px row. */
596
+ .visns-native-date-filter__input::-webkit-calendar-picker-indicator {
597
+ margin: 0;
598
+ padding: 0 2px;
599
+ opacity: 0.55;
600
+ cursor: pointer;
601
+ }
602
+
603
+ .visns-native-date-filter__input::-webkit-calendar-picker-indicator:hover {
604
+ opacity: 1;
605
+ }
606
+
607
+ /* Browser-supplied clear / spin affordances are replaced by our own button,
608
+ which is consistent across engines. */
609
+ .visns-native-date-filter__input::-webkit-clear-button,
610
+ .visns-native-date-filter__input::-webkit-inner-spin-button {
611
+ display: none;
612
+ -webkit-appearance: none;
613
+ }
614
+
615
+ .visns-native-date-filter__input::-moz-calendar-picker-indicator {
616
+ opacity: 0.55;
617
+ cursor: pointer;
618
+ }
619
+
620
+ .visns-native-date-filter__clear {
621
+ flex: none;
622
+ align-self: center;
623
+ display: flex;
624
+ align-items: center;
625
+ justify-content: center;
626
+ width: 18px;
627
+ height: 18px;
628
+ margin: 0 4px 0 0;
629
+ padding: 0;
630
+ border: none;
631
+ border-radius: 50%;
632
+ background: transparent;
633
+ color: #99a1aa;
634
+ cursor: pointer;
635
+ outline: none;
636
+ }
637
+
638
+ .visns-native-date-filter__clear:hover,
639
+ .visns-native-date-filter__clear:focus {
640
+ color: #555e68;
641
+ }
642
+
643
+ /* inrange / notinrange: two of the same input either side of the theme's
644
+ own separator. */
645
+ .visns-native-date-filter__range {
646
+ flex: 1;
647
+ min-width: 0;
648
+ align-items: center;
649
+ }
650
+
651
+ .visns-native-date-filter__range .visns-native-date-filter {
652
+ flex: 1 1 0;
653
+ }
@@ -245,7 +245,9 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
245
245
  .inovua-react-toolkit-text-input__input
246
246
  ):not(.inovua-react-toolkit-numeric-input__input):not(
247
247
  .variable-inserter-search-input
248
- ):not(.react-datepicker__month-select):not(.react-datepicker__year-select) {
248
+ ):not(.react-datepicker__month-select):not(
249
+ .react-datepicker__year-select
250
+ ):not(.visns-native-date-filter__input) {
249
251
  background: white;
250
252
  border-radius: var(--br);
251
253
  border: 1px solid rgba(var(--paragraph-color-rgb), 0.15);
package/src/index.js CHANGED
@@ -38,6 +38,7 @@ import AudioPlayer from './components/controls/AudioPlayer';
38
38
  import GalleryModal from './components/modals/GalleryModal';
39
39
  import ImageModal from './components/ImageModal';
40
40
  import DatePickerPortal from './components/utils/DatePickerPortal';
41
+ import NativeDateFilterEditor from './components/columns/NativeDateFilterEditor';
41
42
  import ImportWizardModal from './components/ImportWizardModal';
42
43
  export * from './components/columns/ColumnRenderers.jsx';
43
44
 
@@ -147,6 +148,7 @@ export {
147
148
  MergeEntity,
148
149
  MultiSelect,
149
150
  MultiCheckbox,
151
+ NativeDateFilterEditor,
150
152
  Navigation,
151
153
  Notification,
152
154
  NotificationList,