Skip to content

Commit

Permalink
First review
Browse files Browse the repository at this point in the history
  • Loading branch information
SimoTod committed Feb 11, 2023
1 parent c47d11d commit c203f28
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 47 deletions.
111 changes: 110 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,113 @@
<script src="./packages/alpinejs/dist/cdn.js" defer></script>
<script src="//cdn.tailwindcss.com"></script>

<div
x-data="{
query: '',
people: [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
],
activePersons: [],
get queryPerson() {
if (! this.query) return null
return {
id: 11, name: this.query,
}
},
onSubmit(e) {
e.preventDefault()
console.log([...new FormData(e.currentTarget).entries()])
},
removePerson(person) {
this.activePersons = this.activePersons.filter((p) => p !== person)
}
}"
class="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12"
>
<div class="w-full max-w-4xl">
<div class="space-y-1">
<form @submit="onSubmit">
<div x-combobox x-model="activePersons" name="people" multiple>
<label x-combobox:label class="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</label>

<div class="relative">
<div>Query: <span x-text="query"></span></div>
<span class="inline-block w-full rounded-md shadow-sm">
<div class="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left transition duration-150 ease-in-out focus-within:border-blue-700 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-700 sm:text-sm sm:leading-5">
<span class="block flex flex-wrap gap-2">
<span x-show="activePersons.length === 0" class="p-0.5">Empty</span>
<template x-for="person in activePersons" :key="person.id">
<span class="flex items-center gap-1 rounded bg-blue-50 px-2 py-0.5">
<span x-text="person.name"></span>
<svg class="h-4 w-4 cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" @click.stop.prevent="removePerson(person)">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</span>
</template>
<input x-combobox:input @change="query = $event.target.value" class="border-none p-0 focus:ring-0" placeholder="Search..." />
</span>
<button x-combobox:button class="absolute inset-y-0 right-0 flex items-center pr-2">
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="none" stroke="currentColor">
<path d="M7 7l3-3 3 3m0 6l-3 3-3-3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
</span>

<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ul x-combobox:options hold class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
<template
x-for="person in people.filter((person) =>
person.name.toLowerCase().includes(query.toLowerCase())
)"
:key="person.id"
>
<li x-combobox:option :value="person" class="relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none" :class="$comboboxOption.isActive ? 'bg-indigo-600 text-white' : 'text-gray-900'">
<span x-text="person.name" class="block truncate" :class="{ 'font-semibold': $comboboxOption.isSelected, 'font-normal': !$comboboxOption.isSelected }">
</span>
<span x-show="$comboboxOption.isSelected" class="absolute inset-y-0 right-0 flex items-center pr-4" :class="{ 'text-white': $comboboxOption.isActive, 'text-indigo-600': !$comboboxOption.isActive }">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</span>
</li>
</template>

<!-- <template x-if="queryPerson">
<li x-combobox:option :value="queryPerson" class="relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none" :class="$comboboxOption.isActive ? 'bg-indigo-600 text-white' : 'text-gray-900'">
<span x-text="'Create ' + queryPerson.name" class="block truncate" :class="{ 'font-semibold': $comboboxOption.isSelected, 'font-normal': !$comboboxOption.isSelected }">
</span>
<span x-show="$comboboxOption.isSelected" class="absolute inset-y-0 right-0 flex items-center pr-4" :class="{ 'text-white': $comboboxOption.isActive, 'text-indigo-600': !$comboboxOption.isActive }">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</span>
</li>
</template> -->
</ul>
</div>
</div>
</div>
<button class="mt-2 inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
Submit
</button>
</form>
</div>
</div>
</div>

<div
x-data="{
query: '',
Expand Down Expand Up @@ -97,7 +204,7 @@
'text-gray-600': ! $comboboxOption.isActive,
'opacity-50 cursor-not-allowed': $comboboxOption.isDisabled,
}"
class="flex items-center cursor-default justify-between gap-2 w-full px-4 py-2 text-sm transition-colors"
class="flex items-center cursor-default justify-between gap-2 w-full px-4 py-2 text-sm"
>
<span x-text="framework.name"></span>

Expand All @@ -115,4 +222,6 @@
</div>




</html>
76 changes: 30 additions & 46 deletions packages/ui/src/combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ function handleRoot(el, Alpine) {
if (! input) return

let value = this.$data.__getCurrentValue()

input.value = value
},
__getCurrentValue() {
Expand Down Expand Up @@ -178,9 +179,9 @@ function handleRoot(el, Alpine) {
let firstSelectedValue

if (this.__isMultiple) {
firstSelectedValue = this.__value.find(i => {
return !! this.__context.getItemByValue(i)
})
let activeElement = this.__context.getItemsByValues(this.__value)

firstSelectedValue = activeElement.length ? activeElement[0].value : null
} else {
firstSelectedValue = this.__value
}
Expand Down Expand Up @@ -262,9 +263,7 @@ function handleInput(el, Alpine) {
Alpine.bind(el, {
// Setup...
'x-ref': '__input',
':id'() {
return this.$id('alpine-combobox-input')
},
':id'() { return this.$id('alpine-combobox-input') },

// Accessibility attributes...
'role': 'combobox',
Expand All @@ -273,25 +272,17 @@ function handleInput(el, Alpine) {

// We need to defer this evaluation a bit because $refs that get declared later
// in the DOM aren't available yet when x-ref is the result of an Alpine.bind object.
async ':aria-controls'() {
return await microtask(() => this.$refs.__options && this.$refs.__options.id)
},
':aria-expanded'() {
return this.$data.__isDisabled ? undefined : this.$data.__isOpen
},
':aria-multiselectable'() {
return this.$data.__isMultiple ? true : undefined
},
async ':aria-controls'() { return await microtask(() => this.$refs.__options && this.$refs.__options.id) },
':aria-expanded'() { return this.$data.__isDisabled ? undefined : this.$data.__isOpen },
':aria-multiselectable'() { return this.$data.__isMultiple ? true : undefined },
':aria-activedescendant'() {
if (! this.$data.__context.hasActive()) return

let active = this.$data.__context.getActiveItem()

return active ? active.el.id : null
},
':aria-labelledby'() {
return this.$refs.__label ? this.$refs.__label.id : (this.$refs.__button ? this.$refs.__button.id : null)
},
':aria-labelledby'() { return this.$refs.__label ? this.$refs.__label.id : (this.$refs.__button ? this.$refs.__button.id : null) },

// Initialize...
'x-init'() {
Expand All @@ -306,9 +297,7 @@ function handleInput(el, Alpine) {
this.$dispatch('change')
}
},
'@blur'() {
this.$data.__stopTyping()
},
'@blur'() { this.$data.__stopTyping(false) },
'@keydown'(e) {
queueMicrotask(() => this.$data.__context.activateByKeyEvent(e, false, () => this.$data.__isOpen, () => this.$data.__open(), (state) => this.$data.__isTyping = state))
},
Expand All @@ -319,9 +308,8 @@ function handleInput(el, Alpine) {

if (! this.$data.__isMultiple) {
this.$data.__close()
this.$data.__resetInput()
}

this.$data.__resetInput()
},
'@keydown.escape.prevent'(e) {
if (! this.$data.__static) e.stopPropagation()
Expand Down Expand Up @@ -362,9 +350,7 @@ function handleButton(el, Alpine) {
Alpine.bind(el, {
// Setup...
'x-ref': '__button',
':id'() {
return this.$id('alpine-combobox-button')
},
':id'() { return this.$id('alpine-combobox-button') },

// Accessibility attributes...
'aria-haspopup': 'true',
Expand Down Expand Up @@ -398,9 +384,7 @@ function handleButton(el, Alpine) {
function handleLabel(el, Alpine) {
Alpine.bind(el, {
'x-ref': '__label',
':id'() {
return this.$id('alpine-combobox-label')
},
':id'() { return this.$id('alpine-combobox-label') },
'@click'() { this.$refs.__input.focus({ preventScroll: true }) },
})
}
Expand All @@ -409,9 +393,7 @@ function handleOptions(el, Alpine) {
Alpine.bind(el, {
// Setup...
'x-ref': '__options',
':id'() {
return this.$id('alpine-combobox-options')
},
':id'() { return this.$id('alpine-combobox-options') },

// Accessibility attributes...
'role': 'combobox',
Expand All @@ -426,21 +408,15 @@ function handleOptions(el, Alpine) {
}
},

'x-show'() {
return this.$data.__isStatic ? true : this.$data.__isOpen
},
'x-show'() { return this.$data.__isStatic ? true : this.$data.__isOpen },
})
}

function handleOption(el, Alpine) {
Alpine.bind(el, {
// Setup...
'x-id'() {
return ['alpine-combobox-option']
},
':id'() {
return this.$id('alpine-combobox-option')
},
'x-id'() { return ['alpine-combobox-option'] },
':id'() { return this.$id('alpine-combobox-option') },

// Accessibility attributes...
'role': 'option',
Expand All @@ -457,11 +433,16 @@ function handleOption(el, Alpine) {
let value = Alpine.extractProp(this.$el, 'value')
let disabled = Alpine.extractProp(this.$el, 'disabled', false, false)

this.$data.__context.registerItem(key, this.$el, value, disabled)
// memoize the context as it's not going to change
// and calling this.$data on mouse action is expensive
this.__context = this.$data.__context
this.__context.registerItem(key, this.$el, value, disabled)
},
destroy() {
this.$data.__context.unregisterItem(this.$el.__optionKey)
this.__context.unregisterItem(this.$el.__optionKey)
this.__context = null
},
__context: null
}
},

Expand All @@ -478,15 +459,18 @@ function handleOption(el, Alpine) {

this.$nextTick(() => this.$refs['__input'].focus({ preventScroll: true }))
},
'@mouseenter'(e) {
this.__context.activateEl(this.$el)
},
'@mousemove'(e) {
if (this.$data.__context.isActiveEl(this.$el)) return
if (this.__context.isActiveEl(this.$el)) return

this.$data.__context.activateEl(this.$el)
this.__context.activateEl(this.$el)
},
'@mouseleave'(e) {
if (this.$data.__hold) return

this.$data.__context.deactivate()
this.__context.deactivate()
},
})
}
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/list-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export function generateContext(multiple, orientation, activateSelectedOrFirst)
return this.items.find(i => i.el === el)
},

getItemsByValues(values) {
let rawValues = values.map(i => Alpine.raw(i));
return this.items.filter(i => rawValues.includes(Alpine.raw(i.value)))
},

getActiveItem() {
if (! this.hasActive()) return null

Expand Down

0 comments on commit c203f28

Please sign in to comment.