Advertisement
Guest User

Tooltip - Flow

a guest
Jan 23rd, 2020
217
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // @flow
  2.  
  3. import React, { Component } from 'react';
  4. import type { ElementProps, ComponentType } from 'react';
  5.  
  6. export type LabelProps = {
  7.     labelAttributes: {
  8.         tabIndex: '0',
  9.         'aria-describedby': string,
  10.         onFocus: () => void,
  11.     },
  12.     isHidden: boolean,
  13. };
  14.  
  15. export type OverlayProps = {
  16.     overlayAttributes: {
  17.         role: 'tooltip',
  18.         tabIndex: '-1',
  19.         id: string,
  20.         'aria-hidden': string,
  21.     },
  22.     isHidden: boolean,
  23. };
  24.  
  25. export type TooltipState = {
  26.     isFocused: boolean,
  27.     isHovered: boolean,
  28. };
  29.  
  30. export type TooltipProps = ElementProps<'div'> & {
  31.     label: ComponentType<LabelProps>,
  32.     overlay: ComponentType<OverlayProps>,
  33. };
  34.  
  35. let counter = 0;
  36.  
  37. class Tooltip extends Component<TooltipProps, TooltipState> {
  38.     constructor(props: TooltipProps) {
  39.         super(props);
  40.         this.identifier = `react-accessible-tooltip-${counter}`;
  41.         counter += 1;
  42.     }
  43.  
  44.     state = {
  45.         isFocused: false,
  46.         isHovered: false,
  47.     };
  48.  
  49.     componentDidMount() {
  50.         document.addEventListener('keydown', this.handleKeyDown);
  51.         document.addEventListener('touchstart', this.handleTouch);
  52.     }
  53.  
  54.     componentWillUnmount() {
  55.         document.removeEventListener('keydown', this.handleKeyDown);
  56.         document.removeEventListener('touchstart', this.handleTouch);
  57.     }
  58.  
  59.     onFocus = () => {
  60.         this.setState({ isFocused: true });
  61.     };
  62.  
  63.     onBlur = ({
  64.         relatedTarget,
  65.         currentTarget,
  66.     }: SyntheticFocusEvent<HTMLElement>) => {
  67.         // relatedTarget is better for React testability etc, but activeElement works as an IE11 fallback:
  68.         const newTarget = relatedTarget || document.activeElement;
  69.  
  70.         // The idea of this logic is that we should only close the tooltip if focus has shifted from the tooltip AND all of its descendents.
  71.         if (!(newTarget && newTarget instanceof HTMLElement)) {
  72.             this.setState({ isFocused: false });
  73.         } else if (!currentTarget.contains(newTarget)) {
  74.             this.setState({ isFocused: false });
  75.         }
  76.     };
  77.  
  78.     onMouseEnter = () => {
  79.         this.setState({ isHovered: true });
  80.     };
  81.  
  82.     onMouseLeave = () => {
  83.         this.setState({ isHovered: false });
  84.     };
  85.  
  86.     // This handles the support for touch devices that do not trigger blur on 'touch-away'.
  87.     handleTouch = ({ target }: Event) => {
  88.         const { activeElement } = document;
  89.  
  90.         if (
  91.             activeElement instanceof Element &&
  92.             target instanceof Element &&
  93.             this.container instanceof Element &&
  94.             !this.container.contains(target) && // touch target not a tooltip descendent
  95.             this.state.isFocused // prevent redundant state change
  96.         ) {
  97.             this.setState({ isFocused: false });
  98.             activeElement.blur();
  99.         } else if (
  100.             activeElement instanceof Element &&
  101.             target instanceof Element &&
  102.             this.container instanceof Element &&
  103.             this.container.contains(target) && // touch target is on tooltip descendant
  104.             !this.state.isFocused // prevent redundant state change
  105.         ) {
  106.             this.setState({ isFocused: true });
  107.         }
  108.     };
  109.  
  110.     handleKeyDown = ({ key, keyCode, which }: KeyboardEvent) => {
  111.         if (key === 'Escape' || keyCode === 27 || which === 27) {
  112.             this.setState({ isFocused: false });
  113.         }
  114.     };
  115.  
  116.     container: ?HTMLDivElement;
  117.     identifier: string;
  118.  
  119.     render() {
  120.         const { label: Label, overlay: Overlay, ...rest } = this.props;
  121.  
  122.         const { isFocused, isHovered } = this.state;
  123.         const isHidden = !(isFocused || isHovered);
  124.  
  125.         const labelProps: LabelProps = {
  126.             labelAttributes: {
  127.                 tabIndex: '0',
  128.                 'aria-describedby': this.identifier,
  129.                 onFocus: this.onFocus,
  130.             },
  131.             isHidden,
  132.         };
  133.  
  134.         const overlayProps: OverlayProps = {
  135.             overlayAttributes: {
  136.                 role: 'tooltip',
  137.                 tabIndex: '-1',
  138.                 id: this.identifier,
  139.                 'aria-hidden': isHidden.toString(),
  140.             },
  141.             isHidden,
  142.         };
  143.  
  144.         return (
  145.             <div
  146.                 {...rest}
  147.                 onBlur={this.onBlur}
  148.                 ref={ref => {
  149.                     this.container = ref;
  150.                 }}
  151.                 onMouseEnter={this.onMouseEnter}
  152.                 onMouseLeave={this.onMouseLeave}
  153.             >
  154.                 <Label {...labelProps} />
  155.                 <Overlay {...overlayProps} />
  156.             </div>
  157.         );
  158.     }
  159. }
  160.  
  161. export default Tooltip;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement