@openvoxproject/voxblocks 0.9.0 → 0.11.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 +17 -9
- package/dist/cdn/voxblocks.css +1 -1
- package/dist/cdn/voxblocks.js +1987 -1279
- package/dist/types/components/accordion/vox-accordion.d.ts +2 -0
- package/dist/types/components/alert/vox-alert.d.ts +2 -1
- package/dist/types/components/callout/vox-callout.d.ts +2 -1
- package/dist/types/components/disclosure/vox-disclosure.d.ts +2 -0
- package/dist/types/components/dropdown/vox-dropdown.d.ts +3 -0
- package/dist/types/components/header/vox-header.d.ts +11 -1
- package/dist/types/components/icon/icon-paths.d.ts +30 -0
- package/dist/types/components/icon/vox-icon.d.ts +22 -0
- package/dist/types/components/link-hub/vox-link-hub.d.ts +3 -0
- package/dist/types/components/pagination/vox-pagination.d.ts +6 -4
- package/dist/types/components/sidenav/vox-sidenav.d.ts +20 -0
- package/dist/types/components/toc/vox-toc.d.ts +43 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/internal/field.d.ts +4 -0
- package/dist/voxblocks.css +1 -1
- package/dist/voxblocks.js +1705 -997
- package/package.json +9 -6
- package/src/components/accordion/vox-accordion.ts +6 -1
- package/src/components/alert/vox-alert.ts +46 -2
- package/src/components/avatar/vox-avatar.ts +9 -2
- package/src/components/callout/vox-callout.ts +53 -3
- package/src/components/card/vox-card.ts +6 -1
- package/src/components/checkbox/vox-checkbox.ts +4 -3
- package/src/components/dialog/vox-dialog.ts +2 -2
- package/src/components/disclosure/vox-disclosure.ts +6 -1
- package/src/components/dropdown/vox-dropdown.ts +32 -8
- package/src/components/empty-state/vox-empty-state.ts +1 -1
- package/src/components/file-input/vox-file-input.ts +4 -1
- package/src/components/header/vox-header.ts +142 -7
- package/src/components/icon/icon-paths.ts +287 -0
- package/src/components/icon/vox-icon.ts +78 -0
- package/src/components/input/vox-input.ts +2 -0
- package/src/components/link-hub/vox-link-hub.ts +21 -0
- package/src/components/pagination/vox-pagination.ts +9 -6
- package/src/components/radio/vox-radio-group.ts +17 -6
- package/src/components/select/vox-select.ts +3 -0
- package/src/components/sidenav/vox-sidenav.ts +164 -4
- package/src/components/step-indicator/vox-step-indicator.ts +16 -2
- package/src/components/tabs/vox-tabs.ts +26 -2
- package/src/components/textarea/vox-textarea.ts +2 -0
- package/src/components/toc/vox-toc.ts +140 -0
- package/src/index.ts +6 -0
- package/src/internal/field.ts +20 -2
- package/src/styles/utilities.css +158 -0
- package/src/tokens/palette.css +95 -0
- package/src/tokens/tokens.css +7 -7
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openvoxproject/voxblocks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Web components for
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Web components for OpenVox community web sites and apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -35,9 +35,11 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"prepare": "npm run build",
|
|
37
37
|
"build": "vite build && vite build --config vite.cdn.config.ts && tsc -p tsconfig.build.json",
|
|
38
|
-
"docs:dev": "
|
|
39
|
-
"docs:
|
|
40
|
-
"docs:
|
|
38
|
+
"docs:dev": "rm -rf docs/assets docs/_data/manifest.json && concurrently -n vite,jekyll -c blue,magenta \"npm:docs:assets:watch\" \"npm:docs:jekyll:wait\"",
|
|
39
|
+
"docs:assets:watch": "vite build --config docs/vite.config.js --watch",
|
|
40
|
+
"docs:jekyll:wait": "wait-on docs/_data/manifest.json && npm run docs:jekyll",
|
|
41
|
+
"docs:jekyll": "BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll serve --livereload --source docs --destination docs/_site",
|
|
42
|
+
"docs:build": "vite build --config docs/vite.config.js && BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build --source docs --destination docs/_site --config docs/_config.yml,docs/_config_production.yml",
|
|
41
43
|
"storybook": "storybook dev -p 6006",
|
|
42
44
|
"build-storybook": "storybook build"
|
|
43
45
|
},
|
|
@@ -47,10 +49,11 @@
|
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"@storybook/web-components": "^10.5.5",
|
|
49
51
|
"@storybook/web-components-vite": "^10.5.5",
|
|
52
|
+
"concurrently": "^9.1.0",
|
|
50
53
|
"storybook": "^10.5.5",
|
|
51
54
|
"typescript": "^5.6.0",
|
|
52
55
|
"vite": "^6.0.0",
|
|
53
|
-
"
|
|
56
|
+
"wait-on": "^9.1.0"
|
|
54
57
|
},
|
|
55
58
|
"allowScripts": {
|
|
56
59
|
"esbuild@0.25.12": true,
|
|
@@ -48,9 +48,13 @@ export class VoxAccordion extends LitElement {
|
|
|
48
48
|
*/
|
|
49
49
|
@customElement('vox-accordion-item')
|
|
50
50
|
export class VoxAccordionItem extends LitElement {
|
|
51
|
+
private static nextId = 0;
|
|
52
|
+
|
|
51
53
|
@property() heading = '';
|
|
52
54
|
@property({ type: Boolean, reflect: true }) open = false;
|
|
53
55
|
|
|
56
|
+
private readonly panelId = `vox-accordion-panel-${VoxAccordionItem.nextId++}`;
|
|
57
|
+
|
|
54
58
|
static styles = css`
|
|
55
59
|
:host {
|
|
56
60
|
display: block;
|
|
@@ -127,6 +131,7 @@ export class VoxAccordionItem extends LitElement {
|
|
|
127
131
|
<button
|
|
128
132
|
class="trigger"
|
|
129
133
|
aria-expanded=${this.open ? 'true' : 'false'}
|
|
134
|
+
aria-controls=${this.panelId}
|
|
130
135
|
@click=${this.toggle}
|
|
131
136
|
>
|
|
132
137
|
${this.heading}
|
|
@@ -135,7 +140,7 @@ export class VoxAccordionItem extends LitElement {
|
|
|
135
140
|
</svg>
|
|
136
141
|
</button>
|
|
137
142
|
</h3>
|
|
138
|
-
<div class="panel" ?hidden=${!this.open}>
|
|
143
|
+
<div id=${this.panelId} class="panel" ?hidden=${!this.open}>
|
|
139
144
|
<slot></slot>
|
|
140
145
|
</div>
|
|
141
146
|
`;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { LitElement, html, css, nothing } from 'lit';
|
|
2
2
|
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
import { ICON_PATHS, type IconName } from '../icon/icon-paths.js';
|
|
3
4
|
|
|
4
5
|
export type AlertVariant = 'info' | 'success' | 'warning' | 'danger';
|
|
5
6
|
|
|
7
|
+
const VARIANT_ICON: Record<AlertVariant, IconName> = {
|
|
8
|
+
info: 'info',
|
|
9
|
+
success: 'check-circle',
|
|
10
|
+
warning: 'warning',
|
|
11
|
+
danger: 'x-circle',
|
|
12
|
+
};
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
|
-
* A page-level notification message, optionally dismissible.
|
|
15
|
+
* A page-level notification message, optionally dismissible. Shows an
|
|
16
|
+
* icon matching `variant` automatically — no icon slot to fill in.
|
|
8
17
|
* For static advisory text inside prose, see `<vox-callout>`.
|
|
9
18
|
*
|
|
10
19
|
* @slot - Alert message.
|
|
@@ -43,6 +52,13 @@ export class VoxAlert extends LitElement {
|
|
|
43
52
|
flex: 1 1 auto;
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
.icon {
|
|
56
|
+
flex: none;
|
|
57
|
+
width: 20px;
|
|
58
|
+
height: 20px;
|
|
59
|
+
margin-top: 1px;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
.heading {
|
|
47
63
|
margin: 0 0 var(--vox-space-1);
|
|
48
64
|
font-size: 14px;
|
|
@@ -56,6 +72,9 @@ export class VoxAlert extends LitElement {
|
|
|
56
72
|
.info .heading {
|
|
57
73
|
color: var(--vox-color-brand-1);
|
|
58
74
|
}
|
|
75
|
+
.info .icon {
|
|
76
|
+
color: var(--vox-color-brand-1);
|
|
77
|
+
}
|
|
59
78
|
|
|
60
79
|
.success {
|
|
61
80
|
background-color: var(--vox-color-tip-soft);
|
|
@@ -64,6 +83,9 @@ export class VoxAlert extends LitElement {
|
|
|
64
83
|
.success .heading {
|
|
65
84
|
color: var(--vox-color-tip-1);
|
|
66
85
|
}
|
|
86
|
+
.success .icon {
|
|
87
|
+
color: var(--vox-color-tip-1);
|
|
88
|
+
}
|
|
67
89
|
|
|
68
90
|
.warning {
|
|
69
91
|
background-color: var(--vox-color-warning-soft);
|
|
@@ -72,6 +94,9 @@ export class VoxAlert extends LitElement {
|
|
|
72
94
|
.warning .heading {
|
|
73
95
|
color: var(--vox-color-warning-1);
|
|
74
96
|
}
|
|
97
|
+
.warning .icon {
|
|
98
|
+
color: var(--vox-color-warning-1);
|
|
99
|
+
}
|
|
75
100
|
|
|
76
101
|
.danger {
|
|
77
102
|
background-color: var(--vox-color-danger-soft);
|
|
@@ -80,6 +105,9 @@ export class VoxAlert extends LitElement {
|
|
|
80
105
|
.danger .heading {
|
|
81
106
|
color: var(--vox-color-danger-1);
|
|
82
107
|
}
|
|
108
|
+
.danger .icon {
|
|
109
|
+
color: var(--vox-color-danger-1);
|
|
110
|
+
}
|
|
83
111
|
|
|
84
112
|
.close {
|
|
85
113
|
flex: none;
|
|
@@ -126,8 +154,24 @@ export class VoxAlert extends LitElement {
|
|
|
126
154
|
}
|
|
127
155
|
|
|
128
156
|
render() {
|
|
157
|
+
const isUrgent = this.variant === 'danger' || this.variant === 'warning';
|
|
129
158
|
return html`
|
|
130
|
-
<div
|
|
159
|
+
<div
|
|
160
|
+
class="alert ${this.variant}"
|
|
161
|
+
role=${isUrgent ? 'alert' : 'status'}
|
|
162
|
+
>
|
|
163
|
+
<svg
|
|
164
|
+
class="icon"
|
|
165
|
+
viewBox="0 0 48 48"
|
|
166
|
+
fill="none"
|
|
167
|
+
stroke="currentColor"
|
|
168
|
+
stroke-width="2"
|
|
169
|
+
stroke-linecap="round"
|
|
170
|
+
stroke-linejoin="round"
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
>
|
|
173
|
+
${ICON_PATHS[VARIANT_ICON[this.variant]]}
|
|
174
|
+
</svg>
|
|
131
175
|
<div class="body">
|
|
132
176
|
${this.heading
|
|
133
177
|
? html`<p class="heading">${this.heading}</p>`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
2
|
import { customElement, property } from 'lit/decorators.js';
|
|
3
3
|
|
|
4
4
|
export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
@@ -63,8 +63,15 @@ export class VoxAvatar extends LitElement {
|
|
|
63
63
|
`;
|
|
64
64
|
|
|
65
65
|
render() {
|
|
66
|
+
// Without `alt`, fall back to letting AT read the initials as plain text
|
|
67
|
+
// rather than hiding them behind an empty aria-label via role="img".
|
|
68
|
+
const labelled = !this.src && this.alt;
|
|
66
69
|
return html`
|
|
67
|
-
<span
|
|
70
|
+
<span
|
|
71
|
+
class="avatar"
|
|
72
|
+
role=${labelled ? 'img' : nothing}
|
|
73
|
+
aria-label=${labelled ? this.alt : nothing}
|
|
74
|
+
>
|
|
68
75
|
${this.src
|
|
69
76
|
? html`<img src=${this.src} alt=${this.alt} />`
|
|
70
77
|
: this.initials}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit';
|
|
2
2
|
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
import { ICON_PATHS, type IconName } from '../icon/icon-paths.js';
|
|
3
4
|
|
|
4
5
|
export type CalloutVariant = 'info' | 'tip' | 'warning' | 'danger';
|
|
5
6
|
|
|
@@ -10,9 +11,17 @@ const DEFAULT_HEADINGS: Record<CalloutVariant, string> = {
|
|
|
10
11
|
danger: 'Danger',
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
const VARIANT_ICON: Record<CalloutVariant, IconName> = {
|
|
15
|
+
info: 'info',
|
|
16
|
+
tip: 'check-circle',
|
|
17
|
+
warning: 'warning',
|
|
18
|
+
danger: 'x-circle',
|
|
19
|
+
};
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* An admonition block matching the custom containers on the OpenVox docs
|
|
15
|
-
* (::: tip, ::: warning, ...).
|
|
23
|
+
* (::: tip, ::: warning, ...). Shows an icon matching `variant`
|
|
24
|
+
* automatically — no icon slot to fill in.
|
|
16
25
|
*
|
|
17
26
|
* @slot - Callout body content.
|
|
18
27
|
*/
|
|
@@ -29,6 +38,9 @@ export class VoxCallout extends LitElement {
|
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
.callout {
|
|
41
|
+
display: flex;
|
|
42
|
+
gap: var(--vox-space-3);
|
|
43
|
+
align-items: flex-start;
|
|
32
44
|
border-radius: var(--vox-radius-md);
|
|
33
45
|
padding: var(--vox-space-4);
|
|
34
46
|
font-family: var(--vox-font-family-base);
|
|
@@ -37,6 +49,18 @@ export class VoxCallout extends LitElement {
|
|
|
37
49
|
color: var(--vox-color-text-2);
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
.icon {
|
|
53
|
+
flex: none;
|
|
54
|
+
width: 18px;
|
|
55
|
+
height: 18px;
|
|
56
|
+
margin-top: 2px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.content {
|
|
60
|
+
flex: 1 1 auto;
|
|
61
|
+
min-width: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
.heading {
|
|
41
65
|
margin: 0 0 var(--vox-space-2);
|
|
42
66
|
font-size: 14px;
|
|
@@ -49,6 +73,9 @@ export class VoxCallout extends LitElement {
|
|
|
49
73
|
.info .heading {
|
|
50
74
|
color: var(--vox-color-text-1);
|
|
51
75
|
}
|
|
76
|
+
.info .icon {
|
|
77
|
+
color: var(--vox-color-text-1);
|
|
78
|
+
}
|
|
52
79
|
|
|
53
80
|
.tip {
|
|
54
81
|
background-color: var(--vox-color-tip-soft);
|
|
@@ -56,6 +83,9 @@ export class VoxCallout extends LitElement {
|
|
|
56
83
|
.tip .heading {
|
|
57
84
|
color: var(--vox-color-tip-1);
|
|
58
85
|
}
|
|
86
|
+
.tip .icon {
|
|
87
|
+
color: var(--vox-color-tip-1);
|
|
88
|
+
}
|
|
59
89
|
|
|
60
90
|
.warning {
|
|
61
91
|
background-color: var(--vox-color-warning-soft);
|
|
@@ -63,6 +93,9 @@ export class VoxCallout extends LitElement {
|
|
|
63
93
|
.warning .heading {
|
|
64
94
|
color: var(--vox-color-warning-1);
|
|
65
95
|
}
|
|
96
|
+
.warning .icon {
|
|
97
|
+
color: var(--vox-color-warning-1);
|
|
98
|
+
}
|
|
66
99
|
|
|
67
100
|
.danger {
|
|
68
101
|
background-color: var(--vox-color-danger-soft);
|
|
@@ -70,6 +103,9 @@ export class VoxCallout extends LitElement {
|
|
|
70
103
|
.danger .heading {
|
|
71
104
|
color: var(--vox-color-danger-1);
|
|
72
105
|
}
|
|
106
|
+
.danger .icon {
|
|
107
|
+
color: var(--vox-color-danger-1);
|
|
108
|
+
}
|
|
73
109
|
|
|
74
110
|
::slotted(p:first-child) {
|
|
75
111
|
margin-top: 0;
|
|
@@ -83,8 +119,22 @@ export class VoxCallout extends LitElement {
|
|
|
83
119
|
render() {
|
|
84
120
|
return html`
|
|
85
121
|
<div class="callout ${this.variant}" role="note">
|
|
86
|
-
<
|
|
87
|
-
|
|
122
|
+
<svg
|
|
123
|
+
class="icon"
|
|
124
|
+
viewBox="0 0 48 48"
|
|
125
|
+
fill="none"
|
|
126
|
+
stroke="currentColor"
|
|
127
|
+
stroke-width="2"
|
|
128
|
+
stroke-linecap="round"
|
|
129
|
+
stroke-linejoin="round"
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
>
|
|
132
|
+
${ICON_PATHS[VARIANT_ICON[this.variant]]}
|
|
133
|
+
</svg>
|
|
134
|
+
<div class="content">
|
|
135
|
+
<p class="heading">${this.heading ?? DEFAULT_HEADINGS[this.variant]}</p>
|
|
136
|
+
<slot></slot>
|
|
137
|
+
</div>
|
|
88
138
|
</div>
|
|
89
139
|
`;
|
|
90
140
|
}
|
|
@@ -32,6 +32,11 @@ export class VoxCard extends LitElement {
|
|
|
32
32
|
border-radius: var(--vox-radius-lg);
|
|
33
33
|
padding: var(--vox-space-6);
|
|
34
34
|
font-family: var(--vox-font-family-base);
|
|
35
|
+
/* Slotted content (e.g. an icon using stroke="currentColor")
|
|
36
|
+
inherits from this element's position in the flat tree — when
|
|
37
|
+
rendered as an <a>, that means the browser's default link color
|
|
38
|
+
without this, since nothing else in .card sets one. */
|
|
39
|
+
color: var(--vox-color-text-1);
|
|
35
40
|
text-decoration: none;
|
|
36
41
|
transition: border-color var(--vox-transition-base);
|
|
37
42
|
}
|
|
@@ -124,7 +129,7 @@ export class VoxCard extends LitElement {
|
|
|
124
129
|
<div class="badge ${this.hasBadge ? 'has-badge' : ''}">
|
|
125
130
|
<slot name="badge" @slotchange=${this.handleBadgeSlotChange}></slot>
|
|
126
131
|
</div>
|
|
127
|
-
<div class="icon ${this.hasIcon ? 'has-icon' : ''}">
|
|
132
|
+
<div class="icon ${this.hasIcon ? 'has-icon' : ''}" aria-hidden="true">
|
|
128
133
|
<slot name="icon" @slotchange=${this.handleIconSlotChange}></slot>
|
|
129
134
|
</div>
|
|
130
135
|
<h3 class="heading">${this.heading}</h3>
|
|
@@ -60,10 +60,9 @@ export class VoxCheckbox extends VoxFieldElement {
|
|
|
60
60
|
|
|
61
61
|
updated() {
|
|
62
62
|
this.internals.setFormValue(this.checked ? this.value : null);
|
|
63
|
+
this.invalid = this.required && !this.checked;
|
|
63
64
|
this.internals.setValidity(
|
|
64
|
-
this.
|
|
65
|
-
? { valueMissing: true }
|
|
66
|
-
: {},
|
|
65
|
+
this.invalid ? { valueMissing: true } : {},
|
|
67
66
|
'Please check this box.',
|
|
68
67
|
this.renderRoot.querySelector('input') ?? undefined,
|
|
69
68
|
);
|
|
@@ -81,6 +80,8 @@ export class VoxCheckbox extends VoxFieldElement {
|
|
|
81
80
|
type="checkbox"
|
|
82
81
|
.checked=${this.checked}
|
|
83
82
|
?disabled=${this.disabled}
|
|
83
|
+
?required=${this.required}
|
|
84
|
+
aria-invalid=${this.invalid ? 'true' : 'false'}
|
|
84
85
|
@change=${this.handleChange}
|
|
85
86
|
/>
|
|
86
87
|
<span class="box" aria-hidden="true">
|
|
@@ -148,12 +148,12 @@ export class VoxDialog extends LitElement {
|
|
|
148
148
|
render() {
|
|
149
149
|
return html`
|
|
150
150
|
<dialog
|
|
151
|
-
aria-
|
|
151
|
+
aria-labelledby=${this.heading ? 'heading' : nothing}
|
|
152
152
|
@close=${this.handleNativeClose}
|
|
153
153
|
@click=${this.handleClick}
|
|
154
154
|
>
|
|
155
155
|
<div class="header">
|
|
156
|
-
<h2 class="heading">${this.heading}</h2>
|
|
156
|
+
<h2 class="heading" id="heading">${this.heading}</h2>
|
|
157
157
|
<button class="close" aria-label="Close dialog" @click=${this.close}>
|
|
158
158
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
|
159
159
|
<path d="M18 6 6 18M6 6l12 12" />
|
|
@@ -9,9 +9,13 @@ import { customElement, property } from 'lit/decorators.js';
|
|
|
9
9
|
*/
|
|
10
10
|
@customElement('vox-disclosure')
|
|
11
11
|
export class VoxDisclosure extends LitElement {
|
|
12
|
+
private static nextId = 0;
|
|
13
|
+
|
|
12
14
|
@property() summary = 'Show details';
|
|
13
15
|
@property({ type: Boolean, reflect: true }) open = false;
|
|
14
16
|
|
|
17
|
+
private readonly panelId = `vox-disclosure-panel-${VoxDisclosure.nextId++}`;
|
|
18
|
+
|
|
15
19
|
static styles = css`
|
|
16
20
|
:host {
|
|
17
21
|
display: block;
|
|
@@ -77,6 +81,7 @@ export class VoxDisclosure extends LitElement {
|
|
|
77
81
|
<button
|
|
78
82
|
class="trigger"
|
|
79
83
|
aria-expanded=${this.open ? 'true' : 'false'}
|
|
84
|
+
aria-controls=${this.panelId}
|
|
80
85
|
@click=${this.toggle}
|
|
81
86
|
>
|
|
82
87
|
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
@@ -84,7 +89,7 @@ export class VoxDisclosure extends LitElement {
|
|
|
84
89
|
</svg>
|
|
85
90
|
${this.summary}
|
|
86
91
|
</button>
|
|
87
|
-
<div class="panel" ?hidden=${!this.open}>
|
|
92
|
+
<div id=${this.panelId} class="panel" ?hidden=${!this.open}>
|
|
88
93
|
<slot></slot>
|
|
89
94
|
</div>
|
|
90
95
|
`;
|
|
@@ -122,12 +122,14 @@ export class VoxDropdown extends LitElement {
|
|
|
122
122
|
super.connectedCallback();
|
|
123
123
|
document.addEventListener('click', this.handleOutsideClick);
|
|
124
124
|
this.addEventListener('keydown', this.handleKeydown);
|
|
125
|
+
this.addEventListener('focusout', this.handleFocusOut);
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
disconnectedCallback() {
|
|
128
129
|
super.disconnectedCallback();
|
|
129
130
|
document.removeEventListener('click', this.handleOutsideClick);
|
|
130
131
|
this.removeEventListener('keydown', this.handleKeydown);
|
|
132
|
+
this.removeEventListener('focusout', this.handleFocusOut);
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
private handleOutsideClick = (event: MouseEvent) => {
|
|
@@ -136,6 +138,20 @@ export class VoxDropdown extends LitElement {
|
|
|
136
138
|
}
|
|
137
139
|
};
|
|
138
140
|
|
|
141
|
+
/** Close when focus leaves the component entirely (e.g. Tab past the last item). */
|
|
142
|
+
private handleFocusOut = (event: FocusEvent) => {
|
|
143
|
+
const next = event.relatedTarget as Node | null;
|
|
144
|
+
if (this.open && !(next && this.contains(next))) {
|
|
145
|
+
this.open = false;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
private focusItem(index: number) {
|
|
150
|
+
const items = [...this.querySelectorAll<HTMLElement>('a, button')];
|
|
151
|
+
if (items.length === 0) return;
|
|
152
|
+
items[(index + items.length) % items.length].focus();
|
|
153
|
+
}
|
|
154
|
+
|
|
139
155
|
private handleKeydown = (event: KeyboardEvent) => {
|
|
140
156
|
if (event.key === 'Escape' && this.open) {
|
|
141
157
|
this.open = false;
|
|
@@ -143,20 +159,27 @@ export class VoxDropdown extends LitElement {
|
|
|
143
159
|
return;
|
|
144
160
|
}
|
|
145
161
|
|
|
146
|
-
if (
|
|
162
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
147
163
|
event.preventDefault();
|
|
148
|
-
const items = [...this.querySelectorAll<HTMLElement>('a, button')];
|
|
149
|
-
if (items.length === 0) return;
|
|
150
|
-
const active = document.activeElement as HTMLElement;
|
|
151
|
-
const index = items.indexOf(active);
|
|
152
164
|
const delta = event.key === 'ArrowDown' ? 1 : -1;
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
if (!this.open) {
|
|
166
|
+
this.open = true;
|
|
167
|
+
this.updateComplete.then(() =>
|
|
168
|
+
this.focusItem(event.key === 'ArrowDown' ? 0 : -1),
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const items = [...this.querySelectorAll<HTMLElement>('a, button')];
|
|
173
|
+
const index = items.indexOf(document.activeElement as HTMLElement);
|
|
174
|
+
this.focusItem(Math.max(index, 0) + delta);
|
|
155
175
|
}
|
|
156
176
|
};
|
|
157
177
|
|
|
158
178
|
private toggle() {
|
|
159
179
|
this.open = !this.open;
|
|
180
|
+
if (this.open) {
|
|
181
|
+
this.updateComplete.then(() => this.focusItem(0));
|
|
182
|
+
}
|
|
160
183
|
}
|
|
161
184
|
|
|
162
185
|
render() {
|
|
@@ -165,6 +188,7 @@ export class VoxDropdown extends LitElement {
|
|
|
165
188
|
class="trigger"
|
|
166
189
|
aria-expanded=${this.open ? 'true' : 'false'}
|
|
167
190
|
aria-haspopup="true"
|
|
191
|
+
aria-controls="menu"
|
|
168
192
|
@click=${this.toggle}
|
|
169
193
|
>
|
|
170
194
|
${this.label}
|
|
@@ -172,7 +196,7 @@ export class VoxDropdown extends LitElement {
|
|
|
172
196
|
<path d="m6 9 6 6 6-6" />
|
|
173
197
|
</svg>
|
|
174
198
|
</button>
|
|
175
|
-
<div class="menu">
|
|
199
|
+
<div id="menu" class="menu">
|
|
176
200
|
<slot @click=${() => (this.open = false)}></slot>
|
|
177
201
|
</div>
|
|
178
202
|
`;
|
|
@@ -60,7 +60,7 @@ export class VoxEmptyState extends LitElement {
|
|
|
60
60
|
render() {
|
|
61
61
|
return html`
|
|
62
62
|
<div class="empty">
|
|
63
|
-
<div class="icon"><slot name="icon"></slot></div>
|
|
63
|
+
<div class="icon" aria-hidden="true"><slot name="icon"></slot></div>
|
|
64
64
|
<h3 class="heading">${this.heading}</h3>
|
|
65
65
|
<div class="body"><slot></slot></div>
|
|
66
66
|
<div class="actions"><slot name="actions"></slot></div>
|
|
@@ -84,8 +84,9 @@ export class VoxFileInput extends VoxFieldElement {
|
|
|
84
84
|
for (const file of files) data.append(this.name, file);
|
|
85
85
|
this.internals.setFormValue(files.length > 0 ? data : null);
|
|
86
86
|
|
|
87
|
+
this.invalid = this.required && files.length === 0;
|
|
87
88
|
this.internals.setValidity(
|
|
88
|
-
this.
|
|
89
|
+
this.invalid ? { valueMissing: true } : {},
|
|
89
90
|
'Please select a file.',
|
|
90
91
|
this.inputEl,
|
|
91
92
|
);
|
|
@@ -103,6 +104,8 @@ export class VoxFileInput extends VoxFieldElement {
|
|
|
103
104
|
accept=${ifDefined(this.accept)}
|
|
104
105
|
?multiple=${this.multiple}
|
|
105
106
|
?disabled=${this.disabled}
|
|
107
|
+
aria-describedby=${ifDefined(this.noteId)}
|
|
108
|
+
aria-invalid=${this.invalid ? 'true' : 'false'}
|
|
106
109
|
@change=${this.handleChange}
|
|
107
110
|
/>
|
|
108
111
|
<span class="picker">
|