@inglorious/ui 9.10.1 → 9.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ui",
3
- "version": "9.10.1",
3
+ "version": "9.11.1",
4
4
  "description": "A design system for Inglorious Web with render-function components and customizable themes",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -341,7 +341,7 @@
341
341
  "peerDependencies": {
342
342
  "@inglorious/store": "^12.1.1",
343
343
  "@inglorious/utils": "^5.1.0",
344
- "@inglorious/web": "^6.14.0"
344
+ "@inglorious/web": "^6.14.1"
345
345
  },
346
346
  "devDependencies": {
347
347
  "@iconify-icons/fa7-brands": "^2.0.0",
@@ -353,7 +353,7 @@
353
353
  "@inglorious/store": "12.1.1",
354
354
  "@inglorious/vite-plugin-jsx": "2.0.6",
355
355
  "@inglorious/vite-plugin-vue": "1.0.7",
356
- "@inglorious/web": "6.14.0",
356
+ "@inglorious/web": "6.14.1",
357
357
  "@lit-labs/ssr": "^4.1.0",
358
358
  "@storybook/addon-docs": "^10.6.5",
359
359
  "@storybook/web-components-vite": "^10.6.5",
@@ -124,7 +124,7 @@
124
124
  user-select: none;
125
125
  padding: var(--iw-space-2) var(--iw-space-3);
126
126
 
127
- &:focused {
127
+ &:focus-visible {
128
128
  background-color: var(--iw-color-bg-secondary);
129
129
  }
130
130
 
@@ -6,3 +6,27 @@ export function format(value, type) {
6
6
  return new Date(value).getTime()
7
7
  return value
8
8
  }
9
+
10
+ const DATE_LENGTH = 10
11
+ const DATETIME_LENGTH = 16
12
+ const MINUTE_MS = 60_000
13
+
14
+ /**
15
+ * A filter value as its field shows it. Date fields read their text as UTC,
16
+ * datetime fields as local time, so each turns back the same way.
17
+ * @param {any} value
18
+ * @param {string} inputType
19
+ * @returns {string|number}
20
+ */
21
+ export function toField(value, inputType) {
22
+ if (value == null || Number.isNaN(value)) return ""
23
+ if (typeof value !== "number") return value
24
+ if (inputType === "date") {
25
+ return new Date(value).toISOString().slice(0, DATE_LENGTH)
26
+ }
27
+ if (inputType === "datetime-local") {
28
+ const offset = new Date(value).getTimezoneOffset() * MINUTE_MS
29
+ return new Date(value - offset).toISOString().slice(0, DATETIME_LENGTH)
30
+ }
31
+ return value
32
+ }
@@ -1,5 +1,5 @@
1
1
  import { Input } from "../../../controls/input/index.js"
2
- import { format } from "./helpers.js"
2
+ import { format, toField } from "./helpers.js"
3
3
 
4
4
  const DEFAULT_PLACEHOLDER = {
5
5
  number: "=",
@@ -10,7 +10,9 @@ export const InputFilter = {
10
10
  render(entity, column) {
11
11
  return Input.render({
12
12
  name: column.id,
13
+ ariaLabel: column.filter.ariaLabel ?? `Filter ${column.title}`,
13
14
  inputType: column.filter.type,
15
+ value: toField(entity.filters[column.id], column.filter.type),
14
16
  placeholder:
15
17
  column.filter.placeholder ?? DEFAULT_PLACEHOLDER[column.filter.type],
16
18
  size: "sm",
@@ -1,7 +1,7 @@
1
1
  import { html } from "@inglorious/web"
2
2
 
3
3
  import { Input } from "../../../controls/input/index.js"
4
- import { format } from "./helpers.js"
4
+ import { format, toField } from "./helpers.js"
5
5
 
6
6
  const RANGE_TYPE = {
7
7
  range: "number",
@@ -15,7 +15,12 @@ export const RangeFilter = {
15
15
  return html`<div class="iw-data-grid-filter-range">
16
16
  ${Input.render({
17
17
  name: `${column.id}Min`,
18
+ ariaLabel: column.filter.minLabel ?? `Filter ${column.title}: min`,
18
19
  inputType: RANGE_TYPE[column.filter.type],
20
+ value: toField(
21
+ entity.filters[column.id]?.min,
22
+ RANGE_TYPE[column.filter.type],
23
+ ),
19
24
  placeholder: column.filter.placeholder ?? "≥",
20
25
  size: "sm",
21
26
  isFullWidth: true,
@@ -29,7 +34,12 @@ export const RangeFilter = {
29
34
  })}
30
35
  ${Input.render({
31
36
  name: `${column.id}Max`,
37
+ ariaLabel: column.filter.maxLabel ?? `Filter ${column.title}: max`,
32
38
  inputType: RANGE_TYPE[column.filter.type],
39
+ value: toField(
40
+ entity.filters[column.id]?.max,
41
+ RANGE_TYPE[column.filter.type],
42
+ ),
33
43
  placeholder: column.filter.placeholder ?? "≤",
34
44
  size: "sm",
35
45
  isFullWidth: true,
@@ -5,6 +5,8 @@ export const SelectFilter = {
5
5
  render(entity, column) {
6
6
  return Select.render({
7
7
  name: column.id,
8
+ ariaLabel: column.filter.ariaLabel ?? `Filter ${column.title}`,
9
+ value: entity.filters[column.id] ?? "",
8
10
  size: "sm",
9
11
  isFullWidth: true,
10
12
  options: column.filter.options,
@@ -433,6 +433,8 @@ function initDataGrid(entity) {
433
433
  entity.isMultiSelect ??= true
434
434
  entity.isVirtualized ??= false
435
435
 
436
+ entity.totalRows ??= null
437
+
436
438
  entity.pagination ??= null
437
439
  if (entity.pagination) {
438
440
  entity.pagination.page ??= 0
@@ -60,11 +60,15 @@ export function getEditedRows(entity) {
60
60
  }
61
61
 
62
62
  /**
63
- * Gets the processed rows (filtered, searched, sorted, paginated).
63
+ * Gets the processed rows (filtered, searched, sorted, paginated). A grid with
64
+ * `totalRows` holds the server's page already processed, so it gets the rows
65
+ * as they are.
64
66
  * @param {TableEntity} entity
65
67
  * @returns {any[]}
66
68
  */
67
69
  export function getRows(entity) {
70
+ if (entity.totalRows != null) return entity.rows
71
+
68
72
  let rows = entity.rows
69
73
  rows = applyFilters(entity, rows)
70
74
  rows = applySearch(entity, rows)
@@ -75,11 +79,14 @@ export function getRows(entity) {
75
79
  }
76
80
 
77
81
  /**
78
- * Gets the total number of rows after filtering and searching.
82
+ * Gets the total number of rows after filtering and searching, or the
83
+ * server's `totalRows` when the grid has it.
79
84
  * @param {TableEntity} entity
80
85
  * @returns {number}
81
86
  */
82
87
  export function getTotalRows(entity) {
88
+ if (entity.totalRows != null) return entity.totalRows
89
+
83
90
  let rows = entity.rows
84
91
  rows = applyFilters(entity, rows)
85
92
  rows = applySearch(entity, rows)
@@ -117,16 +117,22 @@ export const DataGrid = {
117
117
  class="iw-data-grid-header-column"
118
118
  style=${getColumnStyle(column)}
119
119
  >
120
- <div
121
- @click=${() => column.isSortable && props.onSortChange?.(column.id)}
122
- class="iw-data-grid-header-title"
123
- >
124
- ${column.title}
125
- ${when(column.isSortable, () =>
126
- getSortIcon(getSortDirection(props, column.id)),
127
- )}
128
- </div>
129
-
120
+ ${when(
121
+ column.isSortable,
122
+ () =>
123
+ html`<button
124
+ type="button"
125
+ class="iw-data-grid-header-title iw-data-grid-header-sort"
126
+ @click=${() => props.onSortChange?.(column.id)}
127
+ >
128
+ ${column.title}
129
+ <span aria-hidden="true"
130
+ >${getSortIcon(getSortDirection(props, column.id))}</span
131
+ >
132
+ </button>`,
133
+ () =>
134
+ html`<div class="iw-data-grid-header-title">${column.title}</div>`,
135
+ )}
130
136
  ${when(column.isFilterable, () => Filters.render(props, column))}
131
137
 
132
138
  <div
@@ -153,6 +159,7 @@ export const DataGrid = {
153
159
  size: "sm",
154
160
  isFullWidth: true,
155
161
  name: "search",
162
+ ariaLabel: props.search.ariaLabel ?? "Search",
156
163
  inputType: "text",
157
164
  placeholder: props.search.placeholder ?? "Fuzzy search...",
158
165
  value: props.search.value,
@@ -29,6 +29,22 @@
29
29
  white-space: nowrap;
30
30
  }
31
31
 
32
+ .iw-data-grid-header-sort {
33
+ padding: 0;
34
+ border: 0;
35
+ background: none;
36
+ color: inherit;
37
+ font: inherit;
38
+ font-weight: var(--iw-font-weight-bold);
39
+ text-align: start;
40
+ cursor: pointer;
41
+
42
+ &:focus-visible {
43
+ outline: 2px solid var(--iw-color-primary);
44
+ outline-offset: 2px;
45
+ }
46
+ }
47
+
32
48
  .iw-data-grid-column-resizer {
33
49
  &::after {
34
50
  content: "";
@@ -36,6 +36,12 @@ export interface Column {
36
36
  filter?: {
37
37
  type: "text" | "number" | "range" | "select" | "date" | "time" | "datetime"
38
38
  options?: SelectOption[]
39
+ /** The filter field's accessible name. Defaults to "Filter <title>". */
40
+ ariaLabel?: string
41
+ /** A range filter's lower field's accessible name. */
42
+ minLabel?: string
43
+ /** A range filter's upper field's accessible name. */
44
+ maxLabel?: string
39
45
  [key: string]: any
40
46
  }
41
47
  /**
@@ -67,7 +73,12 @@ export interface DataGridProps<T extends Row = Row> {
67
73
  columns: Column[]
68
74
  sorts: { column: string; direction: "asc" | "desc" }[]
69
75
  filters: Record<string, any>
70
- search: { value: string; placeholder?: string } | null
76
+ search: {
77
+ value: string
78
+ placeholder?: string
79
+ /** The search field's accessible name. Defaults to "Search". */
80
+ ariaLabel?: string
81
+ } | null
71
82
  isStriped?: boolean
72
83
  isVirtualized?: boolean
73
84
  virtualization?: DataGridVirtualization
@@ -75,6 +86,13 @@ export interface DataGridProps<T extends Row = Row> {
75
86
  selectionAnchor?: string | number | null
76
87
  pagination?: { page: number; pageSize: number; pageSizes: number[] } | null
77
88
  rowId?: string
89
+ /**
90
+ * How many rows the server has for the current filters and search. When it
91
+ * is set, `rows` is the page the server already filtered, sorted and sliced,
92
+ * so the grid shows them as they are and pages by this total. Set it to `0`
93
+ * before the first load to start the grid server-side.
94
+ */
95
+ totalRows?: number | null
78
96
  isMultiSelect: boolean
79
97
  /**
80
98
  * The user's unsaved edits, by row key and column id, shown over the rows
@@ -159,36 +177,41 @@ export type DataGridType = {
159
177
  virtualMount(entity: DataGridProps, containerEl: HTMLElement): void
160
178
  virtualScroll(entity: DataGridProps, containerEl: HTMLElement): void
161
179
  render(entity: DataGridProps, api: Api): TemplateResult
162
- renderDataGrid(entity: DataGridProps, api: Api): TemplateResult
163
- renderHeader(entity: DataGridProps, api: Api): TemplateResult
164
- renderToolbar(entity: DataGridProps, api: Api): TemplateResult
180
+ renderDataGrid(entity: DataGridProps): TemplateResult
181
+ renderHeader(entity: DataGridProps): TemplateResult
182
+ renderToolbar(entity: DataGridProps): TemplateResult
183
+ renderSearchbar(entity: DataGridProps): TemplateResult
165
184
  renderHeaderColumn(
166
185
  entity: DataGridProps,
167
186
  payload: { column: Column; index: number },
168
- api: Api,
169
187
  ): TemplateResult
170
- renderBody(entity: DataGridProps, api: Api): TemplateResult
188
+ renderBody(entity: DataGridProps): TemplateResult
171
189
  renderRow(
172
190
  entity: DataGridProps,
173
191
  payload: { row: Row; index: number },
174
- api: Api,
175
192
  ): TemplateResult
176
193
  renderCell(
177
194
  entity: DataGridProps,
178
195
  payload: { cell: any; index: number; row?: Row },
179
- api: Api,
180
196
  ): TemplateResult
181
197
  renderEditor(
182
198
  entity: DataGridProps,
183
199
  payload: { value: any; column: Column; row: Row; index: number },
184
- api: Api,
185
200
  ): TemplateResult
186
- renderEdits(entity: DataGridProps, editCount: number): TemplateResult
187
201
  renderValue(
188
202
  entity: DataGridProps,
189
203
  payload: { value: any; column: Column; index: number },
190
- api: Api,
191
204
  ): any
205
+ renderFooter(entity: DataGridProps): TemplateResult
206
+ renderEdits(entity: DataGridProps, editCount: number): TemplateResult
207
+ renderPagination(
208
+ entity: DataGridProps,
209
+ pagination: DataGridPaginationInfo,
210
+ ): TemplateResult
211
+ renderPageSize(
212
+ entity: DataGridProps,
213
+ pagination: DataGridPaginationInfo,
214
+ ): TemplateResult
192
215
  }
193
216
 
194
217
  export declare const DataGrid: DataGridType