@sveltia/ui 0.73.1 → 0.74.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.
@@ -1,21 +1,33 @@
1
1
  <!--
2
2
  @component
3
- The interactive version of `<TableBody>`.
3
+ The interactive version of `<TableBody>`. A labelled row group can be made collapsible, in which
4
+ case the caption doubles as an expander button that shows or hides the rows.
4
5
  @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody
5
6
  @see https://w3c.github.io/aria/#rowgroup
7
+ @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
6
8
  -->
7
9
  <script>
8
- import { _ } from '@sveltia/i18n';
10
+ import { _, isRTL } from '@sveltia/i18n';
11
+ import Button from '../button/button.svelte';
12
+ import Icon from '../icon/icon.svelte';
9
13
 
10
14
  /**
11
15
  * @import { Snippet } from 'svelte';
16
+ * @import { Attachment } from 'svelte/attachments';
12
17
  */
13
18
 
14
19
  /**
15
20
  * @typedef {object} Props
16
21
  * @property {string} [class] The `class` attribute on the wrapper element.
17
22
  * @property {string} [label] Display label for the row group.
23
+ * @property {boolean} [collapsible] Whether the rows can be hidden with an expander in the
24
+ * caption. Requires `label`.
25
+ * @property {boolean} [expanded] Whether the rows are shown. Only applies when `collapsible`. An
26
+ * alias of the `aria-expanded` attribute on the expander button.
18
27
  * @property {Snippet} [children] Primary slot content.
28
+ * @property {Snippet} [chevronIcon] Chevron slot content for the expander.
29
+ * @property {(event: CustomEvent) => void} [onChange] Custom `Change` event handler, called with
30
+ * the new `expanded` state in `detail` when the expander is toggled.
19
31
  */
20
32
 
21
33
  /**
@@ -23,14 +35,52 @@
23
35
  */
24
36
  let {
25
37
  /* eslint-disable prefer-const */
38
+ expanded = $bindable(true),
26
39
  class: className,
27
40
  label = '',
41
+ collapsible = false,
28
42
  children,
43
+ chevronIcon,
44
+ onChange,
29
45
  ...restProps
30
46
  /* eslint-enable prefer-const */
31
47
  } = $props();
32
48
 
33
49
  const id = $props.id();
50
+ const hasExpander = $derived(collapsible && !!label);
51
+
52
+ /**
53
+ * Toggle the `expanded` state and notify the consumer.
54
+ */
55
+ const toggle = () => {
56
+ expanded = !expanded;
57
+ onChange?.(new CustomEvent('change', { detail: { expanded } }));
58
+ };
59
+
60
+ /**
61
+ * Toggle the expander when the caption row itself is clicked. The `Grid` treats the caption as a
62
+ * row: the arrow keys land on it, and Enter clicks it, so this is how a keyboard user who has
63
+ * moved to the caption toggles the group. A click bubbling up from the button is left alone, as
64
+ * the button has toggled the group already.
65
+ * @type {Attachment}
66
+ */
67
+ const handleCaptionClick = (element) => {
68
+ /**
69
+ * Handle the `click` event on the caption row.
70
+ * @param {Event} event `click` event.
71
+ */
72
+ const listener = (event) => {
73
+ if (hasExpander && event.target === element) {
74
+ toggle();
75
+ }
76
+ };
77
+
78
+ element.addEventListener('click', listener);
79
+
80
+ return () => {
81
+ element.removeEventListener('click', listener);
82
+ };
83
+ };
34
84
  </script>
35
85
 
36
86
  <div
@@ -41,14 +91,34 @@
41
91
  aria-roledescription={_('_sui.role_descriptions.grid_body')}
42
92
  >
43
93
  {#if label}
44
- <div role="row" class="row-group-caption">
94
+ <div role="row" class="row-group-caption" {@attach handleCaptionClick}>
45
95
  <!-- We need `colspan` here but cannot place `<th>` under `<div>`, so use a hack -->
46
96
  <svelte:element this={"th"} role="columnheader" id="{id}-label" colspan="9999">
47
- {label}
97
+ {#if hasExpander}
98
+ <!--
99
+ The button is not a tab stop: the grid has a single one, and a keyboard user reaches
100
+ the caption row with the arrow keys and toggles it with Enter
101
+ -->
102
+ <Button class="expander" {label} tabindex="-1" aria-expanded={expanded} onclick={toggle}>
103
+ {#snippet startIcon()}
104
+ <span role="none" class="chevron">
105
+ {#if chevronIcon}
106
+ {@render chevronIcon()}
107
+ {:else}
108
+ <Icon name={isRTL() ? 'chevron_left' : 'chevron_right'} />
109
+ {/if}
110
+ </span>
111
+ {/snippet}
112
+ </Button>
113
+ {:else}
114
+ {label}
115
+ {/if}
48
116
  </svelte:element>
49
117
  </div>
50
118
  {/if}
51
- {@render children?.()}
119
+ {#if !hasExpander || expanded}
120
+ {@render children?.()}
121
+ {/if}
52
122
  </div>
53
123
 
54
124
  <style>[role=rowgroup] {
@@ -66,4 +136,32 @@
66
136
  background-color: var(--sui-secondary-background-color);
67
137
  font-size: var(--sui-font-size-default);
68
138
  text-align: start;
139
+ }
140
+ [role=columnheader]:has(> :global(.expander)) {
141
+ padding: 0;
142
+ }
143
+ [role=columnheader] :global(.expander) {
144
+ justify-content: flex-start;
145
+ margin: 0;
146
+ border-radius: 0;
147
+ padding: 4px 8px 4px 4px;
148
+ width: 100%;
149
+ height: auto;
150
+ color: inherit;
151
+ font-size: inherit;
152
+ text-align: start;
153
+ }
154
+
155
+ .chevron {
156
+ flex: none;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ transition: transform 200ms;
161
+ }
162
+ :global(.expander[aria-expanded='true']) > .chevron:dir(ltr) {
163
+ transform: rotate(90deg);
164
+ }
165
+ :global(.expander[aria-expanded='true']) > .chevron:dir(rtl) {
166
+ transform: rotate(-90deg);
69
167
  }</style>
@@ -4,9 +4,11 @@ type GridBody = {
4
4
  $set?(props: Partial<Props & Record<string, any>>): void;
5
5
  };
6
6
  /**
7
- * The interactive version of `<TableBody>`.
7
+ * The interactive version of `<TableBody>`. A labelled row group can be made collapsible, in which
8
+ * case the caption doubles as an expander button that shows or hides the rows.
8
9
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody
9
10
  * @see https://w3c.github.io/aria/#rowgroup
11
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
10
12
  */
11
13
  declare const GridBody: import("svelte").Component<{
12
14
  /**
@@ -17,11 +19,30 @@ declare const GridBody: import("svelte").Component<{
17
19
  * Display label for the row group.
18
20
  */
19
21
  label?: string | undefined;
22
+ /**
23
+ * Whether the rows can be hidden with an expander in the
24
+ * caption. Requires `label`.
25
+ */
26
+ collapsible?: boolean | undefined;
27
+ /**
28
+ * Whether the rows are shown. Only applies when `collapsible`. An
29
+ * alias of the `aria-expanded` attribute on the expander button.
30
+ */
31
+ expanded?: boolean | undefined;
20
32
  /**
21
33
  * Primary slot content.
22
34
  */
23
35
  children?: Snippet<[]> | undefined;
24
- } & Record<string, any>, {}, "">;
36
+ /**
37
+ * Chevron slot content for the expander.
38
+ */
39
+ chevronIcon?: Snippet<[]> | undefined;
40
+ /**
41
+ * Custom `Change` event handler, called with
42
+ * the new `expanded` state in `detail` when the expander is toggled.
43
+ */
44
+ onChange?: ((event: CustomEvent) => void) | undefined;
45
+ } & Record<string, any>, {}, "expanded">;
25
46
  type Props = {
26
47
  /**
27
48
  * The `class` attribute on the wrapper element.
@@ -31,9 +52,28 @@ type Props = {
31
52
  * Display label for the row group.
32
53
  */
33
54
  label?: string | undefined;
55
+ /**
56
+ * Whether the rows can be hidden with an expander in the
57
+ * caption. Requires `label`.
58
+ */
59
+ collapsible?: boolean | undefined;
60
+ /**
61
+ * Whether the rows are shown. Only applies when `collapsible`. An
62
+ * alias of the `aria-expanded` attribute on the expander button.
63
+ */
64
+ expanded?: boolean | undefined;
34
65
  /**
35
66
  * Primary slot content.
36
67
  */
37
68
  children?: Snippet<[]> | undefined;
69
+ /**
70
+ * Chevron slot content for the expander.
71
+ */
72
+ chevronIcon?: Snippet<[]> | undefined;
73
+ /**
74
+ * Custom `Change` event handler, called with
75
+ * the new `expanded` state in `detail` when the expander is toggled.
76
+ */
77
+ onChange?: ((event: CustomEvent) => void) | undefined;
38
78
  };
39
79
  import type { Snippet } from 'svelte';
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
3
3
  */
4
- export const UI_VERSION: "0.73.1";
4
+ export const UI_VERSION: "0.74.0";
@@ -3,4 +3,4 @@
3
3
  /**
4
4
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
5
5
  */
6
- export const UI_VERSION = '0.73.1';
6
+ export const UI_VERSION = '0.74.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.73.1",
3
+ "version": "0.74.0",
4
4
  "description": "A collection of Svelte components and utilities for building user interfaces.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -57,9 +57,9 @@
57
57
  "@sveltejs/kit": "^2.70.3",
58
58
  "@sveltejs/package": "^2.5.8",
59
59
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
60
- "@vitest/browser-playwright": "^5.0.0",
61
- "@vitest/coverage-v8": "^5.0.0",
62
- "cspell": "^10.3.1",
60
+ "@vitest/browser-playwright": "^5.0.1",
61
+ "@vitest/coverage-v8": "^5.0.1",
62
+ "cspell": "^10.3.2",
63
63
  "eslint": "^9.39.5",
64
64
  "eslint-config-airbnb-extended": "^3.2.0",
65
65
  "eslint-config-prettier": "^10.1.8",
@@ -86,7 +86,7 @@
86
86
  "svelte-preprocess": "^6.0.5",
87
87
  "tslib": "^2.8.1",
88
88
  "vite": "^8.3.0",
89
- "vitest": "^5.0.0",
89
+ "vitest": "^5.0.1",
90
90
  "vitest-browser-svelte": "^3.1.0"
91
91
  },
92
92
  "peerDependencies": {