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