@rozie-ui/data-table-solid 0.4.0 → 0.6.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.
- package/README.md +66 -52
- package/dist/index.cjs +3198 -837
- package/dist/index.d.cts +71 -8
- package/dist/index.d.mts +71 -8
- package/dist/index.mjs +3122 -761
- package/dist/source/index.jsx +6368 -0
- package/package.json +8 -3
- package/src/Column.tsx +3 -3
- package/src/DataTable.tsx +1401 -133
- package/src/EditorCheckbox.tsx +38 -4
- package/src/EditorDate.tsx +79 -8
- package/src/EditorNumber.tsx +52 -6
- package/src/EditorSelect.tsx +51 -6
- package/src/EditorText.tsx +34 -5
- package/src/FilterNumberRange.tsx +79 -12
- package/src/FilterSelect.tsx +18 -3
- package/src/FilterText.tsx +48 -9
- package/src/GroupBar.tsx +75 -3
- package/src/helpers/columnDefUtils.ts +73 -1
- package/src/helpers/dateValue.ts +55 -0
- package/src/themes/base.css +122 -88
- package/src/themes/bootstrap.css +60 -24
- package/src/themes/material.css +54 -22
- package/src/themes/shadcn.css +59 -23
package/src/EditorCheckbox.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JSX } from 'solid-js';
|
|
2
|
-
import { mergeProps, splitProps } from 'solid-js';
|
|
2
|
+
import { createEffect, mergeProps, on, onMount, splitProps, untrack } from 'solid-js';
|
|
3
|
+
import { rozieAttr } from '@rozie/runtime-solid';
|
|
3
4
|
|
|
4
5
|
interface EditorCheckboxProps {
|
|
5
6
|
/**
|
|
@@ -26,11 +27,27 @@ interface EditorCheckboxProps {
|
|
|
26
27
|
* `() => void` — revert the edit (Escape). Null-guarded at call sites.
|
|
27
28
|
*/
|
|
28
29
|
cancel?: ((...args: any[]) => any) | null;
|
|
30
|
+
/**
|
|
31
|
+
* Focus this editor's primary control when true — the host sets it for the one editor that should hold focus; reactive.
|
|
32
|
+
*/
|
|
33
|
+
autofocus?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The column's human header, forwarded by the slot scope — used as the control's accessible name in place of the internal column id.
|
|
36
|
+
*/
|
|
37
|
+
columnLabel?: string;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
export default function EditorCheckbox(_props: EditorCheckboxProps): JSX.Element {
|
|
32
|
-
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null }, _props);
|
|
33
|
-
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel']);
|
|
41
|
+
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, autofocus: false, columnLabel: '' }, _props);
|
|
42
|
+
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'autofocus', 'columnLabel']);
|
|
43
|
+
|
|
44
|
+
onMount(() => {
|
|
45
|
+
if (local.autofocus) inputElRef?.focus();
|
|
46
|
+
});
|
|
47
|
+
createEffect(on(() => (() => local.autofocus)(), (v) => untrack(() => ((v: any) => {
|
|
48
|
+
if (v) inputElRef?.focus();
|
|
49
|
+
})(v)), { defer: true }));
|
|
50
|
+
let inputElRef: HTMLElement | null = null;
|
|
34
51
|
|
|
35
52
|
// Immediate-commit-on-change: read .checked the global-filter way, coerce to a
|
|
36
53
|
// real boolean, and commit it directly.
|
|
@@ -44,9 +61,26 @@ export default function EditorCheckbox(_props: EditorCheckboxProps): JSX.Element
|
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
// C-11 — the accessible name must be the column's HUMAN header, not its internal id.
|
|
65
|
+
// `columnId` is a lookup key (`unit_price`, `col_3`); a screen reader announcing it is
|
|
66
|
+
// reading an implementation detail aloud. table-core keeps the authored header on
|
|
67
|
+
// `column.columnDef.header`, which is a string for every declarative `<Column header>`;
|
|
68
|
+
// a header rendered by a function has no static text, so fall back to the id rather
|
|
69
|
+
// than invent one.
|
|
70
|
+
function a11yLabel() {
|
|
71
|
+
if (typeof local.columnLabel === 'string' && local.columnLabel !== '') return local.columnLabel;
|
|
72
|
+
return local.columnId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// C-02 / editor-owns-focus: focus OUR OWN control when the host says we should hold it.
|
|
76
|
+
// $onMount covers the initial open (autofocus is already true on first render); the LAZY
|
|
77
|
+
// $watch (NOT { immediate: true } — an immediate watch fires PRE-mount and sees a null
|
|
78
|
+
// ref on Lit/Solid) covers a REACTIVE refocus while already mounted, e.g. a row-mode
|
|
79
|
+
// validation failure flipping autofocus back onto this already-open drop-in.
|
|
80
|
+
|
|
47
81
|
return (
|
|
48
82
|
<>
|
|
49
|
-
<input type="checkbox" data-editing-cell="" aria-label={
|
|
83
|
+
<input type="checkbox" data-editing-cell="" aria-label={rozieAttr(a11yLabel())} ref={(el) => { inputElRef = el as HTMLElement; }} class={"rdt-cell-editor"} checked={!!local.value} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onChange($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onKeydown($event); }} data-rozie-s-3d792482="" />
|
|
50
84
|
</>
|
|
51
85
|
);
|
|
52
86
|
}
|
package/src/EditorDate.tsx
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import type { JSX } from 'solid-js';
|
|
2
|
-
import { createSignal, mergeProps, splitProps } from 'solid-js';
|
|
2
|
+
import { createEffect, createSignal, mergeProps, on, onMount, splitProps, untrack } from 'solid-js';
|
|
3
|
+
import { rozieAttr } from '@rozie/runtime-solid';
|
|
4
|
+
// C-07: the native `<input type="date">` accepts ONLY `YYYY-MM-DD` and renders BLANK for
|
|
5
|
+
// anything else, silently. The seed used to be `String($props.value)`, which is not the ISO
|
|
6
|
+
// coercion this component's own docs: string promises — so a `Date`, an ISO datetime string,
|
|
7
|
+
// an epoch number or a localised string all produced an empty editor. `toIsoDateString`
|
|
8
|
+
// (helpers/dateValue.ts) is the real coercion, pure and unit-tested.
|
|
9
|
+
import { toIsoDateString } from './helpers/dateValue';
|
|
10
|
+
|
|
11
|
+
// ── N-04: the draft is DERIVED, not seeded setup-once ───────────────────────────────────
|
|
12
|
+
// A top-level `$data.draft = <read of $props.x>` is setup-once, and on ANGULAR the emitter
|
|
13
|
+
// places setup-once statements in the CONSTRUCTOR, where an `input()` signal still returns its
|
|
14
|
+
// DEFAULT — Angular sets inputs after construction. Measured directly: this editor opened
|
|
15
|
+
// EMPTY on angular for every row, including one whose value was already `YYYY-MM-DD`, while
|
|
16
|
+
// vue and lit seeded correctly. That is why the shape of the incoming value (C-07) could not
|
|
17
|
+
// be the whole story, and why the existing EditorDate coverage never saw either problem: it
|
|
18
|
+
// fills the input before asserting anything.
|
|
19
|
+
//
|
|
20
|
+
// Reading the prop through a derived function instead is correct on all six by construction,
|
|
21
|
+
// with no flash of an empty control on the fine-grained targets and no per-target branch. It
|
|
22
|
+
// is the pattern `FilterSelect` in this same package already uses (`selectValue()`), which is
|
|
23
|
+
// exactly why FilterSelect was the one drop-in unaffected. `touched` is what keeps the user's
|
|
24
|
+
// typing from being overwritten by the live prop read once they have started editing.
|
|
3
25
|
|
|
4
26
|
interface EditorDateProps {
|
|
5
27
|
/**
|
|
@@ -26,29 +48,61 @@ interface EditorDateProps {
|
|
|
26
48
|
* `() => void` — revert the edit (Escape). Null-guarded at call sites.
|
|
27
49
|
*/
|
|
28
50
|
cancel?: ((...args: any[]) => any) | null;
|
|
51
|
+
/**
|
|
52
|
+
* Focus this editor's primary control when true — the host sets it for the one editor that should hold focus; reactive.
|
|
53
|
+
*/
|
|
54
|
+
autofocus?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The column's human header, forwarded by the slot scope — used as the control's accessible name in place of the internal column id.
|
|
57
|
+
*/
|
|
58
|
+
columnLabel?: string;
|
|
29
59
|
}
|
|
30
60
|
|
|
31
61
|
export default function EditorDate(_props: EditorDateProps): JSX.Element {
|
|
32
|
-
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null }, _props);
|
|
33
|
-
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel']);
|
|
62
|
+
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, autofocus: false, columnLabel: '' }, _props);
|
|
63
|
+
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'autofocus', 'columnLabel']);
|
|
34
64
|
|
|
35
65
|
const [draft, setDraft] = createSignal('');
|
|
66
|
+
const [touched, setTouched] = createSignal(false);
|
|
67
|
+
onMount(() => {
|
|
68
|
+
if (local.autofocus) inputElRef?.focus();
|
|
69
|
+
});
|
|
70
|
+
createEffect(on(() => (() => local.autofocus)(), (v) => untrack(() => ((v: any) => {
|
|
71
|
+
if (v) inputElRef?.focus();
|
|
72
|
+
})(v)), { defer: true }));
|
|
73
|
+
let inputElRef: HTMLElement | null = null;
|
|
36
74
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
75
|
+
// ── N-04: the draft is DERIVED, not seeded setup-once ───────────────────────────────────
|
|
76
|
+
// A top-level `$data.draft = <read of $props.x>` is setup-once, and on ANGULAR the emitter
|
|
77
|
+
// places setup-once statements in the CONSTRUCTOR, where an `input()` signal still returns its
|
|
78
|
+
// DEFAULT — Angular sets inputs after construction. Measured directly: this editor opened
|
|
79
|
+
// EMPTY on angular for every row, including one whose value was already `YYYY-MM-DD`, while
|
|
80
|
+
// vue and lit seeded correctly. That is why the shape of the incoming value (C-07) could not
|
|
81
|
+
// be the whole story, and why the existing EditorDate coverage never saw either problem: it
|
|
82
|
+
// fills the input before asserting anything.
|
|
83
|
+
//
|
|
84
|
+
// Reading the prop through a derived function instead is correct on all six by construction,
|
|
85
|
+
// with no flash of an empty control on the fine-grained targets and no per-target branch. It
|
|
86
|
+
// is the pattern `FilterSelect` in this same package already uses (`selectValue()`), which is
|
|
87
|
+
// exactly why FilterSelect was the one drop-in unaffected. `touched` is what keeps the user's
|
|
88
|
+
// typing from being overwritten by the live prop read once they have started editing.
|
|
89
|
+
function draftValue() {
|
|
90
|
+
return touched() ? draft() : toIsoDateString(local.value);
|
|
91
|
+
}
|
|
40
92
|
function onInput(e: any) {
|
|
41
93
|
setDraft(e && e.target ? e.target.value : '');
|
|
94
|
+
setTouched(true);
|
|
42
95
|
}
|
|
43
96
|
function doCommit() {
|
|
44
97
|
// commit the ISO date string the native control already produced.
|
|
45
|
-
local.commit && local.commit(
|
|
98
|
+
local.commit && local.commit(draftValue());
|
|
46
99
|
}
|
|
47
100
|
function doCancel() {
|
|
48
101
|
local.cancel && local.cancel();
|
|
49
102
|
}
|
|
50
103
|
function onChange(e: any) {
|
|
51
104
|
setDraft(e && e.target ? e.target.value : '');
|
|
105
|
+
setTouched(true);
|
|
52
106
|
}
|
|
53
107
|
function onKeydown(e: any) {
|
|
54
108
|
if (e && e.key === 'Enter') {
|
|
@@ -63,9 +117,26 @@ export default function EditorDate(_props: EditorDateProps): JSX.Element {
|
|
|
63
117
|
doCommit();
|
|
64
118
|
}
|
|
65
119
|
|
|
120
|
+
// C-11 — the accessible name must be the column's HUMAN header, not its internal id.
|
|
121
|
+
// `columnId` is a lookup key (`unit_price`, `col_3`); a screen reader announcing it is
|
|
122
|
+
// reading an implementation detail aloud. table-core keeps the authored header on
|
|
123
|
+
// `column.columnDef.header`, which is a string for every declarative `<Column header>`;
|
|
124
|
+
// a header rendered by a function has no static text, so fall back to the id rather
|
|
125
|
+
// than invent one.
|
|
126
|
+
function a11yLabel() {
|
|
127
|
+
if (typeof local.columnLabel === 'string' && local.columnLabel !== '') return local.columnLabel;
|
|
128
|
+
return local.columnId;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// C-02 / editor-owns-focus: focus OUR OWN control when the host says we should hold it.
|
|
132
|
+
// $onMount covers the initial open (autofocus is already true on first render); the LAZY
|
|
133
|
+
// $watch (NOT { immediate: true } — an immediate watch fires PRE-mount and sees a null
|
|
134
|
+
// ref on Lit/Solid) covers a REACTIVE refocus while already mounted, e.g. a row-mode
|
|
135
|
+
// validation failure flipping autofocus back onto this already-open drop-in.
|
|
136
|
+
|
|
66
137
|
return (
|
|
67
138
|
<>
|
|
68
|
-
<input type="date" data-editing-cell="" aria-label={
|
|
139
|
+
<input type="date" data-editing-cell="" aria-label={rozieAttr(a11yLabel())} ref={(el) => { inputElRef = el as HTMLElement; }} class={"rdt-cell-editor"} value={draftValue()} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onInput($event); }} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onChange($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onKeydown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onBlur(); }} data-rozie-s-7abe1a56="" />
|
|
69
140
|
</>
|
|
70
141
|
);
|
|
71
142
|
}
|
package/src/EditorNumber.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JSX } from 'solid-js';
|
|
2
|
-
import { createSignal, mergeProps, splitProps } from 'solid-js';
|
|
2
|
+
import { createEffect, createSignal, mergeProps, on, onMount, splitProps, untrack } from 'solid-js';
|
|
3
|
+
import { rozieAttr } from '@rozie/runtime-solid';
|
|
3
4
|
|
|
4
5
|
interface EditorNumberProps {
|
|
5
6
|
/**
|
|
@@ -26,18 +27,46 @@ interface EditorNumberProps {
|
|
|
26
27
|
* `() => void` — revert the edit (Escape). Null-guarded at call sites.
|
|
27
28
|
*/
|
|
28
29
|
cancel?: ((...args: any[]) => any) | null;
|
|
30
|
+
/**
|
|
31
|
+
* Focus this editor's primary control when true — the host sets it for the one editor that should hold focus; reactive.
|
|
32
|
+
*/
|
|
33
|
+
autofocus?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The column's human header, forwarded by the slot scope — used as the control's accessible name in place of the internal column id.
|
|
36
|
+
*/
|
|
37
|
+
columnLabel?: string;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
export default function EditorNumber(_props: EditorNumberProps): JSX.Element {
|
|
32
|
-
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null }, _props);
|
|
33
|
-
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel']);
|
|
41
|
+
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, autofocus: false, columnLabel: '' }, _props);
|
|
42
|
+
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'autofocus', 'columnLabel']);
|
|
34
43
|
|
|
35
44
|
const [draft, setDraft] = createSignal('');
|
|
45
|
+
const [touched, setTouched] = createSignal(false);
|
|
46
|
+
onMount(() => {
|
|
47
|
+
if (local.autofocus) inputElRef?.focus();
|
|
48
|
+
});
|
|
49
|
+
createEffect(on(() => (() => local.autofocus)(), (v) => untrack(() => ((v: any) => {
|
|
50
|
+
if (v) inputElRef?.focus();
|
|
51
|
+
})(v)), { defer: true }));
|
|
52
|
+
let inputElRef: HTMLElement | null = null;
|
|
36
53
|
|
|
37
54
|
// Seed the draft string once from the incoming value (setup-once).
|
|
38
|
-
|
|
55
|
+
// ── N-04: the draft is DERIVED, not seeded setup-once ───────────────────────────────────
|
|
56
|
+
// A top-level `$data.draft = <read of $props.x>` is setup-once, and on ANGULAR the emitter
|
|
57
|
+
// places setup-once statements in the CONSTRUCTOR, where an `input()` signal still returns its
|
|
58
|
+
// DEFAULT — Angular sets inputs after construction. Measured on the sibling EditorDate: it
|
|
59
|
+
// opened EMPTY on angular for every row while vue and lit seeded correctly. Reading the prop
|
|
60
|
+
// through a derived function is correct on all six by construction, with no flash of an empty
|
|
61
|
+
// control on the fine-grained targets and no per-target branch — the pattern `FilterSelect`
|
|
62
|
+
// already uses (`selectValue()`), which is why FilterSelect was the one drop-in unaffected.
|
|
63
|
+
// `touched` keeps the live prop read from overwriting the user once they start typing.
|
|
64
|
+
function draftValue() {
|
|
65
|
+
return touched() ? draft() : local.value != null ? String(local.value) : '';
|
|
66
|
+
}
|
|
39
67
|
function onInput(e: any) {
|
|
40
68
|
setDraft(e && e.target ? e.target.value : '');
|
|
69
|
+
setTouched(true);
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
// Coerce to a Number at commit time. Defensive guard: an empty/whitespace draft
|
|
@@ -45,7 +74,7 @@ export default function EditorNumber(_props: EditorNumberProps): JSX.Element {
|
|
|
45
74
|
// non-numeric draft also commits null. Otherwise commit the coerced number.
|
|
46
75
|
function doCommit() {
|
|
47
76
|
if (!local.commit) return;
|
|
48
|
-
const raw =
|
|
77
|
+
const raw = draftValue();
|
|
49
78
|
if (raw == null || String(raw).trim() === '') {
|
|
50
79
|
local.commit(null);
|
|
51
80
|
return;
|
|
@@ -69,9 +98,26 @@ export default function EditorNumber(_props: EditorNumberProps): JSX.Element {
|
|
|
69
98
|
doCommit();
|
|
70
99
|
}
|
|
71
100
|
|
|
101
|
+
// C-11 — the accessible name must be the column's HUMAN header, not its internal id.
|
|
102
|
+
// `columnId` is a lookup key (`unit_price`, `col_3`); a screen reader announcing it is
|
|
103
|
+
// reading an implementation detail aloud. table-core keeps the authored header on
|
|
104
|
+
// `column.columnDef.header`, which is a string for every declarative `<Column header>`;
|
|
105
|
+
// a header rendered by a function has no static text, so fall back to the id rather
|
|
106
|
+
// than invent one.
|
|
107
|
+
function a11yLabel() {
|
|
108
|
+
if (typeof local.columnLabel === 'string' && local.columnLabel !== '') return local.columnLabel;
|
|
109
|
+
return local.columnId;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// C-02 / editor-owns-focus: focus OUR OWN control when the host says we should hold it.
|
|
113
|
+
// $onMount covers the initial open (autofocus is already true on first render); the LAZY
|
|
114
|
+
// $watch (NOT { immediate: true } — an immediate watch fires PRE-mount and sees a null
|
|
115
|
+
// ref on Lit/Solid) covers a REACTIVE refocus while already mounted, e.g. a row-mode
|
|
116
|
+
// validation failure flipping autofocus back onto this already-open drop-in.
|
|
117
|
+
|
|
72
118
|
return (
|
|
73
119
|
<>
|
|
74
|
-
<input type="number" data-editing-cell="" aria-label={
|
|
120
|
+
<input type="number" data-editing-cell="" aria-label={rozieAttr(a11yLabel())} ref={(el) => { inputElRef = el as HTMLElement; }} class={"rdt-cell-editor"} value={draftValue()} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onInput($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onKeydown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onBlur(); }} data-rozie-s-b2792b32="" />
|
|
75
121
|
</>
|
|
76
122
|
);
|
|
77
123
|
}
|
package/src/EditorSelect.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSX } from 'solid-js';
|
|
2
|
-
import { createSignal, mergeProps, splitProps } from 'solid-js';
|
|
2
|
+
import { createEffect, createSignal, mergeProps, on, onMount, splitProps, untrack } from 'solid-js';
|
|
3
3
|
import { Key } from '@solid-primitives/keyed';
|
|
4
4
|
import { rozieAttr, rozieDisplay } from '@rozie/runtime-solid';
|
|
5
5
|
|
|
@@ -32,26 +32,54 @@ interface EditorSelectProps {
|
|
|
32
32
|
* The select options — `[{ value, label }]`. Mirrors `<Column editorOptions>`.
|
|
33
33
|
*/
|
|
34
34
|
options?: any[];
|
|
35
|
+
/**
|
|
36
|
+
* Focus this editor's primary control when true — the host sets it for the one editor that should hold focus; reactive.
|
|
37
|
+
*/
|
|
38
|
+
autofocus?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The column's human header, forwarded by the slot scope — used as the control's accessible name in place of the internal column id.
|
|
41
|
+
*/
|
|
42
|
+
columnLabel?: string;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export default function EditorSelect(_props: EditorSelectProps): JSX.Element {
|
|
38
|
-
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, options: (() => [])() as any[] }, _props);
|
|
39
|
-
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'options']);
|
|
46
|
+
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, options: (() => [])() as any[], autofocus: false, columnLabel: '' }, _props);
|
|
47
|
+
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'options', 'autofocus', 'columnLabel']);
|
|
40
48
|
|
|
41
49
|
const [draft, setDraft] = createSignal('');
|
|
50
|
+
const [touched, setTouched] = createSignal(false);
|
|
51
|
+
onMount(() => {
|
|
52
|
+
if (local.autofocus) selectElRef?.focus();
|
|
53
|
+
});
|
|
54
|
+
createEffect(on(() => (() => local.autofocus)(), (v) => untrack(() => ((v: any) => {
|
|
55
|
+
if (v) selectElRef?.focus();
|
|
56
|
+
})(v)), { defer: true }));
|
|
57
|
+
let selectElRef: HTMLElement | null = null;
|
|
42
58
|
|
|
43
59
|
// Seed the draft once from the incoming value (setup-once). Normalize null/undefined
|
|
44
60
|
// to '' so the <select> binds to a string.
|
|
45
|
-
|
|
61
|
+
// ── N-04: the draft is DERIVED, not seeded setup-once ───────────────────────────────────
|
|
62
|
+
// A top-level `$data.draft = <read of $props.x>` is setup-once, and on ANGULAR the emitter
|
|
63
|
+
// places setup-once statements in the CONSTRUCTOR, where an `input()` signal still returns its
|
|
64
|
+
// DEFAULT — Angular sets inputs after construction. Measured on the sibling EditorDate: it
|
|
65
|
+
// opened EMPTY on angular for every row while vue and lit seeded correctly. Reading the prop
|
|
66
|
+
// through a derived function is correct on all six by construction, with no flash of an empty
|
|
67
|
+
// control on the fine-grained targets and no per-target branch — the pattern `FilterSelect`
|
|
68
|
+
// already uses (`selectValue()`), which is why FilterSelect was the one drop-in unaffected.
|
|
69
|
+
// `touched` keeps the live prop read from overwriting the user once they start typing.
|
|
70
|
+
function draftValue() {
|
|
71
|
+
return touched() ? draft() : local.value != null ? String(local.value) : '';
|
|
72
|
+
}
|
|
46
73
|
|
|
47
74
|
// Picking/arrow-cycling an option updates the draft only — no commit.
|
|
48
75
|
function onChange(e: any) {
|
|
49
76
|
setDraft(e && e.target ? e.target.value : '');
|
|
77
|
+
setTouched(true);
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
// commit/cancel are Function props (default null) — guard before calling.
|
|
53
81
|
function doCommit() {
|
|
54
|
-
local.commit && local.commit(
|
|
82
|
+
local.commit && local.commit(draftValue());
|
|
55
83
|
}
|
|
56
84
|
function doCancel() {
|
|
57
85
|
local.cancel && local.cancel();
|
|
@@ -69,9 +97,26 @@ export default function EditorSelect(_props: EditorSelectProps): JSX.Element {
|
|
|
69
97
|
doCommit();
|
|
70
98
|
}
|
|
71
99
|
|
|
100
|
+
// C-11 — the accessible name must be the column's HUMAN header, not its internal id.
|
|
101
|
+
// `columnId` is a lookup key (`unit_price`, `col_3`); a screen reader announcing it is
|
|
102
|
+
// reading an implementation detail aloud. table-core keeps the authored header on
|
|
103
|
+
// `column.columnDef.header`, which is a string for every declarative `<Column header>`;
|
|
104
|
+
// a header rendered by a function has no static text, so fall back to the id rather
|
|
105
|
+
// than invent one.
|
|
106
|
+
function a11yLabel() {
|
|
107
|
+
if (typeof local.columnLabel === 'string' && local.columnLabel !== '') return local.columnLabel;
|
|
108
|
+
return local.columnId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// C-02 / editor-owns-focus: focus OUR OWN control when the host says we should hold it.
|
|
112
|
+
// $onMount covers the initial open (autofocus is already true on first render); the LAZY
|
|
113
|
+
// $watch (NOT { immediate: true } — an immediate watch fires PRE-mount and sees a null
|
|
114
|
+
// ref on Lit/Solid) covers a REACTIVE refocus while already mounted, e.g. a row-mode
|
|
115
|
+
// validation failure flipping autofocus back onto this already-open drop-in.
|
|
116
|
+
|
|
72
117
|
return (
|
|
73
118
|
<>
|
|
74
|
-
<select data-editing-cell="" aria-label={
|
|
119
|
+
<select data-editing-cell="" aria-label={rozieAttr(a11yLabel())} ref={(el) => { selectElRef = el as HTMLElement; }} class={"rdt-cell-editor"} value={draftValue()} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onChange($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onKeydown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onBlur(); }} data-rozie-s-117f1a16="">
|
|
75
120
|
<Key each={local.options as readonly any[]} by={(opt) => opt.value}>{(opt) => <option value={rozieAttr(opt().value)} data-rozie-s-117f1a16="">{rozieDisplay(opt().label)}</option>}</Key>
|
|
76
121
|
</select>
|
|
77
122
|
</>
|
package/src/EditorText.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JSX } from 'solid-js';
|
|
2
2
|
import { createEffect, createSignal, mergeProps, on, onMount, splitProps, untrack } from 'solid-js';
|
|
3
|
+
import { rozieAttr } from '@rozie/runtime-solid';
|
|
3
4
|
|
|
4
5
|
interface EditorTextProps {
|
|
5
6
|
/**
|
|
@@ -30,13 +31,18 @@ interface EditorTextProps {
|
|
|
30
31
|
* Focus this editor's primary input when true — the host sets it for the one editor that should hold focus; reactive.
|
|
31
32
|
*/
|
|
32
33
|
autofocus?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The column's human header, forwarded by the slot scope — used as the control's accessible name in place of the internal column id.
|
|
36
|
+
*/
|
|
37
|
+
columnLabel?: string;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export default function EditorText(_props: EditorTextProps): JSX.Element {
|
|
36
|
-
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, autofocus: false }, _props);
|
|
37
|
-
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'autofocus']);
|
|
41
|
+
const _merged = mergeProps({ columnId: '', column: null, row: null, value: null, commit: null, cancel: null, autofocus: false, columnLabel: '' }, _props);
|
|
42
|
+
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'row', 'value', 'commit', 'cancel', 'autofocus', 'columnLabel']);
|
|
38
43
|
|
|
39
44
|
const [draft, setDraft] = createSignal('');
|
|
45
|
+
const [touched, setTouched] = createSignal(false);
|
|
40
46
|
onMount(() => {
|
|
41
47
|
if (local.autofocus) inputElRef?.focus();
|
|
42
48
|
});
|
|
@@ -47,17 +53,29 @@ export default function EditorText(_props: EditorTextProps): JSX.Element {
|
|
|
47
53
|
|
|
48
54
|
// Seed the draft once at setup from the incoming value (setup-once, NOT in the
|
|
49
55
|
// template). Normalize null/undefined to '' so the input value binds to a string.
|
|
50
|
-
|
|
56
|
+
// ── N-04: the draft is DERIVED, not seeded setup-once ───────────────────────────────────
|
|
57
|
+
// A top-level `$data.draft = <read of $props.x>` is setup-once, and on ANGULAR the emitter
|
|
58
|
+
// places setup-once statements in the CONSTRUCTOR, where an `input()` signal still returns its
|
|
59
|
+
// DEFAULT — Angular sets inputs after construction. Measured on the sibling EditorDate: it
|
|
60
|
+
// opened EMPTY on angular for every row while vue and lit seeded correctly. Reading the prop
|
|
61
|
+
// through a derived function is correct on all six by construction, with no flash of an empty
|
|
62
|
+
// control on the fine-grained targets and no per-target branch — the pattern `FilterSelect`
|
|
63
|
+
// already uses (`selectValue()`), which is why FilterSelect was the one drop-in unaffected.
|
|
64
|
+
// `touched` keeps the live prop read from overwriting the user once they start typing.
|
|
65
|
+
function draftValue() {
|
|
66
|
+
return touched() ? draft() : local.value != null ? String(local.value) : '';
|
|
67
|
+
}
|
|
51
68
|
|
|
52
69
|
// Untyped handler param neutralizes to `any`, so reading e.target.value typechecks
|
|
53
70
|
// ×6 (the global-filter idiom). Never inline `$data.x = $event.target.value`.
|
|
54
71
|
function onInput(e: any) {
|
|
55
72
|
setDraft(e && e.target ? e.target.value : '');
|
|
73
|
+
setTouched(true);
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
// commit/cancel are Function props (default null) — guard before calling.
|
|
59
77
|
function doCommit() {
|
|
60
|
-
local.commit && local.commit(
|
|
78
|
+
local.commit && local.commit(draftValue());
|
|
61
79
|
}
|
|
62
80
|
function doCancel() {
|
|
63
81
|
local.cancel && local.cancel();
|
|
@@ -81,9 +99,20 @@ export default function EditorText(_props: EditorTextProps): JSX.Element {
|
|
|
81
99
|
// a REACTIVE refocus while already mounted (e.g. a row-mode validation failure that flips
|
|
82
100
|
// autofocus back onto this already-open drop-in).
|
|
83
101
|
|
|
102
|
+
// C-11 — the accessible name must be the column's HUMAN header, not its internal id.
|
|
103
|
+
// `columnId` is a lookup key (`unit_price`, `col_3`); a screen reader announcing it is
|
|
104
|
+
// reading an implementation detail aloud. table-core keeps the authored header on
|
|
105
|
+
// `column.columnDef.header`, which is a string for every declarative `<Column header>`;
|
|
106
|
+
// a header rendered by a function has no static text, so fall back to the id rather
|
|
107
|
+
// than invent one.
|
|
108
|
+
function a11yLabel() {
|
|
109
|
+
if (typeof local.columnLabel === 'string' && local.columnLabel !== '') return local.columnLabel;
|
|
110
|
+
return local.columnId;
|
|
111
|
+
}
|
|
112
|
+
|
|
84
113
|
return (
|
|
85
114
|
<>
|
|
86
|
-
<input type="text" data-editing-cell="" aria-label={
|
|
115
|
+
<input type="text" data-editing-cell="" aria-label={rozieAttr(a11yLabel())} ref={(el) => { inputElRef = el as HTMLElement; }} class={"rdt-cell-editor"} value={draftValue()} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onInput($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onKeydown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onBlur(); }} data-rozie-s-0d17f43a="" />
|
|
87
116
|
</>
|
|
88
117
|
);
|
|
89
118
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSX } from 'solid-js';
|
|
2
|
-
import { createSignal, mergeProps, splitProps } from 'solid-js';
|
|
2
|
+
import { createEffect, createSignal, mergeProps, on, splitProps, untrack } from 'solid-js';
|
|
3
3
|
import { rozieAttr } from '@rozie/runtime-solid';
|
|
4
4
|
|
|
5
5
|
interface FilterNumberRangeProps {
|
|
@@ -23,25 +23,78 @@ interface FilterNumberRangeProps {
|
|
|
23
23
|
* The faceted `[min, max]` bounds for this column (`[number, number]` or null) — drives the input placeholders only.
|
|
24
24
|
*/
|
|
25
25
|
minMax?: (unknown) | null;
|
|
26
|
+
/**
|
|
27
|
+
* The column's human header, forwarded by the `#filter` slot scope — used as the control's accessible name in place of the internal column id.
|
|
28
|
+
*/
|
|
29
|
+
columnLabel?: string;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export default function FilterNumberRange(_props: FilterNumberRangeProps): JSX.Element {
|
|
29
|
-
const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null, minMax: null }, _props);
|
|
30
|
-
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'value', 'setFilter', 'minMax']);
|
|
33
|
+
const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null, minMax: null, columnLabel: '' }, _props);
|
|
34
|
+
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'value', 'setFilter', 'minMax', 'columnLabel']);
|
|
31
35
|
|
|
32
36
|
const [minDraft, setMinDraft] = createSignal('');
|
|
33
37
|
const [maxDraft, setMaxDraft] = createSignal('');
|
|
38
|
+
const [touched, setTouched] = createSignal(false);
|
|
39
|
+
createEffect(on(() => (() => Array.isArray(local.value) ? String(local.value[0]) + '\u0000' + String(local.value[1]) : '')(), (v) => untrack(() => (() => {
|
|
40
|
+
setTouched(false);
|
|
41
|
+
})()), { defer: true }));
|
|
34
42
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
// ── C-08 + N-04: both drafts are DERIVED from `$props.value`, with a `touched` latch ─────
|
|
44
|
+
//
|
|
45
|
+
// C-08 — this component mounts once per rendered filter row and is NOT remounted when the
|
|
46
|
+
// filter changes, and both drafts were seeded setup-once. A programmatic reset or an external
|
|
47
|
+
// `columnFilters` write moved the real filter while these two inputs went on displaying the
|
|
48
|
+
// old numbers. See FilterText for the full reasoning; `FilterSelect` was never affected
|
|
49
|
+
// because it reads `$props.value` live, which is the pattern adopted here.
|
|
50
|
+
//
|
|
51
|
+
// N-04 — the setup-once seed was ALSO wrong on Angular from the start: the emitter places
|
|
52
|
+
// setup-once statements in the CONSTRUCTOR, where an `input()` signal still returns its
|
|
53
|
+
// DEFAULT. Measured on the sibling EditorDate.
|
|
54
|
+
//
|
|
55
|
+
// The watch clears `touched` rather than writing the drafts, so the derived readers pick the
|
|
56
|
+
// new value up on their own. It is keyed on the TUPLE ELEMENTS, not the array identity: the
|
|
57
|
+
// funnel writes a fresh `[min, max]` array for every apply, so an identity key would fire on
|
|
58
|
+
// every apply, and a consumer mutating a tuple in place would be missed by it anyway. The key
|
|
59
|
+
// is a joined STRING rather than an array — an array-returning watch getter is compared by
|
|
60
|
+
// identity on Lit, so a fresh array every read means it can never report "unchanged" (measured:
|
|
61
|
+
// the range half of the C-08 case stayed red on lit alone with an array key).
|
|
62
|
+
function minDraftValue() {
|
|
63
|
+
return touched() ? minDraft() : Array.isArray(local.value) && local.value[0] != null ? String(local.value[0]) : '';
|
|
64
|
+
}
|
|
65
|
+
function maxDraftValue() {
|
|
66
|
+
return touched() ? maxDraft() : Array.isArray(local.value) && local.value[1] != null ? String(local.value[1]) : '';
|
|
67
|
+
}
|
|
39
68
|
// Untyped handler params neutralize to `any` (the global-filter idiom).
|
|
69
|
+
// C-01 — the range used to commit from `@change`, which is NOT the same event across
|
|
70
|
+
// targets for a text/number input: React maps `onChange` onto the native `input`
|
|
71
|
+
// event, so on React it fired on EVERY keystroke, in the same tick as the `@input`
|
|
72
|
+
// handler's `$data` write. `$data` is an async `useState` write there, so the commit
|
|
73
|
+
// re-read the PREVIOUS keystroke's draft and the filter ran one keystroke behind (the
|
|
74
|
+
// first keystroke cleared it). The other five use the real `change` event — blur or
|
|
75
|
+
// Enter — and were already correct.
|
|
76
|
+
//
|
|
77
|
+
// Fixed by committing on the events that mean the same thing everywhere, Enter and
|
|
78
|
+
// blur, which is also the contract FilterText already uses; and by making applyRange
|
|
79
|
+
// take both drafts as arguments so it can never re-read a value written in its own
|
|
80
|
+
// tick. That combination is target-independent rather than a React special case.
|
|
40
81
|
function onMinInput(e: any) {
|
|
41
82
|
setMinDraft(e && e.target ? e.target.value : '');
|
|
83
|
+
setTouched(true);
|
|
42
84
|
}
|
|
43
85
|
function onMaxInput(e: any) {
|
|
44
86
|
setMaxDraft(e && e.target ? e.target.value : '');
|
|
87
|
+
setTouched(true);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Commit the range on Enter or blur — the same commit-on-commit contract FilterText
|
|
91
|
+
// uses, NOT per keystroke. Both read the drafts in a tick where nothing has just
|
|
92
|
+
// written them, so every target reads the settled value.
|
|
93
|
+
function onKeydown(e: any) {
|
|
94
|
+
if (e && e.key === 'Enter') applyRange(minDraftValue(), maxDraftValue());
|
|
95
|
+
}
|
|
96
|
+
function onBlur() {
|
|
97
|
+
applyRange(minDraftValue(), maxDraftValue());
|
|
45
98
|
}
|
|
46
99
|
|
|
47
100
|
// Plain string-coercion functions for the placeholders (NOT $computed — the
|
|
@@ -55,9 +108,12 @@ export default function FilterNumberRange(_props: FilterNumberRangeProps): JSX.E
|
|
|
55
108
|
|
|
56
109
|
// Convert a draft to a Number or undefined (empty string → undefined so a
|
|
57
110
|
// one-sided range works). Both undefined → clear the filter.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
111
|
+
// Takes both drafts as ARGUMENTS — never re-reads $data — so it is correct on all six
|
|
112
|
+
// targets regardless of when in the tick it runs. Callers that have just written a
|
|
113
|
+
// draft pass the value they wrote.
|
|
114
|
+
function applyRange(minDraft: any, maxDraft: any) {
|
|
115
|
+
const minNum = minDraft === '' ? undefined : Number(minDraft);
|
|
116
|
+
const maxNum = maxDraft === '' ? undefined : Number(maxDraft);
|
|
61
117
|
if (minNum === undefined && maxNum === undefined) {
|
|
62
118
|
local.setFilter && local.setFilter(local.columnId, '');
|
|
63
119
|
} else {
|
|
@@ -65,12 +121,23 @@ export default function FilterNumberRange(_props: FilterNumberRangeProps): JSX.E
|
|
|
65
121
|
}
|
|
66
122
|
}
|
|
67
123
|
|
|
124
|
+
// C-11 — the accessible name must be the column's HUMAN header, not its internal id.
|
|
125
|
+
// `columnId` is a lookup key (`unit_price`, `col_3`); a screen reader announcing it is
|
|
126
|
+
// reading an implementation detail aloud. table-core keeps the authored header on
|
|
127
|
+
// `column.columnDef.header`, which is a string for every declarative `<Column header>`;
|
|
128
|
+
// a header rendered by a function has no static text, so fall back to the id rather
|
|
129
|
+
// than invent one.
|
|
130
|
+
function a11yLabel() {
|
|
131
|
+
if (typeof local.columnLabel === 'string' && local.columnLabel !== '') return local.columnLabel;
|
|
132
|
+
return local.columnId;
|
|
133
|
+
}
|
|
134
|
+
|
|
68
135
|
return (
|
|
69
136
|
<>
|
|
70
137
|
<span style={{ display: "flex", "align-items": "center" }} data-rozie-s-97b2c090="">
|
|
71
|
-
<input part="col-filter" type="number" aria-label={rozieAttr(
|
|
138
|
+
<input part="col-filter" type="number" aria-label={rozieAttr(a11yLabel() + ' min')} class={"rdt-col-filter"} placeholder={rozieAttr(minPlaceholder())} value={minDraftValue()} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onMinInput($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onKeydown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onBlur(); }} data-rozie-s-97b2c090="" />
|
|
72
139
|
<span data-rozie-s-97b2c090=""> - </span>
|
|
73
|
-
<input part="col-filter" type="number" aria-label={rozieAttr(
|
|
140
|
+
<input part="col-filter" type="number" aria-label={rozieAttr(a11yLabel() + ' max')} class={"rdt-col-filter"} placeholder={rozieAttr(maxPlaceholder())} value={maxDraftValue()} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onMaxInput($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onKeydown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onBlur(); }} data-rozie-s-97b2c090="" />
|
|
74
141
|
</span>
|
|
75
142
|
</>
|
|
76
143
|
);
|