@gntik-ai/blocks 0.5.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/dist/index.d.ts +163 -31
- package/dist/index.js +663 -157
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/SignUpForm/SignUpForm.configured.test.tsx +316 -0
- package/src/auth/SignUpForm/SignUpForm.tsx +194 -50
- package/src/auth/SignUpForm/block.meta.ts +15 -2
- package/src/auth/SignUpForm/examples/CustomFields.tsx +56 -0
- package/src/auth/SignUpForm/examples/Error.tsx +5 -0
- package/src/auth/SignUpForm/examples/Loading.tsx +6 -0
- package/src/auth/SignUpForm/examples/NoTerms.tsx +6 -0
- package/src/auth/SignUpForm/examples/Submitting.tsx +5 -0
- package/src/auth/SignUpForm/examples/Unavailable.tsx +15 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/shared.tsx +34 -0
- package/src/meta.ts +2 -0
- package/src/tables/DataTable/DataTable.labels.test.tsx +291 -0
- package/src/tables/DataTable/DataTable.overflow-labels.test.tsx +59 -0
- package/src/tables/DataTable/DataTable.tsx +20 -9
- package/src/tables/DataTable/block.meta.ts +102 -0
- package/src/tables/DataTable/column-menu.tsx +10 -7
- package/src/tables/DataTable/data-table-body.tsx +3 -2
- package/src/tables/DataTable/data-table-props.ts +3 -0
- package/src/tables/DataTable/data-table-row.tsx +15 -4
- package/src/tables/DataTable/density-toggle.tsx +7 -4
- package/src/tables/DataTable/editable-cell.tsx +21 -8
- package/src/tables/DataTable/header-cell.tsx +4 -0
- package/src/tables/DataTable/labels.ts +54 -0
- package/src/tables/DataTable/resize-handle.tsx +6 -3
- package/src/tables/DataTable/views.tsx +21 -14
- package/src/tables/FilterBar/FilterBar.labels.test.tsx +69 -0
- package/src/tables/FilterBar/FilterBar.tsx +45 -12
- package/src/tables/FilterBar/block.meta.ts +22 -0
- package/src/tables/PaginationFooter/PaginationFooter.labels.test.tsx +57 -0
- package/src/tables/PaginationFooter/PaginationFooter.tsx +31 -7
- package/src/tables/PaginationFooter/block.meta.ts +23 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { SignUpForm } from '../SignUpForm';
|
|
2
|
+
|
|
3
|
+
/** Ordered text/email fields, kit validation, and values.fields keyed by these ids. */
|
|
4
|
+
export default function CustomFields() {
|
|
5
|
+
return (
|
|
6
|
+
<SignUpForm
|
|
7
|
+
sso={null}
|
|
8
|
+
minPasswordLength={12}
|
|
9
|
+
passwordHelp={(minimum) => `Use at least ${minimum} characters.`}
|
|
10
|
+
fields={[
|
|
11
|
+
{
|
|
12
|
+
id: 'username',
|
|
13
|
+
label: 'Username',
|
|
14
|
+
placeholder: 'Account name',
|
|
15
|
+
help: 'Choose your account name.',
|
|
16
|
+
required: true,
|
|
17
|
+
type: 'text',
|
|
18
|
+
autoComplete: 'username',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'displayName',
|
|
22
|
+
label: 'Display name',
|
|
23
|
+
placeholder: 'Avery Collins',
|
|
24
|
+
help: 'The name others will see.',
|
|
25
|
+
required: true,
|
|
26
|
+
autoComplete: 'name',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'email',
|
|
30
|
+
label: 'Email',
|
|
31
|
+
placeholder: 'you@example.com',
|
|
32
|
+
help: 'For account notifications.',
|
|
33
|
+
required: true,
|
|
34
|
+
type: 'email',
|
|
35
|
+
autoComplete: 'email',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'tenant',
|
|
39
|
+
label: 'Tenant identifier',
|
|
40
|
+
placeholder: 'Organization',
|
|
41
|
+
help: 'Your organization identifier.',
|
|
42
|
+
required: true,
|
|
43
|
+
autoComplete: 'off',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'workspace',
|
|
47
|
+
label: 'Workspace identifier',
|
|
48
|
+
placeholder: 'Workspace',
|
|
49
|
+
help: 'Optional. Leave empty to choose later.',
|
|
50
|
+
required: false,
|
|
51
|
+
autoComplete: 'off',
|
|
52
|
+
},
|
|
53
|
+
]}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SignUpForm } from '../SignUpForm';
|
|
2
|
+
|
|
3
|
+
/** Explicit opt-out; omitting terms continues to require consent. */
|
|
4
|
+
export default function NoTerms() {
|
|
5
|
+
return <SignUpForm terms={false} sso={null} passwordHelp={(minimum) => `Use at least ${minimum} characters.`} />;
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Link } from '@gntik-ai/ui';
|
|
2
|
+
import { SignUpForm } from '../SignUpForm';
|
|
3
|
+
|
|
4
|
+
/** No account creation form or submit control while self-service is unavailable. */
|
|
5
|
+
export default function Unavailable() {
|
|
6
|
+
return (
|
|
7
|
+
<SignUpForm
|
|
8
|
+
unavailable={{
|
|
9
|
+
title: 'Registration unavailable',
|
|
10
|
+
message: 'Contact your administrator to request an account.',
|
|
11
|
+
action: <Link href="mailto:admin@example.com">Request access</Link>,
|
|
12
|
+
}}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
package/src/auth/index.ts
CHANGED
|
@@ -13,3 +13,4 @@ export { meta as ssoButtonsMeta } from './SsoButtons/block.meta';
|
|
|
13
13
|
export { ssoProviders } from './SsoButtons/fixtures';
|
|
14
14
|
export { AuthHeader, AuthDivider, type AuthHeaderProps } from './shared';
|
|
15
15
|
export type { AuthIdentifierConfig, AuthValidationMessages, AuthFormAccessibilityProps } from './shared';
|
|
16
|
+
export type { AuthSignUpField, AuthUnavailableState, AuthSignUpValidationMessages } from './shared';
|
package/src/auth/shared.tsx
CHANGED
|
@@ -111,3 +111,37 @@ export function authIdentifierError(value: string, config?: AuthIdentifierConfig
|
|
|
111
111
|
return messages?.identifierInvalid ?? fallback;
|
|
112
112
|
return null;
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/** Ordered text/email fields collected before the sign-up password. Ids must be unique. */
|
|
116
|
+
export interface AuthSignUpField {
|
|
117
|
+
id: string;
|
|
118
|
+
label: string;
|
|
119
|
+
placeholder?: string;
|
|
120
|
+
help?: ReactNode;
|
|
121
|
+
required?: boolean;
|
|
122
|
+
type?: 'text' | 'email';
|
|
123
|
+
autoComplete?: string;
|
|
124
|
+
/** Consumer copy for kit-owned validation. */
|
|
125
|
+
messages?: { required?: string; invalid?: string };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function authSignUpFieldError(value: string, field: AuthSignUpField) {
|
|
129
|
+
const trimmed = value.trim();
|
|
130
|
+
if (!trimmed) return field.required ? (field.messages?.required ?? `Enter ${field.label}.`) : null;
|
|
131
|
+
if (field.type === 'email' && !EMAIL_RE.test(trimmed))
|
|
132
|
+
return field.messages?.invalid ?? 'Enter a valid email address.';
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Consumer-owned copy and optional action when account creation is unavailable. */
|
|
137
|
+
export interface AuthUnavailableState {
|
|
138
|
+
title: ReactNode;
|
|
139
|
+
message: ReactNode;
|
|
140
|
+
action?: ReactNode;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Kit-owned sign-up validation with consumer-owned copy. */
|
|
144
|
+
export interface AuthSignUpValidationMessages {
|
|
145
|
+
passwordTooShort?: (minimum: number) => string;
|
|
146
|
+
termsRequired?: string;
|
|
147
|
+
}
|
package/src/meta.ts
CHANGED
|
@@ -20,4 +20,6 @@ export interface BlockMeta {
|
|
|
20
20
|
description: string;
|
|
21
21
|
/** @gntik-ai/ui (and other kit) components it composes, for docs and agents. */
|
|
22
22
|
uses: string[];
|
|
23
|
+
/** Optional per-instance copy contract, listed in docs and kit metadata. */
|
|
24
|
+
labels?: Array<{ key: string; type: string; state: string }>;
|
|
23
25
|
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { I18nProvider } from '@gntik-ai/ui';
|
|
4
|
+
import { DataTable } from './DataTable';
|
|
5
|
+
import type { DataTableLabels } from './labels';
|
|
6
|
+
|
|
7
|
+
const rows = [
|
|
8
|
+
{ id: 'a', name: 'Alpha', region: 'West' },
|
|
9
|
+
{ id: 'b', name: 'Beta', region: 'West' },
|
|
10
|
+
];
|
|
11
|
+
const columns = [
|
|
12
|
+
{ id: 'name', header: 'Name', accessor: (r: (typeof rows)[number]) => r.name, width: 200 },
|
|
13
|
+
{ id: 'region', header: 'Region', accessor: (r: (typeof rows)[number]) => r.region, width: 160 },
|
|
14
|
+
];
|
|
15
|
+
const labels = {
|
|
16
|
+
scrollRegion: 'Tabla con desplazamiento',
|
|
17
|
+
columns: 'Columnas',
|
|
18
|
+
visibleColumns: 'Columnas visibles',
|
|
19
|
+
pinColumns: 'Fijar columnas',
|
|
20
|
+
pinnedLeft: 'Izquierda',
|
|
21
|
+
pinnedRight: 'Derecha',
|
|
22
|
+
pinLeft: 'Fijar izquierda',
|
|
23
|
+
pinRight: 'Fijar derecha',
|
|
24
|
+
unpinned: 'Sin fijar',
|
|
25
|
+
density: 'Densidad',
|
|
26
|
+
comfortable: 'Cómoda',
|
|
27
|
+
compact: 'Compacta',
|
|
28
|
+
defaultView: 'Vista predeterminada',
|
|
29
|
+
defaultViewOption: 'Sin vista',
|
|
30
|
+
modified: '(modificada)',
|
|
31
|
+
views: 'Vistas',
|
|
32
|
+
saveChanges: 'Guardar cambios',
|
|
33
|
+
saveAs: 'Guardar como…',
|
|
34
|
+
renameView: 'Renombrar vista…',
|
|
35
|
+
deleteView: 'Eliminar vista',
|
|
36
|
+
resetChanges: 'Restablecer cambios',
|
|
37
|
+
saveViewTitle: 'Nueva vista',
|
|
38
|
+
renameViewTitle: 'Cambiar nombre',
|
|
39
|
+
viewName: 'Nombre de vista',
|
|
40
|
+
cancel: 'Cancelar',
|
|
41
|
+
saveView: 'Guardar vista',
|
|
42
|
+
rename: 'Renombrar',
|
|
43
|
+
closeDialog: 'Cerrar diálogo',
|
|
44
|
+
selectionCount: vi.fn((n: number) => `${n} seleccionadas`),
|
|
45
|
+
selectAll: 'Seleccionar página',
|
|
46
|
+
selection: 'Selección',
|
|
47
|
+
actions: 'Acciones',
|
|
48
|
+
selectRow: vi.fn((name: string) => `Seleccionar ${name}`),
|
|
49
|
+
rowActions: vi.fn((name: string) => `Acciones de ${name}`),
|
|
50
|
+
resizeColumn: vi.fn((name: string) => `Cambiar ancho de ${name}`),
|
|
51
|
+
columnWidth: vi.fn((width: number) => `${width} píxeles`),
|
|
52
|
+
} satisfies Partial<DataTableLabels>;
|
|
53
|
+
|
|
54
|
+
it('replaces table menus, selection announcements, resize names and saved-view dialogs', async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
const { container } = render(
|
|
57
|
+
<DataTable
|
|
58
|
+
rows={rows}
|
|
59
|
+
columns={columns}
|
|
60
|
+
caption="Items"
|
|
61
|
+
paginated={false}
|
|
62
|
+
resizable
|
|
63
|
+
showPinMenu
|
|
64
|
+
defaultViews={[]}
|
|
65
|
+
labels={labels}
|
|
66
|
+
rowActions={() => [{ label: 'Inspect', onClick: () => {} }]}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
expect(screen.getByRole('group', { name: 'Densidad' })).toBeVisible();
|
|
70
|
+
await user.click(screen.getByRole('button', { name: 'Compacta' }));
|
|
71
|
+
await user.click(screen.getByRole('button', { name: 'Cómoda' }));
|
|
72
|
+
expect(screen.getByRole('checkbox', { name: 'Seleccionar página' })).toBeVisible();
|
|
73
|
+
await user.click(screen.getByRole('checkbox', { name: 'Seleccionar Alpha' }));
|
|
74
|
+
expect(container.querySelector('[aria-live]')?.textContent).toBe('1 seleccionadas');
|
|
75
|
+
expect(labels.selectionCount).toHaveBeenLastCalledWith(1);
|
|
76
|
+
expect(labels.selectRow).toHaveBeenCalledWith('Alpha');
|
|
77
|
+
expect(screen.getByRole('button', { name: 'Acciones de Alpha' })).toBeVisible();
|
|
78
|
+
const handle = screen.getByRole('separator', { name: 'Cambiar ancho de Name' });
|
|
79
|
+
expect(handle).toHaveAttribute('aria-valuetext', '200 píxeles');
|
|
80
|
+
expect(labels.resizeColumn).toHaveBeenCalledWith('Name');
|
|
81
|
+
expect(labels.columnWidth).toHaveBeenCalledWith(200);
|
|
82
|
+
await user.click(screen.getByRole('button', { name: 'Columnas' }));
|
|
83
|
+
expect(await screen.findByText('Columnas visibles')).toBeVisible();
|
|
84
|
+
expect(screen.getByText('Fijar columnas')).toBeVisible();
|
|
85
|
+
await user.click(screen.getByRole('menuitemcheckbox', { name: 'Region' }));
|
|
86
|
+
await user.keyboard('{Escape}');
|
|
87
|
+
await user.click(screen.getByRole('button', { name: 'Vista predeterminada (modificada)' }));
|
|
88
|
+
expect(await screen.findByText('Vistas', { exact: true })).toBeVisible();
|
|
89
|
+
expect(screen.getByRole('menuitemradio', { name: 'Sin vista' })).toBeVisible();
|
|
90
|
+
for (const name of ['Guardar cambios', 'Renombrar vista…', 'Eliminar vista', 'Restablecer cambios'])
|
|
91
|
+
expect(screen.getByRole('menuitem', { name })).toBeVisible();
|
|
92
|
+
await user.click(screen.getByRole('menuitem', { name: 'Guardar como…' }));
|
|
93
|
+
expect(await screen.findByRole('dialog', { name: 'Nueva vista' })).toBeVisible();
|
|
94
|
+
expect(screen.getByRole('button', { name: 'Cerrar diálogo' })).toBeVisible();
|
|
95
|
+
await user.type(screen.getByRole('textbox', { name: 'Nombre de vista' }), 'My view');
|
|
96
|
+
await user.click(screen.getByRole('button', { name: 'Guardar vista' }));
|
|
97
|
+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
98
|
+
await user.click(screen.getByRole('button', { name: 'My view' }));
|
|
99
|
+
await user.click(await screen.findByRole('menuitem', { name: 'Renombrar vista…' }));
|
|
100
|
+
expect(await screen.findByRole('dialog', { name: 'Cambiar nombre' })).toBeVisible();
|
|
101
|
+
expect(screen.getByRole('button', { name: 'Renombrar' })).toBeVisible();
|
|
102
|
+
await user.click(screen.getByRole('button', { name: 'Cancelar' }));
|
|
103
|
+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('replaces group counts, ungrouped names, empty and loading states, and the nested footer', async () => {
|
|
107
|
+
const selectGroup = vi.fn((name: string) => `Todo en ${name}`);
|
|
108
|
+
const groupRows = vi.fn((count: number) => `filas: ${count}`);
|
|
109
|
+
const loading = vi.fn((caption: string) => `Cargando ${caption}`);
|
|
110
|
+
const extra: Partial<DataTableLabels> = {
|
|
111
|
+
selectGroup,
|
|
112
|
+
groupRows,
|
|
113
|
+
noGroup: 'Sin grupo',
|
|
114
|
+
emptyTitle: 'Sin resultados',
|
|
115
|
+
emptyDescription: 'Cambie los filtros',
|
|
116
|
+
loading,
|
|
117
|
+
rowsPerPage: 'Filas',
|
|
118
|
+
rowsPerPageLabel: 'Por página',
|
|
119
|
+
rangeSummary: (a, b, n) => `${a}/${b}/${n}`,
|
|
120
|
+
paginationNav: (noun) => `Páginas de ${noun}`,
|
|
121
|
+
previous: 'Atrás',
|
|
122
|
+
next: 'Adelante',
|
|
123
|
+
page: (n) => `Ir a ${n}`,
|
|
124
|
+
};
|
|
125
|
+
const { rerender } = render(
|
|
126
|
+
<DataTable rows={rows} columns={columns} groupBy={() => ''} labels={extra} caption="Items" />,
|
|
127
|
+
);
|
|
128
|
+
expect(screen.getByRole('checkbox', { name: 'Todo en Sin grupo' })).toBeVisible();
|
|
129
|
+
expect(screen.getByRole('button', { name: 'Sin grupo 2 filas: 2' })).toBeVisible();
|
|
130
|
+
expect(selectGroup).toHaveBeenCalledWith('Sin grupo');
|
|
131
|
+
expect(groupRows).toHaveBeenCalledWith(2);
|
|
132
|
+
expect(screen.getByRole('navigation', { name: 'Páginas de Items' })).toBeVisible();
|
|
133
|
+
expect(screen.getByRole('combobox', { name: 'Por página' })).toBeVisible();
|
|
134
|
+
expect(screen.getByText('1/2/2')).toBeVisible();
|
|
135
|
+
rerender(<DataTable rows={[]} columns={columns} labels={extra} caption="Items" />);
|
|
136
|
+
expect(screen.getByText('Sin resultados')).toBeVisible();
|
|
137
|
+
expect(screen.getByText('Cambie los filtros')).toBeVisible();
|
|
138
|
+
rerender(<DataTable rows={rows} columns={columns} labels={extra} caption="Items" loading />);
|
|
139
|
+
expect(screen.getByRole('status').textContent).toBe('Cargando Items');
|
|
140
|
+
expect(loading).toHaveBeenCalledWith('Items');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('replaces built-in inline editor names, hints and validation errors', async () => {
|
|
144
|
+
const user = userEvent.setup();
|
|
145
|
+
const editLabel = vi.fn((column: string, row: string) => `${column} de ${row}`);
|
|
146
|
+
const minimumValue = vi.fn((n: number) => `Mínimo ${n}`);
|
|
147
|
+
const maximumValue = vi.fn((n: number) => `Máximo ${n}`);
|
|
148
|
+
const editable = [
|
|
149
|
+
{
|
|
150
|
+
id: 'name',
|
|
151
|
+
header: 'Name',
|
|
152
|
+
accessor: (r: (typeof rows)[number]) => r.name,
|
|
153
|
+
editable: true,
|
|
154
|
+
editor: { type: 'number' as const, min: 1, max: 10 },
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
render(
|
|
158
|
+
<DataTable
|
|
159
|
+
rows={rows}
|
|
160
|
+
columns={editable}
|
|
161
|
+
paginated={false}
|
|
162
|
+
onCellEdit={() => false}
|
|
163
|
+
labels={{
|
|
164
|
+
editHint: 'Pulse Intro para editar',
|
|
165
|
+
editLabel,
|
|
166
|
+
editError: 'No se guardó',
|
|
167
|
+
enterNumber: 'Escriba un número',
|
|
168
|
+
minimumValue,
|
|
169
|
+
maximumValue,
|
|
170
|
+
}}
|
|
171
|
+
/>,
|
|
172
|
+
);
|
|
173
|
+
expect(screen.getByRole('button', { name: 'Alpha' })).toHaveAccessibleDescription('Pulse Intro para editar');
|
|
174
|
+
await user.click(screen.getByRole('button', { name: 'Alpha' }));
|
|
175
|
+
const input = screen.getByRole('spinbutton', { name: 'Name de Alpha' });
|
|
176
|
+
expect(editLabel).toHaveBeenCalledWith('Name', 'Alpha');
|
|
177
|
+
await user.clear(input);
|
|
178
|
+
await user.type(input, '0{Enter}');
|
|
179
|
+
expect(await screen.findByRole('alert')).toHaveTextContent('Mínimo 1');
|
|
180
|
+
expect(minimumValue).toHaveBeenCalledWith(1);
|
|
181
|
+
await user.clear(input);
|
|
182
|
+
await user.type(input, '11{Enter}');
|
|
183
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Máximo 10');
|
|
184
|
+
expect(maximumValue).toHaveBeenCalledWith(10);
|
|
185
|
+
await user.clear(input);
|
|
186
|
+
await user.type(input, '5{Enter}');
|
|
187
|
+
expect(await screen.findByText('No se guardó')).toBeVisible();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('keeps provider translations and legacy literals under a partial override', () => {
|
|
191
|
+
const { container, rerender } = render(
|
|
192
|
+
<I18nProvider locale="es">
|
|
193
|
+
<DataTable rows={rows} columns={columns} paginated={false} defaultViews={[]} defaultSelectedIds={['a']} />
|
|
194
|
+
</I18nProvider>,
|
|
195
|
+
);
|
|
196
|
+
const table = container.querySelector('table')?.outerHTML;
|
|
197
|
+
const groupName = screen.getByRole('group').getAttribute('aria-label');
|
|
198
|
+
rerender(
|
|
199
|
+
<I18nProvider locale="es">
|
|
200
|
+
<DataTable
|
|
201
|
+
rows={rows}
|
|
202
|
+
columns={columns}
|
|
203
|
+
paginated={false}
|
|
204
|
+
defaultViews={[]}
|
|
205
|
+
defaultSelectedIds={['a']}
|
|
206
|
+
labels={{ defaultView: 'Custom view' }}
|
|
207
|
+
/>
|
|
208
|
+
</I18nProvider>,
|
|
209
|
+
);
|
|
210
|
+
expect(container.querySelector('table')?.outerHTML).toBe(table);
|
|
211
|
+
expect(screen.getByRole('group')).toHaveAttribute('aria-label', groupName);
|
|
212
|
+
expect(screen.getByRole('button', { name: 'Custom view' })).toBeVisible();
|
|
213
|
+
expect(screen.getByText('1 selected')).toBeVisible();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('retains the published text-node boundaries for default selection and loading summaries', () => {
|
|
217
|
+
const { container, rerender } = render(
|
|
218
|
+
<DataTable rows={rows} columns={columns} defaultSelectedIds={['a']} paginated={false} caption="Items" />,
|
|
219
|
+
);
|
|
220
|
+
expect([...container.querySelector('[aria-live]')!.childNodes].map((node) => node.textContent)).toEqual([
|
|
221
|
+
'1',
|
|
222
|
+
' selected',
|
|
223
|
+
]);
|
|
224
|
+
rerender(<DataTable rows={rows} columns={columns} loading caption="Items" />);
|
|
225
|
+
expect([...screen.getByRole('status').childNodes].map((node) => node.textContent)).toEqual([
|
|
226
|
+
'Loading ',
|
|
227
|
+
'items',
|
|
228
|
+
'…',
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('replaces every pin option and the single-selection heading without translating caller radio names', async () => {
|
|
233
|
+
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
234
|
+
const full: DataTableLabels = {
|
|
235
|
+
...labels,
|
|
236
|
+
selectGroup: (group) => `Grupo ${group}`,
|
|
237
|
+
groupRows: (count) => `${count} filas`,
|
|
238
|
+
noGroup: 'Sin grupo',
|
|
239
|
+
emptyTitle: 'Vacío',
|
|
240
|
+
emptyDescription: 'Sin coincidencias',
|
|
241
|
+
loading: (caption) => `Cargando ${caption}`,
|
|
242
|
+
editHint: 'Editar',
|
|
243
|
+
editLabel: (column, row) => `${column} de ${row}`,
|
|
244
|
+
editError: 'Error al guardar',
|
|
245
|
+
enterNumber: 'Número requerido',
|
|
246
|
+
minimumValue: (min) => `Mínimo ${min}`,
|
|
247
|
+
maximumValue: (max) => `Máximo ${max}`,
|
|
248
|
+
rowsPerPage: 'Filas por página',
|
|
249
|
+
rowsPerPageLabel: 'Elegir tamaño',
|
|
250
|
+
rangeSummary: (from, to, total) => `${from}|${to}|${total}`,
|
|
251
|
+
paginationNav: (noun) => `Páginas de ${noun}`,
|
|
252
|
+
previous: 'Anterior',
|
|
253
|
+
next: 'Siguiente',
|
|
254
|
+
page: (n) => `Página ${n}`,
|
|
255
|
+
};
|
|
256
|
+
const { rerender } = render(
|
|
257
|
+
<I18nProvider locale="es">
|
|
258
|
+
<DataTable rows={rows} columns={columns} paginated={false} showPinMenu labels={full} />
|
|
259
|
+
</I18nProvider>,
|
|
260
|
+
);
|
|
261
|
+
await user.click(screen.getByRole('button', { name: 'Columnas' }));
|
|
262
|
+
const submenu = await screen.findByRole('menuitem', { name: 'Name' });
|
|
263
|
+
act(() => submenu.focus());
|
|
264
|
+
await user.keyboard('{ArrowRight}');
|
|
265
|
+
for (const name of ['Fijar izquierda', 'Fijar derecha', 'Sin fijar'])
|
|
266
|
+
expect(await screen.findByRole('menuitemradio', { name })).toBeVisible();
|
|
267
|
+
await user.click(screen.getByRole('menuitemradio', { name: 'Fijar izquierda' }));
|
|
268
|
+
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull());
|
|
269
|
+
await waitFor(() => expect(document.querySelector('[data-base-ui-focus-guard]')).toBeNull());
|
|
270
|
+
await user.click(screen.getByRole('button', { name: 'Columnas' }));
|
|
271
|
+
await screen.findByRole('menu');
|
|
272
|
+
expect(await screen.findByRole('menuitem', { name: /Name.*Izquierda/ })).toHaveAccessibleName('NameIzquierda');
|
|
273
|
+
act(() => screen.getByRole('menuitem', { name: /Name.*Izquierda/ }).focus());
|
|
274
|
+
await user.keyboard('{ArrowRight}');
|
|
275
|
+
await user.click(await screen.findByRole('menuitemradio', { name: 'Fijar derecha' }));
|
|
276
|
+
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull());
|
|
277
|
+
await waitFor(() => expect(document.querySelector('[data-base-ui-focus-guard]')).toBeNull());
|
|
278
|
+
await user.click(screen.getByRole('button', { name: 'Columnas' }));
|
|
279
|
+
expect(await screen.findByRole('menuitem', { name: /Name.*Derecha/ })).toBeVisible();
|
|
280
|
+
await user.keyboard('{Escape}');
|
|
281
|
+
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull());
|
|
282
|
+
await waitFor(() => expect(document.querySelector('[data-base-ui-focus-guard]')).toBeNull());
|
|
283
|
+
rerender(
|
|
284
|
+
<I18nProvider locale="es">
|
|
285
|
+
<DataTable rows={rows} columns={columns} labels={full} selectionMode="single" legend="Items" />
|
|
286
|
+
</I18nProvider>,
|
|
287
|
+
);
|
|
288
|
+
expect(screen.getByRole('columnheader', { name: 'Selección' })).toBeInTheDocument();
|
|
289
|
+
expect(screen.getByRole('radio', { name: 'Alpha' })).toBeVisible();
|
|
290
|
+
expect(screen.queryByRole('radio', { name: 'Seleccionar Alpha' })).toBeNull();
|
|
291
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { I18nProvider } from '@gntik-ai/ui';
|
|
3
|
+
import { DataTable } from './DataTable';
|
|
4
|
+
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// jsdom has no layout. Supply the browser geometry/notification boundary so the
|
|
7
|
+
// real Table overflow detection exposes its keyboard-focusable scroll region.
|
|
8
|
+
vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockReturnValue(800);
|
|
9
|
+
vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(300);
|
|
10
|
+
vi.stubGlobal(
|
|
11
|
+
'ResizeObserver',
|
|
12
|
+
class {
|
|
13
|
+
constructor(private readonly notify: () => void) {}
|
|
14
|
+
observe() {
|
|
15
|
+
this.notify();
|
|
16
|
+
}
|
|
17
|
+
unobserve() {}
|
|
18
|
+
disconnect() {}
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
vi.unstubAllGlobals();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it.each([
|
|
29
|
+
['en', 'Scrollable table'],
|
|
30
|
+
['es', 'Tabla desplazable'],
|
|
31
|
+
] as const)('labels the overflowing region per instance over the %s provider default', (locale, defaultName) => {
|
|
32
|
+
const { rerender } = render(
|
|
33
|
+
<I18nProvider locale={locale}>
|
|
34
|
+
<DataTable paginated={false} />
|
|
35
|
+
</I18nProvider>,
|
|
36
|
+
);
|
|
37
|
+
expect(screen.getByRole('region', { name: defaultName })).toHaveAttribute('tabindex', '0');
|
|
38
|
+
rerender(
|
|
39
|
+
<I18nProvider locale={locale}>
|
|
40
|
+
<DataTable paginated={false} labels={{ scrollRegion: 'Tabla con desplazamiento' }} />
|
|
41
|
+
</I18nProvider>,
|
|
42
|
+
);
|
|
43
|
+
expect(screen.getByRole('region', { name: 'Tabla con desplazamiento' })).toHaveAttribute('tabindex', '0');
|
|
44
|
+
expect(screen.queryByRole('region', { name: defaultName })).toBeNull();
|
|
45
|
+
rerender(
|
|
46
|
+
<I18nProvider locale={locale}>
|
|
47
|
+
<DataTable paginated={false} labels={{ columns: 'Elegir columnas' }} />
|
|
48
|
+
</I18nProvider>,
|
|
49
|
+
);
|
|
50
|
+
expect(screen.getByRole('region', { name: defaultName })).toHaveAttribute('tabindex', '0');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('keeps the caller caption as the virtualized region name', () => {
|
|
54
|
+
render(
|
|
55
|
+
<DataTable paginated={false} virtualize caption="Items" labels={{ scrollRegion: 'Tabla con desplazamiento' }} />,
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByRole('region', { name: 'Items' })).toHaveAttribute('tabindex', '0');
|
|
58
|
+
expect(screen.queryByRole('region', { name: 'Tabla con desplazamiento' })).toBeNull();
|
|
59
|
+
});
|
|
@@ -48,6 +48,7 @@ import { useControllable, useTableViews } from './use-table-views';
|
|
|
48
48
|
import { useVirtualRows } from './use-virtual-rows';
|
|
49
49
|
import { ViewSwitcher, type DataTableViewState } from './views';
|
|
50
50
|
|
|
51
|
+
export type { DataTableLabels } from './labels';
|
|
51
52
|
export type { DataTableProps } from './data-table-props';
|
|
52
53
|
|
|
53
54
|
const defaultRowId = (row: unknown): string => {
|
|
@@ -135,6 +136,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
135
136
|
onFiltersChange,
|
|
136
137
|
toolbar,
|
|
137
138
|
className,
|
|
139
|
+
labels,
|
|
138
140
|
}: DataTableProps<T>) {
|
|
139
141
|
const { t } = useI18n();
|
|
140
142
|
const singleMode = selectionMode === 'single';
|
|
@@ -224,7 +226,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
224
226
|
setInnerCollapsed(next);
|
|
225
227
|
onCollapsedGroupsChange?.([...next]);
|
|
226
228
|
};
|
|
227
|
-
const nameOf = (key: string) => key || 'No group';
|
|
229
|
+
const nameOf = (key: string) => key || (labels?.noGroup ?? 'No group');
|
|
228
230
|
const bodyGroups: Array<DataTableBodyGroup<T>> | undefined =
|
|
229
231
|
effectiveGroupBy && allGroups
|
|
230
232
|
? allGroups
|
|
@@ -349,7 +351,11 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
349
351
|
<>
|
|
350
352
|
{selectionSummary && (
|
|
351
353
|
<span className="text-[12.5px] font-semibold text-foreground" aria-live="polite">
|
|
352
|
-
{
|
|
354
|
+
{labels?.selectionCount ? (
|
|
355
|
+
labels.selectionCount(selectedList.length)
|
|
356
|
+
) : (
|
|
357
|
+
<>{selectedList.length} selected</>
|
|
358
|
+
)}
|
|
353
359
|
</span>
|
|
354
360
|
)}
|
|
355
361
|
{selectionSummary && bulkActions && <span aria-hidden className="h-4 w-px bg-border" />}
|
|
@@ -362,6 +368,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
362
368
|
<div className="flex items-center gap-2">
|
|
363
369
|
{showViews && (
|
|
364
370
|
<ViewSwitcher
|
|
371
|
+
labels={labels}
|
|
365
372
|
views={viewsApi.views}
|
|
366
373
|
activeId={viewsApi.activeId}
|
|
367
374
|
dirty={viewsApi.dirty}
|
|
@@ -373,9 +380,10 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
373
380
|
onReset={viewsApi.reset}
|
|
374
381
|
/>
|
|
375
382
|
)}
|
|
376
|
-
{showDensityToggle && <DensityToggle value={density} onChange={changeDensity} />}
|
|
383
|
+
{showDensityToggle && <DensityToggle labels={labels} value={density} onChange={changeDensity} />}
|
|
377
384
|
{showColumnMenu && (
|
|
378
385
|
<ColumnVisibilityMenu
|
|
386
|
+
labels={labels}
|
|
379
387
|
columns={columns}
|
|
380
388
|
hidden={hidden}
|
|
381
389
|
onToggle={toggleColumn}
|
|
@@ -388,7 +396,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
388
396
|
)}
|
|
389
397
|
{anyEditable && (
|
|
390
398
|
<span id={editHintId} className="sr-only">
|
|
391
|
-
Press Enter or F2 to edit.
|
|
399
|
+
{labels?.editHint ?? 'Press Enter or F2 to edit.'}
|
|
392
400
|
</span>
|
|
393
401
|
)}
|
|
394
402
|
{/* The toggle drives a density context for the table: its rows and the controls inside them. */}
|
|
@@ -400,7 +408,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
400
408
|
aria-rowcount={virtual ? items + 1 : undefined}
|
|
401
409
|
stickyHeader={virtualOn}
|
|
402
410
|
containerClassName={virtualOn ? virtualContainerClassName : undefined}
|
|
403
|
-
containerLabel={virtualOn ? caption :
|
|
411
|
+
containerLabel={virtualOn ? caption : labels?.scrollRegion}
|
|
404
412
|
// Keep single-mode resize handles inside their cells. Overflow at the
|
|
405
413
|
// final column would add a scroll-container stop before the header.
|
|
406
414
|
className={cn(
|
|
@@ -423,10 +431,10 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
423
431
|
)}
|
|
424
432
|
style={layout.lead.style}
|
|
425
433
|
>
|
|
426
|
-
{singleMode && <span className="sr-only">{t('table.selection')}</span>}
|
|
434
|
+
{singleMode && <span className="sr-only">{labels?.selection ?? t('table.selection')}</span>}
|
|
427
435
|
{selectable && (
|
|
428
436
|
<Checkbox
|
|
429
|
-
aria-label={t('table.selectAllPage')}
|
|
437
|
+
aria-label={labels?.selectAll ?? t('table.selectAllPage')}
|
|
430
438
|
checked={header.checked}
|
|
431
439
|
indeterminate={header.indeterminate}
|
|
432
440
|
disabled={loading || pageIds.length === 0}
|
|
@@ -439,6 +447,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
439
447
|
const bounds = widthBounds(c);
|
|
440
448
|
return (
|
|
441
449
|
<DataTableHeadCell
|
|
450
|
+
labels={labels}
|
|
442
451
|
key={c.id}
|
|
443
452
|
column={c}
|
|
444
453
|
layout={layout.columns.get(c.id)}
|
|
@@ -467,12 +476,13 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
467
476
|
)}
|
|
468
477
|
style={layout.trail.style}
|
|
469
478
|
>
|
|
470
|
-
<span className="sr-only">{t('table.actions')}</span>
|
|
479
|
+
<span className="sr-only">{labels?.actions ?? t('table.actions')}</span>
|
|
471
480
|
</TableHead>
|
|
472
481
|
)}
|
|
473
482
|
</TableRow>
|
|
474
483
|
</TableHeader>
|
|
475
484
|
<DataTableBody
|
|
485
|
+
labels={labels}
|
|
476
486
|
rows={pageRows}
|
|
477
487
|
columns={visibleColumns}
|
|
478
488
|
getRowId={getRowId}
|
|
@@ -500,11 +510,12 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
500
510
|
</SelectionGroup>
|
|
501
511
|
{loading && (
|
|
502
512
|
<p role="status" className="sr-only">
|
|
503
|
-
Loading {caption.toLowerCase()}
|
|
513
|
+
{labels?.loading ? labels.loading(caption) : <>Loading {caption.toLowerCase()}…</>}
|
|
504
514
|
</p>
|
|
505
515
|
)}
|
|
506
516
|
{paginated && !loading && rows.length > 0 && (
|
|
507
517
|
<PaginationFooter
|
|
518
|
+
labels={labels}
|
|
508
519
|
className="border-t border-border px-4 py-3"
|
|
509
520
|
total={rows.length}
|
|
510
521
|
page={page}
|