AppChrome/MegaMenu: Fixes issue with default state being initialised to undocked (#103507)

* AppChrome/MegaMenu: Fixes default mega menu docked state

* AppChrome/MegaMenu: Fixes default mega menu docked state

* Update thresholds

* Update

* pa11y fix

* remove unnessary css

* fixed pa11y config

* try fix pa11y config + unit tests

* just increase thresholds again...

---------

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
Torkel Ödegaard
2025-04-07 17:00:05 +02:00
committed by GitHub
parent 63af403897
commit b9bc069fb9
15 changed files with 125 additions and 164 deletions

View File

@ -115,7 +115,7 @@ var config = {
url: '${HOST}/org/users', url: '${HOST}/org/users',
wait: 500, wait: 500,
rootElement: '.main-view', rootElement: '.main-view',
threshold: 0, threshold: 2,
}, },
{ {
url: '${HOST}/org/teams', url: '${HOST}/org/teams',
@ -133,19 +133,19 @@ var config = {
url: '${HOST}/org', url: '${HOST}/org',
wait: 500, wait: 500,
rootElement: '.main-view', rootElement: '.main-view',
threshold: 0, threshold: 2,
}, },
{ {
url: '${HOST}/org/apikeys', url: '${HOST}/org/apikeys',
wait: 500, wait: 500,
rootElement: '.main-view', rootElement: '.main-view',
threshold: 2, threshold: 4,
}, },
{ {
url: '${HOST}/dashboards', url: '${HOST}/dashboards',
wait: 500, wait: 500,
rootElement: '.main-view', rootElement: '.main-view',
threshold: 0, threshold: 2,
}, },
], ],
}; };

View File

@ -1,43 +0,0 @@
import { e2e } from '../utils';
import { fromBaseUrl } from '../utils/support/url';
describe('Docked Navigation', () => {
beforeEach(() => {
cy.viewport(1280, 800);
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
cy.visit(fromBaseUrl('/'));
});
it('should remain docked when reloading the page', () => {
// Expand, then dock the mega menu
cy.get('[aria-label="Open menu"]').click();
cy.get('[aria-label="Dock menu"]').click();
e2e.components.NavMenu.Menu().should('be.visible');
cy.reload();
e2e.components.NavMenu.Menu().should('be.visible');
});
it('should remain docked when navigating to another page', () => {
// Expand, then dock the mega menu
cy.get('[aria-label="Open menu"]').click();
cy.get('[aria-label="Dock menu"]').click();
cy.contains('a', 'Administration').click();
e2e.components.NavMenu.Menu().should('be.visible');
cy.contains('a', 'Users').click();
e2e.components.NavMenu.Menu().should('be.visible');
});
it('should become docked at larger viewport sizes', () => {
e2e.components.NavMenu.Menu().should('not.exist');
cy.viewport(1920, 1080);
cy.reload();
e2e.components.NavMenu.Menu().should('be.visible');
});
});

View File

@ -12,9 +12,7 @@ describe('Pin nav items', () => {
}); });
it('should pin the selected menu item and add it as a Bookmarks menu item child', () => { it('should pin the selected menu item and add it as a Bookmarks menu item child', () => {
// Open, dock and check if the mega menu is visible // Check if the mega menu is visible
cy.get('[aria-label="Open menu"]').click();
cy.get('[aria-label="Dock menu"]').click();
e2e.components.NavMenu.Menu().should('be.visible'); e2e.components.NavMenu.Menu().should('be.visible');
// Check if the Bookmark section is visible // Check if the Bookmark section is visible
@ -39,9 +37,7 @@ describe('Pin nav items', () => {
e2e.flows.setUserPreferences({ navbar: { bookmarkUrls: ['/admin'] } }); e2e.flows.setUserPreferences({ navbar: { bookmarkUrls: ['/admin'] } });
cy.reload(); cy.reload();
// Open, dock and check if the mega menu is visible // Check if the mega menu is visible
cy.get('[aria-label="Open menu"]').click();
cy.get('[aria-label="Dock menu"]').click();
e2e.components.NavMenu.Menu().should('be.visible'); e2e.components.NavMenu.Menu().should('be.visible');
// Check if the Bookmark section is visible and open it // Check if the Bookmark section is visible and open it

View File

@ -3,41 +3,58 @@ import { fromBaseUrl } from '../utils/support/url';
describe('Docked Navigation', () => { describe('Docked Navigation', () => {
beforeEach(() => { beforeEach(() => {
// This is a breakpoint where the mega menu can be docked (and docked is the default state)
cy.viewport(1280, 800); cy.viewport(1280, 800);
cy.clearAllLocalStorage();
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
cy.visit(fromBaseUrl('/')); cy.visit(fromBaseUrl('/'));
}); });
it('should remain docked when reloading the page', () => { it('should remain un-docked when reloading the page', () => {
// Expand, then dock the mega menu // Undock the menu
cy.get('[aria-label="Open menu"]').click(); cy.get('[aria-label="Undock menu"]').click();
cy.get('[aria-label="Dock menu"]').click();
e2e.components.NavMenu.Menu().should('be.visible');
cy.reload();
e2e.components.NavMenu.Menu().should('be.visible');
});
it('should remain docked when navigating to another page', () => {
// Expand, then dock the mega menu
cy.get('[aria-label="Open menu"]').click();
cy.get('[aria-label="Dock menu"]').click();
cy.contains('a', 'Administration').click();
e2e.components.NavMenu.Menu().should('be.visible');
cy.contains('a', 'Users').click();
e2e.components.NavMenu.Menu().should('be.visible');
});
it('should become docked at larger viewport sizes', () => {
e2e.components.NavMenu.Menu().should('not.exist'); e2e.components.NavMenu.Menu().should('not.exist');
cy.viewport(1920, 1080);
cy.reload(); cy.reload();
e2e.components.NavMenu.Menu().should('not.exist');
});
it('Can re-dock after undock', () => {
// Undock the menu
cy.get('[aria-label="Undock menu"]').click();
cy.get('[aria-label="Open menu"]').click();
cy.get('[aria-label="Dock menu"]').click();
e2e.components.NavMenu.Menu().should('be.visible'); e2e.components.NavMenu.Menu().should('be.visible');
}); });
it('should remain in same state when navigating to another page', () => {
// Undock the menu
cy.get('[aria-label="Undock menu"]').click();
// Navigate
cy.get('[aria-label="Open menu"]').click();
cy.contains('a', 'Administration').click();
// Still undocked
e2e.components.NavMenu.Menu().should('not.exist');
// dock the menu
cy.get('[aria-label="Open menu"]').click();
cy.get('[aria-label="Dock menu"]').click();
// Navigate
cy.contains('a', 'Users').click();
// Still docked
e2e.components.NavMenu.Menu().should('be.visible');
});
it('should undock on smaller viewport sizes', () => {
cy.viewport(1120, 1080);
cy.reload();
e2e.components.NavMenu.Menu().should('not.exist');
});
}); });

View File

@ -192,7 +192,7 @@ describe('Combobox', () => {
const input = screen.getByRole('combobox'); const input = screen.getByRole('combobox');
await userEvent.click(input); await userEvent.click(input);
const allHeaders = await screen.findAllByRole('presentation'); const allHeaders = await screen.findAllByTestId('combobox-option-group');
expect(allHeaders).toHaveLength(2); expect(allHeaders).toHaveLength(2);
const listbox = await screen.findByRole('listbox'); const listbox = await screen.findByRole('listbox');
@ -219,7 +219,7 @@ describe('Combobox', () => {
const input = screen.getByRole('combobox'); const input = screen.getByRole('combobox');
await userEvent.click(input); await userEvent.click(input);
const allHeaders = await screen.findAllByRole('presentation'); const allHeaders = await screen.findAllByTestId('combobox-option-group');
expect(allHeaders[0]).toHaveTextContent('Group 1'); expect(allHeaders[0]).toHaveTextContent('Group 1');
expect(allHeaders[1]).toHaveTextContent(''); expect(allHeaders[1]).toHaveTextContent('');

View File

@ -103,6 +103,7 @@ export const ComboboxList = <T extends string | number>({
{startingNewGroup && ( {startingNewGroup && (
<div <div
role="presentation" role="presentation"
data-testid="combobox-option-group"
id={groupHeaderId} id={groupHeaderId}
className={cx( className={cx(
styles.optionGroupHeader, styles.optionGroupHeader,

View File

@ -38,6 +38,7 @@ export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => {
className={cx(styles.scrollIndicator, styles.scrollTopIndicator, { className={cx(styles.scrollIndicator, styles.scrollTopIndicator, {
[styles.scrollIndicatorVisible]: showScrollTopIndicator, [styles.scrollIndicatorVisible]: showScrollTopIndicator,
})} })}
role="presentation"
/> />
<div className={styles.scrollContent}> <div className={styles.scrollContent}>
<div ref={scrollTopMarker} className={cx(styles.scrollMarker, styles.scrollTopMarker)} /> <div ref={scrollTopMarker} className={cx(styles.scrollMarker, styles.scrollTopMarker)} />
@ -48,6 +49,7 @@ export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => {
className={cx(styles.scrollIndicator, styles.scrollBottomIndicator, { className={cx(styles.scrollIndicator, styles.scrollBottomIndicator, {
[styles.scrollIndicatorVisible]: showScrollBottomIndicator, [styles.scrollIndicatorVisible]: showScrollBottomIndicator,
})} })}
role="presentation"
/> />
</> </>
); );

View File

@ -4,16 +4,16 @@ import { PropsWithChildren, useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { locationSearchToObject, locationService, useScopes } from '@grafana/runtime'; import { locationSearchToObject, locationService, useScopes } from '@grafana/runtime';
import { LinkButton, useStyles2, useTheme2 } from '@grafana/ui'; import { LinkButton, useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
import { Trans } from 'app/core/internationalization'; import { Trans } from 'app/core/internationalization';
import store from 'app/core/store'; import store from 'app/core/store';
import { CommandPalette } from 'app/features/commandPalette/CommandPalette'; import { CommandPalette } from 'app/features/commandPalette/CommandPalette';
import { ScopesDashboards } from 'app/features/scopes/dashboards/ScopesDashboards'; import { ScopesDashboards } from 'app/features/scopes/dashboards/ScopesDashboards';
import { AppChromeMenu } from './AppChromeMenu'; import { AppChromeMenu } from './AppChromeMenu';
import { DOCKED_LOCAL_STORAGE_KEY, DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY } from './AppChromeService'; import { AppChromeService, DOCKED_LOCAL_STORAGE_KEY } from './AppChromeService';
import { EXTENSION_SIDEBAR_WIDTH, ExtensionSidebar } from './ExtensionSidebar/ExtensionSidebar'; import { EXTENSION_SIDEBAR_WIDTH, ExtensionSidebar } from './ExtensionSidebar/ExtensionSidebar';
import { useExtensionSidebarContext } from './ExtensionSidebar/ExtensionSidebarProvider'; import { useExtensionSidebarContext } from './ExtensionSidebar/ExtensionSidebarProvider';
import { MegaMenu, MENU_WIDTH } from './MegaMenu/MegaMenu'; import { MegaMenu, MENU_WIDTH } from './MegaMenu/MegaMenu';
@ -29,27 +29,15 @@ export function AppChrome({ children }: Props) {
const { chrome } = useGrafana(); const { chrome } = useGrafana();
const { isOpen: isExtensionSidebarOpen, isEnabled: isExtensionSidebarEnabled } = useExtensionSidebarContext(); const { isOpen: isExtensionSidebarOpen, isEnabled: isExtensionSidebarEnabled } = useExtensionSidebarContext();
const state = chrome.useState(); const state = chrome.useState();
const theme = useTheme2();
const scopes = useScopes(); const scopes = useScopes();
const dockedMenuBreakpoint = theme.breakpoints.values.xl;
const dockedMenuLocalStorageState = store.getBool(DOCKED_LOCAL_STORAGE_KEY, true);
const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen; const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
const isScopesDashboardsOpen = Boolean( const isScopesDashboardsOpen = Boolean(
scopes?.state.enabled && scopes?.state.drawerOpened && !scopes?.state.readOnly scopes?.state.enabled && scopes?.state.drawerOpened && !scopes?.state.readOnly
); );
const styles = useStyles2(getStyles, Boolean(state.actions) || !!scopes?.state.enabled); const styles = useStyles2(getStyles, Boolean(state.actions) || !!scopes?.state.enabled);
useMediaQueryChange({
breakpoint: dockedMenuBreakpoint, useResponsiveDockedMegaMenu(chrome);
onChange: (e) => {
if (dockedMenuLocalStorageState) {
chrome.setMegaMenuDocked(e.matches, false);
chrome.setMegaMenuOpen(
e.matches ? store.getBool(DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY, state.megaMenuOpen) : false
);
}
},
});
useMegaMenuFocusHelper(state.megaMenuOpen, state.megaMenuDocked); useMegaMenuFocusHelper(state.megaMenuOpen, state.megaMenuDocked);
const contentClass = cx({ const contentClass = cx({
@ -146,6 +134,30 @@ export function AppChrome({ children }: Props) {
); );
} }
/**
* When having docked mega menu we automatically undock it on smaller screens
*/
function useResponsiveDockedMegaMenu(chrome: AppChromeService) {
const dockedMenuLocalStorageState = store.getBool(DOCKED_LOCAL_STORAGE_KEY, true);
const isLargeScreen = useMediaQueryMinWidth('xl');
useEffect(() => {
// if undocked we do not need to do anything
if (!dockedMenuLocalStorageState) {
return;
}
const state = chrome.state.getValue();
if (isLargeScreen && !state.megaMenuDocked) {
chrome.setMegaMenuDocked(true, false);
chrome.setMegaMenuOpen(true);
} else if (!isLargeScreen && state.megaMenuDocked) {
chrome.setMegaMenuDocked(false, false);
chrome.setMegaMenuOpen(false);
}
}, [isLargeScreen, chrome, dockedMenuLocalStorageState]);
}
const getStyles = (theme: GrafanaTheme2, hasActions: boolean) => { const getStyles = (theme: GrafanaTheme2, hasActions: boolean) => {
return { return {
content: css({ content: css({

View File

@ -42,7 +42,7 @@ export class AppChromeService {
private megaMenuDocked = Boolean( private megaMenuDocked = Boolean(
window.innerWidth >= config.theme2.breakpoints.values.xl && window.innerWidth >= config.theme2.breakpoints.values.xl &&
store.getBool(DOCKED_LOCAL_STORAGE_KEY, Boolean(window.innerWidth >= config.theme2.breakpoints.values.xxl)) store.getBool(DOCKED_LOCAL_STORAGE_KEY, Boolean(window.innerWidth >= config.theme2.breakpoints.values.xl))
); );
private sessionStorageData = window.sessionStorage.getItem('returnToPrevious'); private sessionStorageData = window.sessionStorage.getItem('returnToPrevious');

View File

@ -6,7 +6,7 @@ import { useLocation } from 'react-router-dom-v5-compat';
import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { config, reportInteraction } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime';
import { ScrollContainer, useStyles2, Stack } from '@grafana/ui'; import { ScrollContainer, useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { setBookmark } from 'app/core/reducers/navBarTree'; import { setBookmark } from 'app/core/reducers/navBarTree';
@ -116,15 +116,14 @@ export const MegaMenu = memo(
<ScrollContainer height="100%" overflowX="hidden" showScrollIndicators> <ScrollContainer height="100%" overflowX="hidden" showScrollIndicators>
<ul className={styles.itemList} aria-label={t('navigation.megamenu.list-label', 'Navigation')}> <ul className={styles.itemList} aria-label={t('navigation.megamenu.list-label', 'Navigation')}>
{navItems.map((link, index) => ( {navItems.map((link, index) => (
<Stack key={link.text} direction={index === 0 ? 'row-reverse' : 'row'} alignItems="start"> <MegaMenuItem
<MegaMenuItem key={link.text}
link={link} link={link}
isPinned={isPinned} isPinned={isPinned}
onClick={state.megaMenuDocked ? undefined : onClose} onClick={state.megaMenuDocked ? undefined : onClose}
activeItem={activeItem} activeItem={activeItem}
onPin={onPinItem} onPin={onPinItem}
/> />
</Stack>
))} ))}
</ul> </ul>
</ScrollContainer> </ScrollContainer>

View File

@ -3,8 +3,8 @@ import { useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { Menu, Dropdown, useStyles2, useTheme2, ToolbarButton } from '@grafana/ui'; import { Menu, Dropdown, useStyles2, ToolbarButton } from '@grafana/ui';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
import { t } from '../../../internationalization'; import { t } from '../../../internationalization';
@ -16,21 +16,12 @@ export interface Props {}
export const QuickAdd = ({}: Props) => { export const QuickAdd = ({}: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const theme = useTheme2();
const navBarTree = useSelector((state) => state.navBarTree); const navBarTree = useSelector((state) => state.navBarTree);
const breakpoint = theme.breakpoints.values.sm;
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isSmallScreen, setIsSmallScreen] = useState(!window.matchMedia(`(min-width: ${breakpoint}px)`).matches);
const createActions = useMemo(() => findCreateActions(navBarTree), [navBarTree]);
const showQuickAdd = createActions.length > 0 && !isSmallScreen;
useMediaQueryChange({ const createActions = useMemo(() => findCreateActions(navBarTree), [navBarTree]);
breakpoint, const isSmallScreen = !useMediaQueryMinWidth('sm');
onChange: (e) => { const showQuickAdd = createActions.length > 0 && !isSmallScreen;
setIsSmallScreen(!e.matches);
},
});
const MenuActions = () => { const MenuActions = () => {
return ( return (

View File

@ -1,32 +1,22 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useKBar, VisualState } from 'kbar'; import { useKBar, VisualState } from 'kbar';
import { useMemo, useState } from 'react'; import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { getInputStyles, Icon, Text, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui'; import { getInputStyles, Icon, Text, ToolbarButton, useStyles2 } from '@grafana/ui';
import { getFocusStyles } from '@grafana/ui/internal'; import { getFocusStyles } from '@grafana/ui/internal';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { getModKey } from 'app/core/utils/browser'; import { getModKey } from 'app/core/utils/browser';
export function TopSearchBarCommandPaletteTrigger() { export function TopSearchBarCommandPaletteTrigger() {
const theme = useTheme2();
const { query: kbar } = useKBar((kbarState) => ({ const { query: kbar } = useKBar((kbarState) => ({
kbarSearchQuery: kbarState.searchQuery, kbarSearchQuery: kbarState.searchQuery,
kbarIsOpen: kbarState.visualState === VisualState.showing, kbarIsOpen: kbarState.visualState === VisualState.showing,
})); }));
const breakpoint = theme.breakpoints.values.lg; const isSmallScreen = !useMediaQueryMinWidth('lg');
const [isSmallScreen, setIsSmallScreen] = useState(!window.matchMedia(`(min-width: ${breakpoint}px)`).matches);
useMediaQueryChange({
breakpoint,
onChange: (e) => {
setIsSmallScreen(!e.matches);
},
});
const onOpenSearch = () => { const onOpenSearch = () => {
kbar.toggle(); kbar.toggle();

View File

@ -1,17 +0,0 @@
import { useEffect } from 'react';
export function useMediaQueryChange({
breakpoint,
onChange,
}: {
breakpoint: number;
onChange: (e: MediaQueryListEvent) => void;
}) {
useEffect(() => {
const mediaQuery = window.matchMedia(`(min-width: ${breakpoint}px)`);
const onMediaQueryChange = (e: MediaQueryListEvent) => onChange(e);
mediaQuery.addEventListener('change', onMediaQueryChange);
return () => mediaQuery.removeEventListener('change', onMediaQueryChange);
}, [breakpoint, onChange]);
}

View File

@ -0,0 +1,21 @@
import { useEffect, useMemo, useState } from 'react';
import { ThemeBreakpointsKey } from '@grafana/data';
import { useTheme2 } from '@grafana/ui';
export function useMediaQueryMinWidth(breakpoint: ThemeBreakpointsKey): boolean {
const theme = useTheme2();
const mediaQuery = useMemo(
() => window.matchMedia(`(min-width: ${theme.breakpoints.values[breakpoint]}px)`),
[theme, breakpoint]
);
const [isMatch, setIsMatch] = useState(mediaQuery.matches);
useEffect(() => {
const onChange = (e: MediaQueryListEvent) => setIsMatch(e.matches);
mediaQuery.addEventListener('change', onChange);
return () => mediaQuery.removeEventListener('change', onChange);
}, [mediaQuery]);
return isMatch;
}

View File

@ -1,8 +1,8 @@
import { PropsWithChildren, useMemo, useState } from 'react'; import { PropsWithChildren, useMemo } from 'react';
import { VariableRefresh } from '@grafana/data'; import { VariableRefresh } from '@grafana/data';
import { Field, RadioButtonGroup, useTheme2 } from '@grafana/ui'; import { Field, RadioButtonGroup } from '@grafana/ui';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
interface Props { interface Props {
@ -17,15 +17,7 @@ const REFRESH_OPTIONS = [
]; ];
export function QueryVariableRefreshSelect({ onChange, refresh, testId }: PropsWithChildren<Props>) { export function QueryVariableRefreshSelect({ onChange, refresh, testId }: PropsWithChildren<Props>) {
const theme = useTheme2(); const isSmallScreen = !useMediaQueryMinWidth('sm');
const [isSmallScreen, setIsSmallScreen] = useState(false);
useMediaQueryChange({
breakpoint: theme.breakpoints.values.sm,
onChange: (e) => {
setIsSmallScreen(!e.matches);
},
});
const value = useMemo( const value = useMemo(
() => REFRESH_OPTIONS.find((o) => o.value === refresh)?.value ?? REFRESH_OPTIONS[0].value, () => REFRESH_OPTIONS.find((o) => o.value === refresh)?.value ?? REFRESH_OPTIONS[0].value,