diff --git a/packages/ui/src/combobox.js b/packages/ui/src/combobox.js index b0ac15b72..50739fcc8 100644 --- a/packages/ui/src/combobox.js +++ b/packages/ui/src/combobox.js @@ -246,7 +246,15 @@ function handleRoot(el, Alpine) { if (typeof by === 'string') { let property = by - by = (a, b) => a[property] === b[property] + by = (a, b) => { + // Handle null values + if ((! a || typeof a !== 'object') || (! b || typeof b !== 'object')) { + return Alpine.raw(a) === Alpine.raw(b) + } + + + return a[property] === b[property]; + } } return by(a, b) @@ -432,9 +440,9 @@ function handleOption(el, Alpine) { // Only the active element should have aria-selected="true"... 'x-effect'() { - this.$comboboxOption.isActive + this.$comboboxOption.isSelected ? el.setAttribute('aria-selected', true) - : el.removeAttribute('aria-selected') + : el.setAttribute('aria-selected', false) }, ':aria-disabled'() { return this.$comboboxOption.isDisabled }, diff --git a/packages/ui/src/list-context.js b/packages/ui/src/list-context.js index da95148bc..bd93a2748 100644 --- a/packages/ui/src/list-context.js +++ b/packages/ui/src/list-context.js @@ -292,6 +292,8 @@ export function generateContext(Alpine, multiple, orientation, activateSelectedO break; case 'Home': case 'PageUp': + if (e.key == 'Home' && e.shiftKey) return; + e.preventDefault(); e.stopPropagation() setIsTyping(false) this.reorderKeys(); hasActive = this.hasActive() @@ -300,6 +302,8 @@ export function generateContext(Alpine, multiple, orientation, activateSelectedO case 'End': case 'PageDown': + if (e.key == 'End' && e.shiftKey) return; + e.preventDefault(); e.stopPropagation() setIsTyping(false) this.reorderKeys(); hasActive = this.hasActive() diff --git a/tests/cypress/integration/plugins/ui/combobox.spec.js b/tests/cypress/integration/plugins/ui/combobox.spec.js index a529fc4f0..b3225dfba 100644 --- a/tests/cypress/integration/plugins/ui/combobox.spec.js +++ b/tests/cypress/integration/plugins/ui/combobox.spec.js @@ -708,6 +708,155 @@ test('"multiple" and "name" props together', }, ); +test('"by" prop with string value', + [html` +
+ + + + + + +
+ `], + ({ get }) => { + get('ul').should(notBeVisible()) + get('button').click() + get('ul').should(beVisible()) + get('button').click() + get('ul').should(notBeVisible()) + get('button').click() + get('[option="2"]').click() + get('ul').should(notBeVisible()) + get('input').should(haveValue('2')) + get('button').should(haveText('2')) + get('button').click() + get('ul').should(contain('Wade Cooper')) + .should(contain('Arlene Mccoy')) + .should(contain('Devon Webb')) + get('[option="3"]').click() + get('ul').should(notBeVisible()) + get('input').should(haveValue('3')) + get('button').should(haveText('3')) + get('button').click() + get('ul').should(contain('Wade Cooper')) + .should(contain('Arlene Mccoy')) + .should(contain('Devon Webb')) + get('[option="1"]').click() + get('ul').should(notBeVisible()) + get('input').should(haveValue('1')) + get('button').should(haveText('1')) + }, +); + +test('"by" prop with string value and "nullable"', + [html` +
+ + + + + + +
+ `], + ({ get }) => { + get('ul').should(notBeVisible()) + get('button').click() + get('ul').should(beVisible()) + get('button').click() + get('ul').should(notBeVisible()) + get('button').click() + get('[option="2"]').click() + get('ul').should(notBeVisible()) + get('input').should(haveValue('Arlene Mccoy')) + get('button').should(haveText('Arlene Mccoy')) + get('button').click() + get('ul').should(contain('Wade Cooper')) + .should(contain('Arlene Mccoy')) + .should(contain('Devon Webb')) + get('[option="3"]').click() + get('ul').should(notBeVisible()) + get('input').should(haveValue('Devon Webb')) + get('button').should(haveText('Devon Webb')) + get('button').click() + get('ul').should(contain('Wade Cooper')) + .should(contain('Arlene Mccoy')) + .should(contain('Devon Webb')) + get('[option="1"]').click() + get('ul').should(notBeVisible()) + get('input').should(haveValue('Wade Cooper')) + get('button').should(haveText('Wade Cooper')) + }, +); + + test('keyboard controls', [html`
@@ -1287,19 +1441,120 @@ test('active element logic when opening a combobox', get('button').click() // First option is selected on opening if no preselection get('ul').should(beVisible()) - get('[option="1"]').should(haveAttribute('aria-selected', 'true')) + get('[option="1"]').should(haveAttribute('aria-selected', 'false')) + get('[option="1"]').should(haveClasses(['active'])) // First match is selected while typing - get('[option="4"]').should(notHaveAttribute('aria-selected')) + get('[option="4"]').should(haveAttribute('aria-selected', 'false')) + get('[option="4"]').should(notHaveClasses(['active'])) get('input').type('T') get('input').trigger('change') - get('[option="4"]').should(haveAttribute('aria-selected', 'true')) + get('[option="4"]').should(haveAttribute('aria-selected', 'false')) + get('[option="4"]').should(haveClasses(['active'])) // Reset state and select option 3 get('button').click() get('button').click() get('[option="3"]').click() // Previous selection is selected get('button').click() - get('[option="4"]').should(notHaveAttribute('aria-selected')) + get('[option="4"]').should(haveAttribute('aria-selected', 'false')) get('[option="3"]').should(haveAttribute('aria-selected', 'true')) } ) + +test('can remove an option without other options getting removed', + [html`
+
+
+ +
+ +
+
+ + +
+ +
+
    + +
+ +

No frameworks match your query.

+
+
+
+
+ `], + ({ get }) => { + get('input').type('a').trigger('input') + cy.wait(100) + get('[option="1"]').click() + get('[option="2"]').click() + get('[option="3"]').click() + get('[remove-option="3"]').click() + get('[option="1"]').should(haveAttribute('aria-selected', 'true')) + get('[option="2"]').should(haveAttribute('aria-selected', 'true')) + get('[option="3"]').should(haveAttribute('aria-selected', 'false')) + get('input').type('a').trigger('input') + get('[check="1"]').should(beVisible()) + get('[check="2"]').should(beVisible()) + get('[check="3"]').should(notBeVisible()) + }, +);