diff --git a/docs/src/assets/styles.css b/docs/src/assets/styles.css index 5421644..9f88666 100644 --- a/docs/src/assets/styles.css +++ b/docs/src/assets/styles.css @@ -2580,16 +2580,6 @@ color: var(--color-muted-foreground); } &:not([aria-disabled='true']) { - &:hover { - @media (hover: hover) { - background-color: var(--color-accent); - } - } - &:hover { - @media (hover: hover) { - color: var(--color-accent-foreground); - } - } &:focus-visible { background-color: var(--color-accent); } @@ -2617,6 +2607,20 @@ border-color: var(--color-border); } } + &:not([data-dropdown-menu-initialized]) [data-popover] { + [role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio'] { + &:hover { + @media (hover: hover) { + background-color: var(--color-accent); + } + } + &:hover { + @media (hover: hover) { + color: var(--color-accent-foreground); + } + } + } + } } } @layer components { @@ -3193,6 +3197,10 @@ background-position: center right 0.5rem; background-repeat: no-repeat; } + &.active, &:focus-visible { + background-color: var(--color-accent); + color: var(--color-accent-foreground); + } } [role='listbox'] [role='heading'] { display: flex; @@ -3275,9 +3283,17 @@ content: var(--tw-content); } } - [role='option']:hover, &[data-select-initialized] [role='option'].active, &[data-select-initialized] [role='option']:focus-visible { - background-color: var(--color-accent); - color: var(--color-accent-foreground); + &:not([data-select-idnitialized]) [data-popover] [role='option'] { + &:hover { + @media (hover: hover) { + background-color: var(--color-accent); + } + } + &:hover { + @media (hover: hover) { + color: var(--color-accent-foreground); + } + } } } } diff --git a/src/css/basecoat.css b/src/css/basecoat.css index 1e5a414..00131ff 100644 --- a/src/css/basecoat.css +++ b/src/css/basecoat.css @@ -516,7 +516,7 @@ @apply aria-hidden:hidden [&_svg]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none [&_svg]:shrink-0 [&_svg]:size-4 aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate; &:not([aria-disabled='true']) { - @apply focus-visible:bg-accent focus-visible:text-accent-foreground hover:bg-accent hover:text-accent-foreground; + @apply focus-visible:bg-accent focus-visible:text-accent-foreground; } &.active { @@ -530,6 +530,13 @@ @apply border-border -mx-1 my-1; } } + &:not([data-dropdown-menu-initialized]) [data-popover] { + [role='menuitem'], + [role='menuitemcheckbox'], + [role='menuitemradio'] { + @apply hover:bg-accent hover:text-accent-foreground; + } + } } } @@ -737,6 +744,10 @@ &[aria-selected='true'] { @apply bg-[image:var(--check-icon)] bg-no-repeat bg-position-[center_right_0.5rem] bg-size-[0.875rem]; } + &.active, + &:focus-visible { + @apply bg-accent text-accent-foreground; + } } [role='listbox'] [role='heading'] { @apply flex text-muted-foreground px-2 py-1.5 text-xs; @@ -767,11 +778,8 @@ @apply content-['No_results_found']; } } - - [role='option']:hover, - &[data-select-initialized] [role='option'].active, - &[data-select-initialized] [role='option']:focus-visible { - @apply bg-accent text-accent-foreground; + &:not([data-select-idnitialized]) [data-popover] [role='option'] { + @apply hover:bg-accent hover:text-accent-foreground; } } } diff --git a/src/js/dropdown-menu.js b/src/js/dropdown-menu.js index c692368..b1145e6 100644 --- a/src/js/dropdown-menu.js +++ b/src/js/dropdown-menu.js @@ -29,7 +29,7 @@ setActiveItem(-1); }; - const openPopover = () => { + const openPopover = (initialSelection = false) => { document.dispatchEvent(new CustomEvent('basecoat:popover', { detail: { source: dropdownMenuComponent } })); @@ -40,8 +40,13 @@ !item.hasAttribute('disabled') && item.getAttribute('aria-disabled') !== 'true' ); - if (menuItems.length > 0) { - setActiveItem(0); + + if (menuItems.length > 0 && initialSelection) { + if (initialSelection === 'first') { + setActiveItem(0); + } else if (initialSelection === 'last') { + setActiveItem(menuItems.length - 1); + } } }; @@ -64,7 +69,7 @@ if (isExpanded) { closePopover(); } else { - openPopover(); + openPopover(false); } }); @@ -77,9 +82,15 @@ } if (!isExpanded) { - if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) { + if (['Enter', ' '].includes(event.key)) { event.preventDefault(); - openPopover(); + openPopover(false); + } else if (event.key === 'ArrowDown') { + event.preventDefault(); + openPopover('first'); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + openPopover('last'); } return; } @@ -91,11 +102,11 @@ switch (event.key) { case 'ArrowDown': event.preventDefault(); - nextIndex = Math.min(activeIndex + 1, menuItems.length - 1); + nextIndex = activeIndex === -1 ? 0 : Math.min(activeIndex + 1, menuItems.length - 1); break; case 'ArrowUp': event.preventDefault(); - nextIndex = Math.max(activeIndex - 1, 0); + nextIndex = activeIndex === -1 ? menuItems.length - 1 : Math.max(activeIndex - 1, 0); break; case 'Home': event.preventDefault(); @@ -118,6 +129,20 @@ } }); + menu.addEventListener('mousemove', (event) => { + const menuItem = event.target.closest('[role^="menuitem"]'); + if (menuItem && menuItems.includes(menuItem)) { + const index = menuItems.indexOf(menuItem); + if (index !== activeIndex) { + setActiveItem(index); + } + } + }); + + menu.addEventListener('mouseleave', () => { + setActiveItem(-1); + }); + menu.addEventListener('click', (event) => { if (event.target.closest('[role^="menuitem"]')) { closePopover(); diff --git a/src/js/select.js b/src/js/select.js index dcc2427..f741d19 100644 --- a/src/js/select.js +++ b/src/js/select.js @@ -20,6 +20,26 @@ let visibleOptions = [...options]; let activeIndex = -1; + const setActiveOption = (index) => { + if (activeIndex > -1 && options[activeIndex]) { + options[activeIndex].classList.remove('active'); + } + + activeIndex = index; + + if (activeIndex > -1) { + const activeOption = options[activeIndex]; + activeOption.classList.add('active'); + if (activeOption.id) { + trigger.setAttribute('aria-activedescendant', activeOption.id); + } else { + trigger.removeAttribute('aria-activedescendant'); + } + } else { + trigger.removeAttribute('aria-activedescendant'); + } + }; + const hasTransition = () => { const style = getComputedStyle(popover); return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0; @@ -54,9 +74,7 @@ if (focusOnTrigger) trigger.focus(); popover.setAttribute('aria-hidden', 'true'); trigger.setAttribute('aria-expanded', 'false'); - trigger.removeAttribute('aria-activedescendant'); - if (activeIndex > -1) options[activeIndex]?.classList.remove('active'); - activeIndex = -1; + setActiveOption(-1); } const selectOption = (option) => { @@ -92,11 +110,7 @@ const filterOptions = () => { const searchTerm = filter.value.trim().toLowerCase(); - if (activeIndex > -1) { - options[activeIndex].classList.remove('active'); - trigger.removeAttribute('aria-activedescendant'); - activeIndex = -1; - } + setActiveOption(-1); visibleOptions = []; options.forEach(option => { @@ -173,21 +187,31 @@ } if (nextVisibleIndex !== currentVisibleIndex) { - if (currentVisibleIndex > -1) { - visibleOptions[currentVisibleIndex].classList.remove('active'); - } - const newActiveOption = visibleOptions[nextVisibleIndex]; - newActiveOption.classList.add('active'); - activeIndex = options.indexOf(newActiveOption); - - if (newActiveOption.id) { - trigger.setAttribute('aria-activedescendant', newActiveOption.id); - } + setActiveOption(options.indexOf(newActiveOption)); newActiveOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } }; + listbox.addEventListener('mousemove', (event) => { + const option = event.target.closest('[role="option"]'); + if (option && visibleOptions.includes(option)) { + const index = options.indexOf(option); + if (index !== activeIndex) { + setActiveOption(index); + } + } + }); + + listbox.addEventListener('mouseleave', () => { + const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]'); + if (selectedOption) { + setActiveOption(options.indexOf(selectedOption)); + } else { + setActiveOption(-1); + } + }); + trigger.addEventListener('keydown', handleKeyNavigation); if (filter) { filter.addEventListener('keydown', handleKeyNavigation); @@ -213,14 +237,7 @@ const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]'); if (selectedOption) { - if (activeIndex > -1) { - options[activeIndex]?.classList.remove('active'); - } - activeIndex = options.indexOf(selectedOption); - selectedOption.classList.add('active'); - if (selectedOption.id) { - trigger.setAttribute('aria-activedescendant', selectedOption.id); - } + setActiveOption(options.indexOf(selectedOption)); selectedOption.scrollIntoView({ block: 'nearest' }); } };