TimePickerCalendar: adds keyboard navigation to the widget (#39070)
* WIP: adds react-aria package to package.json * adds focus trapping to the calendar widget * makes focus to move in and out of the widget * Chore: remove unused component * WIP: adds keyboard nav to calendar * adds close button to calendar on wide screen * Chore: update test to reflect new changes * use more descriptive aria label * prune duplicate absolute timeRange aria label * TimePicker: Use aria overlays to enable closing with esc (#40045) * Move timepicker to function component * update overlayprops * Remove unused import * Fix for picker closing before setting the range when selecting quick ranges * use more descriptive aria label * update test to correspond with new label * chore: some nit fix * chore: used specific version for react-aria/overlay package * Chore: refactor timePickerCalendar component * chore: nit fixes * chore: nit fixes * reverts back to main and re-add deps with yarn 2 * chore: removes react-aria deps from root * Chore: replace default export Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
This commit is contained in:
co-authored by
Oscar Kilhed
Hugo Häggmark
parent
f59aabbd3b
commit
3cec0b1227
@@ -12,10 +12,14 @@
|
||||
export const Components = {
|
||||
TimePicker: {
|
||||
openButton: 'data-testid TimePicker Open Button',
|
||||
fromField: 'TimePicker from field',
|
||||
toField: 'TimePicker to field',
|
||||
fromField: 'Time Range from field',
|
||||
toField: 'Time Range to field',
|
||||
applyTimeRange: 'data-testid TimePicker submit button',
|
||||
calendar: 'TimePicker calendar',
|
||||
calendar: {
|
||||
label: 'Time Range calendar',
|
||||
openButton: 'Open time range calendar',
|
||||
closeButton: 'Close time range Calendar',
|
||||
},
|
||||
absoluteTimeRangeTitle: 'data-testid-absolute-time-range-narrow',
|
||||
},
|
||||
DataSource: {
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@monaco-editor/react": "4.2.2",
|
||||
"@popperjs/core": "2.5.4",
|
||||
"@react-aria/focus": "3.4.1",
|
||||
"@react-aria/overlays": "3.7.2",
|
||||
"@sentry/browser": "5.25.0",
|
||||
"ansicolor": "1.1.95",
|
||||
"classnames": "2.2.6",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '../../../themes';
|
||||
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
import { getBodyStyles } from '../TimeRangePicker/TimePickerCalendar';
|
||||
import { getBodyStyles } from '../TimeRangePicker/CalendarBody';
|
||||
|
||||
/** @public */
|
||||
export interface DatePickerProps {
|
||||
|
||||
+2
-1
@@ -5,9 +5,10 @@ import { css, cx } from '@emotion/css';
|
||||
import { dateTimeFormat, DateTime, dateTime, GrafanaTheme2, isDateTime } from '@grafana/data';
|
||||
import { Button, ClickOutsideWrapper, HorizontalGroup, Icon, InlineField, Input, Portal } from '../..';
|
||||
import { TimeOfDayPicker } from '../TimeOfDayPicker';
|
||||
import { getBodyStyles, getStyles as getCalendarStyles } from '../TimeRangePicker/TimePickerCalendar';
|
||||
import { getStyles as getCalendarStyles } from '../TimeRangePicker/TimePickerCalendar';
|
||||
import { useStyles2, useTheme2 } from '../../../themes';
|
||||
import { isValid } from '../utils';
|
||||
import { getBodyStyles } from '../TimeRangePicker/CalendarBody';
|
||||
|
||||
export interface Props {
|
||||
/** Input date for the component */
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Libraries
|
||||
import React, { PureComponent, memo, FormEvent } from 'react';
|
||||
import React, { memo, FormEvent, createRef, useState, ReactElement } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
// Components
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
|
||||
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
|
||||
// Utils & Services
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
@@ -26,6 +25,8 @@ import { Themeable } from '../../types';
|
||||
import { quickOptions } from './options';
|
||||
import { ButtonGroup, ToolbarButton } from '../Button';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
|
||||
/** @public */
|
||||
export interface TimeRangePickerProps extends Themeable {
|
||||
@@ -49,95 +50,80 @@ export interface State {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps, State> {
|
||||
state: State = {
|
||||
isOpen: false,
|
||||
export function UnthemedTimeRangePicker(props: TimeRangePickerProps): ReactElement {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const {
|
||||
value,
|
||||
onMoveBackward,
|
||||
onMoveForward,
|
||||
onZoom,
|
||||
timeZone,
|
||||
fiscalYearStartMonth,
|
||||
timeSyncButton,
|
||||
isSynced,
|
||||
theme,
|
||||
history,
|
||||
onChangeTimeZone,
|
||||
onChangeFiscalYearStartMonth,
|
||||
hideQuickRanges,
|
||||
} = props;
|
||||
|
||||
const onChange = (timeRange: TimeRange) => {
|
||||
props.onChange(timeRange);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
onChange = (timeRange: TimeRange) => {
|
||||
this.props.onChange(timeRange);
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
onOpen = (event: FormEvent<HTMLButtonElement>) => {
|
||||
const { isOpen } = this.state;
|
||||
const onOpen = (event: FormEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.setState({ isOpen: !isOpen });
|
||||
setOpen(!isOpen);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
onKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.code === 'Escape') {
|
||||
this.onClose();
|
||||
}
|
||||
const onClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
const ref = createRef<HTMLElement>();
|
||||
const { overlayProps } = useOverlay({ onClose, isOpen }, ref);
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
onMoveBackward,
|
||||
onMoveForward,
|
||||
onZoom,
|
||||
timeZone,
|
||||
fiscalYearStartMonth,
|
||||
timeSyncButton,
|
||||
isSynced,
|
||||
theme,
|
||||
history,
|
||||
onChangeTimeZone,
|
||||
onChangeFiscalYearStartMonth,
|
||||
hideQuickRanges,
|
||||
} = this.props;
|
||||
const styles = getStyles(theme);
|
||||
const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
|
||||
const variant = isSynced ? 'active' : 'default';
|
||||
|
||||
const { isOpen } = this.state;
|
||||
const styles = getStyles(theme);
|
||||
const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
|
||||
const variant = isSynced ? 'active' : 'default';
|
||||
return (
|
||||
<ButtonGroup className={styles.container}>
|
||||
{hasAbsolute && (
|
||||
<ToolbarButton
|
||||
aria-label="Move time range backwards"
|
||||
variant={variant}
|
||||
onClick={onMoveBackward}
|
||||
icon="angle-left"
|
||||
narrow
|
||||
/>
|
||||
)}
|
||||
|
||||
return (
|
||||
<ButtonGroup className={styles.container}>
|
||||
{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">
|
||||
<ToolbarButton
|
||||
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}
|
||||
icon="clock-nine"
|
||||
isOpen={isOpen}
|
||||
variant={variant}
|
||||
>
|
||||
<TimePickerButtonLabel {...this.props} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{isOpen && (
|
||||
<ClickOutsideWrapper includeButtonPress={false} onClick={this.onClose}>
|
||||
<Tooltip content={<TimePickerTooltip timeRange={value} timeZone={timeZone} />} placement="bottom">
|
||||
<ToolbarButton
|
||||
data-testid={selectors.components.TimePicker.openButton}
|
||||
aria-label={`Time range picker with current time range ${formattedRange(value, timeZone)} selected`}
|
||||
aria-controls="TimePickerContent"
|
||||
onClick={onOpen}
|
||||
icon="clock-nine"
|
||||
isOpen={isOpen}
|
||||
variant={variant}
|
||||
>
|
||||
<TimePickerButtonLabel {...props} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{isOpen && (
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<section ref={ref} {...overlayProps}>
|
||||
<TimePickerContent
|
||||
timeZone={timeZone}
|
||||
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onChange={onChange}
|
||||
quickOptions={quickOptions}
|
||||
history={history}
|
||||
showHistory
|
||||
@@ -145,27 +131,27 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
|
||||
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||
hideQuickRanges={hideQuickRanges}
|
||||
/>
|
||||
</ClickOutsideWrapper>
|
||||
)}
|
||||
</section>
|
||||
</FocusScope>
|
||||
)}
|
||||
|
||||
{timeSyncButton}
|
||||
{timeSyncButton}
|
||||
|
||||
{hasAbsolute && (
|
||||
<ToolbarButton
|
||||
aria-label="Move time range forwards"
|
||||
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">
|
||||
<ToolbarButton aria-label="Zoom out time range" onClick={onZoom} icon="search-minus" variant={variant} />
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
<Tooltip content={ZoomOutTooltip} placement="bottom">
|
||||
<ToolbarButton aria-label="Zoom out time range" onClick={onZoom} icon="search-minus" variant={variant} />
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const ZoomOutTooltip = () => (
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useStyles2 } from '../../../themes';
|
||||
import Calendar from 'react-calendar';
|
||||
import { css } from '@emotion/css';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
import { TimePickerCalendarProps } from './TimePickerCalendar';
|
||||
import { GrafanaTheme2, dateTime, dateTimeParse, DateTime, TimeZone } from '@grafana/data';
|
||||
|
||||
export function Body({ onChange, from, to, timeZone }: TimePickerCalendarProps) {
|
||||
const value = inputToValue(from, to);
|
||||
const onCalendarChange = useOnCalendarChange(onChange, timeZone);
|
||||
const styles = useStyles2(getBodyStyles);
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
selectRange={true}
|
||||
next2Label={null}
|
||||
prev2Label={null}
|
||||
className={styles.body}
|
||||
tileClassName={styles.title}
|
||||
value={value}
|
||||
nextLabel={<Icon name="angle-right" />}
|
||||
prevLabel={<Icon name="angle-left" />}
|
||||
onChange={onCalendarChange}
|
||||
locale="en"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Body.displayName = 'Body';
|
||||
|
||||
export function inputToValue(from: DateTime, to: DateTime, invalidDateDefault: Date = new Date()): Date[] {
|
||||
const fromAsDate = from.toDate();
|
||||
const toAsDate = to.toDate();
|
||||
const fromAsValidDate = dateTime(fromAsDate).isValid() ? fromAsDate : invalidDateDefault;
|
||||
const toAsValidDate = dateTime(toAsDate).isValid() ? toAsDate : invalidDateDefault;
|
||||
|
||||
if (fromAsValidDate > toAsValidDate) {
|
||||
return [toAsValidDate, fromAsValidDate];
|
||||
}
|
||||
return [fromAsValidDate, toAsValidDate];
|
||||
}
|
||||
|
||||
function useOnCalendarChange(onChange: (from: DateTime, to: DateTime) => void, timeZone?: TimeZone) {
|
||||
return useCallback(
|
||||
(value: Date | Date[]) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return console.error('onCalendarChange: should be run in selectRange={true}');
|
||||
}
|
||||
|
||||
const from = dateTimeParse(dateInfo(value[0]), { timeZone });
|
||||
const to = dateTimeParse(dateInfo(value[1]), { timeZone });
|
||||
|
||||
onChange(from, to);
|
||||
},
|
||||
[onChange, timeZone]
|
||||
);
|
||||
}
|
||||
|
||||
function dateInfo(date: Date): number[] {
|
||||
return [date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()];
|
||||
}
|
||||
|
||||
export const getBodyStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
title: css`
|
||||
color: ${theme.colors.text};
|
||||
background-color: ${theme.colors.background.primary};
|
||||
font-size: ${theme.typography.size.md};
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
position: relative;
|
||||
}
|
||||
`,
|
||||
body: css`
|
||||
z-index: ${theme.zIndex.modal};
|
||||
background-color: ${theme.colors.background.primary};
|
||||
width: 268px;
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation__arrow,
|
||||
.react-calendar__navigation {
|
||||
padding-top: 4px;
|
||||
background-color: inherit;
|
||||
color: ${theme.colors.text};
|
||||
border: 0;
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays {
|
||||
background-color: inherit;
|
||||
text-align: center;
|
||||
color: ${theme.colors.primary.text};
|
||||
|
||||
abbr {
|
||||
border: 0;
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 4px 0 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.react-calendar__tile,
|
||||
.react-calendar__tile--now {
|
||||
margin-bottom: 4px;
|
||||
background-color: inherit;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation > button:focus,
|
||||
.time-picker-calendar-tile:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active,
|
||||
.react-calendar__tile--active:hover {
|
||||
color: ${theme.colors.primary.contrastText};
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
background: ${theme.colors.primary.main};
|
||||
box-shadow: none;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd,
|
||||
.react-calendar__tile--rangeStart {
|
||||
padding: 0;
|
||||
border: 0px;
|
||||
color: ${theme.colors.primary.contrastText};
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
background: ${theme.colors.primary.main};
|
||||
|
||||
abbr {
|
||||
background-color: ${theme.colors.primary.main};
|
||||
border-radius: 100px;
|
||||
display: block;
|
||||
padding-top: 2px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeStart {
|
||||
border-top-left-radius: 20px;
|
||||
border-bottom-left-radius: 20px;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd {
|
||||
border-top-right-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { useStyles2 } from '../../../themes';
|
||||
import { Button } from '../../Button';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { TimePickerCalendarProps } from './TimePickerCalendar';
|
||||
|
||||
export function Footer({ onClose, onApply }: TimePickerCalendarProps) {
|
||||
const styles = useStyles2(getFooterStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button className={styles.apply} onClick={onApply}>
|
||||
Apply time range
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Footer.displayName = 'Footer';
|
||||
|
||||
const getFooterStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
align-items: stretch;
|
||||
`,
|
||||
apply: css`
|
||||
margin-right: 4px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { TimePickerTitle } from './TimePickerTitle';
|
||||
import { Button } from '../../Button';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { TimePickerCalendarProps } from './TimePickerCalendar';
|
||||
import { useStyles2 } from '../../../themes';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export function Header({ onClose }: TimePickerCalendarProps) {
|
||||
const styles = useStyles2(getHeaderStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<TimePickerTitle>Select a time range</TimePickerTitle>
|
||||
<Button
|
||||
aria-label={selectors.components.TimePicker.calendar.closeButton}
|
||||
icon="times"
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Header.displayName = 'Header';
|
||||
|
||||
const getHeaderStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 7px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { dateTime } from '@grafana/data';
|
||||
|
||||
import { inputToValue } from './TimePickerCalendar';
|
||||
import { inputToValue } from './CalendarBody';
|
||||
|
||||
describe('inputToValue', () => {
|
||||
describe('when called with valid dates', () => {
|
||||
|
||||
+31
-237
@@ -1,16 +1,16 @@
|
||||
import React, { FormEvent, memo, useCallback } from 'react';
|
||||
import React, { FormEvent, memo } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import Calendar from 'react-calendar';
|
||||
import { dateTime, DateTime, dateTimeParse, GrafanaTheme2, TimeZone } from '@grafana/data';
|
||||
import { stylesFactory, useTheme2 } from '../../../themes';
|
||||
import { TimePickerTitle } from './TimePickerTitle';
|
||||
import { Button } from '../../Button';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
import { DateTime, GrafanaTheme2, TimeZone } from '@grafana/data';
|
||||
import { useTheme2 } from '../../../themes';
|
||||
import { Header } from './CalendarHeader';
|
||||
import { Portal } from '../../Portal/Portal';
|
||||
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
import { Body } from './CalendarBody';
|
||||
import { Footer } from './CalendarFooter';
|
||||
|
||||
export const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed = false) => {
|
||||
export const getStyles = (theme: GrafanaTheme2, isReversed = false) => {
|
||||
return {
|
||||
container: css`
|
||||
top: -1px;
|
||||
@@ -57,134 +57,9 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed = false
|
||||
text-align: center;
|
||||
`,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getFooterStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
align-items: stretch;
|
||||
`,
|
||||
apply: css`
|
||||
margin-right: 4px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export const getBodyStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
title: css`
|
||||
color: ${theme.colors.text};
|
||||
background-color: ${theme.colors.background.primary};
|
||||
font-size: ${theme.typography.size.md};
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
position: relative;
|
||||
}
|
||||
`,
|
||||
body: css`
|
||||
z-index: ${theme.zIndex.modal};
|
||||
background-color: ${theme.colors.background.primary};
|
||||
width: 268px;
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation__arrow,
|
||||
.react-calendar__navigation {
|
||||
padding-top: 4px;
|
||||
background-color: inherit;
|
||||
color: ${theme.colors.text};
|
||||
border: 0;
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays {
|
||||
background-color: inherit;
|
||||
text-align: center;
|
||||
color: ${theme.colors.primary.text};
|
||||
|
||||
abbr {
|
||||
border: 0;
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 4px 0 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.react-calendar__tile,
|
||||
.react-calendar__tile--now {
|
||||
margin-bottom: 4px;
|
||||
background-color: inherit;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label,
|
||||
.react-calendar__navigation > button:focus,
|
||||
.time-picker-calendar-tile:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active,
|
||||
.react-calendar__tile--active:hover {
|
||||
color: ${theme.colors.primary.contrastText};
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
background: ${theme.colors.primary.main};
|
||||
box-shadow: none;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd,
|
||||
.react-calendar__tile--rangeStart {
|
||||
padding: 0;
|
||||
border: 0px;
|
||||
color: ${theme.colors.primary.contrastText};
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
background: ${theme.colors.primary.main};
|
||||
|
||||
abbr {
|
||||
background-color: ${theme.colors.primary.main};
|
||||
border-radius: 100px;
|
||||
display: block;
|
||||
padding-top: 2px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeStart {
|
||||
border-top-left-radius: 20px;
|
||||
border-bottom-left-radius: 20px;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd {
|
||||
border-top-right-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getHeaderStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 7px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface Props {
|
||||
export interface TimePickerCalendarProps {
|
||||
isOpen: boolean;
|
||||
from: DateTime;
|
||||
to: DateTime;
|
||||
@@ -198,10 +73,12 @@ interface Props {
|
||||
|
||||
const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopPropagation();
|
||||
|
||||
export const TimePickerCalendar = memo<Props>((props) => {
|
||||
function TimePickerCalendar(props: TimePickerCalendarProps) {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, props.isReversed);
|
||||
const { isOpen, isFullscreen } = props;
|
||||
const ref = React.createRef<HTMLElement>();
|
||||
const { overlayProps } = useOverlay(props, ref);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
@@ -209,118 +86,35 @@ export const TimePickerCalendar = memo<Props>((props) => {
|
||||
|
||||
if (isFullscreen) {
|
||||
return (
|
||||
<ClickOutsideWrapper onClick={props.onClose}>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<section
|
||||
className={styles.container}
|
||||
onClick={stopPropagation}
|
||||
aria-label={selectors.components.TimePicker.calendar}
|
||||
aria-label={selectors.components.TimePicker.calendar.label}
|
||||
ref={ref}
|
||||
{...overlayProps}
|
||||
>
|
||||
<Header {...props} />
|
||||
<Body {...props} />
|
||||
</section>
|
||||
</ClickOutsideWrapper>
|
||||
</FocusScope>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div className={styles.modal} onClick={stopPropagation}>
|
||||
<div className={styles.content} aria-label={selectors.components.TimePicker.calendar}>
|
||||
<Header {...props} />
|
||||
<Body {...props} />
|
||||
<Footer {...props} />
|
||||
</div>
|
||||
</div>
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<section className={styles.modal} onClick={stopPropagation} ref={ref} {...overlayProps}>
|
||||
<div className={styles.content} aria-label={selectors.components.TimePicker.calendar.label}>
|
||||
<Header {...props} />
|
||||
<Body {...props} />
|
||||
<Footer {...props} />
|
||||
</div>
|
||||
</section>
|
||||
</FocusScope>
|
||||
<div className={styles.backdrop} onClick={stopPropagation} />
|
||||
</Portal>
|
||||
);
|
||||
});
|
||||
|
||||
}
|
||||
export default memo(TimePickerCalendar);
|
||||
TimePickerCalendar.displayName = 'TimePickerCalendar';
|
||||
|
||||
const Header = memo<Props>(({ onClose }) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getHeaderStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<TimePickerTitle>Select a time range</TimePickerTitle>
|
||||
<Icon name="times" onClick={onClose} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Header.displayName = 'Header';
|
||||
|
||||
export const Body = memo<Props>(({ onChange, from, to, timeZone }) => {
|
||||
const value = inputToValue(from, to);
|
||||
const theme = useTheme2();
|
||||
const onCalendarChange = useOnCalendarChange(onChange, timeZone);
|
||||
const styles = getBodyStyles(theme);
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
selectRange={true}
|
||||
next2Label={null}
|
||||
prev2Label={null}
|
||||
className={styles.body}
|
||||
tileClassName={styles.title}
|
||||
value={value}
|
||||
nextLabel={<Icon name="angle-right" />}
|
||||
prevLabel={<Icon name="angle-left" />}
|
||||
onChange={onCalendarChange}
|
||||
locale="en"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Body.displayName = 'Body';
|
||||
|
||||
const Footer = memo<Props>(({ onClose, onApply }) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getFooterStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button className={styles.apply} onClick={onApply}>
|
||||
Apply time range
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Footer.displayName = 'Footer';
|
||||
|
||||
export function inputToValue(from: DateTime, to: DateTime, invalidDateDefault: Date = new Date()): Date[] {
|
||||
const fromAsDate = from.toDate();
|
||||
const toAsDate = to.toDate();
|
||||
const fromAsValidDate = dateTime(fromAsDate).isValid() ? fromAsDate : invalidDateDefault;
|
||||
const toAsValidDate = dateTime(toAsDate).isValid() ? toAsDate : invalidDateDefault;
|
||||
|
||||
if (fromAsValidDate > toAsValidDate) {
|
||||
return [toAsValidDate, fromAsValidDate];
|
||||
}
|
||||
return [fromAsValidDate, toAsValidDate];
|
||||
}
|
||||
|
||||
function useOnCalendarChange(onChange: (from: DateTime, to: DateTime) => void, timeZone?: TimeZone) {
|
||||
return useCallback(
|
||||
(value: Date | Date[]) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return console.error('onCalendarChange: should be run in selectRange={true}');
|
||||
}
|
||||
|
||||
const from = dateTimeParse(dateInfo(value[0]), { timeZone });
|
||||
const to = dateTimeParse(dateInfo(value[1]), { timeZone });
|
||||
|
||||
onChange(from, to);
|
||||
},
|
||||
[onChange, timeZone]
|
||||
);
|
||||
}
|
||||
|
||||
function dateInfo(date: Date): number[] {
|
||||
return [date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()];
|
||||
}
|
||||
|
||||
+4
-4
@@ -94,22 +94,22 @@ describe('TimePickerContent', () => {
|
||||
|
||||
it('renders with absolute picker when absolute value and quick ranges are visible', () => {
|
||||
renderComponent({ value: absoluteValue, isFullscreen: false });
|
||||
expect(screen.queryByLabelText(/timepicker from field/i)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with absolute picker when absolute value and quick ranges are hidden', () => {
|
||||
renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true });
|
||||
expect(screen.queryByLabelText(/timepicker from field/i)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without absolute picker when narrow screen and quick ranges are visible', () => {
|
||||
renderComponent({ value: relativeValue, isFullscreen: false });
|
||||
expect(screen.queryByLabelText(/timepicker from field/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/time range from field/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with absolute picker when narrow screen and quick ranges are hidden', () => {
|
||||
renderComponent({ value: relativeValue, isFullscreen: false, hideQuickRanges: true });
|
||||
expect(screen.queryByLabelText(/timepicker from field/i)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without timezone picker', () => {
|
||||
|
||||
+23
-15
@@ -31,20 +31,22 @@ function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc'): TimeRan
|
||||
|
||||
describe('TimeRangeForm', () => {
|
||||
it('should render form correcty', () => {
|
||||
const { getByLabelText, getByText } = setup();
|
||||
const { getByLabelText, getByText, getAllByRole } = setup();
|
||||
const { TimePicker } = selectors.components;
|
||||
|
||||
expect(getByText('Apply time range')).toBeInTheDocument();
|
||||
expect(getAllByRole('button', { name: TimePicker.calendar.openButton })).toHaveLength(2);
|
||||
expect(getByLabelText(TimePicker.fromField)).toBeInTheDocument();
|
||||
expect(getByLabelText(TimePicker.toField)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display calendar when clicking the from input field', () => {
|
||||
const { getByLabelText } = setup();
|
||||
it('should display calendar when clicking the calendar icon', () => {
|
||||
const { getByLabelText, getAllByRole } = setup();
|
||||
const { TimePicker } = selectors.components;
|
||||
const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton });
|
||||
|
||||
fireEvent.focus(getByLabelText(TimePicker.fromField));
|
||||
expect(getByLabelText(TimePicker.calendar)).toBeInTheDocument();
|
||||
fireEvent.click(openCalendarButton[0]);
|
||||
expect(getByLabelText(TimePicker.calendar.label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have passed time range entered in form', () => {
|
||||
@@ -58,26 +60,31 @@ describe('TimeRangeForm', () => {
|
||||
expect(getByLabelText(TimePicker.toField)).toHaveValue(toValue);
|
||||
});
|
||||
|
||||
it('should display calendar when clicking the to input field', () => {
|
||||
const { getByLabelText } = setup();
|
||||
it('should close calendar when clicking the close icon', () => {
|
||||
const { queryByLabelText, getAllByRole, getByRole } = setup();
|
||||
const { TimePicker } = selectors.components;
|
||||
const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton });
|
||||
|
||||
fireEvent.focus(getByLabelText(TimePicker.toField));
|
||||
expect(getByLabelText(TimePicker.calendar)).toBeInTheDocument();
|
||||
fireEvent.click(openCalendarButton[0]);
|
||||
expect(getByRole('button', { name: TimePicker.calendar.closeButton })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(getByRole('button', { name: TimePicker.calendar.closeButton }));
|
||||
expect(queryByLabelText(TimePicker.calendar.label)).toBeNull();
|
||||
});
|
||||
|
||||
it('should not display calendar without clicking any input field', () => {
|
||||
it('should not display calendar without clicking the calendar icon', () => {
|
||||
const { queryByLabelText } = setup();
|
||||
const { TimePicker } = selectors.components;
|
||||
|
||||
expect(queryByLabelText(TimePicker.calendar)).toBeNull();
|
||||
expect(queryByLabelText(TimePicker.calendar.label)).toBeNull();
|
||||
});
|
||||
|
||||
it('should have passed time range selected in calendar', () => {
|
||||
const { getByLabelText, getCalendarDayByLabelText } = setup();
|
||||
const { getAllByRole, getCalendarDayByLabelText } = setup();
|
||||
const { TimePicker } = selectors.components;
|
||||
const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton });
|
||||
|
||||
fireEvent.focus(getByLabelText(TimePicker.toField));
|
||||
fireEvent.click(openCalendarButton[0]);
|
||||
const from = getCalendarDayByLabelText('June 17, 2021');
|
||||
const to = getCalendarDayByLabelText('June 19, 2021');
|
||||
|
||||
@@ -86,10 +93,11 @@ describe('TimeRangeForm', () => {
|
||||
});
|
||||
|
||||
it('should select correct time range in calendar when having a custom time zone', () => {
|
||||
const { getByLabelText, getCalendarDayByLabelText } = setup(defaultTimeRange, 'Asia/Tokyo');
|
||||
const { getAllByRole, getCalendarDayByLabelText } = setup(defaultTimeRange, 'Asia/Tokyo');
|
||||
const { TimePicker } = selectors.components;
|
||||
const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton });
|
||||
|
||||
fireEvent.focus(getByLabelText(TimePicker.toField));
|
||||
fireEvent.click(openCalendarButton[1]);
|
||||
const from = getCalendarDayByLabelText('June 17, 2021');
|
||||
const to = getCalendarDayByLabelText('June 19, 2021');
|
||||
|
||||
|
||||
+10
-15
@@ -18,7 +18,7 @@ import { useStyles2 } from '../../..';
|
||||
import { Button } from '../../Button';
|
||||
import { Field } from '../../Forms/Field';
|
||||
import { Input } from '../../Input/Input';
|
||||
import { TimePickerCalendar } from './TimePickerCalendar';
|
||||
import TimePickerCalendar from './TimePickerCalendar';
|
||||
|
||||
interface Props {
|
||||
isFullscreen: boolean;
|
||||
@@ -65,16 +65,6 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
||||
[setOpen]
|
||||
);
|
||||
|
||||
const onFocus = useCallback(
|
||||
(event: FormEvent<HTMLElement>) => {
|
||||
if (!isFullscreen) {
|
||||
return;
|
||||
}
|
||||
onOpen(event);
|
||||
},
|
||||
[isFullscreen, onOpen]
|
||||
);
|
||||
|
||||
const onApply = useCallback(
|
||||
(e: FormEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -111,15 +101,21 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const icon = isFullscreen ? null : <Button icon="calendar-alt" variant="secondary" onClick={onOpen} />;
|
||||
const icon = (
|
||||
<Button
|
||||
aria-label={selectors.components.TimePicker.calendar.openButton}
|
||||
icon="calendar-alt"
|
||||
variant="secondary"
|
||||
onClick={onOpen}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div aria-label="Absolute time ranges">
|
||||
<div>
|
||||
<div className={style.fieldContainer}>
|
||||
<Field label="From" invalid={from.invalid} error={from.errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onFocus={onFocus}
|
||||
onChange={(event) => onChange(event.currentTarget.value, to.value)}
|
||||
addonAfter={icon}
|
||||
aria-label={selectors.components.TimePicker.fromField}
|
||||
@@ -132,7 +128,6 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
||||
<Field label="To" invalid={to.invalid} error={to.errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onFocus={onFocus}
|
||||
onChange={(event) => onChange(from.value, event.currentTarget.value)}
|
||||
addonAfter={icon}
|
||||
aria-label={selectors.components.TimePicker.toField}
|
||||
|
||||
@@ -1764,7 +1764,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
|
||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
|
||||
version: 7.15.4
|
||||
resolution: "@babel/runtime@npm:7.15.4"
|
||||
dependencies:
|
||||
@@ -2247,6 +2247,55 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/ecma402-abstract@npm:1.9.9":
|
||||
version: 1.9.9
|
||||
resolution: "@formatjs/ecma402-abstract@npm:1.9.9"
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher": 0.2.21
|
||||
tslib: ^2.1.0
|
||||
checksum: f0834658319ed9add4a64ee13704f1616c0af771038debe38ec7ba96a79a65e684b8e363c1573841798856d87c832c29a8be249b50c41cf46980a1a0a38c5cf7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/fast-memoize@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@formatjs/fast-memoize@npm:1.2.0"
|
||||
dependencies:
|
||||
tslib: ^2.1.0
|
||||
checksum: fbc94672c4d0abc595c5680052c1fdaa652e7ffca98175a631a19ec44c5b6e2861ce0410c8ea3c4b46827aad5d229f89c0143a2ccf34ca2fbff79bdf63d27377
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/icu-messageformat-parser@npm:2.0.12":
|
||||
version: 2.0.12
|
||||
resolution: "@formatjs/icu-messageformat-parser@npm:2.0.12"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": 1.9.9
|
||||
"@formatjs/icu-skeleton-parser": 1.2.13
|
||||
tslib: ^2.1.0
|
||||
checksum: e895428d242b72454fbaab6fd32638c4f963294ea7999a245004bf9326daac8669153995567c8161b55847197f00fb519a0bbbfe06a89dd600ccf45736ac26c7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/icu-skeleton-parser@npm:1.2.13":
|
||||
version: 1.2.13
|
||||
resolution: "@formatjs/icu-skeleton-parser@npm:1.2.13"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": 1.9.9
|
||||
tslib: ^2.1.0
|
||||
checksum: 4d17783f2b4a4e6fa3aa3901fb6249706a7dcec25811c86e979a76e14500d9f2c1034bbb4bf4a47b46ed6c027bdbd9353eda96ad1b6e7bf7a693267384526708
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-localematcher@npm:0.2.21":
|
||||
version: 0.2.21
|
||||
resolution: "@formatjs/intl-localematcher@npm:0.2.21"
|
||||
dependencies:
|
||||
tslib: ^2.1.0
|
||||
checksum: d766eb8ce8b2628d781fdb34fd0833a0a1b28f20e70a72dfabbca27cf02bd1b994a72c357b2b3d4888bc20c33b6b7cc7e10e92847ec228a40745a2e84d8d2e24
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@gar/promisify@npm:^1.0.1":
|
||||
version: 1.1.2
|
||||
resolution: "@gar/promisify@npm:1.1.2"
|
||||
@@ -2602,6 +2651,8 @@ __metadata:
|
||||
"@grafana/tsconfig": ^1.0.0-rc1
|
||||
"@monaco-editor/react": 4.2.2
|
||||
"@popperjs/core": 2.5.4
|
||||
"@react-aria/focus": 3.4.1
|
||||
"@react-aria/overlays": 3.7.2
|
||||
"@rollup/plugin-commonjs": 16.0.0
|
||||
"@rollup/plugin-image": 2.0.5
|
||||
"@rollup/plugin-node-resolve": 10.0.0
|
||||
@@ -2715,6 +2766,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@internationalized/message@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "@internationalized/message@npm:3.0.2"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
intl-messageformat: ^9.6.12
|
||||
checksum: 3448ac0be3e6ed5359a8db2e423e6f0ff75d414dc08f24fb05097401f6358c2efe8b43913f4bb85a84509c3b1bc378fc03aa8c6681a1305e61e4be3bbf4b1bd2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@internationalized/number@npm:^3.0.2":
|
||||
version: 3.0.3
|
||||
resolution: "@internationalized/number@npm:3.0.3"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
checksum: 338cba3cde1385586ee9c1c913809ddc41f48349096b34cd9dd337e34798685771923cceca693bc708991b38f5d72dc02ddfdecd829f018369825f73f92bb890
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@istanbuljs/load-nyc-config@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "@istanbuljs/load-nyc-config@npm:1.1.0"
|
||||
@@ -4598,6 +4668,165 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/focus@npm:3.4.1":
|
||||
version: 3.4.1
|
||||
resolution: "@react-aria/focus@npm:3.4.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
"@react-aria/interactions": ^3.5.1
|
||||
"@react-aria/utils": ^3.8.2
|
||||
"@react-types/shared": ^3.8.0
|
||||
clsx: ^1.1.1
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: 14b500a506068a6a9223a4f27f71e380a098586af26adc01bf7aff9261684cbf53b3319450297d1788ec5cac124ee29a6975eae56cc7d4a5114a37595a61c581
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/i18n@npm:^3.3.2":
|
||||
version: 3.3.2
|
||||
resolution: "@react-aria/i18n@npm:3.3.2"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
"@internationalized/message": ^3.0.2
|
||||
"@internationalized/number": ^3.0.2
|
||||
"@react-aria/ssr": ^3.0.3
|
||||
"@react-aria/utils": ^3.8.2
|
||||
"@react-types/shared": ^3.8.0
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: f1065adc6aeda5c6888f9035ed130214ca1f86a3220dae21326a98469805e446df71f25e2a64ed30fbc9722a99e8fdb368a68e062bf406d60041a7b04ba94f97
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/interactions@npm:^3.5.1":
|
||||
version: 3.6.0
|
||||
resolution: "@react-aria/interactions@npm:3.6.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
"@react-aria/utils": ^3.9.0
|
||||
"@react-types/shared": ^3.9.0
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: 8f87574fdb68816eac5af92e87c8a2046564d12d6855061a909661b9036083776f29df22201c93f2576378f4596d253ede610c3bd762e74844cb642f59d014b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/overlays@npm:3.7.2":
|
||||
version: 3.7.2
|
||||
resolution: "@react-aria/overlays@npm:3.7.2"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
"@react-aria/i18n": ^3.3.2
|
||||
"@react-aria/interactions": ^3.5.1
|
||||
"@react-aria/utils": ^3.8.2
|
||||
"@react-aria/visually-hidden": ^3.2.3
|
||||
"@react-stately/overlays": ^3.1.3
|
||||
"@react-types/button": ^3.4.1
|
||||
"@react-types/overlays": ^3.5.1
|
||||
dom-helpers: ^3.3.1
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
react-dom: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: e9681409940ac092db616fb8e868cf480be3fdbea7f488c7e8a5de9f3a5c07174f00d715cc9691895ec74e8961efb91c5950c1d51502924499e3a2917480a5a2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/ssr@npm:^3.0.3, @react-aria/ssr@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "@react-aria/ssr@npm:3.1.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: ae376d45dbd19e990891353a2eee10c13f3b54dbd807a2e528928983741c0ea9dbfefcc132c46c685e53440a53f7be82bd36a5c61bdfaec3c3518f02ae444e7f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/utils@npm:^3.8.2, @react-aria/utils@npm:^3.9.0":
|
||||
version: 3.9.0
|
||||
resolution: "@react-aria/utils@npm:3.9.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
"@react-aria/ssr": ^3.1.0
|
||||
"@react-stately/utils": ^3.2.2
|
||||
"@react-types/shared": ^3.9.0
|
||||
clsx: ^1.1.1
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: ef465effe2d007870342e27e73b179ef51dc21403918a8807931a785606e6199b67e3b6b5e1f0a23ecc8200f4bd8dba558bcdfb1eb6a322955d55c7089ef1136
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/visually-hidden@npm:^3.2.3":
|
||||
version: 3.2.3
|
||||
resolution: "@react-aria/visually-hidden@npm:3.2.3"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
"@react-aria/interactions": ^3.5.1
|
||||
"@react-aria/utils": ^3.8.2
|
||||
clsx: ^1.1.1
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: 3a7ebc55e54f51ac9118afaa2f032313adf03278b24e03a0e60c40d5cbf501d32c371314b85ab9590535b8e6873927f71f44be34b6adaca7c3e14372c2ecde90
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-stately/overlays@npm:^3.1.3":
|
||||
version: 3.1.3
|
||||
resolution: "@react-stately/overlays@npm:3.1.3"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
"@react-stately/utils": ^3.2.2
|
||||
"@react-types/overlays": ^3.5.1
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: 7319502c2cfd9d86490145b314bb6c7505b8a4a18ba2db77d961d64e7885d3808b3f3079541a46dd1027bc1590e0dd2822245cc7acbcb31f2ad7e86929be81e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-stately/utils@npm:^3.2.2":
|
||||
version: 3.2.2
|
||||
resolution: "@react-stately/utils@npm:3.2.2"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.6.2
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: c51a6f4f173c7db5f00e22f5984427f1b3613e6d2e2eb8ee536448920962975cf04ebe52e07ffb0712b34a7a4e5aa309d03e6e2c364441d7765f84cfefdb5709
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-types/button@npm:^3.4.1":
|
||||
version: 3.4.1
|
||||
resolution: "@react-types/button@npm:3.4.1"
|
||||
dependencies:
|
||||
"@react-types/shared": ^3.8.0
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: 3d5c05ce228117b2c57085e9d2a5827bb4f2c5a6303902faf355495badd69fcbe751963be2266c7f79ba04681c98e0ac6f087aab3554089a5fd582493f21d03f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-types/overlays@npm:^3.5.1":
|
||||
version: 3.5.1
|
||||
resolution: "@react-types/overlays@npm:3.5.1"
|
||||
dependencies:
|
||||
"@react-types/shared": ^3.8.0
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: a068dce4cf1f066c5015a2ef1e345815163f8cee005854d0cc1c6fd548447f1ed1afdee5b3b5407bf8f64101f4ce9dd87b65dee4546035a88ed48a7ae693db83
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-types/shared@npm:^3.8.0, @react-types/shared@npm:^3.9.0":
|
||||
version: 3.9.0
|
||||
resolution: "@react-types/shared@npm:3.9.0"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1
|
||||
checksum: 0be11ba4234767d47707dff8601605f46bfd9e3e07dcd3213119517f9a627cca4b71e9030478d3941241d8a6be8b8bc00e92278342daad8f7397bc9eee290f1c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@reduxjs/toolkit@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@reduxjs/toolkit@npm:1.6.1"
|
||||
@@ -14312,6 +14541,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom-helpers@npm:^3.3.1":
|
||||
version: 3.4.0
|
||||
resolution: "dom-helpers@npm:3.4.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.1.2
|
||||
checksum: 58d9f1c4a96daf77eddc63ae1236b826e1cddd6db66bbf39b18d7e21896d99365b376593352d52a60969d67fa4a8dbef26adc1439fa2c1b355efa37cacbaf637
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom-helpers@npm:^5.0.1":
|
||||
version: 5.2.1
|
||||
resolution: "dom-helpers@npm:5.2.1"
|
||||
@@ -18968,6 +19206,17 @@ fsevents@~2.1.2:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"intl-messageformat@npm:^9.6.12":
|
||||
version: 9.9.2
|
||||
resolution: "intl-messageformat@npm:9.9.2"
|
||||
dependencies:
|
||||
"@formatjs/fast-memoize": 1.2.0
|
||||
"@formatjs/icu-messageformat-parser": 2.0.12
|
||||
tslib: ^2.1.0
|
||||
checksum: a9dbe98375f6c735f0ced03f9ba6bb599afbc96c109238fbb52a8753b664649a78d98752c92813a6a535dea5c67a659801f29c4c32c57db8608c655203794564
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"invariant@npm:^2.2.2, invariant@npm:^2.2.3, invariant@npm:^2.2.4":
|
||||
version: 2.2.4
|
||||
resolution: "invariant@npm:2.2.4"
|
||||
|
||||
Reference in New Issue
Block a user