TimeRangePicker: Accessiblity improvements (#36912)
* Add low hanging aria markup * Add a list of radio buttons * Add ref to put focus * Add landmark markup * Add labels to icon buttons * Add radio button groupings * Add keyboard focus styles * Improve markup * Update test selector * Remove testid in favor of text * Fiddle around with test labels * Get correct testid selector * Add aria-expanded to button * Change order of quick select for keyboard nav * Create headings for easier heading navigation * Add another h3 * Add testid * Use selectors in e2e test * Add dataid selector for narrow absolute button * Update e2e test with proper selectors * Add more verbose aria-label * Change editor to picker
This commit is contained in:
@@ -6,11 +6,12 @@
|
|||||||
// prefix your selector string with 'data-test-id' so that when create the selectors we know to search for it on the right attribute
|
// prefix your selector string with 'data-test-id' so that when create the selectors we know to search for it on the right attribute
|
||||||
export const Components = {
|
export const Components = {
|
||||||
TimePicker: {
|
TimePicker: {
|
||||||
openButton: 'TimePicker Open Button',
|
openButton: 'data-testid TimePicker Open Button',
|
||||||
fromField: 'TimePicker from field',
|
fromField: 'TimePicker from field',
|
||||||
toField: 'TimePicker to field',
|
toField: 'TimePicker to field',
|
||||||
applyTimeRange: 'TimePicker submit button',
|
applyTimeRange: 'data-testid TimePicker submit button',
|
||||||
calendar: 'TimePicker calendar',
|
calendar: 'TimePicker calendar',
|
||||||
|
absoluteTimeRangeTitle: 'data-testid-absolute-time-range-narrow',
|
||||||
},
|
},
|
||||||
DataSource: {
|
DataSource: {
|
||||||
TestData: {
|
TestData: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface TimeRangeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => {
|
export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => {
|
||||||
e2e().get('[aria-label="TimePicker Open Button"]').click();
|
e2e.components.TimePicker.openButton().click();
|
||||||
|
|
||||||
if (zone) {
|
if (zone) {
|
||||||
e2e().contains('button', 'Change time zone').click();
|
e2e().contains('button', 'Change time zone').click();
|
||||||
@@ -21,9 +21,10 @@ export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For smaller screens
|
// For smaller screens
|
||||||
e2e().get('[aria-label="TimePicker absolute time range"]').click();
|
e2e.components.TimePicker.absoluteTimeRangeTitle().click();
|
||||||
|
|
||||||
e2e().get('[aria-label="TimePicker from field"]').clear().type(from);
|
e2e.components.TimePicker.fromField().clear().type(from);
|
||||||
e2e().get('[aria-label="TimePicker to field"]').clear().type(to);
|
e2e.components.TimePicker.toField().clear().type(to);
|
||||||
e2e().get('[aria-label="TimePicker submit button"]').click();
|
|
||||||
|
e2e.components.TimePicker.applyTimeRange().click();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,7 +69,13 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, Props>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<button ref={ref} className={buttonStyles} aria-label={getButttonAriaLabel(ariaLabel, tooltip)} {...rest}>
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={buttonStyles}
|
||||||
|
aria-label={getButttonAriaLabel(ariaLabel, tooltip)}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
{renderIcon(icon)}
|
{renderIcon(icon)}
|
||||||
{imgSrc && <img className={styles.img} src={imgSrc} />}
|
{imgSrc && <img className={styles.img} src={imgSrc} />}
|
||||||
{children && !iconOnly && <div className={contentStyles}>{children}</div>}
|
{children && !iconOnly && <div className={contentStyles}>{children}</div>}
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ describe('TimePicker', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(container.queryByLabelText(/timepicker open button/i)).toBeInTheDocument();
|
expect(container.queryByLabelText(/Time range picker/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import { Themeable } from '../../types';
|
import { Themeable } from '../../types';
|
||||||
import { otherOptions, quickOptions } from './rangeOptions';
|
import { otherOptions, quickOptions } from './rangeOptions';
|
||||||
import { ButtonGroup, ToolbarButton } from '../Button';
|
import { ButtonGroup, ToolbarButton } from '../Button';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export interface TimeRangePickerProps extends Themeable {
|
export interface TimeRangePickerProps extends Themeable {
|
||||||
@@ -89,11 +90,21 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup className={styles.container}>
|
<ButtonGroup className={styles.container}>
|
||||||
{hasAbsolute && <ToolbarButton variant={variant} onClick={onMoveBackward} icon="angle-left" narrow />}
|
{hasAbsolute && (
|
||||||
|
<ToolbarButton
|
||||||
|
aria-label="Move time range backwards"
|
||||||
|
variant={variant}
|
||||||
|
onClick={onMoveBackward}
|
||||||
|
icon="angle-left"
|
||||||
|
narrow
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip content={<TimePickerTooltip timeRange={value} timeZone={timeZone} />} placement="bottom">
|
<Tooltip content={<TimePickerTooltip timeRange={value} timeZone={timeZone} />} placement="bottom">
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
aria-label="TimePicker Open Button"
|
data-testid={selectors.components.TimePicker.openButton}
|
||||||
|
aria-label={`Time range picker with current time range ${formattedRange(value, timeZone)} selected`}
|
||||||
|
aria-controls="TimePickerContent"
|
||||||
onClick={this.onOpen}
|
onClick={this.onOpen}
|
||||||
icon="clock-nine"
|
icon="clock-nine"
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -120,10 +131,18 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
|
|||||||
|
|
||||||
{timeSyncButton}
|
{timeSyncButton}
|
||||||
|
|
||||||
{hasAbsolute && <ToolbarButton onClick={onMoveForward} icon="angle-right" narrow variant={variant} />}
|
{hasAbsolute && (
|
||||||
|
<ToolbarButton
|
||||||
|
aria-label="Move time range forwards"
|
||||||
|
onClick={onMoveForward}
|
||||||
|
icon="angle-right"
|
||||||
|
narrow
|
||||||
|
variant={variant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip content={ZoomOutTooltip} placement="bottom">
|
<Tooltip content={ZoomOutTooltip} placement="bottom">
|
||||||
<ToolbarButton onClick={onZoom} icon="search-minus" variant={variant} />
|
<ToolbarButton aria-label="Zoom out time range" onClick={onZoom} icon="search-minus" variant={variant} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -210,13 +210,13 @@ export const TimePickerCalendar = memo<Props>((props) => {
|
|||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
return (
|
return (
|
||||||
<ClickOutsideWrapper onClick={props.onClose}>
|
<ClickOutsideWrapper onClick={props.onClose}>
|
||||||
<div
|
<section
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
onClick={stopPropagation}
|
onClick={stopPropagation}
|
||||||
aria-label={selectors.components.TimePicker.calendar}
|
aria-label={selectors.components.TimePicker.calendar}
|
||||||
>
|
>
|
||||||
<Body {...props} />
|
<Body {...props} />
|
||||||
</div>
|
</section>
|
||||||
</ClickOutsideWrapper>
|
</ClickOutsideWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-23
@@ -10,6 +10,8 @@ import { TimePickerTitle } from './TimePickerTitle';
|
|||||||
import { TimeRangeForm } from './TimeRangeForm';
|
import { TimeRangeForm } from './TimeRangeForm';
|
||||||
import { TimeRangeList } from './TimeRangeList';
|
import { TimeRangeList } from './TimeRangeList';
|
||||||
import { TimePickerFooter } from './TimePickerFooter';
|
import { TimePickerFooter } from './TimePickerFooter';
|
||||||
|
import { getFocusStyles } from '../../../themes/mixins';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRanges, isContainerTall) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRanges, isContainerTall) => {
|
||||||
return {
|
return {
|
||||||
@@ -30,6 +32,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRang
|
|||||||
`,
|
`,
|
||||||
body: css`
|
body: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
height: ${isContainerTall ? '381px' : '217px'};
|
height: ${isContainerTall ? '381px' : '217px'};
|
||||||
`,
|
`,
|
||||||
leftSide: css`
|
leftSide: css`
|
||||||
@@ -64,6 +67,16 @@ const getNarrowScreenStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
border-bottom: 1px solid ${theme.colors.border.weak};
|
border-bottom: 1px solid ${theme.colors.border.weak};
|
||||||
padding: 7px 9px 7px 9px;
|
padding: 7px 9px 7px 9px;
|
||||||
`,
|
`,
|
||||||
|
expandButton: css`
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
${getFocusStyles(theme)}
|
||||||
|
}
|
||||||
|
`,
|
||||||
body: css`
|
body: css`
|
||||||
border-bottom: 1px solid ${theme.colors.border.weak};
|
border-bottom: 1px solid ${theme.colors.border.weak};
|
||||||
`,
|
`,
|
||||||
@@ -163,13 +176,8 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.container, className)}>
|
<div id="TimePickerContent" className={cx(styles.container, className)}>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{isFullscreen && (
|
|
||||||
<div className={styles.leftSide}>
|
|
||||||
<FullScreenForm {...props} historyOptions={historyOptions} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(!isFullscreen || !hideQuickRanges) && (
|
{(!isFullscreen || !hideQuickRanges) && (
|
||||||
<CustomScrollbar className={styles.rightSide}>
|
<CustomScrollbar className={styles.rightSide}>
|
||||||
{!isFullscreen && <NarrowScreenForm {...props} historyOptions={historyOptions} />}
|
{!isFullscreen && <NarrowScreenForm {...props} historyOptions={historyOptions} />}
|
||||||
@@ -192,6 +200,11 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
|||||||
)}
|
)}
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
)}
|
)}
|
||||||
|
{isFullscreen && (
|
||||||
|
<div className={styles.leftSide}>
|
||||||
|
<FullScreenForm {...props} historyOptions={historyOptions} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!hideTimeZone && isFullscreen && <TimePickerFooter timeZone={timeZone} onChangeTimeZone={onChangeTimeZone} />}
|
{!hideTimeZone && isFullscreen && <TimePickerFooter timeZone={timeZone} onChangeTimeZone={onChangeTimeZone} />}
|
||||||
</div>
|
</div>
|
||||||
@@ -218,21 +231,25 @@ const NarrowScreenForm: React.FC<FormProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<fieldset>
|
||||||
<div
|
<h3 className={styles.header}>
|
||||||
aria-label="TimePicker absolute time range"
|
<button
|
||||||
className={styles.header}
|
className={styles.expandButton}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!hideQuickRanges) {
|
if (!hideQuickRanges) {
|
||||||
setCollapsedFlag(!collapsed);
|
setCollapsedFlag(!collapsed);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
data-testid={selectors.components.TimePicker.absoluteTimeRangeTitle}
|
||||||
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
aria-expanded={!collapsed}
|
||||||
{!hideQuickRanges && <Icon name={!collapsed ? 'angle-up' : 'angle-down'} />}
|
aria-controls="expanded-timerange"
|
||||||
</div>
|
>
|
||||||
|
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
||||||
|
{!hideQuickRanges && <Icon name={!collapsed ? 'angle-up' : 'angle-down'} />}
|
||||||
|
</button>
|
||||||
|
</h3>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className={styles.body}>
|
<div className={styles.body} id="expanded-timerange">
|
||||||
<div className={styles.form}>
|
<div className={styles.form}>
|
||||||
<TimeRangeForm value={value} onApply={onChange} timeZone={timeZone} isFullscreen={false} />
|
<TimeRangeForm value={value} onApply={onChange} timeZone={timeZone} isFullscreen={false} />
|
||||||
</div>
|
</div>
|
||||||
@@ -246,7 +263,7 @@ const NarrowScreenForm: React.FC<FormProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</fieldset>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -261,9 +278,9 @@ const FullScreenForm: React.FC<FormProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div aria-label="TimePicker absolute time range" className={styles.title}>
|
<h3 className={styles.title} data-testid={selectors.components.TimePicker.absoluteTimeRangeTitle}>
|
||||||
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
||||||
</div>
|
</h3>
|
||||||
<TimeRangeForm
|
<TimeRangeForm
|
||||||
value={props.value}
|
value={props.value}
|
||||||
timeZone={props.timeZone}
|
timeZone={props.timeZone}
|
||||||
|
|||||||
+4
-4
@@ -46,7 +46,7 @@ export const TimePickerFooter: FC<Props> = (props) => {
|
|||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return (
|
return (
|
||||||
<div className={cx(style.container, style.editContainer)}>
|
<div className={cx(style.container, style.editContainer)}>
|
||||||
<div aria-label={selectors.components.TimeZonePicker.container} className={style.timeZoneContainer}>
|
<section aria-label={selectors.components.TimeZonePicker.container} className={style.timeZoneContainer}>
|
||||||
<TimeZonePicker
|
<TimeZonePicker
|
||||||
includeInternal={true}
|
includeInternal={true}
|
||||||
onChange={(timeZone) => {
|
onChange={(timeZone) => {
|
||||||
@@ -59,13 +59,13 @@ export const TimePickerFooter: FC<Props> = (props) => {
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
onBlur={onToggleChangeTz}
|
onBlur={onToggleChangeTz}
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.container}>
|
<section aria-label="Time zone selection" className={style.container}>
|
||||||
<div className={style.timeZoneContainer}>
|
<div className={style.timeZoneContainer}>
|
||||||
<div className={style.timeZone}>
|
<div className={style.timeZone}>
|
||||||
<TimeZoneTitle title={info.name} />
|
<TimeZoneTitle title={info.name} />
|
||||||
@@ -78,7 +78,7 @@ export const TimePickerFooter: FC<Props> = (props) => {
|
|||||||
<Button variant="secondary" onClick={onToggleChangeTz} size="sm">
|
<Button variant="secondary" onClick={onToggleChangeTz} size="sm">
|
||||||
Change time zone
|
Change time zone
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -9,6 +9,8 @@ const getStyle = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
font-size: ${theme.typography.size.md};
|
font-size: ${theme.typography.size.md};
|
||||||
font-weight: ${theme.typography.weight.semibold};
|
font-weight: ${theme.typography.weight.semibold};
|
||||||
color: ${theme.colors.formLabel};
|
color: ${theme.colors.formLabel};
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -17,7 +19,7 @@ export const TimePickerTitle = memo<PropsWithChildren<{}>>(({ children }) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getStyle(theme);
|
const styles = getStyle(theme);
|
||||||
|
|
||||||
return <span className={styles.text}>{children}</span>;
|
return <h3 className={styles.text}>{children}</h3>;
|
||||||
});
|
});
|
||||||
|
|
||||||
TimePickerTitle.displayName = 'TimePickerTitle';
|
TimePickerTitle.displayName = 'TimePickerTitle';
|
||||||
|
|||||||
+2
-2
@@ -31,10 +31,10 @@ function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc'): TimeRan
|
|||||||
|
|
||||||
describe('TimeRangeForm', () => {
|
describe('TimeRangeForm', () => {
|
||||||
it('should render form correcty', () => {
|
it('should render form correcty', () => {
|
||||||
const { getByLabelText } = setup();
|
const { getByLabelText, getByText } = setup();
|
||||||
const { TimePicker } = selectors.components;
|
const { TimePicker } = selectors.components;
|
||||||
|
|
||||||
expect(getByLabelText(TimePicker.applyTimeRange)).toBeInTheDocument();
|
expect(getByText('Apply time range')).toBeInTheDocument();
|
||||||
expect(getByLabelText(TimePicker.fromField)).toBeInTheDocument();
|
expect(getByLabelText(TimePicker.fromField)).toBeInTheDocument();
|
||||||
expect(getByLabelText(TimePicker.toField)).toBeInTheDocument();
|
expect(getByLabelText(TimePicker.toField)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-3
@@ -96,7 +96,7 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
|||||||
const icon = isFullscreen ? null : <Button icon="calendar-alt" variant="secondary" onClick={onOpen} />;
|
const icon = isFullscreen ? null : <Button icon="calendar-alt" variant="secondary" onClick={onOpen} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<form aria-label="Absolute time ranges">
|
||||||
<Field label="From" invalid={from.invalid} error={from.errorMessage}>
|
<Field label="From" invalid={from.invalid} error={from.errorMessage}>
|
||||||
<Input
|
<Input
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
@@ -117,7 +117,7 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
|||||||
value={to.value}
|
value={to.value}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Button aria-label={selectors.components.TimePicker.applyTimeRange} onClick={onApply}>
|
<Button data-testid={selectors.components.TimePicker.applyTimeRange} onClick={onApply}>
|
||||||
Apply time range
|
Apply time range
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
|||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
isReversed={isReversed}
|
isReversed={isReversed}
|
||||||
/>
|
/>
|
||||||
</>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+15
-10
@@ -26,7 +26,7 @@ const getOptionsStyles = stylesFactory(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title: string;
|
||||||
options: TimeOption[];
|
options: TimeOption[];
|
||||||
value?: TimeOption;
|
value?: TimeOption;
|
||||||
onChange: (option: TimeOption) => void;
|
onChange: (option: TimeOption) => void;
|
||||||
@@ -46,30 +46,35 @@ export const TimeRangeList: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section aria-label={title}>
|
||||||
<div className={styles.title}>
|
<form>
|
||||||
<TimePickerTitle>{title}</TimePickerTitle>
|
<fieldset>
|
||||||
</div>
|
<div className={styles.title}>
|
||||||
<Options {...props} />
|
<TimePickerTitle>{title}</TimePickerTitle>
|
||||||
</>
|
</div>
|
||||||
|
<Options {...props} />
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Options: React.FC<Props> = ({ options, value, onChange }) => {
|
const Options: React.FC<Props> = ({ options, value, onChange, title }) => {
|
||||||
const styles = getOptionsStyles();
|
const styles = getOptionsStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<ul aria-roledescription="Time range selection">
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
<TimeRangeOption
|
<TimeRangeOption
|
||||||
key={keyForOption(option, index)}
|
key={keyForOption(option, index)}
|
||||||
value={option}
|
value={option}
|
||||||
selected={isEqual(option, value)}
|
selected={isEqual(option, value)}
|
||||||
onSelect={onChange}
|
onSelect={onChange}
|
||||||
|
name={title}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
<div className={styles.grow} />
|
<div className={styles.grow} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+37
-8
@@ -2,12 +2,15 @@ import React, { memo } from 'react';
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { GrafanaTheme2, TimeOption } from '@grafana/data';
|
import { GrafanaTheme2, TimeOption } from '@grafana/data';
|
||||||
import { useStyles2 } from '../../../themes/ThemeContext';
|
import { useStyles2 } from '../../../themes/ThemeContext';
|
||||||
|
import { getFocusStyles } from '../../../themes/mixins';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
container: css`
|
container: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-direction: row-reverse;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 7px 9px 7px 9px;
|
padding: 7px 9px 7px 9px;
|
||||||
|
|
||||||
@@ -16,10 +19,20 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
selected: css`
|
selected: css`
|
||||||
background: ${theme.colors.action.selected};
|
background: ${theme.colors.action.selected};
|
||||||
}
|
font-weight: ${theme.typography.fontWeightMedium};
|
||||||
`,
|
`,
|
||||||
|
radio: css`
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:focus-visible + label {
|
||||||
|
${getFocusStyles(theme)};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
label: css`
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,15 +40,31 @@ interface Props {
|
|||||||
value: TimeOption;
|
value: TimeOption;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
onSelect: (option: TimeOption) => void;
|
onSelect: (option: TimeOption) => void;
|
||||||
|
/**
|
||||||
|
* Input identifier. This should be the same for all options in a group.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimeRangeOption = memo<Props>(({ value, onSelect, selected = false }) => {
|
export const TimeRangeOption = memo<Props>(({ value, onSelect, selected = false, name }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
// In case there are more of the same timerange in the list
|
||||||
|
const id = uuidv4();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.container, selected && styles.selected)} onClick={() => onSelect(value)} tabIndex={-1}>
|
<li className={cx(styles.container, selected && styles.selected)}>
|
||||||
<span>{value.display}</span>
|
<input
|
||||||
</div>
|
className={styles.radio}
|
||||||
|
checked={selected}
|
||||||
|
name={name}
|
||||||
|
type="checkbox"
|
||||||
|
id={id}
|
||||||
|
onChange={() => onSelect(value)}
|
||||||
|
/>
|
||||||
|
<label className={styles.label} htmlFor={id}>
|
||||||
|
{value.display}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user