소스 검색

First review

Simone Todaro 2 년 전
부모
커밋
c203f28cba
3개의 변경된 파일145개의 추가작업 그리고 47개의 파일을 삭제
  1. 110 1
      index.html
  2. 30 46
      packages/ui/src/combobox.js
  3. 5 0
      packages/ui/src/list-context.js

+ 110 - 1
index.html

@@ -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: '',
@@ -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>
 
@@ -115,4 +222,6 @@
 </div>
 
 
+
+
 </html>

+ 30 - 46
packages/ui/src/combobox.js

@@ -125,6 +125,7 @@ function handleRoot(el, Alpine) {
                     if (! input) return
 
                     let value = this.$data.__getCurrentValue()
+
                     input.value = value
                 },
                 __getCurrentValue() {
@@ -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
                     }
@@ -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',
@@ -273,15 +272,9 @@ 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
 
@@ -289,9 +282,7 @@ function handleInput(el, Alpine) {
 
             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'() {
@@ -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))
         },
@@ -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()
@@ -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',
@@ -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 }) },
     })
 }
@@ -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',
@@ -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',
@@ -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
             }
         },
 
@@ -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()
         },
     })
 }

+ 5 - 0
packages/ui/src/list-context.js

@@ -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