Просмотр исходного кода

Fix dropdown active item outline clipped by adjacent items (#32073)

* Initial plan

* Fix selected item outline overlapping next item in dropdown menus

Replace position:relative + absolute positioning with flexbox layout
on .dropdown-item to prevent stacking context overlap of active outline.

Co-authored-by: aeaton-overleaf <75253002+aeaton-overleaf@users.noreply.github.com>

* Fix invalid HTML - replace `div` with `span` inside dropdown item description container

* Do not suppress keyboard focus outlines in toolbar dropdown menus

* Add explicit keyboard focus ring for dropdown items

* Avoid overlapping link focus rings with inset box-shadow

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aeaton-overleaf <75253002+aeaton-overleaf@users.noreply.github.com>
Co-authored-by: Rebeka <o.dekany@gmail.com>
GitOrigin-RevId: ce4d1b01f04476fd154b6c05a52fc5632bf8b8dc
Copilot 4 месяцев назад
Родитель
Сommit
fb9fed2b74

+ 7 - 7
services/web/frontend/js/features/contact-form/search.ts

@@ -64,19 +64,14 @@ export function setupSearch(formEl: Element) {
         linkEl.setAttribute('role', 'menuitem')
         liEl.append(linkEl)
 
-        const contentWrapperEl = document.createElement('div')
+        const contentWrapperEl = document.createElement('span')
         contentWrapperEl.className = 'dropdown-item-description-container'
         linkEl.append(contentWrapperEl)
 
-        const pageNameEl = document.createElement('div')
+        const pageNameEl = document.createElement('span')
         pageNameEl.innerHTML = DOMPurify.sanitize(rawPageName)
         contentWrapperEl.append(pageNameEl)
 
-        const iconEl = materialIcon('open_in_new')
-        iconEl.classList.add('dropdown-item-trailing-icon')
-        iconEl.setAttribute('aria-hidden', 'true')
-        contentWrapperEl.append(iconEl)
-
         if (sectionName) {
           const sectionEl = document.createElement('span')
           sectionEl.className = 'dropdown-item-description'
@@ -84,6 +79,11 @@ export function setupSearch(formEl: Element) {
           contentWrapperEl.append(sectionEl)
         }
 
+        const iconEl = materialIcon('open_in_new')
+        iconEl.classList.add('dropdown-item-trailing-icon')
+        iconEl.setAttribute('aria-hidden', 'true')
+        linkEl.append(iconEl)
+
         resultsContainerEl.append(liEl)
       }
       if (nbHits > 0) {

+ 8 - 10
services/web/frontend/js/shared/components/dropdown/dropdown-menu.tsx

@@ -83,17 +83,15 @@ function DropdownItem(
       ref={ref}
     >
       {leadingIconComponent}
-      <div
-        className={classnames({
-          'dropdown-item-description-container': description,
-        })}
-      >
-        {children}
-        {trailingIconComponent}
-        {description && (
+      {description ? (
+        <span className="dropdown-item-description-container">
+          {children}
           <span className="dropdown-item-description">{description}</span>
-        )}
-      </div>
+        </span>
+      ) : (
+        children
+      )}
+      {trailingIconComponent}
     </BS5DropdownItem>
   )
 }

+ 2 - 1
services/web/frontend/stylesheets/abstracts/mixins.scss

@@ -117,7 +117,8 @@
 }
 
 @mixin box-shadow-button-input {
-  box-shadow: 0 0 0 2px var(--blue-30);
+  box-shadow: inset 0 0 0 2px var(--blue-30);
+  outline: 2px solid transparent;
 }
 
 @mixin animation($animation) {

+ 12 - 11
services/web/frontend/stylesheets/components/dropdown-menu.scss

@@ -110,11 +110,9 @@ $dropdown-item-min-height: 36px;
 
   --bs-dropdown-item-border-radius: var(--border-radius-base);
 
-  display: grid;
-  grid-auto-flow: column;
-  place-content: center start;
+  display: flex;
+  align-items: center;
   min-height: $dropdown-item-min-height; // a minimum height of 36px to be accessible for touch screens
-  position: relative;
   background-color: var(--dropdown-background);
 
   &:active {
@@ -131,6 +129,10 @@ $dropdown-item-min-height: 36px;
     background: none;
   }
 
+  &:focus-visible {
+    @include box-shadow-button-input;
+  }
+
   &:hover:not(.active),
   &:focus-visible:not(.active),
   &.nested-dropdown-toggle-shown {
@@ -179,17 +181,16 @@ $dropdown-item-min-height: 36px;
 }
 
 .dropdown-item-description-container {
-  grid-auto-flow: row;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  flex: 1;
   min-height: 44px;
-  display: grid;
-  place-content: center start;
 }
 
 .dropdown-item-trailing-icon {
-  position: absolute;
-  right: 10px;
-  top: 50%;
-  transform: translateY(-50%);
+  margin-left: auto;
+  padding-left: var(--spacing-04);
 }
 
 .dropdown-item-leading-icon {

+ 0 - 5
services/web/frontend/stylesheets/pages/editor/toolbar.scss

@@ -123,11 +123,6 @@
     }
   }
 
-  > a:focus,
-  button:focus {
-    outline: none;
-  }
-
   > a:not(.btn),
   > button,
   .toolbar-left > a:not(.btn),