Skip to content
Open
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
26 changes: 17 additions & 9 deletions app/assets/javascripts/hotwire_combobox.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ function visible(target) {
return !(target.hidden || target.closest("[hidden]"))
}

function enabled(target) {
return !(target.ariaDisabled === "true" || target.closest("[aria-disabled='true']"))
}

function wrapAroundAccess(array, index) {
const first = 0;
const last = array.length - 1;
Expand Down Expand Up @@ -984,7 +988,7 @@ Combobox.Navigation = Base => class extends Base {
cancel(event);
},
End: (event) => {
this._selectIndex(this._visibleOptionElements.length - 1);
this._selectIndex(this._selectableOptionElements.length - 1);
cancel(event);
},
Enter: (event) => {
Expand Down Expand Up @@ -1078,6 +1082,10 @@ Combobox.Options = Base => class extends Base {
return [ ...this._allFilterableOptionElements ].filter(visible)
}

get _selectableOptionElements() {
return [ ...this._visibleOptionElements ].filter(enabled)
}

get _selectedOptionElement() {
return this._actingListbox.querySelector("[role=option][aria-selected=true]:not([data-multiselected])")
}
Expand All @@ -1087,7 +1095,7 @@ Combobox.Options = Base => class extends Base {
}

get _selectedOptionIndex() {
return [ ...this._visibleOptionElements ].indexOf(this._selectedOptionElement)
return [ ...this._selectableOptionElements ].indexOf(this._selectedOptionElement)
}

get _isUnjustifiablyBlank() {
Expand Down Expand Up @@ -1118,8 +1126,8 @@ Combobox.Selection = Base => class extends Base {
this._deselect();
} else if (inputType === "hw:lockInSelection" && this._ensurableOption) {
this._select(this._ensurableOption, this._softAutocomplete.bind(this));
} else if (this._isOpen && this._visibleOptionElements[0]) {
this._select(this._visibleOptionElements[0], this._softAutocomplete.bind(this));
} else if (this._isOpen && this._selectableOptionElements[0]) {
this._select(this._selectableOptionElements[0], this._softAutocomplete.bind(this));
} else if (this._isOpen) {
this._resetOptionsAndNotify();
this._markInvalid();
Expand Down Expand Up @@ -1170,7 +1178,7 @@ Combobox.Selection = Base => class extends Base {
}

_selectIndex(index) {
const option = wrapAroundAccess(this._visibleOptionElements, index);
const option = wrapAroundAccess(this._selectableOptionElements, index);
this._forceSelectionWithoutFiltering(option);
}

Expand Down Expand Up @@ -1240,7 +1248,7 @@ Combobox.Selection = Base => class extends Base {
}

get _ensurableOption() {
return this._selectedOptionElement || this._visibleOptionElements[0]
return this._selectedOptionElement || this._selectableOptionElements[0]
}
};

Expand Down Expand Up @@ -1306,7 +1314,7 @@ var preventDefault = function preventDefault(rawEvent) {
var setOverflowHidden = function setOverflowHidden(options) {
// If previousBodyPaddingRight is already set, don't set it again.
if (previousBodyPaddingRight === undefined) {
var _reserveScrollBarGap = !!options && options.reserveScrollBarGap === true;
var _reserveScrollBarGap = false;
var scrollBarGap = window.innerWidth - document.documentElement.clientWidth;

if (_reserveScrollBarGap && scrollBarGap > 0) {
Expand Down Expand Up @@ -1436,15 +1444,15 @@ var disableBodyScroll = function disableBodyScroll(targetElement, options) {

var lock = {
targetElement: targetElement,
options: options || {}
options: {}
};

locks = [].concat(_toConsumableArray(locks), [lock]);

if (isIosDevice) {
setPositionFixed();
} else {
setOverflowHidden(options);
setOverflowHidden();
}

if (isIosDevice) {
Expand Down
4 changes: 4 additions & 0 deletions app/assets/javascripts/hw_combobox/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export function visible(target) {
return !(target.hidden || target.closest("[hidden]"))
}

export function enabled(target) {
return !(target.ariaDisabled === "true" || target.closest("[aria-disabled='true']"))
}

export function wrapAroundAccess(array, index) {
const first = 0
const last = array.length - 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Combobox.Navigation = Base => class extends Base {
cancel(event)
},
End: (event) => {
this._selectIndex(this._visibleOptionElements.length - 1)
this._selectIndex(this._selectableOptionElements.length - 1)
cancel(event)
},
Enter: (event) => {
Expand Down
8 changes: 6 additions & 2 deletions app/assets/javascripts/hw_combobox/models/combobox/options.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Combobox from "hw_combobox/models/combobox/base"
import { visible } from "hw_combobox/helpers"
import { visible, enabled } from "hw_combobox/helpers"

Combobox.Options = Base => class extends Base {
_resetOptionsSilently() {
Expand Down Expand Up @@ -39,6 +39,10 @@ Combobox.Options = Base => class extends Base {
return [ ...this._allFilterableOptionElements ].filter(visible)
}

get _selectableOptionElements() {
return [ ...this._visibleOptionElements ].filter(enabled)
}

get _selectedOptionElement() {
return this._actingListbox.querySelector("[role=option][aria-selected=true]:not([data-multiselected])")
}
Expand All @@ -48,7 +52,7 @@ Combobox.Options = Base => class extends Base {
}

get _selectedOptionIndex() {
return [ ...this._visibleOptionElements ].indexOf(this._selectedOptionElement)
return [ ...this._selectableOptionElements ].indexOf(this._selectedOptionElement)
}

get _isUnjustifiablyBlank() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Combobox.Selection = Base => class extends Base {
this._deselect()
} else if (inputType === "hw:lockInSelection" && this._ensurableOption) {
this._select(this._ensurableOption, this._softAutocomplete.bind(this))
} else if (this._isOpen && this._visibleOptionElements[0]) {
this._select(this._visibleOptionElements[0], this._softAutocomplete.bind(this))
} else if (this._isOpen && this._selectableOptionElements[0]) {
this._select(this._selectableOptionElements[0], this._softAutocomplete.bind(this))
} else if (this._isOpen) {
this._resetOptionsAndNotify()
this._markInvalid()
Expand Down Expand Up @@ -80,7 +80,7 @@ Combobox.Selection = Base => class extends Base {
}

_selectIndex(index) {
const option = wrapAroundAccess(this._visibleOptionElements, index)
const option = wrapAroundAccess(this._selectableOptionElements, index)
this._forceSelectionWithoutFiltering(option)
}

Expand Down Expand Up @@ -150,6 +150,6 @@ Combobox.Selection = Base => class extends Base {
}

get _ensurableOption() {
return this._selectedOptionElement || this._visibleOptionElements[0]
return this._selectedOptionElement || this._selectableOptionElements[0]
}
}
5 changes: 5 additions & 0 deletions app/assets/stylesheets/hotwire_combobox.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@
border-bottom: var(--hw-border-width--slim) solid var(--hw-border-color);
}

.hw-combobox__option--disabled {
pointer-events:none;
opacity:0.6;
}

.hw-combobox__option:hover,
.hw-combobox__option--navigated,
.hw-combobox__option--selected {
Expand Down
11 changes: 8 additions & 3 deletions app/presenters/hotwire_combobox/listbox/option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ def autocompletable_as
end

private
Data = Struct.new :id, :value, :display, :content, :blank, :filterable_as, :autocompletable_as, keyword_init: true
Data = Struct.new :id, :value, :display, :content, :blank, :filterable_as, :disabled, :autocompletable_as, keyword_init: true

attr_reader :option

def options
{ id: id, role: :option, tabindex: "-1",
class: [ "hw-combobox__option", { "hw-combobox__option--blank": blank? } ],
disabled: disabled?,
class: [ "hw-combobox__option", { "hw-combobox__option--blank": blank?, "hw-combobox__option--disabled": disabled? } ],
data: { action: "click->hw-combobox#selectOnClick", filterable_as: filterable_as, autocompletable_as: autocompletable_as, value: value },
aria: { selected: false } }
aria: { selected: false, disabled: ("true" if disabled?) } }
end

def id
Expand All @@ -44,4 +45,8 @@ def content
def blank?
option.try(:blank).present?
end

def disabled?
option.try(:disabled).present?
end
end
3 changes: 3 additions & 0 deletions test/dummy/app/controllers/comboboxes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ def turbo_streamed_block
end

def disabled
@disabled_options = State.all.map do |state|
{ display: state.name, value: state.abbreviation, disabled: state.abbreviation[0] == "A" ? "true" : nil }
end
end

private
Expand Down
5 changes: 5 additions & 0 deletions test/dummy/app/views/comboboxes/disabled.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
<%= label_tag "state-field", "State (disabled)" %>
<%= combobox_tag "state", @state_options, id: "state-field", disabled: true %>
<br>

<%= label_tag "disabled-state-field", "State (with disabled options)" %>
<%= combobox_tag "state_disabled_options", @disabled_options, id: "disabled-state-field"%>
10 changes: 10 additions & 0 deletions test/presenters/hotwire_combobox/listbox/option_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ class HotwireCombobox::Listbox::OptionTest < ApplicationViewTestCase
assert_attrs render(option), tag_name: :li, "data-autocompletable-as": "bar"
end

test "renders option with aria-disabled when disabled" do
option = { disabled: true }
assert_attrs render(option), tag_name: :li, "aria-disabled": "true"
end

test "renders option without aria-disabled when not disabled" do
option = { disabled: false }
assert_attrs render(option), tag_name: :li
end

private
def render(option)
view.render HotwireCombobox::Listbox::Option.new(option)
Expand Down
19 changes: 18 additions & 1 deletion test/system/hotwire_combobox_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,24 @@ class HotwireComboboxTest < ApplicationSystemTestCase
open_combobox "#state-field"
assert_closed_combobox

find('[data-hw-combobox-target="handle"]').click
find('#state-field').click
assert_closed_combobox
end

test "combobox does not allow disabled options to be selected" do
visit disabled_path

open_combobox "#disabled-state-field"
alaska_option = find('li', text: 'Alaska', match: :first)

assert_equal 'true', alaska_option[:'aria-disabled']
assert_includes alaska_option[:class], 'hw-combobox__option--disabled'
end

test "combobox with disabled options allows selection of enabled options" do
visit disabled_path
open_combobox "#disabled-state-field"
click_on_option "California"
assert_combobox_display_and_value "#disabled-state-field", "California", "CA"
end
end