mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Accessibility: Fix text selection when using FocusScope (#44770)
* Add tabIndex={-1} to places using focusScope to allow for text highlighting
* use useDialog
* don't need explicit tabIndex anymore
* remove duplicate spreading of props
This commit is contained in:
@@ -248,6 +248,7 @@
|
||||
"@opentelemetry/semantic-conventions": "1.0.1",
|
||||
"@popperjs/core": "2.11.2",
|
||||
"@react-aria/button": "3.3.4",
|
||||
"@react-aria/dialog": "3.1.4",
|
||||
"@react-aria/focus": "3.5.0",
|
||||
"@react-aria/interactions": "3.7.0",
|
||||
"@react-aria/menu": "3.3.0",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@monaco-editor/react": "4.3.1",
|
||||
"@popperjs/core": "2.11.2",
|
||||
"@react-aria/button": "3.3.4",
|
||||
"@react-aria/dialog": "3.1.4",
|
||||
"@react-aria/focus": "3.5.0",
|
||||
"@react-aria/menu": "3.3.0",
|
||||
"@react-aria/overlays": "3.7.3",
|
||||
|
||||
@@ -104,9 +104,9 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Reac
|
||||
<>
|
||||
{Object.keys(customPickers).map((key) => {
|
||||
return (
|
||||
<div className={this.getTabClassName(key)} onClick={this.onTabChange(key)} key={key}>
|
||||
<button className={this.getTabClassName(key)} onClick={this.onTabChange(key)} key={key}>
|
||||
{customPickers[key].name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
@@ -118,7 +118,11 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Reac
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div className={styles.colorPickerPopover}>
|
||||
{/*
|
||||
tabIndex=-1 is needed here to support highlighting text within the picker when using FocusScope
|
||||
see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668
|
||||
*/}
|
||||
<div tabIndex={-1} className={styles.colorPickerPopover}>
|
||||
<div className={styles.colorPickerPopoverTabs}>
|
||||
<button className={this.getTabClassName('palette')} onClick={this.onTabChange('palette')}>
|
||||
Colors
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Themeable } from '../../types';
|
||||
import { quickOptions } from './options';
|
||||
import { ButtonGroup, ToolbarButton } from '../Button';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
|
||||
@@ -86,6 +87,7 @@ export function UnthemedTimeRangePicker(props: TimeRangePickerProps): ReactEleme
|
||||
|
||||
const ref = createRef<HTMLElement>();
|
||||
const { overlayProps } = useOverlay({ onClose, isDismissable: true, isOpen }, ref);
|
||||
const { dialogProps } = useDialog({}, ref);
|
||||
|
||||
const styles = getStyles(theme);
|
||||
const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
|
||||
@@ -118,7 +120,7 @@ export function UnthemedTimeRangePicker(props: TimeRangePickerProps): ReactEleme
|
||||
</Tooltip>
|
||||
{isOpen && (
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<section ref={ref} {...overlayProps}>
|
||||
<section ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<TimePickerContent
|
||||
timeZone={timeZone}
|
||||
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||
|
||||
@@ -78,6 +78,10 @@ export const getBodyStyles = (theme: GrafanaTheme2) => {
|
||||
background-color: ${theme.colors.background.primary};
|
||||
width: 268px;
|
||||
|
||||
.react-calendar__navigation {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation__arrow,
|
||||
.react-calendar__navigation {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DateTime, GrafanaTheme2, TimeZone } from '@grafana/data';
|
||||
import { useTheme2 } from '../../../themes';
|
||||
import { Header } from './CalendarHeader';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { OverlayContainer, useOverlay } from '@react-aria/overlays';
|
||||
import { Body } from './CalendarBody';
|
||||
@@ -77,6 +78,12 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
|
||||
const styles = getStyles(theme, props.isReversed);
|
||||
const { isOpen, isFullscreen, onClose } = props;
|
||||
const ref = React.createRef<HTMLElement>();
|
||||
const { dialogProps } = useDialog(
|
||||
{
|
||||
'aria-label': selectors.components.TimePicker.calendar.label,
|
||||
},
|
||||
ref
|
||||
);
|
||||
const { overlayProps } = useOverlay(
|
||||
{
|
||||
isDismissable: true,
|
||||
@@ -93,13 +100,7 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
|
||||
if (isFullscreen) {
|
||||
return (
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<section
|
||||
className={styles.container}
|
||||
onClick={stopPropagation}
|
||||
aria-label={selectors.components.TimePicker.calendar.label}
|
||||
ref={ref}
|
||||
{...overlayProps}
|
||||
>
|
||||
<section className={styles.container} onClick={stopPropagation} ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<Header {...props} />
|
||||
<Body {...props} />
|
||||
</section>
|
||||
@@ -110,7 +111,7 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<section className={styles.modal} onClick={stopPropagation} ref={ref} {...overlayProps}>
|
||||
<section className={styles.modal} onClick={stopPropagation} ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<div className={styles.content} aria-label={selectors.components.TimePicker.calendar.label}>
|
||||
<Header {...props} />
|
||||
<Body {...props} />
|
||||
|
||||
@@ -8,6 +8,7 @@ import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { stylesFactory, useTheme2 } from '../../themes';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
|
||||
export interface Props {
|
||||
@@ -34,6 +35,11 @@ export interface Props {
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2, scrollableContent: boolean) => {
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`,
|
||||
drawer: css`
|
||||
.drawer-content {
|
||||
background-color: ${theme.colors.background.primary};
|
||||
@@ -76,10 +82,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, scrollableContent: boolea
|
||||
`,
|
||||
content: css`
|
||||
padding: ${theme.spacing(2)};
|
||||
flex-grow: 1;
|
||||
flex: 1;
|
||||
overflow: ${!scrollableContent ? 'hidden' : 'auto'};
|
||||
z-index: 0;
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
});
|
||||
@@ -101,6 +106,7 @@ export const Drawer: FC<Props> = ({
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const currentWidth = isExpanded ? '100%' : width;
|
||||
const overlayRef = React.useRef(null);
|
||||
const { dialogProps, titleProps } = useDialog({}, overlayRef);
|
||||
const { overlayProps } = useOverlay(
|
||||
{
|
||||
isDismissable: true,
|
||||
@@ -132,45 +138,47 @@ export const Drawer: FC<Props> = ({
|
||||
}
|
||||
>
|
||||
<FocusScope restoreFocus contain autoFocus>
|
||||
{typeof title === 'string' && (
|
||||
<div className={drawerStyles.header} {...overlayProps} ref={overlayRef}>
|
||||
<div className={drawerStyles.actions}>
|
||||
{expandable && !isExpanded && (
|
||||
<div className={drawerStyles.container} {...overlayProps} {...dialogProps} ref={overlayRef}>
|
||||
{typeof title === 'string' && (
|
||||
<div className={drawerStyles.header}>
|
||||
<div className={drawerStyles.actions}>
|
||||
{expandable && !isExpanded && (
|
||||
<IconButton
|
||||
name="angle-left"
|
||||
size="xl"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
surface="header"
|
||||
aria-label={selectors.components.Drawer.General.expand}
|
||||
/>
|
||||
)}
|
||||
{expandable && isExpanded && (
|
||||
<IconButton
|
||||
name="angle-right"
|
||||
size="xl"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
surface="header"
|
||||
aria-label={selectors.components.Drawer.General.contract}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
name="angle-left"
|
||||
name="times"
|
||||
size="xl"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
onClick={onClose}
|
||||
surface="header"
|
||||
aria-label={selectors.components.Drawer.General.expand}
|
||||
aria-label={selectors.components.Drawer.General.close}
|
||||
/>
|
||||
)}
|
||||
{expandable && isExpanded && (
|
||||
<IconButton
|
||||
name="angle-right"
|
||||
size="xl"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
surface="header"
|
||||
aria-label={selectors.components.Drawer.General.contract}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
name="times"
|
||||
size="xl"
|
||||
onClick={onClose}
|
||||
surface="header"
|
||||
aria-label={selectors.components.Drawer.General.close}
|
||||
/>
|
||||
</div>
|
||||
<div className={drawerStyles.titleWrapper}>
|
||||
<h3>{title}</h3>
|
||||
{typeof subtitle === 'string' && <div className="muted">{subtitle}</div>}
|
||||
{typeof subtitle !== 'string' && subtitle}
|
||||
</div>
|
||||
<div className={drawerStyles.titleWrapper}>
|
||||
<h3 {...titleProps}>{title}</h3>
|
||||
{typeof subtitle === 'string' && <div className="muted">{subtitle}</div>}
|
||||
{typeof subtitle !== 'string' && subtitle}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{typeof title !== 'string' && title}
|
||||
<div className={drawerStyles.content}>
|
||||
{!scrollableContent ? children : <CustomScrollbar>{children}</CustomScrollbar>}
|
||||
</div>
|
||||
)}
|
||||
{typeof title !== 'string' && title}
|
||||
<div className={drawerStyles.content} {...overlayProps} ref={overlayRef}>
|
||||
{!scrollableContent ? children : <CustomScrollbar>{children}</CustomScrollbar>}
|
||||
</div>
|
||||
</FocusScope>
|
||||
</RcDrawer>
|
||||
|
||||
@@ -57,7 +57,11 @@ const ButtonSelectComponent = <T,>(props: Props<T>) => {
|
||||
<div className={styles.menuWrapper}>
|
||||
<ClickOutsideWrapper onClick={state.close} parent={document} includeButtonPress={false}>
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<Menu onClose={state.close} {...menuProps}>
|
||||
{/*
|
||||
tabIndex=-1 is needed here to support highlighting text within the menu when using FocusScope
|
||||
see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668
|
||||
*/}
|
||||
<Menu tabIndex={-1} onClose={state.close} {...menuProps}>
|
||||
{options.map((item) => (
|
||||
<MenuItem
|
||||
key={`${item.value}`}
|
||||
|
||||
@@ -80,7 +80,11 @@ export function Modal(props: PropsWithChildren<Props>) {
|
||||
onClick={onClickBackdrop || (closeOnBackdropClick ? onDismiss : undefined)}
|
||||
/>
|
||||
<FocusScope contain={trapFocus} autoFocus restoreFocus>
|
||||
<div className={cx(styles.modal, className)}>
|
||||
{/*
|
||||
tabIndex=-1 is needed here to support highlighting text within the modal when using FocusScope
|
||||
see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668
|
||||
*/}
|
||||
<div tabIndex={-1} className={cx(styles.modal, className)}>
|
||||
<div className={headerClass}>
|
||||
{typeof title === 'string' && <DefaultModalHeader {...props} title={title} />}
|
||||
{typeof title !== 'string' && title}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useMenuTriggerState } from '@react-stately/menu';
|
||||
import { useMenuTrigger } from '@react-aria/menu';
|
||||
import { useFocusWithin, useHover, useKeyboard } from '@react-aria/interactions';
|
||||
import { useButton } from '@react-aria/button';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { DismissButton, useOverlay } from '@react-aria/overlays';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
|
||||
@@ -130,6 +131,7 @@ export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactE
|
||||
}
|
||||
|
||||
const overlayRef = React.useRef(null);
|
||||
const { dialogProps } = useDialog({}, overlayRef);
|
||||
const { overlayProps } = useOverlay(
|
||||
{
|
||||
onClose: () => state.close(),
|
||||
@@ -155,7 +157,7 @@ export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactE
|
||||
}}
|
||||
>
|
||||
<FocusScope restoreFocus>
|
||||
<div {...overlayProps} ref={overlayRef}>
|
||||
<div {...overlayProps} {...dialogProps} ref={overlayRef}>
|
||||
<DismissButton onDismiss={() => state.close()} />
|
||||
{menu}
|
||||
<DismissButton onDismiss={() => state.close()} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useRef } from 'react';
|
||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||
import { CustomScrollbar, Icon, IconButton, IconName, useTheme2 } from '@grafana/ui';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
import { css } from '@emotion/css';
|
||||
import { NavBarMenuItem } from './NavBarMenuItem';
|
||||
@@ -16,6 +17,7 @@ export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
const ref = useRef(null);
|
||||
const { dialogProps } = useDialog({}, ref);
|
||||
const { overlayProps } = useOverlay(
|
||||
{
|
||||
isDismissable: true,
|
||||
@@ -27,7 +29,7 @@ export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
||||
|
||||
return (
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div data-testid="navbarmenu" className={styles.container} ref={ref} {...overlayProps}>
|
||||
<div data-testid="navbarmenu" className={styles.container} ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<div className={styles.header}>
|
||||
<Icon name="bars" size="xl" />
|
||||
<IconButton aria-label="Close navigation menu" name="times" onClick={onClose} size="xl" variant="secondary" />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { css } from '@emotion/css';
|
||||
import { Button, ButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
|
||||
import { MediaType, PickerTabType, ResourceFolderName } from '../types';
|
||||
@@ -25,6 +26,7 @@ export const ResourcePickerPopover = (props: Props) => {
|
||||
};
|
||||
|
||||
const ref = createRef<HTMLElement>();
|
||||
const { dialogProps } = useDialog({}, ref);
|
||||
const { overlayProps } = useOverlay({ onClose, isDismissable: true, isOpen: true }, ref);
|
||||
|
||||
const [newValue, setNewValue] = useState<string>(value ?? '');
|
||||
@@ -59,7 +61,7 @@ export const ResourcePickerPopover = (props: Props) => {
|
||||
|
||||
return (
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<section ref={ref} {...overlayProps}>
|
||||
<section ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<div className={styles.resourcePickerPopover}>
|
||||
<div className={styles.resourcePickerPopoverTabs}>
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { createRef } from 'react';
|
||||
import { VizTooltipContainer } from '@grafana/ui';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
|
||||
import { ComplexDataHoverView } from './components/ComplexDataHoverView';
|
||||
@@ -14,12 +15,13 @@ interface Props {
|
||||
export const GeomapTooltip = ({ ttip, onClose, isOpen }: Props) => {
|
||||
const ref = createRef<HTMLElement>();
|
||||
const { overlayProps } = useOverlay({ onClose, isDismissable: true, isOpen }, ref);
|
||||
const { dialogProps } = useDialog({}, ref);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ttip && ttip.layers && (
|
||||
<VizTooltipContainer position={{ x: ttip.pageX, y: ttip.pageY }} offset={{ x: 10, y: 10 }} allowPointerEvents>
|
||||
<section ref={ref} {...overlayProps}>
|
||||
<section ref={ref} {...overlayProps} {...dialogProps}>
|
||||
<ComplexDataHoverView {...ttip} isOpen={isOpen} onClose={onClose} />
|
||||
</section>
|
||||
</VizTooltipContainer>
|
||||
|
||||
31
yarn.lock
31
yarn.lock
@@ -4042,6 +4042,7 @@ __metadata:
|
||||
"@monaco-editor/react": 4.3.1
|
||||
"@popperjs/core": 2.11.2
|
||||
"@react-aria/button": 3.3.4
|
||||
"@react-aria/dialog": 3.1.4
|
||||
"@react-aria/focus": 3.5.0
|
||||
"@react-aria/menu": 3.3.0
|
||||
"@react-aria/overlays": 3.7.3
|
||||
@@ -6520,7 +6521,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/focus@npm:3.5.0, @react-aria/focus@npm:^3.5.0":
|
||||
"@react-aria/dialog@npm:3.1.4":
|
||||
version: 3.1.4
|
||||
resolution: "@react-aria/dialog@npm:3.1.4"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
"@react-aria/focus": ^3.4.1
|
||||
"@react-aria/utils": ^3.8.2
|
||||
"@react-stately/overlays": ^3.1.3
|
||||
"@react-types/dialog": ^3.3.1
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: 968328a9b22e545ea79084cca2714f3bc7954c02eb0dc7cab78a1094d4c5dbd19520a0cc28e3dcbb7cb8dd7c505d6b9cb77fbb0fb4735fdf2e99908d481af3ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/focus@npm:3.5.0, @react-aria/focus@npm:^3.4.1, @react-aria/focus@npm:^3.5.0":
|
||||
version: 3.5.0
|
||||
resolution: "@react-aria/focus@npm:3.5.0"
|
||||
dependencies:
|
||||
@@ -6835,6 +6851,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-types/dialog@npm:^3.3.1":
|
||||
version: 3.3.1
|
||||
resolution: "@react-types/dialog@npm:3.3.1"
|
||||
dependencies:
|
||||
"@react-types/overlays": ^3.5.1
|
||||
"@react-types/shared": ^3.8.0
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: 6b0260e7018f26387c1d16585a048af5617f8b146d42d28439f0d4fd0fb041b1f004ca1a0a021a4e07a200a0045e34da8b2e0a3ff235272426b6815994e1c291
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-types/menu@npm:3.4.1, @react-types/menu@npm:^3.3.0":
|
||||
version: 3.4.1
|
||||
resolution: "@react-types/menu@npm:3.4.1"
|
||||
@@ -19664,6 +19692,7 @@ __metadata:
|
||||
"@pmmmwh/react-refresh-webpack-plugin": 0.5.4
|
||||
"@popperjs/core": 2.11.2
|
||||
"@react-aria/button": 3.3.4
|
||||
"@react-aria/dialog": 3.1.4
|
||||
"@react-aria/focus": 3.5.0
|
||||
"@react-aria/interactions": 3.7.0
|
||||
"@react-aria/menu": 3.3.0
|
||||
|
||||
Reference in New Issue
Block a user