@inglorious/ui 9.11.0 → 9.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ui",
3
- "version": "9.11.0",
3
+ "version": "9.12.0",
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",
@@ -57,6 +57,10 @@ export default {
57
57
  options: ["button", "submit", "reset", "menu"],
58
58
  description: "Native HTML button type attribute.",
59
59
  },
60
+ href: {
61
+ control: "text",
62
+ description: "Renders a link styled as a button.",
63
+ },
60
64
  accept: {
61
65
  control: "text",
62
66
  description: "File types the picker offers (with `onFileSelect`).",
@@ -178,6 +182,14 @@ FilePicker.args = {
178
182
  onFileSelect: () => {},
179
183
  }
180
184
 
185
+ export const Link = {}
186
+ Link.args = {
187
+ ...Default.args,
188
+ children: "Open events",
189
+ color: "primary",
190
+ href: "#events",
191
+ }
192
+
181
193
  export const CustomContent = {}
182
194
  CustomContent.args = {
183
195
  ...Default.args,
@@ -14,6 +14,7 @@ import { applyElementProps } from "../../shared/apply-element-props.js"
14
14
  * Renders a themed button for actions and form submission.
15
15
  * Supports variants, sizes, shapes, pressed state, and full-width layout.
16
16
  * With `onFileSelect`, the button also opens a file picker on click.
17
+ * With `href`, it is a link that looks like a button.
17
18
  * @example
18
19
  * // Stateless usage
19
20
  * button.render({ children: 'Click me', color: 'primary' })
@@ -21,6 +22,9 @@ import { applyElementProps } from "../../shared/apply-element-props.js"
21
22
  * // File picker
22
23
  * button.render({ children: 'Upload', accept: 'image/*', onFileSelect })
23
24
  *
25
+ * // Link
26
+ * button.render({ children: 'Open', href: '/events' })
27
+ *
24
28
  * @param {ButtonProps} props
25
29
  * @returns {TemplateResult}
26
30
  */
@@ -36,6 +40,7 @@ export function render(props) {
36
40
  isPressed = false,
37
41
  isFullWidth = false,
38
42
  buttonType = "button",
43
+ href,
39
44
  ariaLabel,
40
45
  isAriaPressed,
41
46
  className = "",
@@ -72,6 +77,18 @@ export function render(props) {
72
77
  }
73
78
  : onClick
74
79
 
80
+ if (href != null) {
81
+ return html`<a
82
+ href=${isDisabled ? nothing : href}
83
+ aria-label=${ariaLabel || nothing}
84
+ aria-disabled=${isDisabled ? "true" : nothing}
85
+ class=${classMap(classes)}
86
+ @click=${isDisabled ? nothing : onClick}
87
+ ${ref((element) => applyElementProps(element, rest))}
88
+ >${children}</a
89
+ >`
90
+ }
91
+
75
92
  return html`
76
93
  <button
77
94
  type=${buttonType}
@@ -33,9 +33,11 @@ export function render(props) {
33
33
  ...rest
34
34
  } = props
35
35
 
36
+ const isSelection = value != null || isMultiple
37
+
36
38
  const groupClasses = {
37
39
  "iw-button-group": true,
38
- "iw-button-group-selection": value != null || isMultiple,
40
+ "iw-button-group-selection": isSelection,
39
41
  [`iw-button-group-${direction}`]: true,
40
42
  "iw-button-group-attached": isAttached,
41
43
  }
@@ -86,7 +88,8 @@ export function render(props) {
86
88
  size: itemSize ?? size,
87
89
  isDisabled: isDisabled || !!itemDisabled,
88
90
  isPressed,
89
- isAriaPressed: isPressed ? true : undefined,
91
+ isAriaPressed: isSelection ? isPressed : undefined,
92
+ ariaLabel: children != null ? label : undefined,
90
93
  onClick: () => {
91
94
  if (!onChange) return
92
95
 
@@ -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,
@@ -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: "";
@@ -5,6 +5,7 @@ import type { Renderable } from "../shared/renderable"
5
5
  export interface ButtonGroupItem {
6
6
  id?: string
7
7
  value?: string
8
+ /** The button's text, or its accessible name when `children` are given */
8
9
  label: string
9
10
  children?: Renderable
10
11
  isDisabled?: boolean
@@ -21,6 +21,8 @@ export interface ButtonProps {
21
21
  isPressed?: boolean
22
22
  isFullWidth?: boolean
23
23
  buttonType?: "button" | "submit" | "reset" | "menu"
24
+ /** Renders a link styled as a button instead of a `<button>` */
25
+ href?: string
24
26
  ariaLabel?: string
25
27
  isAriaPressed?: boolean
26
28
  className?: string
@@ -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
@@ -166,36 +177,41 @@ export type DataGridType = {
166
177
  virtualMount(entity: DataGridProps, containerEl: HTMLElement): void
167
178
  virtualScroll(entity: DataGridProps, containerEl: HTMLElement): void
168
179
  render(entity: DataGridProps, api: Api): TemplateResult
169
- renderDataGrid(entity: DataGridProps, api: Api): TemplateResult
170
- renderHeader(entity: DataGridProps, api: Api): TemplateResult
171
- 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
172
184
  renderHeaderColumn(
173
185
  entity: DataGridProps,
174
186
  payload: { column: Column; index: number },
175
- api: Api,
176
187
  ): TemplateResult
177
- renderBody(entity: DataGridProps, api: Api): TemplateResult
188
+ renderBody(entity: DataGridProps): TemplateResult
178
189
  renderRow(
179
190
  entity: DataGridProps,
180
191
  payload: { row: Row; index: number },
181
- api: Api,
182
192
  ): TemplateResult
183
193
  renderCell(
184
194
  entity: DataGridProps,
185
195
  payload: { cell: any; index: number; row?: Row },
186
- api: Api,
187
196
  ): TemplateResult
188
197
  renderEditor(
189
198
  entity: DataGridProps,
190
199
  payload: { value: any; column: Column; row: Row; index: number },
191
- api: Api,
192
200
  ): TemplateResult
193
- renderEdits(entity: DataGridProps, editCount: number): TemplateResult
194
201
  renderValue(
195
202
  entity: DataGridProps,
196
203
  payload: { value: any; column: Column; index: number },
197
- api: Api,
198
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
199
215
  }
200
216
 
201
217
  export declare const DataGrid: DataGridType