Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 336 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "solid-ui",
"version": "3.1.3-5",
"version": "3.1.3-6",
"description": "UI library for Solid applications",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -85,6 +85,7 @@
},
"homepage": "https://github.com/SolidOS/solid-ui",
"dependencies": {
"@awesome.me/webawesome": "^3.9.0",
"@lit/context": "^1.1.6",
"@noble/curves": "^2.2.0",
"@noble/hashes": "^2.2.0",
Expand Down
17 changes: 7 additions & 10 deletions src/components/account/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import '@/components/button'
import '@/components/login-button'
import '@/components/logout-button'
import '@/components/menu-item'
import '@/components/menu-items'
import '@/components/menu'
import '@/components/signup-button'
import '~icons/lucide/chevron-down'
Expand Down Expand Up @@ -61,20 +60,18 @@ export default class Account extends WebComponent {
}

return html`
<solid-ui-menu>
<solid-ui-menu placement="bottom-end" distance="5">
<button type="button" slot="trigger">
<solid-ui-avatar></solid-ui-avatar>
<icon-lucide-chevron-down slot="right-icon"></icon-lucide-chevron-down>
</button>

<solid-ui-menu-items>
<solid-ui-logout-button>
<solid-ui-menu-item slot="trigger">
<icon-lucide-log-out slot="left-icon"></icon-lucide-log-out>
Sign out
</solid-ui-menu-item>
</solid-ui-logout-button>
</solid-ui-menu-items>
<solid-ui-logout-button>
<solid-ui-menu-item slot="trigger">
<icon-lucide-log-out slot="left-icon"></icon-lucide-log-out>
Sign out
</solid-ui-menu-item>
</solid-ui-logout-button>
</solid-ui-menu>
`
}
Expand Down
103 changes: 67 additions & 36 deletions src/components/combobox/Combobox.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { customElement, WebComponent } from '@/lib/components'
import { html } from 'lit'
import { property, query, state } from 'lit/decorators.js'
import { generateId, WebComponent, customElement } from '@/lib/components'
import ComboboxOption from '@/components/combobox-option/ComboboxOption'
import InputTrait from '@/lib/components/traits/InputTrait'
import type ComboboxOption from '@/components/combobox-option/ComboboxOption'

import '~icons/lucide/chevron-down'

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

@property({ type: String, reflect: true })
accessor name = ''

@property({ type: String })
accessor value = ''

@property({ type: String, reflect: true })
accessor placeholder = ''

@property({ type: Boolean, reflect: true })
accessor required = false

@query('[popover]')
private accessor popoverElement: HTMLDivElement | null = null

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

private inputId = `combobox-${generateId()}`
private inputTrait: InputTrait

protected render () {
const options = this.getOptions().filter(option => option.toLowerCase().includes(this.filter))
constructor () {
super()

return html`
${this.label ? html`<label for="${this.inputId}">${this.label}</label>` : ''}
<div class="input-wrapper">
<input
id="${this.inputId}"
.value=${this.value}
type="text"
@keydown=${this.onInputKeyDown}
@click=${this.onInputClick}
@input=${this.onInputChange}
/>
<icon-lucide-chevron-down></icon-lucide-chevron-down>
</div>
<div role="listbox" aria-labelledby="${this.inputId}" popover>
${options.map(option => html`<div role="option" aria-selected="false" @click=${() => this.onOptionClick(option)}>${option}</div>`)}
</div>
`
this.inputTrait = this.addTrait(
new InputTrait(this, {
getInputElement: () => this.inputElement,
getInternals: () => this.getInternals(),
onValueChanged: (value) => {
this.filter = value.toLowerCase()
},
})
)
}

private getOptions (): string[] {
const options = this.querySelectorAll<ComboboxOption>('solid-ui-combobox-option')
protected render () {
const options = this.getOptions().filter((option) =>
option.label.toLowerCase().includes(this.filter)
)

return Array.from(options).map(option => option.value)
return html`
${this.inputTrait.renderLabel()}
<div class="input-wrapper">
<input
id="${this.inputTrait.inputId}"
type="text"
name=${this.name}
?placeholder=${this.placeholder}
?required=${this.required}
.value=${this.value}
@keydown=${this.onInputKeyDown}
@click=${this.onInputClick}
@input=${() => this.inputTrait.onInput()}
/>
<icon-lucide-chevron-down></icon-lucide-chevron-down>
</div>
<div role="listbox" aria-labelledby="${this.inputTrait.inputId}" popover>
${options.map(
(option) =>
html`<div
role="option"
aria-selected="false"
@click=${() => this.onOptionClick(option.value)}
>
${option.label}
</div>`
)}
</div>
`
}

private setValue (value: string) {
this.filter = value.toLowerCase()
this.value = value
private getOptions (): { value: string; label: string }[] {
const options = this.querySelectorAll<ComboboxOption>(
'solid-ui-combobox-option'
)

this.getInternals().setFormValue(value)
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }))
return Array.from(options).map((option) => ({
value: option.value,
label: option.textContent,
}))
}

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

this.getInternals().form?.requestSubmit()
this.inputTrait.onSubmit()
}
break
}
Expand All @@ -87,12 +122,8 @@ export default class Combobox extends WebComponent {
this.popoverElement?.showPopover()
}

private onInputChange () {
this.setValue(this.inputElement?.value ?? '')
}

private onOptionClick (option: string) {
this.setValue(option)
this.inputTrait.setValue(option)

this.popoverElement?.hidePopover()
}
Expand Down
24 changes: 15 additions & 9 deletions src/components/dialog/Dialog.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { customElement, WebComponent } from '@/lib/components'
import { consume } from '@lit/context'
import { html, nothing } from 'lit'
import { property, query } from 'lit/decorators.js'
import { customElement, WebComponent } from '@/lib/components'
import { DialogContext, dialogContext } from '../../lib/dialogs/context'
import { CloseDialogEvent } from '../../lib/dialogs/events/close-dialog'
import { DialogContext, dialogContext, DEFAULT_DIALOG_CONTEXT } from '@/lib/dialogs/context'
import DialogTrait from '@/lib/components/traits/DialogTrait'

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

@consume({ context: dialogContext, subscribe: true })
private accessor context!: DialogContext
private accessor context: DialogContext = DEFAULT_DIALOG_CONTEXT

public close () {
if (!this.context) {
throw new Error('Dialog context missing')
}
private dialogTrait: DialogTrait<unknown>

constructor () {
super()

this.dialogTrait = this.addTrait(new DialogTrait(this, {
getContext: () => this.context
}))
}

window.dispatchEvent(new CloseDialogEvent(this.context.id))
public close (data?: unknown) {
this.dialogTrait.close(data)
}

protected firstUpdated () {
Expand Down
3 changes: 3 additions & 0 deletions src/components/dialogs-root/DialogsRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export default class DialogsRoot extends WebComponent {
this.addGlobalEventListener(CloseDialogEvent.eventName, (event) => {
event.stopImmediatePropagation()

const dialog = this.dialogs.find(dialog => dialog.id === event.id)
this.dialogs = this.dialogs.filter(dialog => dialog.id !== event.id)

dialog?.closed(event.data)
})
}

Expand Down
36 changes: 36 additions & 0 deletions src/components/input/Input.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { html } from 'lit'

import './Input'
import { defineStoryRender } from '@/storybook'

const meta = {
title: 'Input',
args: {
label: 'Name',
value: '',
placeholder: 'Enter your name',
type: 'text',
},
argTypes: {
label: { control: 'text' },
value: { control: 'text' },
placeholder: { control: 'text' },
type: {
control: 'select',
options: ['text', 'email', 'password', 'search', 'url'],
},
},
} as const

const render = defineStoryRender<typeof meta.argTypes>(({ label, value, placeholder, type }) => html`
<solid-ui-input
label="${label}"
.value=${value}
placeholder="${placeholder}"
type="${type}"
></solid-ui-input>
`)

export default meta

export const Primary = { render }
27 changes: 27 additions & 0 deletions src/components/input/Input.styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
:host {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 5px;

label {
color: var(--solid-ui-color-gray-600);
font-size: var(--solid-ui-font-size-sm);
font-weight: 400;
}

.input-wrapper {
position: relative;
width: 100%;

input {
border: 1px solid var(--solid-ui-color-gray-400);
border-radius: 5px;
padding: 10px;
width: 100%;
background: white;
color: var(--solid-ui-color-gray-700);
font-size: inherit;
}
}
}
71 changes: 71 additions & 0 deletions src/components/input/Input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { customElement, WebComponent } from '@/lib/components'
import { html } from 'lit'
import { property, query } from 'lit/decorators.js'
import InputTrait from '@/lib/components/traits/InputTrait'

import styles from './Input.styles.css'

@customElement('solid-ui-input')
export default class Input extends WebComponent {
static styles = styles
static formAssociated = true

@property({ type: String, reflect: true })
accessor label = '';

@property({ type: String, reflect: true })
accessor name = '';

@property({ type: String })
accessor value = '';

@property({ type: String, reflect: true })
accessor type = 'text';

@property({ type: String, reflect: true })
accessor placeholder = '';

@property({ type: Boolean, reflect: true })
accessor required = false;

@query('input')
private accessor inputElement: HTMLInputElement | null = null;

private inputTrait: InputTrait

constructor () {
super()

this.inputTrait = this.addTrait(new InputTrait(this, {
getInputElement: () => this.inputElement,
getInternals: () => this.getInternals(),
}))
}

protected render () {
return html`
${this.inputTrait.renderLabel()}

<div class="input-wrapper">
<input
id=${this.inputTrait.inputId}
type=${this.type}
name=${this.name}
placeholder=${this.placeholder}
?required=${this.required}
.value=${this.value}
@input=${() => this.inputTrait.onInput()}
@keydown=${this.onKeyDown}
/>
</div>
`
}

private onKeyDown (e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault()

this.inputTrait.onSubmit()
}
}
}
4 changes: 4 additions & 0 deletions src/components/input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Input from './Input'

export { Input }
export default Input
Loading
Loading