diff --git a/src/components/chart_container.tsx b/src/components/chart_container.tsx index bcd973a77f..69cd25a348 100644 --- a/src/components/chart_container.tsx +++ b/src/components/chart_container.tsx @@ -22,7 +22,12 @@ import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { SettingsSpec } from '../specs'; -import { onMouseUp, onMouseDown, onPointerMove } from '../state/actions/mouse'; +import { onKeyPress as onKeyPressAction } from '../state/actions/key'; +import { + onMouseUp as onMouseUpAction, + onMouseDown as onMouseDownAction, + onPointerMove as onPointerMoveAction, +} from '../state/actions/mouse'; import { GlobalChartState, BackwardRef } from '../state/chart_state'; import { getInternalChartRendererSelector } from '../state/selectors/get_chart_type_components'; import { getInternalPointerCursor } from '../state/selectors/get_internal_cursor_pointer'; @@ -46,9 +51,10 @@ interface ChartContainerComponentStateProps { ) => JSX.Element | null; } interface ChartContainerComponentDispatchProps { - onPointerMove: typeof onPointerMove; - onMouseUp: typeof onMouseUp; - onMouseDown: typeof onMouseDown; + onPointerMove: typeof onPointerMoveAction; + onMouseUp: typeof onMouseUpAction; + onMouseDown: typeof onMouseDownAction; + onKeyPress: typeof onKeyPressAction; } interface ChartContainerComponentOwnProps { @@ -74,6 +80,7 @@ class ChartContainerComponent extends React.Component { if (isChartEmpty) { return; } + onPointerMove( { x: offsetX, @@ -101,9 +108,13 @@ class ChartContainerComponent extends React.Component { if (isChartEmpty) { return; } + if (isBrushingAvailable) { window.addEventListener('mouseup', this.handleBrushEnd); } + + window.addEventListener('keyup', this.handleKeyUp); + onMouseDown( { x: offsetX, @@ -118,6 +129,9 @@ class ChartContainerComponent extends React.Component { if (isChartEmpty) { return; } + + window.removeEventListener('keyup', this.handleKeyUp); + onMouseUp( { x: offsetX, @@ -127,10 +141,22 @@ class ChartContainerComponent extends React.Component { ); }; + handleKeyUp = ({ key }: KeyboardEvent) => { + window.removeEventListener('keyup', this.handleKeyUp); + + const { isChartEmpty, onKeyPress } = this.props; + if (isChartEmpty) { + return; + } + + onKeyPress(key); + }; + handleBrushEnd = () => { const { onMouseUp } = this.props; window.removeEventListener('mouseup', this.handleBrushEnd); + requestAnimationFrame(() => { onMouseUp( { @@ -179,9 +205,10 @@ class ChartContainerComponent extends React.Component { const mapDispatchToProps = (dispatch: Dispatch): ChartContainerComponentDispatchProps => bindActionCreators( { - onPointerMove, - onMouseUp, - onMouseDown, + onPointerMove: onPointerMoveAction, + onMouseUp: onMouseUpAction, + onMouseDown: onMouseDownAction, + onKeyPress: onKeyPressAction, }, dispatch, ); diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index 4b17236e8d..4cd570db79 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -21,6 +21,7 @@ import { ChartActions } from './chart'; import { ChartSettingsActions } from './chart_settings'; import { ColorsActions } from './colors'; import { EventsActions } from './events'; +import { KeyActions } from './key'; import { LegendActions } from './legend'; import { MouseActions } from './mouse'; import { SpecActions } from './specs'; @@ -33,4 +34,5 @@ export type StateActions = | LegendActions | EventsActions | MouseActions + | KeyActions | ColorsActions; diff --git a/src/state/actions/key.ts b/src/state/actions/key.ts new file mode 100644 index 0000000000..483de47c1b --- /dev/null +++ b/src/state/actions/key.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const ON_KEY_UP = 'ON_KEY_UP'; + +interface KeyUpAction { + type: typeof ON_KEY_UP; + /** + * Keyboard key from event + */ + key: string; +} + +/** + * Action called on `keyup` event + * @param key keyboard key + * @internal + */ +export function onKeyPress(key: string): KeyUpAction { + return { type: ON_KEY_UP, key }; +} + +/** @internal */ +export type KeyActions = KeyUpAction; diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index bdf695da6f..d46123181f 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -42,6 +42,7 @@ import { interactionsReducer } from './reducers/interactions'; import { getInternalIsInitializedSelector, InitStatus } from './selectors/get_internal_is_intialized'; import { getLegendItemsSelector } from './selectors/get_legend_items'; import { LegendItemLabel } from './selectors/get_legend_items_labels'; +import { getInitialPointerState } from './utils'; export type BackwardRef = () => React.RefObject; @@ -252,20 +253,7 @@ export const getInitialState = (chartId: string): GlobalChartState => ({ chartType: null, internalChartState: null, interactions: { - pointer: { - dragging: false, - current: { - position: { - x: -1, - y: -1, - }, - time: 0, - }, - down: null, - up: null, - lastDrag: null, - lastClick: null, - }, + pointer: getInitialPointerState(), legendCollapsed: false, highlightedLegendItemKey: null, deselectedDataSeries: [], diff --git a/src/state/reducers/interactions.ts b/src/state/reducers/interactions.ts index 175d60192e..dae848510f 100644 --- a/src/state/reducers/interactions.ts +++ b/src/state/reducers/interactions.ts @@ -20,6 +20,7 @@ import { getSeriesIndex } from '../../chart_types/xy_chart/utils/series'; import { LegendItem } from '../../commons/legend'; import { SeriesIdentifier } from '../../commons/series_id'; +import { ON_KEY_UP, KeyActions } from '../actions/key'; import { ON_TOGGLE_LEGEND, ON_LEGEND_ITEM_OUT, @@ -30,14 +31,25 @@ import { } from '../actions/legend'; import { ON_MOUSE_DOWN, ON_MOUSE_UP, ON_POINTER_MOVE, MouseActions } from '../actions/mouse'; import { InteractionsState } from '../chart_state'; +import { getInitialPointerState } from '../utils'; /** @internal */ export function interactionsReducer( state: InteractionsState, - action: LegendActions | MouseActions, + action: LegendActions | MouseActions | KeyActions, legendItems: LegendItem[], ): InteractionsState { switch (action.type) { + case ON_KEY_UP: + if (action.key === 'Escape') { + return { + ...state, + pointer: getInitialPointerState(), + }; + } + + return state; + case ON_POINTER_MOVE: return { ...state, diff --git a/src/state/utils.ts b/src/state/utils.ts index 0e7148d345..77be56cbbd 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -43,3 +43,19 @@ export function isClicking(prevClick: PointerState | null, lastClick: PointerSta } return false; } + +/** @internal */ +export const getInitialPointerState = () => ({ + dragging: false, + current: { + position: { + x: -1, + y: -1, + }, + time: 0, + }, + down: null, + up: null, + lastDrag: null, + lastClick: null, +});