diff --git a/lib/ruby_ui/combobox/combobox.rb b/lib/ruby_ui/combobox/combobox.rb index c77ff785..e3156e93 100644 --- a/lib/ruby_ui/combobox/combobox.rb +++ b/lib/ruby_ui/combobox/combobox.rb @@ -2,6 +2,11 @@ module RubyUI class Combobox < Base + def initialize(term: "items", **) + @term = term + super(**) + end + def view_template(&) div(**attrs, &) end @@ -10,14 +15,11 @@ def view_template(&) def default_attrs { + role: "combobox", data: { controller: "ruby-ui--combobox", - ruby_ui__combobox_open_value: "false", - action: "click@window->ruby-ui--combobox#onClickOutside", - ruby_ui__combobox_ruby_ui__combobox_content_outlet: ".combobox-content", - ruby_ui__combobox_ruby_ui__combobox_item_outlet: ".combobox-item" - }, - class: "group/combobox w-full relative" + ruby_ui__combobox_term_value: @term.to_s + } } end end diff --git a/lib/ruby_ui/combobox/combobox_checkbox.rb b/lib/ruby_ui/combobox/combobox_checkbox.rb new file mode 100644 index 00000000..e3806d83 --- /dev/null +++ b/lib/ruby_ui/combobox/combobox_checkbox.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module RubyUI + class ComboboxCheckbox < Base + def view_template + input(type: "checkbox", **attrs) + end + + private + + def default_attrs + { + class: [ + "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + "disabled:cursor-not-allowed disabled:opacity-50" + ], + data: { + ruby_ui__combobox_target: "input", + action: "ruby-ui--combobox#inputChanged" + } + } + end + end +end diff --git a/lib/ruby_ui/combobox/combobox_content.rb b/lib/ruby_ui/combobox/combobox_content.rb deleted file mode 100644 index 090083fa..00000000 --- a/lib/ruby_ui/combobox/combobox_content.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module RubyUI - class ComboboxContent < Base - def initialize(**attrs) - @id = "content#{SecureRandom.hex(4)}" - super - end - - def view_template(&) - div(**attrs) do - div(class: "min-w-max max-h-[300px] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-out group-data-[ruby-ui--combobox-open-value=true]/combobox:animate-in fade-out-0 group-data-[ruby-ui--combobox-open-value=true]/combobox:fade-in-0 zoom-out-95 group-data-[ruby-ui--combobox-open-value=true]/combobox:zoom-in-95 slide-in-from-top-2", &) - end - end - - private - - def default_attrs - { - id: @id, - role: "listbox", - data: { - controller: "ruby-ui--combobox-content", - ruby_ui__combobox_target: "content", - action: "keydown.enter->ruby-ui--combobox#onKeyEnter keydown.esc->ruby-ui--combobox#onEscKey keydown.down->ruby-ui--combobox#onKeyDown keydown.up->ruby-ui--combobox#onKeyUp" - }, - class: "combobox-content hidden w-full absolute top-0 left-0 z-50" - } - end - end -end diff --git a/lib/ruby_ui/combobox/combobox_content_controller.js b/lib/ruby_ui/combobox/combobox_content_controller.js deleted file mode 100644 index e93d5e42..00000000 --- a/lib/ruby_ui/combobox/combobox_content_controller.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Controller } from "@hotwired/stimulus"; - -export default class extends Controller { - static targets = ["item", "empty", "group"]; - - handleSearchInput(value) { - const query = this.#sanitizeStr(value); - - this.#toggleVisibility(this.itemTargets, false); - - const visibleItems = this.#filterItems(query); - this.#toggleVisibility(visibleItems, true); - - this.#toggleVisibility(this.emptyTargets, visibleItems.length === 0); - - this.#updateGroupVisibility(); - } - - #updateGroupVisibility() { - this.groupTargets.forEach((group) => { - const hasVisibleItems = - group.querySelectorAll( - "[data-ruby-ui--combobox-content-target='item']:not(.hidden)", - ).length > 0; - this.#toggleVisibility([group], hasVisibleItems); - }); - } - - #filterItems(query) { - return this.itemTargets.filter((item) => - this.#sanitizeStr(item.innerText).includes(query), - ); - } - - #toggleVisibility(elements, isVisible) { - elements.forEach((el) => el.classList.toggle("hidden", !isVisible)); - } - - #sanitizeStr(str) { - return str.toLowerCase().trim(); - } -} diff --git a/lib/ruby_ui/combobox/combobox_controller.js b/lib/ruby_ui/combobox/combobox_controller.js index c2876053..7372ed40 100644 --- a/lib/ruby_ui/combobox/combobox_controller.js +++ b/lib/ruby_ui/combobox/combobox_controller.js @@ -1,201 +1,159 @@ import { Controller } from "@hotwired/stimulus"; -import { computePosition, autoUpdate, offset } from "@floating-ui/dom"; - -export const POPOVER_OPENED = "ruby-ui--combobox#popoverOpened"; +import { computePosition, autoUpdate, offset, flip } from "@floating-ui/dom"; +// Connects to data-controller="ruby-ui--combobox" export default class extends Controller { + static values = { + term: String + } + static targets = [ "input", - "trigger", - "value", - "content", - "search", - "list", + "popover", "item", - ]; - static values = { open: Boolean }; - static outlets = ["ruby-ui--combobox-item", "ruby-ui--combobox-content"]; + "emptyState", + "searchInput", + "trigger", + "triggerContent" + ] - constructor(...args) { - super(...args); - this.cleanup; - } + selectedItemIndex = null connect() { - this.#setFloatingElement(); - this.#generateItemsIds(); + this.updateTriggerContent() } disconnect() { this.cleanup(); } - onTriggerClick(event) { - event.preventDefault(); + inputChanged(e) { + this.updateTriggerContent() - if (this.openValue) { - this.#closeContent(); - } else { - this.#openContent(); + if (e.target.type == "radio") { + this.closePopover() } } - onItemSelected(event) { - event.preventDefault(); - - this.#setValueDispatchEventAndCloseContent(event.target); + inputContent(input) { + return input.dataset.text || input.parentElement.innerText } - onKeyEnter(event) { - event.preventDefault(); - - const currentItem = this.itemTargets.find( - (item) => item.getAttribute("aria-current") === "true", - ); + updateTriggerContent() { + const checkedInputs = this.inputTargets.filter(input => input.checked) - if (!currentItem) this.#closeContent(); - - this.#setValueDispatchEventAndCloseContent(currentItem); - } - - onSearchInput(event) { - this.rubyUiComboboxContentOutlet.handleSearchInput(event.target.value); - this.#findAndSetCurrentAndActiveDescendant(); + if (checkedInputs.length == 0) { + this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder + } else if (checkedInputs.length === 1) { + this.triggerContentTarget.innerText = this.inputContent(checkedInputs[0]) + } else { + this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}` + } } - onClickOutside(event) { - if (!this.openValue) return; - if (this.element.contains(event.target)) return; + openPopover(event) { + event.preventDefault() - event.preventDefault(); - this.#closeContent(); + this.updatePopoverPosition() + this.updatePopoverWidth() + this.triggerTarget.ariaExpanded = "true" + this.selectedItemIndex = null + this.itemTargets.forEach(item => item.ariaCurrent = "false") + this.popoverTarget.showPopover() } - onEscKey(event) { - event.preventDefault(); - - this.#closeContent(); + closePopover() { + this.triggerTarget.ariaExpanded = "false" + this.popoverTarget.hidePopover() } - onKeyDown(event) { - event.preventDefault(); - - const currentIndex = this.itemTargets.findIndex( - (item) => item.getAttribute("aria-current") === "true", - ); - - if (currentIndex + 1 < this.itemTargets.length) { - this.itemTargets[currentIndex].removeAttribute("aria-current"); - - const currentItem = this.itemTargets[currentIndex + 1]; - this.#setCurrentAndActiveDescendant(currentItem); + filterItems(e) { + if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) { + return } - } - onKeyUp(event) { - event.preventDefault(); - const currentIndex = this.itemTargets.findIndex( - (item) => item.getAttribute("aria-current") === "true", - ); + const filterTerm = this.searchInputTarget.value.toLowerCase() + let resultCount = 0 - if (currentIndex > 0) { - this.itemTargets[currentIndex].removeAttribute("aria-current"); + this.selectedItemIndex = null - const currentItem = this.itemTargets[currentIndex - 1]; - this.#setCurrentAndActiveDescendant(currentItem); - } - } + this.inputTargets.forEach((input) => { + const text = this.inputContent(input).toLowerCase() - #closeContent() { - this.openValue = false; - this.contentTarget.classList.add("hidden"); - this.triggerTarget.setAttribute("aria-expanded", false); - this.triggerTarget.setAttribute("aria-activedescendant", true); - this.itemTargets.forEach((item) => item.removeAttribute("aria-current")); + if (text.indexOf(filterTerm) > -1) { + input.parentElement.classList.remove("hidden") + resultCount++ + } else { + input.parentElement.classList.add("hidden") + } + }) - this.triggerTarget.focus({ preventScroll: true }); + this.emptyStateTarget.classList.toggle("hidden", resultCount !== 0) } - #openContent() { - this.openValue = true; - this.contentTarget.classList.remove("hidden"); - this.triggerTarget.setAttribute("aria-expanded", true); - - this.#findAndSetCurrentAndActiveDescendant(); - this.searchTarget.focus({ preventScroll: true }); - } - - #findAndSetCurrentAndActiveDescendant() { - const selectedItem = this.itemTargets.find( - (item) => item.getAttribute("aria-selected") === "true", - ); - - if (selectedItem) { - this.#setCurrentAndActiveDescendant(selectedItem); - return; + keyDownPressed() { + if (this.selectedItemIndex !== null) { + this.selectedItemIndex++ + } else { + this.selectedItemIndex = 0 } - const selectedVisible = this.itemTargets.find( - (item) => !item.classList.contains("hidden"), - ); - this.#setCurrentAndActiveDescendant(selectedVisible); + this.focusSelectedInput() } - #setCurrentAndActiveDescendant(item) { - if (!item) return; + keyUpPressed() { + if (this.selectedItemIndex !== null) { + this.selectedItemIndex-- + } else { + this.selectedItemIndex = -1 + } - item.setAttribute("aria-current", "true"); - this.triggerTarget.setAttribute( - "aria-activedescendant", - item.getAttribute("id"), - ); + this.focusSelectedInput() } - #setValueDispatchEventAndCloseContent(item) { - const oldValue = this.inputTarget.value; - const newValue = item.dataset.value; - - this.rubyUiComboboxItemOutlets.forEach((item) => - item.handleItemSelected(newValue), - ); + focusSelectedInput() { + const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains("hidden")) - this.inputTarget.value = item.dataset.value; - this.valueTarget.innerText = item.innerText; + this.wrapSelectedInputIndex(visibleInputs.length) - this.#dispatchOnChange(oldValue, newValue); - this.#closeContent(); + visibleInputs.forEach((input, index) => { + if (index == this.selectedItemIndex) { + input.parentElement.ariaCurrent = "true" + input.parentElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }) + } else { + input.parentElement.ariaCurrent = "false" + } + }) } - #dispatchOnChange(oldValue, newValue) { - if (oldValue === newValue) return; + keyEnterPressed(event) { + event.preventDefault() + const option = this.itemTargets.find(item => item.ariaCurrent === "true") - const event = new InputEvent("change", { - bubbles: true, - cancelable: true, - }); - - this.inputTarget.dispatchEvent(event); + if (option) { + option.click() + } } - #generateItemsIds() { - const listId = this.listTarget.getAttribute("id"); - this.triggerTarget.setAttribute("aria-controls", listId); - - this.itemTargets.forEach((item, index) => { - item.id = `${listId}-${index}`; - }); + wrapSelectedInputIndex(length) { + this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length } - #setFloatingElement() { - this.cleanup = autoUpdate(this.triggerTarget, this.contentTarget, () => { - computePosition(this.triggerTarget, this.contentTarget, { - middleware: [offset(4)], + updatePopoverPosition() { + this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => { + computePosition(this.triggerTarget, this.popoverTarget, { + placement: 'bottom-start', + middleware: [offset(4), flip()], }).then(({ x, y }) => { - Object.assign(this.contentTarget.style, { + Object.assign(this.popoverTarget.style, { left: `${x}px`, top: `${y}px`, }); }); }); } + + updatePopoverWidth() { + this.popoverTarget.style.width = `${this.triggerTarget.offsetWidth}px` + } } diff --git a/lib/ruby_ui/combobox/combobox_empty.rb b/lib/ruby_ui/combobox/combobox_empty_state.rb similarity index 76% rename from lib/ruby_ui/combobox/combobox_empty.rb rename to lib/ruby_ui/combobox/combobox_empty_state.rb index 552c5a3c..c2c6e110 100644 --- a/lib/ruby_ui/combobox/combobox_empty.rb +++ b/lib/ruby_ui/combobox/combobox_empty_state.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module RubyUI - class ComboboxEmpty < Base + class ComboboxEmptyState < Base def view_template(&) div(**attrs, &) end @@ -13,7 +13,7 @@ def default_attrs role: "presentation", class: "hidden py-6 text-center text-sm", data: { - ruby_ui__combobox_content_target: "empty" + ruby_ui__combobox_target: "emptyState" } } end diff --git a/lib/ruby_ui/combobox/combobox_group.rb b/lib/ruby_ui/combobox/combobox_group.rb deleted file mode 100644 index 1620d032..00000000 --- a/lib/ruby_ui/combobox/combobox_group.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module RubyUI - class ComboboxGroup < Base - def initialize(heading: nil, **attrs) - @heading = heading - super(**attrs) - end - - def view_template(&block) - div(**attrs) do - render_header if @heading - render_items(&block) - end - end - - private - - def render_header - div(group_heading: @heading, class: "px-2 py-1.5 text-xs font-medium text-muted-foreground") { @heading } - end - - def render_items(&) - div(group_items: "", role: "group", &) - end - - def default_attrs - { - class: "overflow-hidden p-1 text-foreground", - role: "presentation", - data: { - value: @heading, - ruby_ui__combobox_content_target: "group" - } - } - end - end -end diff --git a/lib/ruby_ui/combobox/combobox_input.rb b/lib/ruby_ui/combobox/combobox_input.rb deleted file mode 100644 index 9de6d7f8..00000000 --- a/lib/ruby_ui/combobox/combobox_input.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module RubyUI - class ComboboxInput < Base - def view_template - input(**attrs) - end - - private - - def default_attrs - { - class: "hidden", - data: { - ruby_ui__combobox_target: "input", - ruby_ui__form_field_target: "input", - action: "change->ruby-ui--form-field#onChange invalid->ruby-ui--form-field#onInvalid" - } - } - end - end -end diff --git a/lib/ruby_ui/combobox/combobox_item.rb b/lib/ruby_ui/combobox/combobox_item.rb index d8ed4f08..a9753d7d 100644 --- a/lib/ruby_ui/combobox/combobox_item.rb +++ b/lib/ruby_ui/combobox/combobox_item.rb @@ -2,51 +2,23 @@ module RubyUI class ComboboxItem < Base - def initialize(value: nil, **attrs) - @value = value - super(**attrs) - end - - def view_template(&block) - div(**attrs) do - div(class: "invisible group-aria-selected:visible") { icon } - block.call - end + def view_template(&) + label(**attrs, &) end private - def icon - svg( - xmlns: "http://www.w3.org/2000/svg", - viewbox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - class: "mr-2 h-4 w-4", - stroke_width: "2", - stroke_linecap: "round", - stroke_linejoin: "round" - ) do |s| - s.path( - d: "M20 6 9 17l-5-5" - ) - end - end - def default_attrs { + class: [ + "flex flex-row w-full text-wrap truncate gap-2 items-center rounded-sm px-2 py-1.5 text-sm outline-none cursor-pointer", + "select-none has-[:checked]:bg-accent hover:bg-accent p-2", + "[&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 aria-[current=true]:bg-accent aria-[current=true]:ring aria-[current=true]:ring-offset-2" + ], role: "option", - tabindex: "0", - class: - "combobox-item group relative flex cursor-pointer select-none items-center gap-x-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground aria-[current]:bg-accent aria-[current]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", data: { - value: @value, - ruby_ui__combobox_target: "item", - ruby_ui__combobox_content_target: "item", - controller: "ruby-ui--combobox-item", - action: "click->ruby-ui--combobox#onItemSelected" - }, - aria_selected: "false" + ruby_ui__combobox_target: "item" + } } end end diff --git a/lib/ruby_ui/combobox/combobox_item_controller.js b/lib/ruby_ui/combobox/combobox_item_controller.js deleted file mode 100644 index 48f3c771..00000000 --- a/lib/ruby_ui/combobox/combobox_item_controller.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Controller } from "@hotwired/stimulus"; - -export default class extends Controller { - handleItemSelected(value) { - if (this.element.dataset.value == value) { - this.element.setAttribute("aria-selected", true); - } else { - this.element.removeAttribute("aria-selected"); - } - } -} diff --git a/lib/ruby_ui/combobox/combobox_list.rb b/lib/ruby_ui/combobox/combobox_list.rb index 3770d043..e4bce6e9 100644 --- a/lib/ruby_ui/combobox/combobox_list.rb +++ b/lib/ruby_ui/combobox/combobox_list.rb @@ -2,11 +2,6 @@ module RubyUI class ComboboxList < Base - def initialize(**attrs) - @id = "list#{SecureRandom.hex(4)}" - super - end - def view_template(&) div(**attrs, &) end @@ -15,12 +10,8 @@ def view_template(&) def default_attrs { - id: @id, - data: { - ruby_ui__combobox_target: "list" - }, - role: "listbox", - tabindex: "-1" + class: "flex flex-col gap-1 p-1 max-h-72 overflow-y-auto text-foreground", + role: "listbox" } end end diff --git a/lib/ruby_ui/combobox/combobox_list_group.rb b/lib/ruby_ui/combobox/combobox_list_group.rb new file mode 100644 index 00000000..d1b62bb8 --- /dev/null +++ b/lib/ruby_ui/combobox/combobox_list_group.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class ComboboxListGroup < Base + LABEL_CLASSES = "before:content-[attr(label)] before:px-2 before:py-1.5 before:text-xs before:font-medium before:text-muted-foreground before:not-italic" + + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: ["hidden has-[label:not(.hidden)]:flex flex-col py-1 gap-1 border-b", LABEL_CLASSES], + role: "group" + } + end + end +end diff --git a/lib/ruby_ui/combobox/combobox_popover.rb b/lib/ruby_ui/combobox/combobox_popover.rb new file mode 100644 index 00000000..68b937f4 --- /dev/null +++ b/lib/ruby_ui/combobox/combobox_popover.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module RubyUI + class ComboboxPopover < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "inset-auto m-0 absolute border bg-background shadow-lg rounded-lg", + role: "popover", + autofocus: true, + popover: true, + data: { + ruby_ui__combobox_target: "popover", + action: %w[ + keydown.down->ruby-ui--combobox#keyDownPressed + keydown.up->ruby-ui--combobox#keyUpPressed + keydown.enter->ruby-ui--combobox#keyEnterPressed + keydown.esc->ruby-ui--combobox#closeDialog:prevent + resize@window->ruby-ui--combobox#updatePopoverWidth + ] + } + } + end + end +end diff --git a/lib/ruby_ui/combobox/combobox_radio.rb b/lib/ruby_ui/combobox/combobox_radio.rb new file mode 100644 index 00000000..d27c7dae --- /dev/null +++ b/lib/ruby_ui/combobox/combobox_radio.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module RubyUI + class ComboboxRadio < Base + def view_template + input(type: "radio", **attrs) + end + + private + + def default_attrs + { + class: "aspect-square h-4 w-4 rounded-full border border-primary accent-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", + data: { + ruby_ui__combobox_target: "input", + ruby_ui__form_field_target: "input", + action: %w[ + ruby-ui--combobox#inputChanged + input->ruby-ui--form-field#onInput + invalid->ruby-ui--form-field#onInvalid + ] + } + } + end + end +end diff --git a/lib/ruby_ui/combobox/combobox_search_input.rb b/lib/ruby_ui/combobox/combobox_search_input.rb index cce5cbb0..728ae56f 100644 --- a/lib/ruby_ui/combobox/combobox_search_input.rb +++ b/lib/ruby_ui/combobox/combobox_search_input.rb @@ -2,21 +2,37 @@ module RubyUI class ComboboxSearchInput < Base - def initialize(placeholder:, **attrs) + def initialize(placeholder:, **) @placeholder = placeholder - super(**attrs) + super(**) end def view_template - input_container do - search_icon + div class: "flex text-muted-foreground items-center border-b px-3" do + icon input(**attrs) end end private - def search_icon + def default_attrs + { + type: "search", + class: "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none border-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", + role: "searchbox", + placeholder: @placeholder, + data: { + ruby_ui__combobox_target: "searchInput", + action: "keyup->ruby-ui--combobox#filterItems search->ruby-ui--combobox#filterItems" + }, + autocomplete: "off", + autocorrect: "off", + spellcheck: "false" + } + end + + def icon svg( xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 24 24", @@ -33,24 +49,5 @@ def search_icon ) end end - - def input_container(&) - div(class: "flex items-center border-b px-3", &) - end - - def default_attrs - { - class: - "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", - placeholder: @placeholder, - data: { - action: "input->ruby-ui--combobox#onSearchInput", - ruby_ui__combobox_target: "search" - }, - autocomplete: "off", - autocorrect: "off", - spellcheck: false - } - end end end diff --git a/lib/ruby_ui/combobox/combobox_separator.rb b/lib/ruby_ui/combobox/combobox_separator.rb deleted file mode 100644 index a2a18732..00000000 --- a/lib/ruby_ui/combobox/combobox_separator.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module RubyUI - class ComboboxSeparator < Base - def view_template(&) - div(**attrs, &) - end - - private - - def default_attrs - {class: "-mx-1 h-px bg-border"} - end - end -end diff --git a/lib/ruby_ui/combobox/combobox_trigger.rb b/lib/ruby_ui/combobox/combobox_trigger.rb index 5b73584c..93fc9a98 100644 --- a/lib/ruby_ui/combobox/combobox_trigger.rb +++ b/lib/ruby_ui/combobox/combobox_trigger.rb @@ -2,15 +2,38 @@ module RubyUI class ComboboxTrigger < Base - def view_template(&block) + def initialize(placeholder: "", **) + @placeholder = placeholder + super(**) + end + + def view_template button(**attrs) do - block&.call + span(class: "truncate", data: {ruby_ui__combobox_target: "triggerContent"}) do + @placeholder + end icon end end private + def default_attrs + { + type: "button", + class: "flex h-full w-full items-center whitespace-nowrap rounded-md text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 justify-between", + data: { + placeholder: @placeholder, + ruby_ui__combobox_target: "trigger", + action: "ruby-ui--combobox#openPopover" + }, + aria: { + haspopup: "listbox", + expanded: "false" + } + } + end + def icon svg( xmlns: "http://www.w3.org/2000/svg", @@ -30,23 +53,5 @@ def icon ) end end - - def default_attrs - { - data: { - action: "ruby-ui--combobox#onTriggerClick", - ruby_ui__combobox_target: "trigger" - }, - type: "button", - role: "combobox", - aria: { - expanded: "false", - haspopup: "listbox", - autocomplete: "none", - activedescendant: true - }, - class: "flex h-full w-full items-center whitespace-nowrap rounded-md text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 w-[200px] justify-between" - } - end end end diff --git a/lib/ruby_ui/combobox/combobox_value.rb b/lib/ruby_ui/combobox/combobox_value.rb deleted file mode 100644 index f011a5b1..00000000 --- a/lib/ruby_ui/combobox/combobox_value.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module RubyUI - class ComboboxValue < Base - def initialize(placeholder: nil, **attrs) - @placeholder = placeholder - super(**attrs) - end - - def view_template(&block) - span(**attrs) do - block ? block.call : @placeholder - end - end - - private - - def default_attrs - { - data: { - ruby_ui__combobox_target: "value" - }, - class: "pointer-events-none" - } - end - end -end diff --git a/test/ruby_ui/combobox_test.rb b/test/ruby_ui/combobox_test.rb index 1354096c..9b3c3bdd 100644 --- a/test/ruby_ui/combobox_test.rb +++ b/test/ruby_ui/combobox_test.rb @@ -3,47 +3,71 @@ require "test_helper" class RubyUI::ComboboxTest < ComponentTest - def test_render_with_all_items + def test_render_with_radio_items output = phlex do - RubyUI.Combobox do - RubyUI.ComboboxInput() - RubyUI.ComboboxTrigger do - RubyUI.ComboboxValue(placeholder: "Select event...") - end - RubyUI.ComboboxContent do - RubyUI.ComboboxSearchInput(placeholder: "Search event...") + RubyUI.Combobox(multiple: true, term: "frameworks") do + RubyUI.ComboboxTrigger placeholder: "Select your framework" + + RubyUI.ComboboxPopover do + RubyUI.ComboboxSearchInput(placeholder: "Type the framework name") + RubyUI.ComboboxList do - RubyUI.ComboboxEmpty { "No results found." } - RubyUI.ComboboxGroup(heading: "Suggestions") do - RubyUI.ComboboxItem(value: "railsworld") do |item| - item.span { "Rails World" } - end - RubyUI.ComboboxItem(value: "tropicalrb") do |item| - item.span { "Tropical.rb" } + RubyUI.ComboboxEmptyState { "No results" } + + RubyUI.ComboboxListGroup label: "Ruby" do + RubyUI.ComboboxItem do + RubyUI.ComboboxRadio(name: "Rails", value: "rails") end - RubyUI.ComboboxItem(value: "friendly.rb") do |item| - item.span { "Friendly.rb" } + RubyUI.ComboboxItem do + RubyUI.ComboboxRadio(name: "Hanami", value: "hanami") end end - RubyUI.ComboboxSeparator() + RubyUI.ComboboxItem do + RubyUI.ComboboxRadio(name: "Lucky", value: "lucky") + end + RubyUI.ComboboxItem do + RubyUI.ComboboxRadio(name: "Kemal", value: "kemal") + end + end + end + end + end + + assert_match(/Hanami/, output) + end - RubyUI.ComboboxGroup(heading: "Others") do - RubyUI.ComboboxItem(value: "railsconf") do |item| - item.span { "RailsConf" } - end - RubyUI.ComboboxItem(value: "euruko") do |item| - item.span { "Euruko" } + def test_render_with_checkbox_items + output = phlex do + RubyUI.Combobox(multiple: true, term: "frameworks") do + RubyUI.ComboboxTrigger placeholder: "Select your framework" + + RubyUI.ComboboxPopover do + RubyUI.ComboboxSearchInput(placeholder: "Type the framework name") + + RubyUI.ComboboxList do + RubyUI.ComboboxEmptyState { "No results" } + + RubyUI.ComboboxListGroup label: "Ruby" do + RubyUI.ComboboxItem do + RubyUI.ComboboxCheckbox(name: "Rails", value: "rails") end - RubyUI.ComboboxItem(value: "rubykaigi") do |item| - item.span { "RubyKaigi" } + RubyUI.ComboboxItem do + RubyUI.ComboboxCheckbox(name: "Hanami", value: "hanami") end end + + RubyUI.ComboboxItem do + RubyUI.ComboboxCheckbox(name: "Lucky", value: "lucky") + end + RubyUI.ComboboxItem do + RubyUI.ComboboxCheckbox(name: "Kemal", value: "kemal") + end end end end end - assert_match(/Tropical.rb/, output) + assert_match(/Hanami/, output) end end