Fazemos janelas modais para o site. Nos preocupamos com a conveniĂŞncia e acessibilidade

Estou envolvido com layout e programação de sites. Quase todos os layouts que fiz têm janelas modais. Normalmente, são formulários de pedido de chamada em páginas de entrada, notificações sobre a conclusão de alguns processos ou mensagens de erro.



O layout dessas janelas parece, a princípio, uma tarefa simples. Modais podem ser feitos mesmo sem a ajuda de JS usando apenas CSS, mas na prática eles se mostram inconvenientes e por causa de pequenas falhas os modais incomodam os visitantes do site.



Como resultado, ele foi concebido para fazer minha própria solução simples.





De um modo geral, existem vários scripts prontos, bibliotecas JavaScript que implementam a funcionalidade de janelas modais, por exemplo:



  • Arctic Modal,
  • jquery-modal,
  • iziModal,
  • Micromodal.js,
  • tingle.js,
  • Bootstrap modal (da biblioteca Bootstrap), etc.


(não consideramos soluções baseadas em estruturas de front-end no artigo)



Usei vários deles, mas quase todos encontraram algumas falhas. Alguns deles requerem a inclusão da biblioteca jQuery, que não está disponível em todos os projetos. Para desenvolver sua solução, você deve primeiro decidir sobre os requisitos.



? , «, » , - NikoX «arcticModal — jQuery- ».



, ?



  • , , .
  • . / .
  • .
  • . data-, .
  • – .
  • , .
  • IE11+


: , (HystModal) GitHub, +.



.



1. HTML CSS



1.1.



? : HTML . / CSS.



HTML ( «hystmodal»):



<div class="hystmodal" id="myModal">
    <div class="hystmodal__window">
        <button data-hystclose class="hystmodal__close">Close</button>  
          .
        <img src="img/photo.jpg" alt="  " />
    </div>
</div>


, </body> (.hystmodal). . id ( #myModal) ( ).



, .hystmodal . , CSS top, bottom, left right .



.hystmodal {
    position: fixed;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    overflow: hidden;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    display: flex;
    flex-flow: column nowrap;
    justify-content: center; /* .  */
    align-items: center;
    z-index: 99;
    /*      
       */
    padding:30px 0;
}


:



  1. , .hystmodal flex- .
  2. , overflow-y: auto, . , ( Safari) -webkit-overflow-scrolling: touch, .


.



.hystmodal__window {
    background: #fff;

    /*     600px
           */
    width: 600px;
    max-width: 100%;

    /*     */
    transition: transform 0.15s ease 0s, opacity 0.15s ease 0s;
    transform: scale(1);
}


.



â„–1. , .





- justify-content: center. ( ), . stackoverflow. – justify-content: flex-start, margin:auto. .



â„–2. ie-11 , .



: flex-shrink:0 – .



â„–3. Chrome (.. padding-bottom ).



, :



  • ::after padding
  • .


. .hystmodal__wrap. â„–1, padding margin-top margin-top .hystmodal__window.



html:



<div class="hystmodal" id="myModal" aria-hidden="true" >
    <div class="hystmodal__wrap">
        <div class="hystmodal__window" role="dialog" aria-modal="true" >
            <button data-hystclose class="hystmodal__close">Close</button>  
            <h1>  </h1>
            <p>   ...</p>
            <img src="img/photo.jpg" alt="" width="400" />
            <p>    ...</p>
        </div>
    </div>
</div>


aria role .



CSS .



.hystmodal__wrap {
    flex-shrink: 0;
    flex-grow: 0;
    width: 100%;
    min-height: 100%;
    margin: auto;
    display: flex;
    flex-flow: column nowrap;
    align-items: center;
    justify-content: center;
}
.hystmodal__window {
    margin: 50px 0;
    flex-shrink: 0;
    flex-grow: 0;
    background: #fff;
    width: 600px;
    max-width: 100%;
    overflow: visible;
    transition: transform 0.2s ease 0s, opacity 0.2s ease 0s;
    transform: scale(0.9);
    opacity: 0;
}


1.2



. , display none flex.



, display . , transition, .



visibility:hidden. , .

– . , visibility:hidden , - aria-hidden="true".



:



.hystmodal--active{
    visibility: visible;
}
.hystmodal--active .hystmodal__window{
    transform: scale(1);
    opacity: 1;
}


1.3



, html- . .hystmodal , ( opacity) . , .



.hysymodal__shadow </body>. , , js .



:



.hystmodal__shadow{
    position: fixed;
    border:none;
    display: block;
    width: 100%;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    overflow: hidden;
    pointer-events: none;
    z-index: 98;
    opacity: 0;
    transition: opacity 0.15s ease;
    background-color: black;
}
/*   */
.hystmodal__shadow--show{
    pointer-events: auto;
    opacity: 0.6;
}


1.4



, , .

— overflow:hidden body html, . :



â„–4. Safari iOS , html body overflow:hidden.

, (touchmove, touchend touchsart) js :



targetElement.ontouchend = (e) => {
    e.preventDefault();
};


, , . js, , .



ps: scroll-lock, , .



– CSS. , <html> .hystmodal__opened:



.hystmodal__opened {
    position: fixed;
    right: 0;
    left: 0;
    overflow: hidden;
}


position:fixed, safari, :



â„–5. / .

, - position, .



, JS ():



:



//   html   
let html = document.documentElement;
//  :
let scrollPosition = window.pageYOffset;
//  top  html  
html.style.top = -scrollPosition + "px";
html.classList.add("hystmodal__opened");


:



html.classList.remove("hystmodal__opened");
//     
window.scrollTo(0, scrollPosition);
html.style.top = "";


, JavaScript .



2. JavaScript



2.2



IE11 2 :



  • ES5, , .
  • ES6, Babel, .

    , .

    .


HystModal. , .



class HystModal{
    /**
     *    ,    
     * js-  .   
     *       props
     */
    constructor(props){
        /**
         *       
         *      
         *      Object.assign
         */
        let defaultConfig = {
            linkAttributeName: 'data-hystmodal',
            // ...   
        }
        this.config = Object.assign(defaultConfig, props);

        //    
        this.init();
    }

    /** 
     *   _shadow   div  
     * .   , ..  
     *   ,    
     * 
     */
    static _shadow = false;

    init(){
        /**
         *   ,   ...
         */
        this.isOpened = false; //   
        this.openedWindow = false; //   .hystmodal
        this._modalBlock = false; //   .hystmodal__window
        this.starter = false, //   ""  
        // (      )
        this._nextWindows = false; //  .hystmodal   
        this._scrollPosition = 0; //  (. )

        /**
         * ... 
         */

        //          body
        if(!HystModal._shadow){
            HystModal._shadow = document.createElement('div');
            HystModal._shadow.classList.add('hystmodal__shadow');
            document.body.appendChild(HystModal._shadow);
        }

        //     . .
        this.eventsFeeler();
    }

    eventsFeeler(){

        /** 
         *          data-
         *      - this.config.linkAttributeName
         * 
         *      ,   
         *      html
         * 
         */
        document.addEventListener("click", function (e) {
            /**
             *      ,
             *   
             */ 
            const clickedlink = e.target.closest("[" + this.config.linkAttributeName + "]");

            /**      
             *   ,  
             *  ,  
             *  _nextWindows  _starter  
             *   open (. )
             */
            if (clickedlink) { 
                e.preventDefault();
                this.starter = clickedlink;
                let targetSelector = this.starter.getAttribute(this.config.linkAttributeName);
                this._nextWindows = document.querySelector(targetSelector);
                this.open();
                return;
            }

            /**     
             *   data- data-hystclose,
             *      
             */
            if (e.target.closest('[data-hystclose]')) {
                this.close();
                return;
            }
        }.bind(this));
        /**  ,     this
         *      .
         *      this   
         *  ,      .bind().
         */ 

        //  escape  tab
        window.addEventListener("keydown", function (e) {   
            //   escape
            if (e.which == 27 && this.isOpened) {
                e.preventDefault();
                this.close();
                return;
            }

            /**       Tab
             *      
             * (  )
             */ 
            if (e.which == 9 && this.isOpened) {
                this.focusCatcher(e);
                return;
            }
        }.bind(this));

    }

    open(selector){
        this.openedWindow = this._nextWindows;
        this._modalBlock = this.openedWindow.querySelector('.hystmodal__window');

        /**    
         *   /
         *      this.isOpened
         */
        this._bodyScrollControl();
        HystModal._shadow.classList.add("hystmodal__shadow--show");
        this.openedWindow.classList.add("hystmodal--active");
        this.openedWindow.setAttribute('aria-hidden', 'false');

        this.focusContol(); //    (. )
        this.isOpened = true;
    }

    close(){
        /**
         *    .  
         *    .
         */
        if (!this.isOpened) {
            return;
        }
        this.openedWindow.classList.remove("hystmodal--active");
        HystModal._shadow.classList.remove("hystmodal__shadow--show");
        this.openedWindow.setAttribute('aria-hidden', 'true');

        //      
        this.focusContol();

        // 
        this._bodyScrollControl();
        this.isOpened = false;
    }

    _bodyScrollControl(){

        let html = document.documentElement;
        if (this.isOpened === true) {
            // 
            html.classList.remove("hystmodal__opened");
            html.style.marginRight = "";
            window.scrollTo(0, this._scrollPosition);
            html.style.top = "";
            return;
        }

        // 
        this._scrollPosition = window.pageYOffset;
        html.style.top = -this._scrollPosition + "px";
        html.classList.add("hystmodal__opened");
    }

}


, HystModal. , :



const myModal = new HystModal({
    linkAttributeName: 'data-hystmodal', 
});


/ data-hystmodal, : <a href="#" data-hystmodal="#myModal"> </a>

. :



â„–6: ( ), / , .





– . , html, .



. , (, Chrome Android). .



_bodyScrollControl()



//  
let marginSize = window.innerWidth - html.clientWidth;
//         ( html)
if (marginSize) {
    html.style.marginRight = marginSize + "px";
} 
//  
html.style.marginRight = "";


close() ? , CSS , .



â„–7. , visibility:hidden .



: visibility:hidden . , , , , .



  • CSS- .hystmodal—moved - .hystmodal--active


.hystmodal--moved{
    visibility: visible;
}


  • «transitionend» . `.hystmodal—active, css-. , «transitionend», .


: :



close(){
    if (!this.isOpened) {
        return;
    }
    this.openedWindow.classList.add("hystmodal--moved");
    this.openedWindow.addEventListener("transitionend", this._closeAfterTransition);
    this.openedWindow.classList.remove("hystmodal--active");
}

_closeAfterTransition(){
    this.openedWindow.classList.remove("hystmodal--moved");
    this.openedWindow.removeEventListener("transitionend", this._closeAfterTransition);
    HystModal._shadow.classList.remove("hystmodal__shadow--show");
    this.openedWindow.setAttribute('aria-hidden', 'true');
    this.focusContol();
    this._bodyScrollControl();
    this.isOpened = false;
}


, _closeAfterTransition() . , transitionend , removeEventListener , .



, , this._closeAfterTransition() .



, addEventListener, this , , this.



// 
this._closeAfterTransition = this._closeAfterTransition.bind(this)


2.2



– .hystmodal__wrap. .hystmodal__wrap :



document.addEventListener("click", function (e) {
    const wrap = e.target.classList.contains('hystmodal__wrap');
    if(!wrap) return;
    e.preventDefault();
    this.close();
}.bind(this));


, .



â„–8. , ( ), .



, . , , . , .



, , click , .hystmodal__wrap.



html, div .hystmodal__window . div .



addEventListener : mousedown mouseup .hystmodal__wrap. eventsFeeler()



document.addEventListener('mousedown', function (e) {
    /**
    *      .hystmodal__wrap,
    *      this._overlayChecker
    */
    if (!e.target.classList.contains('hystmodal__wrap')) return;
    this._overlayChecker = true;
}.bind(this));

document.addEventListener('mouseup', function (e) {
    /**
    *       .hystmodal__wrap,
    *       ,   
    *   this._overlayChecker   
    */
    if (this._overlayChecker && e.target.classList.contains('hystmodal__wrap')) {
        e.preventDefault();
        !this._overlayChecker;
        this.close();
        return;
    }
    this._overlayChecker = false;
}.bind(this));


2.3



: focusContol() , focusCatcher(event) .



js- «Micromodal» (Indrashish Ghosh). :



1.  css ( init()):



//  init  
this._focusElements = [
    'a[href]',
    'area[href]',
    'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
    'select:not([disabled]):not([aria-hidden])',
    'textarea:not([disabled]):not([aria-hidden])',
    'button:not([disabled]):not([aria-hidden])',
    'iframe',
    'object',
    'embed',
    '[contenteditable]',
    '[tabindex]:not([tabindex^="-"])'
];


2.  focusContol() , . – this.starter:



focusContol(){
    /**       
     *   ,  ,   
     * .   .
     */
    const nodes = this.openedWindow.querySelectorAll(this._focusElements);
    if (this.isOpened && this.starter) {
        this.starter.focus();
    } else {
        if (nodes.length) nodes[0].focus();
    }
}


3.  focusCatcher() . , , ( Tab Shift+Tab ).



focusCatcher:



focusCatcher(e){
    /**          TAB
     *      .
     */

    //       
    const nodes = this.openedWindow.querySelectorAll(this._focusElements);

    //  
    const nodesArray = Array.prototype.slice.call(nodes);

    //    ,      
    if (!this.openedWindow.contains(document.activeElement)) {
        nodesArray[0].focus();
        e.preventDefault();
    } else {
        const focusedItemIndex = nodesArray.indexOf(document.activeElement)
        if (e.shiftKey && focusedItemIndex === 0) {
            //    
            focusableNodes[nodesArray.length - 1].focus();
        }
        if (!e.shiftKey && focusedItemIndex === nodesArray.length - 1) {
            //    
            nodesArray[0].focus();
            e.preventDefault();
        }
    }
}


, :



â„–9. IE11 Element.closest() Object.assign().



Element.closest, closest matches MDN.



, webpack, element-closest-polyfill .



Object.assign, babel- @babel/plugin-transform-object-assign



3.



, , hystModal MIT-. 3 gzip. .



hystModal, :



  • (/ , , )
  • ( ( ))
  • - , ( ).
  • - CSS
  • CSS JS Webpack.


, GitHub, Issues . ( , , , . Instagram




All Articles