morkro / vue-a11y-dialog

Vue.js component for a11y-dialog

Home Page:https://www.npmjs.com/package/vue-a11y-dialog

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

remarks on implementation

y-nk opened this issue · comments

  1. You shouldn't be using a function to display your dialog, rather use a v-if which is less verbose and more optimized : your way of implementating will result in the dialog being rendered and inserted to the dom wether or not it's shown, killing the entire benefit of Vue's vdom diffing logic. On a side effect, it will probably make it harder to implement custom transitions also, since "appear/disappear" cycles won't be triggered. A simple way to fix this would be to remove some logic and using v-if on the consumer side instead of using your own flow.

  2. A span for title is arguable, but it's very neat that you made it a <slot /> so it's consumer's discretion to choose what to use.

  3. In case you want to have a more granular separation of concern, you may want to leave the discretion of insertion point to the consumer of your component - meaning that the portal should not be included inside your component ; a dialog is a dialog, the way to use it, the place to insert it can depend on each project. That last point is very debattable since it could be also an explicit implementation constraint but it seems to be that the benefit of embedding portal is not a lot compared to the modularity brought by leaving this choice to your end user.

An ideal implementation should be :

<template>
  <div id="app">
    <!-- ... -->
    <button type="button" @click="dialog = true">
      Open dialog
    </button>

    <portal to="the-dialog-place">
      <a11y-dialog ref="dialog" v-if="dialog">
        <span slot="title">Your dialog title</span>
        <div>
          <p>Your content</p>
        </div>
      </a11y-dialog>
    </portal>
  </div>
</template>

<script>
export default {
  name: 'YourComponent',

  data: () => ({
    dialog: false
  }),
}
</script>

Hey @y-nk, thanks for your comments.

1. You shouldn't be using a function to display your dialog, rather use a v-if which is less verbose and more optimized : your way of implementating will result in the dialog being rendered and inserted to the dom wether or not it's shown, killing the entire benefit of Vue's vdom diffing logic. On a side effect, it will probably make it harder to implement custom transitions also, since "appear/disappear" cycles won't be triggered. A simple way to fix this would be to remove some logic and using v-if on the consumer side instead of using your own flow.

I've intended this component to be a a very thin layer around A11yDialog to provide most flexibility for its users. There might be situations where a user needs full control over the state of the dialog instance (as there are more methods than just .show() and .hide()). Only giving control via v-if seems very limited to me. In the end, nothing is stopping you from wrapping this component in your app and implementing additional functionality yourself.

I don't fully understand why you think the current implementation will result in the dialog being destroyed and inserted? After all, the dialog will live outside of the Vue application anyway. Only the contents inside the dialog is being updated. In terms of hiding and showing the dialog, this is just a matter of either toggling the open attribute on the native <dialog> element or toggling the class names (with custom CSS). See the demo link in the README and watch the DOM changes.

2. A span for title is arguable, but it's very neat that you made it a <slot /> so it's consumer's discretion to choose what to use.

The <span> is meant as a container element to be put into <h1>. As far as my a11y knowledge goes, this isn't an issue. I've been considering refactoring that part and make the slot detect the passed nodeName for better flexibility. I agree it's currently not ideal.

3. In case you want to have a more granular separation of concern, you may want to leave the discretion of insertion point to the consumer of your component - meaning that the portal should not be included inside your component ; a dialog is a dialog, the way to use it, the place to insert it can depend on each project. That last point is very debattable since it could be also an explicit implementation constraint but it seems to be that the benefit of embedding portal is not a lot compared to the modularity brought by leaving this choice to your end user.

I see your point, however even with the portal you have full flexibility where in your app the dialog should be inserted. Up until the last released version this has been the implementation. However, there were a11y concerns raised (#5), so I've decided to introduce PortalVue as part of the component. To be honest, there just hasn't been enough feedback for me to consider different implementations. I understand that one of your concerns is that the user should be able to decide whether they want to wrap the component in a <portal> or not? If so, the #app container will get a aria-hidden attribute while the dialog is visible inside #app. This breaks a11y.

I am happy to reconsider implementation details if it turns out they can be improved and/or are breaking accessibility concerns.

hey ! thank you for taking the time to respond :) please be sure i meant and mean no harm whatsoever, i only thought my opinion would benefit this project.

I've intended this component to be a a very thin layer around A11yDialog to provide most flexibility for its users. There might be situations where a user needs full control over the state of the dialog instance (as there are more methods than just .show() and .hide()). Only giving control via v-if seems very limited to me. In the end, nothing is stopping you from wrapping this component in your app and implementing additional functionality yourself.

I honestly didn't see the part where it was written "wrapper for [non Vue project]. I'm sorry about it ; tho, my statement is still valid (but slightly different) in my opinion. You're providing a library as a wrapper for an other library, but it also implies you have to absorb the gap and translate it to the best of the technology you're programming with. For example, as a strong Vue developer, i would probably toss that choice because of not using v-if instead of show/hide - or i would write a concurrent version which benefits from all the good parts of Vue (i won't, don't worry 😅 - i also understand this is somehow subjective)

I don't fully understand why you think the current implementation will result in the dialog being destroyed and inserted? After all, the dialog will live outside of the Vue application anyway. Only the contents inside the dialog is being updated. In terms of hiding and showing the dialog, this is just a matter of either toggling the open attribute on the native <dialog> element or toggling the class names (with custom CSS). See the demo link in the README and watch the DOM changes.

My point was the exact opposite : wether or not the dialog is visible on screen, its representation in the vdom will be present, and so does the associated dom node. The consequence will be unnecessary vdom nodes presents, and useless diffing cycles. Of course it doesn't cost so much, but still is not the fittest use of Vue. To me, the benefit of Vue's vdom diffing logic are that only the DOM nodes that should be displayed are in the DOM tree.

Understanding that you're wrapping a non Vue library changes my opinion a little on that. Of course, the diffing part is still valid, but i do now understand better why those methods exist. That said, i would argue that you'd mutate the content of a vdom outside of Vue's control. It shouldn't matter so much in the end, but still is not the best use of the tech ; again, it's working... so, subjective point of view :)

I see your point, however even with the portal you have full flexibility where in your app the dialog should be inserted. Up until the last released version this has been the implementation. However, there were a11y concerns raised (#5), so I've decided to introduce PortalVue as part of the component. To be honest, there just hasn't been enough feedback for me to consider different implementations. I understand that one of your concerns is that the user should be able to decide whether they want to wrap the component in a <portal> or not? If so, the #app container will get a aria-hidden attribute while the dialog is visible inside #app. This breaks a11y.

Sorry i didn't check so much of the tickets history 😅 I think that i understand your point about it. Correct me if I'm wrong but, if the #app is aria-hidden, then all the app is hidden so it makes sense that it breaks a11y. Wouldn't it be better to consider to include this inside the app as an other layer so it stays in the realm of Vue ?

I mean that now, the solution is :

<div id="vue_app_root" aria-hidden="true">{{ vue app }}</div>
<div id="dialog_root">{{ not vue application, also portal target }}</div>

but what's the difference with :

<div id="vue_app_root">
  <div aria-hidden="true">{{ vue app }}</div>
  <div id="dialog_root">{{ still vue application, also can be portal target eventually }}</div>
</div>

This also makes me wonder why the dialog has to be outside the app's domain (root dom node). If an app triggers a dialog, the code which triggers it belongs to the app, and therefore it should be inside it, right ? Usually it wouldn't make much difference either you're developing an SPA~ and then you manage all the layers of the page, or, in case of modules or smaller apps, I guess it would be safer to use the global scope dialog (for consistency of design ?)

I'm seriously enjoying the exchange of ideas, i hope it's the same on the other side 🙏 (if not, just close and ignore 😅 )

hey ! thank you for taking the time to respond :) please be sure i meant and mean no harm whatsoever, i only thought my opinion would benefit this project.

No worries, no harm done at all. Happy to discuss, that's open source :)

Understanding that you're wrapping a non Vue library changes my opinion a little on that. Of course, the diffing part is still valid, but i do now understand better why those methods exist. That said, i would argue that you'd mutate the content of a vdom outside of Vue's control. It shouldn't matter so much in the end, but still is not the best use of the tech ; again, it's working... so, subjective point of view :)

Yeah, that's the catch. If this would be an a11y dialog implementation from scratch, I would design the integration to make best use of the virtual DOM. I think in the end we're just talking about micro optimisations, since the library itself is already doing a great job. Vue itself is using the same Web API after all.

That said, i would argue that you'd mutate the content of a vdom outside of Vue's control. It shouldn't matter so much in the end, but still is not the best use of the tech ; again, it's working... so, subjective point of view :)

I understand your point, but this is a given limitation with any 3rd-party library ported to another library such as Vue, React, Preact, Angular, etc. All you can do is maximising API portability. Basically, let the 3rd-party library do its thing while integrating its API to suit your (as in Vue, React, ...) own methodologies and fit in context.

Sorry i didn't check so much of the tickets history 😅

No worries, I don't expect that. Just wanted to give clarity :)

Correct me if I'm wrong but, if the #app is aria-hidden, then all the app is hidden so it makes sense that it breaks a11y. Wouldn't it be better to consider to include this inside the app as an other layer so it stays in the realm of Vue ?
[...]
This also makes me wonder why the dialog has to be outside the app's domain (root dom node).

You're not limited to that. Your example would work with the current implementation just fine. It's just an example I'm giving. Just make sure the application content, which will be aria-hidden doesn't contain your dialog. This works fine:

<template>
  <div id="app">
    <div id="dialog-root" />
    <div id="content">
      ...

      <a11y-dialog
        id="app-dialog"
        app-root="#content"
        dialog-root="#dialog-root"
        @dialog-ref="assignDialogRef"
      >
          <!-- dialog content -->
      </a11y-dialog>
    </div>
  </div>
</template>

I'm seriously enjoying the exchange of ideas, i hope it's the same on the other side 🙏

Yes, happy to discuss! :)

Just closing this discussion as it seems to be inactive and finished. Feel free to open a new issue for any other discussions.