Skip to content

Commit 400ef7a

Browse files
authored
Merge pull request #815 from NoelDeMartin/773-a11y
#773 Improve a11y + prepare components for contacts-pane
2 parents 988558e + 94d727b commit 400ef7a

45 files changed

Lines changed: 1068 additions & 209 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 336 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "solid-ui",
3-
"version": "3.1.3-5",
3+
"version": "3.1.3-6",
44
"description": "UI library for Solid applications",
55
"main": "dist/index.cjs.js",
66
"types": "dist/index.d.ts",
@@ -85,6 +85,7 @@
8585
},
8686
"homepage": "https://github.com/SolidOS/solid-ui",
8787
"dependencies": {
88+
"@awesome.me/webawesome": "^3.9.0",
8889
"@lit/context": "^1.1.6",
8990
"@noble/curves": "^2.2.0",
9091
"@noble/hashes": "^2.2.0",

src/components/account/Account.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import '@/components/button'
88
import '@/components/login-button'
99
import '@/components/logout-button'
1010
import '@/components/menu-item'
11-
import '@/components/menu-items'
1211
import '@/components/menu'
1312
import '@/components/signup-button'
1413
import '~icons/lucide/chevron-down'
@@ -61,20 +60,18 @@ export default class Account extends WebComponent {
6160
}
6261

6362
return html`
64-
<solid-ui-menu>
63+
<solid-ui-menu placement="bottom-end" distance="5">
6564
<button type="button" slot="trigger">
6665
<solid-ui-avatar></solid-ui-avatar>
6766
<icon-lucide-chevron-down slot="right-icon"></icon-lucide-chevron-down>
6867
</button>
6968
70-
<solid-ui-menu-items>
71-
<solid-ui-logout-button>
72-
<solid-ui-menu-item slot="trigger">
73-
<icon-lucide-log-out slot="left-icon"></icon-lucide-log-out>
74-
Sign out
75-
</solid-ui-menu-item>
76-
</solid-ui-logout-button>
77-
</solid-ui-menu-items>
69+
<solid-ui-logout-button>
70+
<solid-ui-menu-item slot="trigger">
71+
<icon-lucide-log-out slot="left-icon"></icon-lucide-log-out>
72+
Sign out
73+
</solid-ui-menu-item>
74+
</solid-ui-logout-button>
7875
</solid-ui-menu>
7976
`
8077
}

src/components/combobox/Combobox.ts

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { customElement, WebComponent } from '@/lib/components'
12
import { html } from 'lit'
23
import { property, query, state } from 'lit/decorators.js'
3-
import { generateId, WebComponent, customElement } from '@/lib/components'
4-
import ComboboxOption from '@/components/combobox-option/ComboboxOption'
4+
import InputTrait from '@/lib/components/traits/InputTrait'
5+
import type ComboboxOption from '@/components/combobox-option/ComboboxOption'
56

67
import '~icons/lucide/chevron-down'
78

@@ -15,9 +16,18 @@ export default class Combobox extends WebComponent {
1516
@property({ type: String, reflect: true })
1617
accessor label = ''
1718

19+
@property({ type: String, reflect: true })
20+
accessor name = ''
21+
1822
@property({ type: String })
1923
accessor value = ''
2024

25+
@property({ type: String, reflect: true })
26+
accessor placeholder = ''
27+
28+
@property({ type: Boolean, reflect: true })
29+
accessor required = false
30+
2131
@query('[popover]')
2232
private accessor popoverElement: HTMLDivElement | null = null
2333

@@ -27,42 +37,67 @@ export default class Combobox extends WebComponent {
2737
@state()
2838
private accessor filter = ''
2939

30-
private inputId = `combobox-${generateId()}`
40+
private inputTrait: InputTrait
3141

32-
protected render () {
33-
const options = this.getOptions().filter(option => option.toLowerCase().includes(this.filter))
42+
constructor () {
43+
super()
3444

35-
return html`
36-
${this.label ? html`<label for="${this.inputId}">${this.label}</label>` : ''}
37-
<div class="input-wrapper">
38-
<input
39-
id="${this.inputId}"
40-
.value=${this.value}
41-
type="text"
42-
@keydown=${this.onInputKeyDown}
43-
@click=${this.onInputClick}
44-
@input=${this.onInputChange}
45-
/>
46-
<icon-lucide-chevron-down></icon-lucide-chevron-down>
47-
</div>
48-
<div role="listbox" aria-labelledby="${this.inputId}" popover>
49-
${options.map(option => html`<div role="option" aria-selected="false" @click=${() => this.onOptionClick(option)}>${option}</div>`)}
50-
</div>
51-
`
45+
this.inputTrait = this.addTrait(
46+
new InputTrait(this, {
47+
getInputElement: () => this.inputElement,
48+
getInternals: () => this.getInternals(),
49+
onValueChanged: (value) => {
50+
this.filter = value.toLowerCase()
51+
},
52+
})
53+
)
5254
}
5355

54-
private getOptions (): string[] {
55-
const options = this.querySelectorAll<ComboboxOption>('solid-ui-combobox-option')
56+
protected render () {
57+
const options = this.getOptions().filter((option) =>
58+
option.label.toLowerCase().includes(this.filter)
59+
)
5660

57-
return Array.from(options).map(option => option.value)
61+
return html`
62+
${this.inputTrait.renderLabel()}
63+
<div class="input-wrapper">
64+
<input
65+
id="${this.inputTrait.inputId}"
66+
type="text"
67+
name=${this.name}
68+
?placeholder=${this.placeholder}
69+
?required=${this.required}
70+
.value=${this.value}
71+
@keydown=${this.onInputKeyDown}
72+
@click=${this.onInputClick}
73+
@input=${() => this.inputTrait.onInput()}
74+
/>
75+
<icon-lucide-chevron-down></icon-lucide-chevron-down>
76+
</div>
77+
<div role="listbox" aria-labelledby="${this.inputTrait.inputId}" popover>
78+
${options.map(
79+
(option) =>
80+
html`<div
81+
role="option"
82+
aria-selected="false"
83+
@click=${() => this.onOptionClick(option.value)}
84+
>
85+
${option.label}
86+
</div>`
87+
)}
88+
</div>
89+
`
5890
}
5991

60-
private setValue (value: string) {
61-
this.filter = value.toLowerCase()
62-
this.value = value
92+
private getOptions (): { value: string; label: string }[] {
93+
const options = this.querySelectorAll<ComboboxOption>(
94+
'solid-ui-combobox-option'
95+
)
6396

64-
this.getInternals().setFormValue(value)
65-
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }))
97+
return Array.from(options).map((option) => ({
98+
value: option.value,
99+
label: option.textContent,
100+
}))
66101
}
67102

68103
private onInputKeyDown (e: KeyboardEvent) {
@@ -75,7 +110,7 @@ export default class Combobox extends WebComponent {
75110
if (!this.popoverElement?.matches(':popover-open')) {
76111
e.preventDefault()
77112

78-
this.getInternals().form?.requestSubmit()
113+
this.inputTrait.onSubmit()
79114
}
80115
break
81116
}
@@ -87,12 +122,8 @@ export default class Combobox extends WebComponent {
87122
this.popoverElement?.showPopover()
88123
}
89124

90-
private onInputChange () {
91-
this.setValue(this.inputElement?.value ?? '')
92-
}
93-
94125
private onOptionClick (option: string) {
95-
this.setValue(option)
126+
this.inputTrait.setValue(option)
96127

97128
this.popoverElement?.hidePopover()
98129
}

src/components/dialog/Dialog.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { customElement, WebComponent } from '@/lib/components'
12
import { consume } from '@lit/context'
23
import { html, nothing } from 'lit'
34
import { property, query } from 'lit/decorators.js'
4-
import { customElement, WebComponent } from '@/lib/components'
5-
import { DialogContext, dialogContext } from '../../lib/dialogs/context'
6-
import { CloseDialogEvent } from '../../lib/dialogs/events/close-dialog'
5+
import { DialogContext, dialogContext, DEFAULT_DIALOG_CONTEXT } from '@/lib/dialogs/context'
6+
import DialogTrait from '@/lib/components/traits/DialogTrait'
77

88
import '~icons/lucide/x'
99
import '@/components/dialog-header'
@@ -22,14 +22,20 @@ export default class Dialog extends WebComponent {
2222
private accessor nativeDialog: HTMLDialogElement | null = null
2323

2424
@consume({ context: dialogContext, subscribe: true })
25-
private accessor context!: DialogContext
25+
private accessor context: DialogContext = DEFAULT_DIALOG_CONTEXT
2626

27-
public close () {
28-
if (!this.context) {
29-
throw new Error('Dialog context missing')
30-
}
27+
private dialogTrait: DialogTrait<unknown>
28+
29+
constructor () {
30+
super()
31+
32+
this.dialogTrait = this.addTrait(new DialogTrait(this, {
33+
getContext: () => this.context
34+
}))
35+
}
3136

32-
window.dispatchEvent(new CloseDialogEvent(this.context.id))
37+
public close (data?: unknown) {
38+
this.dialogTrait.close(data)
3339
}
3440

3541
protected firstUpdated () {

src/components/dialogs-root/DialogsRoot.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export default class DialogsRoot extends WebComponent {
2424
this.addGlobalEventListener(CloseDialogEvent.eventName, (event) => {
2525
event.stopImmediatePropagation()
2626

27+
const dialog = this.dialogs.find(dialog => dialog.id === event.id)
2728
this.dialogs = this.dialogs.filter(dialog => dialog.id !== event.id)
29+
30+
dialog?.closed(event.data)
2831
})
2932
}
3033

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { html } from 'lit'
2+
3+
import './Input'
4+
import { defineStoryRender } from '@/storybook'
5+
6+
const meta = {
7+
title: 'Input',
8+
args: {
9+
label: 'Name',
10+
value: '',
11+
placeholder: 'Enter your name',
12+
type: 'text',
13+
},
14+
argTypes: {
15+
label: { control: 'text' },
16+
value: { control: 'text' },
17+
placeholder: { control: 'text' },
18+
type: {
19+
control: 'select',
20+
options: ['text', 'email', 'password', 'search', 'url'],
21+
},
22+
},
23+
} as const
24+
25+
const render = defineStoryRender<typeof meta.argTypes>(({ label, value, placeholder, type }) => html`
26+
<solid-ui-input
27+
label="${label}"
28+
.value=${value}
29+
placeholder="${placeholder}"
30+
type="${type}"
31+
></solid-ui-input>
32+
`)
33+
34+
export default meta
35+
36+
export const Primary = { render }
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
:host {
2+
display: inline-flex;
3+
flex-direction: column;
4+
align-items: flex-start;
5+
gap: 5px;
6+
7+
label {
8+
color: var(--solid-ui-color-gray-600);
9+
font-size: var(--solid-ui-font-size-sm);
10+
font-weight: 400;
11+
}
12+
13+
.input-wrapper {
14+
position: relative;
15+
width: 100%;
16+
17+
input {
18+
border: 1px solid var(--solid-ui-color-gray-400);
19+
border-radius: 5px;
20+
padding: 10px;
21+
width: 100%;
22+
background: white;
23+
color: var(--solid-ui-color-gray-700);
24+
font-size: inherit;
25+
}
26+
}
27+
}

src/components/input/Input.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { customElement, WebComponent } from '@/lib/components'
2+
import { html } from 'lit'
3+
import { property, query } from 'lit/decorators.js'
4+
import InputTrait from '@/lib/components/traits/InputTrait'
5+
6+
import styles from './Input.styles.css'
7+
8+
@customElement('solid-ui-input')
9+
export default class Input extends WebComponent {
10+
static styles = styles
11+
static formAssociated = true
12+
13+
@property({ type: String, reflect: true })
14+
accessor label = '';
15+
16+
@property({ type: String, reflect: true })
17+
accessor name = '';
18+
19+
@property({ type: String })
20+
accessor value = '';
21+
22+
@property({ type: String, reflect: true })
23+
accessor type = 'text';
24+
25+
@property({ type: String, reflect: true })
26+
accessor placeholder = '';
27+
28+
@property({ type: Boolean, reflect: true })
29+
accessor required = false;
30+
31+
@query('input')
32+
private accessor inputElement: HTMLInputElement | null = null;
33+
34+
private inputTrait: InputTrait
35+
36+
constructor () {
37+
super()
38+
39+
this.inputTrait = this.addTrait(new InputTrait(this, {
40+
getInputElement: () => this.inputElement,
41+
getInternals: () => this.getInternals(),
42+
}))
43+
}
44+
45+
protected render () {
46+
return html`
47+
${this.inputTrait.renderLabel()}
48+
49+
<div class="input-wrapper">
50+
<input
51+
id=${this.inputTrait.inputId}
52+
type=${this.type}
53+
name=${this.name}
54+
placeholder=${this.placeholder}
55+
?required=${this.required}
56+
.value=${this.value}
57+
@input=${() => this.inputTrait.onInput()}
58+
@keydown=${this.onKeyDown}
59+
/>
60+
</div>
61+
`
62+
}
63+
64+
private onKeyDown (e: KeyboardEvent) {
65+
if (e.key === 'Enter') {
66+
e.preventDefault()
67+
68+
this.inputTrait.onSubmit()
69+
}
70+
}
71+
}

src/components/input/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Input from './Input'
2+
3+
export { Input }
4+
export default Input

0 commit comments

Comments
 (0)