Search code examples
sapui5

restrict addEventDelegate to parent hbox only


This is a custom control created for my previous question Custom font for currency signs

I have two span elements coming next to each other. They sit in the FormattedText. The FormattedText itself sits in the HBox. I want popover fired when user mouses over/out from the hbox. Unfortunately, as I have 2 spans this fires separately when user hovers overs them (thus showing 2 separate popovers, when in fact it should be one). My assumption is that this causes because onmouseover/out is attached to both spans under the hood. Can I restrict these events to hbox only?

sap.ui.define([
  'sap/ui/core/Control',
  'sap/m/FormattedText',
  'sap/m/HBox',
], function (Control, FormattedText, HBox) {

  return Control.extend('drex.control.TherapyCosts', {
    metadata: {
      properties: {
        rank: {
          type: 'int',
          defaultValue: 0
        },
      },
      aggregations: {
        _rankedTherapyCost: {type: 'sap.m.FormattedText', multiple: false, singularName: '_rankedTherapyCost'},
        _popover: {type: 'sap.m.Popover', multiple: false, singularName: '_popover'},
        _hbox: {type: 'sap.m.HBox', multiple: false}
      }
    },

    init: function () {
      Control.prototype.init.apply(this, arguments);
    },

    onBeforeRendering: function () {
      const highlighedCurrency = this.getCurrency().repeat(this.getRank());
      const fadedCurrency = this.getCurrency().repeat(7 - this.getRank());
      const _popover = new sap.m.Popover({
        showHeader: false,
        placement: 'VerticalPreferredBottom',
        content: new sap.m.Text({text: 'test'})
      });
      this.setAggregation('_popover', _popover);
      const _formattedText = new FormattedText({
        htmlText:
          `<span class="currencyHighlighted">${highlighedCurrency}</span>` +
          `<span class="currencyFaded">${fadedCurrency}</span>`
      });
      this.setAggregation('_rankedTherapyCost', _formattedText);
      const _hbox = new HBox(
        { items: [this.getAggregation('_rankedTherapyCost')]})
        .addEventDelegate({
          onmouseover: () => {
            this.getAggregation('_popover').openBy(this);
          },
          onmouseout: () => {
            this.getAggregation('_popover').close()
          }
        });
      this.setAggregation('_hbox', _hbox)
    },

    renderer: function (rm, oControl) {
      const _hbox = oControl.getAggregation('_hbox');
      rm.write('<div');
      rm.writeControlData(oControl);
      rm.write('>');
      rm.renderControl(_hbox);
      rm.write('</div>');
    }
  });
});

Here is the video of the issue https://streamable.com/fjw408


Solution

  • The key here is the mouseout event is triggered when the mouse moves out of any child element of the element that is listening for the event. In this case, for example, moving from the emphasised text to the faded text, you get a mouseout event when moving off the emphasised text, which closes the popup, then a mouseover event on the faded text which opens it again.

    Since you opening an already open popup doesn't do anything, you only need add a line on mouseout to inspect the element that you've gone to. If it is not a child of the current element, only then close the popover.

    if (!this.getDomRef().contains(e.toElement)) {
      popOver.close()
    }
    

    Building on D.Seah's answer, I've added this to the JSBin. I personally don't like using onBeforeRendering and onAfterRendering like this, so I've refactored a little to construct everything on init and simply change the controls on property change. This way, you're doing less on rendering for performance reasons.

    sap.ui.define([
      'sap/ui/core/Control',
      'sap/m/FormattedText',
    ], function (Control, FormattedText) {
    
      Control.extend('TherapyCosts', {
        metadata: {
          properties: {
            rank: {
              type: 'int',
              defaultValue: 0
            },
            currency: {
              type: "string",
              defaultValue: "$"
            }
          },
          aggregations: {
            _highlighted: {type: 'sap.m.FormattedText', multiple: false},
            _faded: {type: 'sap.m.FormattedText', multiple: false},
            _popover: {type: 'sap.m.Popover', multiple: false, singularName: '_popover'}
          }
        },
    
        init: function () {
          Control.prototype.init.apply(this, arguments);
          this.addStyleClass('therapy-cost');
          
          const highlight = new FormattedText();
          highlight.addStyleClass("currency-highlight");
          this.setAggregation('_highlighted', highlight);
    
          const faded = new FormattedText();
          faded.addStyleClass("currency-faded");
          this.setAggregation('_faded', faded);
          
          const _popover = new sap.m.Popover({
            showHeader: false,
            placement: 'VerticalPreferredBottom',
            content: new sap.m.Text({text: 'test'})
          });
          this.setAggregation('_popover', _popover);
        },
        
        _changeAggr: function () {
          const highlighedCurrency = this.getCurrency().repeat(this.getRank());
          const highlight = this.getAggregation("_highlighted");
          highlight.setHtmlText(highlighedCurrency);
          
          const fadedCurrency = this.getCurrency().repeat(7 - this.getRank());
          const faded = this.getAggregation("_faded");
          faded.setHtmlText(fadedCurrency);
        },
        
        setRank: function(rank) {
           if (this.getProperty("rank") !== rank) {
             this.setProperty("rank", rank);
             this._changeAggr();
           }
        },
    
        setCurrency: function(curr) {
           if (this.getProperty("currency") !== curr) {
             this.setProperty("currency", curr);
             this._changeAggr();
           }
        },
        
        renderer: function (rm, oControl) {
          rm.write('<div');
          rm.writeControlData(oControl);
          rm.writeClasses(oControl);
          rm.writeStyles(oControl);
          rm.write('>');
          rm.renderControl(oControl.getAggregation('_highlighted'));
          rm.renderControl(oControl.getAggregation('_faded'));
          rm.write('</div>');
        },
        
        onmouseover: function (e) {
          const popOver = this.getAggregation('_popover');
          popOver.openBy(this);
        },
        
        onmouseout: function (e) {
          if (!this.getDomRef().contains(e.toElement)) {
            const popOver = this.getAggregation('_popover');
            popOver.close();
          }
        }
      });
      (new TherapyCosts({
        rank: 5,
        currency: "£"
      })).placeAt('content')
    });
    .therapy-cost {
      display: inline-flex;
      flex-direction: row;
    }
    
    .therapy-cost .currency-highlighted {
      color: black;
    }
    
    .therapy-cost .currency-faded {
      color: lightgrey;
    }
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>JS Bin</title>
        <script 
                src="https://openui5.hana.ondemand.com/resources/sap-ui-core.js" 
                id="sap-ui-bootstrap" 
                data-sap-ui-theme="sap_bluecrystal" 
                data-sap-ui-xx-bindingSyntax="complex" 
                data-sap-ui-libs="sap.m"></script>
      </head>
      <body class="sapUiBody sapUiSizeCompact">
        <div id='content'></div>
      </body>
    </html>

    https://jsbin.com/gukedamemu/3/edit?css,js,output