frend / frend.co

Frend — A collection of accessible, modern front-end components.
http://frend.co
MIT License
635 stars 25 forks source link

Introduce event callback option for all components #70

Open adamduncan opened 8 years ago

adamduncan commented 8 years ago

A lot of quality plugins have event emitter setups, but that might be a bit overkill for Frend bits.

Would be nice for this to be consistent amongst components. Is it as simple as adding an onToggle property to the options object that takes a function and passes the instance?

var myTabs = Frtabs({
  onToggle: function (tabs) {
    // do something
  }
});

As part of this, do we need to start storing things like the active element on each instance, so they can be referenced in these callbacks? I.e. tabs.activeTab

thomasdigby commented 8 years ago

Initial tester, any idea how we're going to return the instance?

Ambient-Impact commented 6 years ago

Has there been any more development on this? I'm specifically trying to use the off canvas component, but discovered there aren't any open and close events I can hook into, which is surprising.

adamduncan commented 6 years ago

Hey @Ambient-Impact - thanks for enquiring on this. Unfortunately we lost a bit of steam on v2 with other obligations. In hindsight, exposing show/hide methods in the original API would have been super helpful.

Were you hoping to fire events when opening/closing the off-canvas panel? In which case, binding your own event handlers on the buttons will do the trick: https://codepen.io/adamduncan/pen/JLaoRM?editors=0010

Manually invoking open/close from elsewhere in your code would also be possible, but at this point would be a case of replicating the logic in _showPanel and _hidePanel I'm afraid.

Hope that helps.

Ambient-Impact commented 6 years ago

No worries. I started to replicate those event handlers, but then realized I also needed to account for the user hitting the escape key or clicking outside of the panel, and it started to get more complicated. I'm implementing an overlay (and using ally.js to trap focus in the off canvas panel), so I need a way to accurately know when the panel is opened and closed. After a bit of tinkering, I decided to use MutationObserver to watch for changes to aria-hidden on the panel. I'll probably just fall back to not using an overlay if it isn't supported. It still feels like a bit of a hack, but it's the next best thing.

adamduncan commented 6 years ago

Sure, I see. Sorry about that. MutationObserver sounds like a nice, modern approach.

Other a11y packages (like Micromodal) have nice open/close event APIs. If you're implementing the off-canvas in conjunction with a modal overlay, I wonder whether the case could be made for treating the off-canvas as a modal-of-sorts as well 🤔

gunzip commented 6 years ago

@Ambient-Impact I've done something similar using

$(opts.contentSelector).on('transitionend', function()... )

which was better supported than MutationObserver when it was written (I'd rather use MutationObserve today)

results here: https://italia.github.io/design-web-toolkit/components/detail/offcanvas.html

adamduncan commented 6 years ago

Nice one @gunzip!

kevinmamaqi commented 6 years ago

Hey @Ambient-Impact I am using MutationObserver, but it fires twice. Do you have the same problem?

I have the following script to activate a fadIn/fadeOut when the offCanvas opens/closes:

// Offcanvas
    var myOffcanvas = Froffcanvas();

    // Listen to offCanvas close/open
    var offCanvasObserver = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        if (mutation.type == "attributes") {
          if (mutation.target.getAttribute('aria-hidden') == "true") {
             $('.overlay-offcanvas').fadeOut( 60 );
              console.log('closes');
          } else {
             $('.overlay-offcanvas').fadeIn( 60 );
             console.log('open');
          }      
        }
      });
    });

    var config = {
      attributes: true, //configure it to listen to attribute changes
      attributeFilter: ['aria-hidden'] // filter your attributes
    };

    // offCanvasObserver.observe(document.getElementById('offcanvas-sidebar'), config);
    offCanvasObserver.observe(document.getElementById('offcanvas-cart'), config);

`

There must be something firing it twice, but I can not find what it is. I have two offCanvas, and they don't work well together.

Ambient-Impact commented 6 years ago

@kevinmamaqi I've written my own complex wrapper around Froffcanvas, and it seems to work fine with more than one offcanvas on the page. With regards to your issue with MutationObserver, I think it might be due to the attribute changing more than once and the observer picking that up. There's no guarantee that it won't fire more than once, so what I've done is to instruct it to also keep the old value of aria-hidden, so I can compare them and only do something if it goes from true to false or vice versa. The below is a private function from my wrapper, hope it helps:

/**
 * Bind events so that we can provide open and close events.
 *
 * Froffcanvas doesn't currently offer any events, so we use a
 * MutationObserver watching for changes to the 'aria-hidden' attribute to
 * determine if the panel has been opened or closed. See link for
 * Froffcanvas issue. If MutationObserver is not supported by the browser,
 * no events will be fired.
 *
 * @param {jQuery} $panel
 *
 * @link https://github.com/frend/frend.co/issues/70
 * @link https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
 */
function bindEvents($panel) {
    if (!('MutationObserver' in window)) {
        return;
    }

    var panel = $panel[0];

    panel.aiOffcanvas.mutationObserver =
        new MutationObserver(function(mutations) {
            var action;

            for (var i = 0; i < mutations.length; i++) {
                var mutatedPanel = mutations[i].target;

                if (
                    mutations[i].oldValue === 'false' &&
                    mutatedPanel.getAttribute('aria-hidden') === 'true'
                ) {
                    action = 'closed';
                } else if (
                    mutations[i].oldValue === 'true' &&
                    mutatedPanel.getAttribute('aria-hidden') === 'false'
                ) {
                    action = 'opened';
                }
            }

            switch (action) {
                case 'opened':
                    openEvent($panel);

                    break;

                case 'closed':
                    closeEvent($panel);

                    break;
            }
        });
    panel.aiOffcanvas.mutationObserver.observe(panel, {
        attributes:         true,
        attributeFilter:    ['aria-hidden'],
        attributeOldValue:  true
    });
};