Copilot commented on code in PR #38791:
URL: https://github.com/apache/superset/pull/38791#discussion_r2989544416


##########
superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx:
##########
@@ -49,148 +44,97 @@ const DetailsPanelPopover = ({
   onHighlightFilterSource,
   children,
   popoverVisible,
-  popoverContentRef,
-  popoverTriggerRef,
   setPopoverVisible,
 }: DetailsPanelProps) => {

Review Comment:
   `popoverContentRef`/`popoverTriggerRef` are still part of 
`DetailsPanelProps` but are no longer used by this component (they aren’t 
destructured or attached to any element). The parent `FiltersBadge` still 
attempts to focus `popoverContentRef.current` when opening, so `current` will 
remain `null` now and keyboard focus/navigation will regress. Either attach 
`popoverContentRef` to a real focusable element in the dropdown (e.g., a 
wrapper around `Menu` with `tabIndex={-1}` / Menu autoFocus) or remove these 
refs from the API and also remove the parent focus effect.



##########
superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx:
##########
@@ -49,148 +44,97 @@ const DetailsPanelPopover = ({
   onHighlightFilterSource,
   children,
   popoverVisible,
-  popoverContentRef,
-  popoverTriggerRef,
   setPopoverVisible,
 }: DetailsPanelProps) => {
+  const theme = useTheme();
   const activeTabs = useSelector<RootState>(
     state => state.dashboardState?.activeTabs,
   );
-  // Combined ref array for all filter indicator elements
-  const indicatorRefs = useRef<(HTMLButtonElement | null)[]>([]);
-
-  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
-    switch (event.key) {
-      case 'Escape':
-      case 'Enter':
-        // timing out to allow for filter selection to happen first
-        setTimeout(() => {
-          // move back to the popover trigger element
-          popoverTriggerRef?.current?.focus();
-          // Close popover on ESC or ENTER
-          setPopoverVisible(false);
-        });
-        break;
-      case 'ArrowDown':
-      case 'ArrowUp': {
-        event.preventDefault(); // Prevent scrolling
-        // Navigate through filters with arrows up/down
-        const currentFocusIndex = indicatorRefs.current.findIndex(
-          ref => ref === document.activeElement,
-        );
-        const maxIndex = indicatorRefs.current.length - 1;
-        let nextFocusIndex = 0;
-
-        if (event.key === 'ArrowDown') {
-          nextFocusIndex =
-            currentFocusIndex >= maxIndex ? 0 : currentFocusIndex + 1;
-        } else if (event.key === 'ArrowUp') {
-          nextFocusIndex =
-            currentFocusIndex <= 0 ? maxIndex : currentFocusIndex - 1;
-        }
-        indicatorRefs.current[nextFocusIndex]?.focus();
-        break;
-      }
-      case 'Tab':
-        // forcing popover context until ESC or ENTER are pressed
-        event.preventDefault();
-        break;
-      default:
-        break;
-    }
-  };
-
-  const handleVisibility = (isOpen: boolean) => {
-    setPopoverVisible(isOpen);
-  };
-
-  // we don't need to clean up useEffect, setting { once: true } removes the 
event listener after handle function is called
-  useEffect(() => {
-    if (popoverVisible) {
-      window.addEventListener('resize', () => setPopoverVisible(false), {
-        once: true,
-      });
-    }
-  }, [popoverVisible]);
 
-  // if tabs change, popover doesn't close automatically
   useEffect(() => {
     setPopoverVisible(false);
-  }, [activeTabs]);
+  }, [activeTabs, setPopoverVisible]);
 
   const indicatorKey = (indicator: Indicator): string =>
-    `${indicator.column} - ${indicator.name}`;
-  const theme = useTheme();
-  const content = (
-    <FiltersDetailsContainer
-      ref={popoverContentRef}
-      tabIndex={-1}
-      onMouseLeave={() => setPopoverVisible(false)}
-      onKeyDown={handleKeyDown}
-      role="menu"
-    >
-      <div>
-        {appliedCrossFilterIndicators.length ? (
-          <div>
-            <SectionName>
-              {t(
-                'Applied cross-filters (%d)',
-                appliedCrossFilterIndicators.length,
-              )}
-            </SectionName>
-            <FiltersContainer>
-              {appliedCrossFilterIndicators.map(indicator => (
-                <FilterIndicator
-                  ref={el => indicatorRefs.current.push(el)}
-                  key={indicatorKey(indicator)}
-                  indicator={indicator}
-                  onClick={onHighlightFilterSource}
-                />
-              ))}
-            </FiltersContainer>
-          </div>
-        ) : null}
-        {appliedCrossFilterIndicators.length && appliedIndicators.length ? (
-          <Separator />
-        ) : null}
-        {appliedIndicators.length ? (
-          <div>
-            <SectionName>
-              {t('Applied filters (%d)', appliedIndicators.length)}
-            </SectionName>
-            <FiltersContainer>
-              <List
-                dataSource={appliedIndicators}
-                renderItem={indicator => (
-                  <List.Item>
-                    <FilterIndicator
-                      ref={el => indicatorRefs.current.push(el)}
-                      key={indicatorKey(indicator)}
-                      indicator={indicator}
-                      onClick={onHighlightFilterSource}
-                    />
-                  </List.Item>
-                )}
-              />
-            </FiltersContainer>
-          </div>
-        ) : null}
-      </div>
-    </FiltersDetailsContainer>
-  );
+    `${indicator.column} - ${indicator.name} - ${indicator.path?.join('>') ?? 
''}`;
+
+  const menuItems: MenuProps['items'] = [];
+
+  if (appliedCrossFilterIndicators.length > 0) {
+    menuItems.push({
+      key: 'grp-cross',
+      type: 'group',
+      label: t(
+        'Applied cross-filters (%d)',
+        appliedCrossFilterIndicators.length,
+      ),
+      children: appliedCrossFilterIndicators.map(indicator => ({
+        key: `cross-${indicatorKey(indicator)}`,
+        label: (
+          <FilterIndicator
+            indicator={indicator}
+            onClick={onHighlightFilterSource}
+          />
+        ),
+      })),
+    });
+  }
+
+  if (appliedIndicators.length > 0) {
+    if (appliedCrossFilterIndicators.length > 0) {
+      menuItems.push({ type: 'divider' });
+    }
+    menuItems.push({
+      key: 'grp-applied',
+      type: 'group',
+      label: t('Applied filters (%d)', appliedIndicators.length),
+      children: appliedIndicators.map(indicator => ({
+        key: `applied-${indicatorKey(indicator)}`,
+        label: (
+          <FilterIndicator
+            indicator={indicator}
+            onClick={onHighlightFilterSource}
+          />
+        ),
+      })),
+    });
+  }
 
   return (
-    <Popover
-      color={`${theme.colorBgElevated}cc`}
-      content={content}
+    <NoAnimationDropdown
+      popupRender={() => (
+        <Menu
+          selectable={false}
+          items={menuItems}
+          onClick={() => setPopoverVisible(false)}
+        />
+      )}
+      overlayStyle={{ zIndex: 1001, animationDuration: '0s' }}
+      trigger={['click', 'hover']}
       open={popoverVisible}
-      onOpenChange={handleVisibility}
+      onOpenChange={visible => setPopoverVisible(visible)}
       placement="bottomRight"
-      trigger={['hover']}
-      data-test="filter-status-popover"
     >
-      {children}
-    </Popover>
+      <span
+        role="button"
+        aria-label={t('View applied filters')}
+        aria-haspopup="menu"
+        aria-expanded={popoverVisible}
+        css={css`
+          display: inline-flex;
+          outline: none;
+          cursor: pointer;
+          border-radius: 4px;
+
+          &:focus-visible {
+            outline: 2px solid ${theme.colorPrimary};
+            outline-offset: 1px;
+          }
+        `}
+      >
+        {children}
+      </span>

Review Comment:
   The dropdown trigger is a non-focusable `<span role="button">` wrapping a 
focusable child (`StyledFilterCount` has `tabIndex={0}` + `role="button"`). 
This creates nested interactive roles (invalid a11y) and also means 
`NoAnimationDropdown`’s `autoFocus` can’t reliably focus the trigger element. 
Consider using the actual focusable child as the dropdown trigger (remove the 
wrapper span), or make the wrapper the only interactive element (add 
`tabIndex={0}`, remove role/tabIndex from the child, and forward the 
ref/handlers).



##########
superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/DetailsPanel.test.tsx:
##########
@@ -243,28 +245,15 @@ test('Arrow key navigation switches focus between 
indicators', () => {
   );
 
   // Query the indicators
-  const firstIndicator = screen.getByRole('button', {
-    name: 'search Clinical Stage',
-  });
-  const secondIndicator = screen.getByRole('button', {
-    name: 'search Age Group',
-  });
+  const firstMenuItem = screen.queryByText('Clinical Stage')?.closest('li')!;
+  const secondMenuItem = screen.queryByText('Age Group')?.closest('li')!;
 
-  // Focus the first indicator
-  firstIndicator.focus();
-  expect(firstIndicator).toHaveFocus();
+  expect(firstMenuItem).toBeInTheDocument();
+  expect(secondMenuItem).toBeInTheDocument();
 
-  // Simulate ArrowDown key press
-  fireEvent.keyDown(document.activeElement as Element, {
-    key: 'ArrowDown',
-    code: 'ArrowDown',
-  });
-  expect(secondIndicator).toHaveFocus();
+  userEvent.type(firstMenuItem, '{arrowdown}');
+  expect(firstMenuItem).toHaveFocus();
 
-  // Simulate ArrowUp key press
-  fireEvent.keyDown(document.activeElement as Element, {
-    key: 'ArrowUp',
-    code: 'ArrowUp',
-  });
-  expect(firstIndicator).toHaveFocus();
+  userEvent.type(secondMenuItem, '{arrowdown}');
+  expect(secondMenuItem).toHaveFocus();

Review Comment:
   The updated arrow-key navigation test doesn’t actually verify focus 
movement: it never explicitly focuses the first item before typing, and after 
`{arrowdown}` it asserts the *same* item still has focus. Also 
`userEvent.type(...)` is async in most setups, so these calls should be awaited 
to avoid race conditions. Update the test to (1) focus the first menu item, (2) 
assert focus moves to the next/previous item on ArrowDown/ArrowUp, and (3) 
await userEvent interactions.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to