This is the first day of my participation in the Gwen Challenge in November. Check out the details: the last Gwen Challenge in 2021.

preface

Today will continue our biweekly feature article sharing of a widget. The component described in this chapter is Dialog. I believe that you have come into contact with this type of component in your daily development. Many major component libraries provide a Dialog component that is easy to use, so in this article we will show you how to write a simple React based Dialog component.

This article will look at the three main parts that make up a Dialog component: DOM elements, event handling, and animation effects.

DOM elements

The DOM structure of our typical Dialog component consists of wrapper, Content, and Mask.

Their HTML structure is as follows:

        <! --mask-->
        <Mask prefixCls={this.prefixCls} visible={mask && visible} / >
        <! --wrapper-->
        <div
          ref={this.wrapperRef}
          role="dialog"
         >
        <! --content-->
          <Content
             .
          >
            {children}
          </Content>
        </div>
Copy the code

mask

As a Dialog mask layer, the mask layer provides a translucent element to cover the visible area of the page. Its main purpose is to provide a better visual effect. In fact, this layer element is the lowest in the entire Dialog hierarchy and has no real purpose (so masks are often optional). Implementing a mask can be done simply by setting CSS; Fixed to the window by setting position: Fixed;

& -mask {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
  background-color: rgb(55.55.55);
  background-color: rgba(55.55.55.0.6);
  height: 100%;
  filter: alpha(opacity=50);
  z-index: 1050;    
 }
Copy the code

wrapper

The Wrapper layer is often overlooked, but implementing a complete Dialog is crucial. It is the element that hosts the Content body of the Dialog content and also plays a role in the event mechanism described later. It also uses fixed positioning and keeps the same Z-index as the mask, but follows the HTML flow and the Wrapper layer is actually above the mask layer.

  &-wrapper {
    position: fixed;
    overflow: auto;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 1050;
    -webkit-overflow-scrolling: touch;
    outline: 0;
  }
Copy the code

content

The Content layer is the core of the Dialog component and is responsible for rendering the specific content within the Dialog, as well as the body of subsequent event mechanisms and animation effects. The basic style Settings for the Content layer are simple:

.@{prefixCls} {
  position: relative;
  width: auto;
  margin: 10px;
 }
Copy the code

When the HTML and CSS are ready, they need to be inserted into the page as a Dialog component, often on the outer layer of the Document, as a child element of the body tag.

Using the React-DOM API, reactdom.createPortal renders nodes to specified DOM nodes with a few simple lines of code.

ReactDOM.createPortal(dialogDom, this.el);
Copy the code

But first we need to finish creating a child element in the body and set className to dialog-root

class Dialog<P> extends React.Component<IDialog> {...constructor(props: IDialog) {
    super(props);
    this.el = document.createElement('div');
    this.el.className = 'dialog-root';
  }

  componentDidMount() {
    document.body.appendChild(this.el);
  }

  componentWillUnmount() {
    document.body.removeChild(this.el); }... }Copy the code

And that’s done implementing oneDialogThe requiredDOMElements.

Dialog is displayed in center

If dialog centralization is needed, just a few lines of simple CSS styles are needed:

Set the Wrapper layer text-align: center and apply vertical-align: middle to the content layer with the fake element:

    &-wrapper--center {
      text-align: center;
      &::before {
        display: inline-block;
        width: 0;
        height: 100%;
        vertical-align: middle;
        content: ' ';
      }
    }
    
   &-wrapper-center .@{prefixCls} {
    top: 0;
    display: inline-block;
    text-align: left;
    vertical-align: middle;
  }
Copy the code

The event processing

Event handling is the core of the Dialog component, and we can roughly classify these events into two categories based on their role: focus management and interaction events.

The focus of management

When a Dialog is displayed on a page, it usually means that the focus element of our page has changed, so we want to implement three functions in the overall dialog-related focus management:

  1. whenDialogWhen visible, automaticallyfocusDialogOn.
  2. whenDialogWhen hidden, focus automatically returns to the previous pagefocusElement.
  3. Stop byTABKey to switch toDialogOutside of the element.

Function point 1 simply requires calling the DOM element’s Focus API when the Dialog’s Visible property changes to True.

Implementing Function point 2 requires us to save the element of the previous focus before implementing function 1. With Document.ActiveElement, you can easily get the active element of the current page. Save it before switching to the Dialog until the Dialog’s viable is false and refocus using the Focus API.

Function point 3 requires the use of two blank placeholder elements in the Content layer, before and after the actual rendering of the content.

const emptyStyle = { width: 0, height: 0, overflow: 'hidden', outline: 'none' }; .<div tabIndex={0} ref={emptyStartRef} style={emptyStyle} aria-hidden="true" />
   {dialogContent}
  <div tabIndex={0} ref={emptyEndRef} style={emptyStyle} aria-hidden="true" />
Copy the code

With these two elements, the control TAB switch is easy. Just use the Focus API to transfer the focus element to the other element when you press TAB to focus the element on one of them. This makes it possible that no matter how much you press the TAB key, the focus element will only switch between the two placeholders.

 const { activeElement } = document;

 if (activeElement === emptyEndRef.current) {
        emptyStartRef.current.focus();
  } else if (activeElement === emptyStartRef.current) {
        emptyEndRef.current.focus();
 }
Copy the code

Complete code to implement the above three functions:

Listen for visible changes, switch focus (Function 1 and function 2) :

  const onVisibleChanged = (newVisible: boolean) = > {
    if (newVisible) {
        lastOutSideActiveElementRef.current = document.activeElement asHTMLElement; contentRef.current? .focus(); }else {
      if (lastOutSideActiveElementRef.current) {
        lastOutSideActiveElementRef.current.focus({ preventScroll: true });
        lastOutSideActiveElementRef.current = null; }}};Copy the code

Monitor wapper layer keyboard TAB key, control focus (function 3) :

  function onWrapperKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
    // // keep focus inside dialog
    if (visible) {
    // TAB key keyCode === 9
    if (e.keyCode === 9) {
       const { activeElement } = document;
       if (activeElement === emptyEndRef.current) {
        emptyStartRef.current.focus();
       } else if(activeElement === emptyStartRef.current) { emptyEndRef.current.focus(); }}}"Copy the code

Interaction events

We primarily define two interaction events on the Dialog:

  1. The mouse to clickDialogThe outer region triggers a shutdown.
  2. Keyboard keysESCTrigger shutdown.

Function 1 listens for click events in the Wrapper layer and executes onDialogClose(). It is important to note, however, that the Content layer is inside the Wrapper layer, and clicking on the Content layer will also trigger a click on the Wrapper layer. This is obviously not what we expected, so additional processing is required to mask the impact of content layer click events:

    onWrapperClick = (e) = > {
      if(wrapperRef.current === e.target) { onDialogClose(e); }};Copy the code

Function 2 Listen for the key event in the Wrapper layer and check whether it is ESC:

  function onWrapperKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
    // The ESC key's keyCode === 27
    if (e.keyCode === 27) {
      e.stopPropagation();
      onDialogClose(e);
      return; }}Copy the code

This completes the required event handling in the Dialog.

Animation effects

Animation effects constitute the soul of Dialog, and Dialog with additional animation effects can be more perfect in visual presentation. We mainly achieve two animation effects:

  1. maskLayer added fade-in animation.
  2. contentLayer added zoom in and out animation.

To achieve the animation effect, you need to use cSS3’s animation features, animation or Transition.

maskThe fading

Animating the mask increases opacity from 0 to 1 and from 1 to 0 when the mask appears or is hidden:

   &-fade-enter&-fade-enter-active,&-fade-appear&-fade-appear-active  {
      animation-name: rcDialogFadeIn;
      animation-play-state: running;
    }
  
    &-fade-leave&-fade-leave-active {
      animation-name: rcDialogFadeOut;
      animation-play-state: running;
    }
  
    @keyframes maskFadeIn {
      0% {
        opacity: 0;
      }
      100% {
        opacity: 1; }}@keyframes maskFadeOut {
      0% {
        opacity: 1;
      }
      100% {
        opacity: 0; }}Copy the code

contentZoom in on

The content layer animation is a bit more complex, and we want to show the effect of zooming from the point where the Dialog is triggered by a click to the final position when the Dialog appears; As the Dialog hides, it shrinks from its final position to its initial position. The diagram below:

Transform: scale(0,0) to transform: scale(1,1) to scale from 0 to 1 and from 1 to 0:

  @keyframes dialogZoomIn {
    0% {
      opacity: 0;
      transform: scale(0.0);
    }
    100% {
      opacity: 1;
      transform: scale(1.1); }}@keyframes dialogZoomOut {
    0% {

      transform: scale(1.1);
    }
    100% {
      opacity: 0;
      transform: scale(0.0); }}Copy the code

To realize the change to a certain position, the x value and y value of position of the click event need to be obtained first. By defining a class that listens for click events, we get the location of each click event, and set the duration of mouse position information to 100ms by default (this is compatible with Dialog scenarios that are not triggered by click events, only zooming in and out).

// Click the event listener class
export class MonitorClickEvent {
  constructor(state: IState) {
    this.init();
    // set the validity time of mouse position information. The default time is 100ms
    this.time = state.time || 100;
  }
  private time: number;
  public mousePosition: { x: number; y: number } | null;
  // Listen for the click event to get the click event location
  getClickPosition = (e: MouseEvent) = > {
     // Set the position of the click event to null after the validity period
    this.mousePosition = {
      x: e.pageX,
      y: e.pageY,
    };
    setTimeout(() = > {
      this.mousePosition = null;
    }, this.time);
  };

  init() {
    document.documentElement.addEventListener('click'.this.getClickPosition, true); }}Copy the code

After obtaining the position of the click event, we need to combine the transform-Origin property of CSS3, which can control the origin of the element deformation.

By clicking on the event location and the content layer itself, top and left, you can calculate the offsets of both and assign the offsets to the Transform-Origin property. This way, our zooming in and out animation will be offset to the appropriate position, showing the animation we want.

contentStyle.transformOrigin = 
`${mousePosition.x - elementOffset.left}px 
${mousePosition.y - elementOffset.top}px`
Copy the code

This completes all the animation required for the Dialog.

In the real implementation process, in order to make the control animation more convenient, also encapsulated THE CSSTrasition component application in it. This component will be covered in more detail in future articles, as will the CSS animations used in this article.

conclusion

The three main components that make up the Dialog component are: A detailed introduction to the DOM elements, event handling, animation effects, and some of the main code posted here will help you understand the implementation principle of Dialog and diy simple Dialog component. More exciting articles will be presented in the future, please look forward to!