Skip to content

Commit

Permalink
feat(layer): implement keyboard event handlers (uber#5080)
Browse files Browse the repository at this point in the history
* feat(layer): implement keyboard event handlers

* feat(layer): implement keyboard event handlers

* test(vrt): update visual snapshots for 22b9052 [skip ci]

Co-authored-by: Chase Starr <[email protected]>
Co-authored-by: UberOpenSourceBot <[email protected]>
  • Loading branch information
3 people authored Aug 10, 2022
1 parent 0a7fa59 commit 7694aa9
Show file tree
Hide file tree
Showing 19 changed files with 442 additions and 4 deletions.
12 changes: 12 additions & 0 deletions documentation-site/pages/components/layer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,16 @@ greater than the highest `z-index` value of all top level stacking contexts in y
<TetherBasic />
</Example>

## Layer events

The `Layer` component supports the following events:

- onEscape
- onDocumentClick
- onKeyDown
- onKeyPress
- onKeyUp

Events are handled by `LayerManager` and passed only to the top-most `Layer`. That means that the event handler function will be triggered only for the active layer.

<Exports component={LayerExports} title="Layer exports" path="baseui/layer" />
149 changes: 149 additions & 0 deletions src/layer/__tests__/key-handlers.scenario.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
Copyright (c) Uber Technologies, Inc.
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
import * as React from 'react';
import { Layer, TetherBehavior, TETHER_PLACEMENT } from '..';
import { Block } from '../../block';
import { Button } from '../../button';
import type { NormalizedOffsets } from '../types';

function BlockComponent(props) {
const { children, forwardedRef, offset, color, ...restProps } = props;
return (
<Block
ref={forwardedRef}
position="absolute"
top={`${offset.top}px` || '50%'}
left={`${offset.left}px` || '50%'}
width="200px"
paddingTop="20px"
paddingBottom="20px"
paddingLeft="20px"
paddingRight="20px"
backgroundColor={color}
overrides={{
Block: {
style: {
textAlign: 'center',
},
},
}}
{...restProps}
>
{children}
</Block>
);
}

export class Scenario extends React.Component<
{},
{
isFirstOpen: boolean;
isSecondOpen: boolean;
isFirstMounted: boolean;
isSecondMounted: boolean;
offset1: {
top: number;
left: number;
};
offset2: {
top: number;
left: number;
};
}
> {
anchorRef1 = React.createRef<HTMLButtonElement>();
popperRef1 = React.createRef<HTMLElement>();
anchorRef2 = React.createRef<HTMLButtonElement>();
popperRef2 = React.createRef<HTMLElement>();

state = {
isFirstOpen: false,
isSecondOpen: false,
isFirstMounted: false,
isSecondMounted: false,
offset1: { top: 0, left: 0 },
offset2: { top: 0, left: 0 },
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPopperUpdate = (order: 1 | 2, normalizedOffsets: NormalizedOffsets, _) => {
// @ts-expect-error partial state update
this.setState({
[`offset${order}`]: normalizedOffsets.popper,
});
};

render() {
return (
<Block display="flex" justifyContent="flex-start" alignItems="center">
<Block>
<Button ref={this.anchorRef1} onClick={() => this.setState({ isFirstOpen: true })}>
Render Yellow Layer
</Button>
{this.state.isFirstOpen ? (
<Layer
onMount={() => this.setState({ isFirstMounted: true })}
onUnmount={() => this.setState({ isFirstMounted: false })}
onKeyPress={(e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
this.setState({ isFirstOpen: false });
}
}}
>
<TetherBehavior
anchorRef={this.anchorRef1.current}
popperRef={this.popperRef1.current}
onPopperUpdate={(...args) => this.onPopperUpdate(1, ...args)}
placement={TETHER_PLACEMENT.right}
>
<BlockComponent
forwardedRef={this.popperRef1}
offset={this.state.offset1}
color="rgba(255, 255, 190, 0.86)"
>
Press &quot;Enter&quot; to Close
</BlockComponent>
</TetherBehavior>
</Layer>
) : null}
<Block padding="5px" />
<Button ref={this.anchorRef2} onClick={() => this.setState({ isSecondOpen: true })}>
Render Green Layer
</Button>
{this.state.isSecondOpen ? (
<Layer
onMount={() => this.setState({ isSecondMounted: true })}
onUnmount={() => this.setState({ isSecondMounted: false })}
onKeyPress={(e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
this.setState({ isSecondOpen: false });
}
}}
>
<TetherBehavior
anchorRef={this.anchorRef2.current}
popperRef={this.popperRef2.current}
onPopperUpdate={(...args) => this.onPopperUpdate(2, ...args)}
placement={TETHER_PLACEMENT.right}
>
<BlockComponent
forwardedRef={this.popperRef2}
offset={this.state.offset2}
color="rgba(190, 255, 190, 0.86)"
>
Press &quot;Enter&quot; to Close
</BlockComponent>
</TetherBehavior>
</Layer>
) : null}
</Block>
</Block>
);
}
}
2 changes: 2 additions & 0 deletions src/layer/__tests__/layer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import { Scenario as LayerZIndex } from './layer-z-index.scenario';
import { Scenario as LayerKeyHandlers } from './key-handlers.scenario';

export const ZIndex = () => <LayerZIndex />;
export const KeyHandlers = () => <LayerKeyHandlers />;
19 changes: 18 additions & 1 deletion src/layer/__tests__/layer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
import * as React from 'react';
import { render, getByTestId, getByText } from '@testing-library/react';
import { render, getByTestId, getByText, fireEvent } from '@testing-library/react';

import { TestBaseProvider } from '../../test/test-utils';

Expand Down Expand Up @@ -107,4 +107,21 @@ describe('Layer', () => {
expect(layersContainer.children[0].textContent).toBe(contentTwo);
expect(layersContainer.children[1].textContent).toBe(contentOne);
});

it('passes keyboard events only to the last layer', () => {
const firstKeyHandler = jest.fn();
const secondKeyHandler = jest.fn();

const { container } = render(
<LayersManager>
<Layer onKeyPress={firstKeyHandler}>Layer 1</Layer>
<Layer onKeyPress={secondKeyHandler}>Layer 2</Layer>
</LayersManager>
);

fireEvent.keyPress(container, { key: 'Enter', code: 'Enter', charCode: 13 });

expect(firstKeyHandler).toBeCalledTimes(0);
expect(secondKeyHandler).toBeCalledTimes(1);
});
});
25 changes: 25 additions & 0 deletions src/layer/layer.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class LayerComponent extends React.Component<LayerComponentPropsT, LayerStateT>

componentDidMount() {
this.context.addEscapeHandler(this.onEscape);
this.context.addKeyDownHandler(this.onKeyDown);
this.context.addKeyUpHandler(this.onKeyUp);
this.context.addKeyPressHandler(this.onKeyPress);

if (!this.props.isHoverLayer) {
this.context.addDocClickHandler(this.onDocumentClick);
}
Expand Down Expand Up @@ -74,6 +78,9 @@ class LayerComponent extends React.Component<LayerComponentPropsT, LayerStateT>

componentWillUnmount() {
this.context.removeEscapeHandler(this.onEscape);
this.context.removeKeyDownHandler(this.onKeyDown);
this.context.removeKeyUpHandler(this.onKeyUp);
this.context.removeKeyPressHandler(this.onKeyPress);
this.context.removeDocClickHandler(this.onDocumentClick);

if (this.props.onUnmount) {
Expand All @@ -95,6 +102,24 @@ class LayerComponent extends React.Component<LayerComponentPropsT, LayerStateT>
}
};

onKeyDown = (event: KeyboardEvent) => {
if (this.props.onKeyDown) {
this.props.onKeyDown(event);
}
};

onKeyUp = (event: KeyboardEvent) => {
if (this.props.onKeyUp) {
this.props.onKeyUp(event);
}
};

onKeyPress = (event: KeyboardEvent) => {
if (this.props.onKeyPress) {
this.props.onKeyPress(event);
}
};

onDocumentClick = (event: MouseEvent) => {
if (this.props.onDocumentClick) {
this.props.onDocumentClick(event);
Expand Down
25 changes: 25 additions & 0 deletions src/layer/layer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class LayerComponent extends React.Component<LayerComponentProps, LayerState> {

componentDidMount() {
this.context.addEscapeHandler(this.onEscape);
this.context.addKeyDownHandler(this.onKeyDown);
this.context.addKeyUpHandler(this.onKeyUp);
this.context.addKeyPressHandler(this.onKeyPress);

if (!this.props.isHoverLayer) {
this.context.addDocClickHandler(this.onDocumentClick);
}
Expand Down Expand Up @@ -78,6 +82,9 @@ class LayerComponent extends React.Component<LayerComponentProps, LayerState> {

componentWillUnmount() {
this.context.removeEscapeHandler(this.onEscape);
this.context.removeKeyDownHandler(this.onKeyDown);
this.context.removeKeyUpHandler(this.onKeyUp);
this.context.removeKeyPressHandler(this.onKeyPress);
this.context.removeDocClickHandler(this.onDocumentClick);

if (this.props.onUnmount) {
Expand All @@ -99,6 +106,24 @@ class LayerComponent extends React.Component<LayerComponentProps, LayerState> {
}
};

onKeyDown = (event: KeyboardEvent) => {
if (this.props.onKeyDown) {
this.props.onKeyDown(event);
}
};

onKeyUp = (event: KeyboardEvent) => {
if (this.props.onKeyUp) {
this.props.onKeyUp(event);
}
};

onKeyPress = (event: KeyboardEvent) => {
if (this.props.onKeyPress) {
this.props.onKeyPress(event);
}
};

onDocumentClick = (event: MouseEvent) => {
if (this.props.onDocumentClick) {
this.props.onDocumentClick(event);
Expand Down
Loading

0 comments on commit 7694aa9

Please sign in to comment.