Page MenuHomePhabricator

Ensure that Codex Dialogs are compatible with Vector
Closed, ResolvedPublic5 Estimated Story PointsBUG REPORT

Description

What teams or group is this for?

This request comes from the Design Systems Team, but the problem currently impacts two other teams (Growth and Abstract Wiki) and may impact more in the future.

Who is your main point of contact and contact preference?

@egardner, Phab or Slack (#talk-to-design-systems channel is good).

Description of problem

Use of the Codex Dialog component on wiki pages where the Vector skin is applied will cause z-index collisions with other navigational elements on the page.

It is possible to work around this problem (by using Vue's built-in <teleport> feature to render the Dialog outside of the main body content area), but this leads to unexpected stylistic shifts due to how Vector manages font-sizing in the content area.

Below is a screenshot demonstrating the problem when the Dialog is launched from inside the main content area of a wiki page:

Screen Shot 2023-08-01 at 09.20.26.png (1×2 px, 281 KB)

Non-logged-in users can reproduce this issue themselves at the new Wikifunctions site. Simply go to a function page (such as https://www.wikifunctions.org/wiki/Z10004) and click one of the "details" links in the right-hand sidebar.

Technical Details

The main #bodyContent area on the page receives the following CSS styles that break the Codex Dialog z-index behavior when it is rendered here:

.vector-feature-zebra-design-disabled .vector-body {
    position: relative;
    z-index: 0;
}

I understand that the z-index: 0 rule may be necessary for security reasons, so that malicious user-provided content cannot try to masquerade as navigational elements in order to fool the user and make them click on a harmful link.

Therefore, I believe that the recommendation should be that users of Codex Dialog components on-wiki should use Vue's built-in Teleport feature (https://vuejs.org/guide/built-ins/teleport.html#teleport).

This is simple to do, and usage would look like this:

<div class="my-feature">
    <button @click="open = true">Launch dialog</button>

    <teleport to="body">
        <cdx-dialog
	    v-model:open="open"
	    title="Save changes"
	    close-button-label="Close"
	    :primary-action="primaryAction"
	    :default-action="defaultAction"
	    @primary="onPrimaryAction"
	    @default="open = false"
        >
	    <p>Do you want to save your changes?</p>
        </cdx-dialog>
    </teleport>
</div>

However, currently this workaround creates other problems, also due to styles applied by .vector-body.

Content inside of .vector-body inherits a rule that overrides the global font size for all elements:

.vector-body {
    font-size: calc(1em * 0.875);
}

I assume that this is done so that article body text defaults to 14px even though the page itself technically uses 16px.

Unfortunately, this style rule means that if a user teleports a Codex Dialog outside of the main content area (so as to not conflict with the inherited z-index rule from .vector-body), the final size of text and other elements inside of the dialog will appear too large compared to the rest of the page.

Proposed Solution

I believe that Vector (or maybe MW Core itself) should add an element at the end of the <body> tag which could serve as the designated teleport target for any Dialog users who are launching a dialog that originates inside of the main content area. Vector (and any other skins which feature similar behavior) could then ensure that any appropriate styles get applied to this element so that content rendered here will match the body text in terms of size, etc.

My understanding is that something similar happens for OOUI dialogs, for the same reasons.

Is this request urgent or time sensitive?

Yes. This issue is live in production and is very user-noticeable. Codex Dialogs will have a broken user experience until this can be fixed.

Event Timeline

egardner triaged this task as High priority.Aug 3 2023, 5:03 PM
egardner created this task.

This issue is present to a lesser extent in Minerva:

Screenshot 2023-08-03 at 10.12.42 AM.png (2×1 px, 945 KB)

Some icons appear above the dialog backdrop (likely due to the same z-index issues).

I imagine that the proposed solution above could apply to all skins if we added a teleport "landing pad" in Core and then allowed each skin to apply appropriate body styles to it.

egardner renamed this task from [Bug] Ensure that Codex Dialogs are compatible with Vector to Ensure that Codex Dialogs are compatible with Vector.Aug 3 2023, 5:45 PM
egardner changed the subtype of this task from "Deadline" to "Bug Report".

I think the best solution here is to add a div.mw-overlay-container in core via javascript and then for products to teleport to that element. We were discussing a Vector specific one in T333349 as we already have one in mobile.

Change 945825 had a related patch set uploaded (by Eric Gardner; author: Eric Gardner):

[mediawiki/core@master] Add "mediawiki.page.modalTarget" module to core

https://gerrit.wikimedia.org/r/945825

I think the best solution here is to add a div.mw-overlay-container in core via javascript and then for products to teleport to that element. We were discussing a Vector specific one in T333349 as we already have one in mobile.

@Jdlrobson I've opened a patch in Core that explores creating a new dedicated RL module which does exactly that. I've also set up a demonstration of how someone would use this with CodexDialog over at CodexExample (see here).

There is still the issue of ensuring that any styles from Vector or Minerva which are applied to the main content area (or at least crucial ones like font size) also get applied to this element. Do you all have any interest in opening a patch on top of my Core one that could handle this? It could be as simple as just grabbing the ID of the overlay in some skin script and adding a class (like vector-body). The new RL module exports the dom node so that it's available to other code programmatically, which hopefully will keep folks from having to keep track of a specific ID string in their code.

Anyway, would love to hear your thoughts on the feasibility of this approach.

This LGTM. I left a small review on the Gerrit patch regarding where the code should live.

Change 946637 had a related patch set uploaded (by Catrope; author: Catrope):

[design/codex@main] [Proof of concept] Dialog: Automatically teleport to provided target

https://gerrit.wikimedia.org/r/946637

Minor issue and not strictly related to the teleport mechanism, but I wonder if the default title in a h2 we'll likely yield accessibility heading order issues. I remember seeing some library component (maybe codex already does?) providing a prop headingLevel for this.

In addition to providing a div for dialogs to be teleported into, we should also think about what we want the developer experience of using dialogs in MediaWiki to be. We've been discussing two options internally within DST, and we're curious to hear feedback from Codex users on these options.

Option 1: Tell developers to manually <teleport> each dialog

This is essentially what the task description proposes. Each usage of the Dialog component would have to manually teleport it to the modalTarget div. The code for using a Dialog would look something like this:

<template>
    ...
    <cdx-button @click="isDialogOpen = true">Open dialog</cdx-button>
    <teleport :to="teleportTarget">
        <cdx-dialog
            v-model:open="isDialogOpen"
            title="Hello"
            ...other props...
        >
            ...dialog contents...
        </cdx-dialog>
    </teleport>
</template>

<script>
const modalTarget = require( 'mediawiki.page.modalTarget' );
const { CdxDialog } = require( '@wikimedia/codex' );
// @vue/component
module.exports = {
    components: {
        CdxDialog
    },
    data: () => ( {
        isDialogOpen: false,
        teleportTarget: `#${modalTarget.id}`
    } )
};
</script>

Pros:

  • No changes needed in Codex itself
  • Dialogs aren't teleported unless they need to be
  • There is no <teleport> usage in the Dialog component itself
  • Developers in non-MW environments don't have to do anything different

Cons:

  • Developers in MW have to remember to teleport each dialog
  • Developers in MW have to duplicate the teleport tag for each dialog, and the code getting the teleport target for each component that uses dialogs
Option 2: Teleport all dialogs within Codex

In this option, we would modify the Dialog component in Codex to teleport itself. The teleport target would default to 'body', but could be overridden using provide() (and maybe using a teleport-target prop as well). In MediaWiki, we would ensure that a custom teleport target is provide()d in the setup code in Vue.createMwApp(), so that developers don't have to set it themselves.

The code for using a Dialog in MediaWiki would look like this. Nothing special would be needed, but the dialog would automatically be teleported to the right place:

<template>
    ...
    <cdx-button @click="isDialogOpen = true">Open dialog</cdx-button>
    <!-- No teleport tag or special props needed. The dialog is teleported to #mw-modal-target automatically -->
    <cdx-dialog
        v-model:open="isDialogOpen"
        title="Hello"
        ...other props...
    >
        ...dialog contents...
    </cdx-dialog>
</template>

<script>
// No modalTarget code needed here
const { CdxDialog } = require( '@wikimedia/codex' );
// @vue/component
module.exports = {
    components: {
        CdxDialog
    },
    data: () => ( {
        isDialogOpen: false
    } )
};
</script>

In order to make this work, we would have to add something like app.provide( 'CdxDialogTeleportTarget', '#mw-modal-target' ); in Vue.createMwApp(), and we would have to make changes to the Dialog component in Codex so that the provided teleport target is used automatically.

Pros:

  • Developers in MW don't have to remember to teleport or duplicate any code, it just works

Cons:

  • All dialogs would be teleported, even those not used in MW
  • <teleport> usage and other additional complexity would be added to the Dialog component in Codex

Open questions:

  • Are there any use cases in MediaWiki where a dialog shouldn't be teleported, or should be teleported to a different place than the mw-modal-target div?
  • By default (if no teleport target is provided, this would only happen outside of MW), would the dialog teleport to the bottom of the <body>, or not teleport at all?
    • We think it might make more sense to always teleport, for consistency, so that the MW default case and the non-MW default case are more similar to each other
  • Should there be a teleport-target prop to allow overriding the teleport target on a per-dialog basis?
    • Maybe we should only add this if and when someone needs it?
  • If the dialog teleports by default, should there be a way to prevent it from teleporting?
    • Maybe this could be implemented by setting the teleport target to null, either via the prop (if we add one) or via provide()?

On the first two open questions:

  1. Use cases of dialog not to be teleported: I can't think of any somewhat common use case where a dialog shouldn't be teleported. Esoteric cases to question (and maybe to test for) are dialogs on top of dialogs, or fullscreen videos including their backdrops triggered from a dialog.
  2. Yes, in my opinion definitely to be teleported to body bottom. Again, what is to consider when a tooltip (z-index 800) is showing on a background element. In my opinion a dialog (z-index 450) should always come last and have a backdrop (transparent, z-index 400) enabled to catch and avoid triggering tooltips outside at same time as a dialog is open.
  3. I'm leaning towards your idea of only adding when needed. As it might open a door to have the viewport hierarchy of elements mixed up. Use case for such functionality should be clearly stated and strong.
  4. Stated already above. Can't think of a use case where not teleporting makes application-wise more sense than teleporting.

Note that there are components that need to appear on top of the dialog layer, such as mw.notify popups.

I think Option1: Tell developers to manually <teleport> each dialog is preferable because it keeps CdxDialog component MW-agnostic. This issue title is specific about Vector but per T343476#9067392 it seems the problem might also exist for Minerva and other skins so thinking about this from a platform pov and evaluate the two scenarios NPM/MW seems interesting. So we can keep Codex lean and light for non MW-projects, eg: toolforge apps.

  • Are there any use cases in MediaWiki where a dialog shouldn't be teleported, or should be teleported to a different place than the mw-modal-target div?

It's hard to predict these use cases; @Volker_E mentions about dialogs on top of dialogs; I think this can be achieved with teleported dialogs (maybe I'm wrong) and it seems like a very valid scenario to account for.

  • By default (if no teleport target is provided, this would only happen outside of MW), would the dialog teleport to the bottom of the <body>, or not teleport at all?

If that was the case I suggest to not teleport at all or make the teleport target a required prop. Note that teleporting to <body> could lead to unpredictable behavior with SSR, see vuejs.org/guide/scaling-up/ssr.html#teleports.

  • We think it might make more sense to always teleport, for consistency, so that the MW default case and the non-MW default case are more similar to each other

What about a companion component <CdxTeleportableDialog> so developers can pick depending on their needs. MW ( createMWApp() ) could provide a default target and skins could overwrite it if needed.

  • Should there be a teleport-target prop to allow overriding the teleport target on a per-dialog basis?

Maybe

  • Maybe we should only add this if and when someone needs it?

Exactly

  • If the dialog teleports by default, should there be a way to prevent it from teleporting?
    • Maybe this could be implemented by setting the teleport target to null, either via the prop (if we add one) or via provide()?

Would the disable prop work for this?

@egardner From the code example, is there any way to move const teleportTarget = require( 'mediawiki.page.modalTarget' ); somewhere behind .createMwApp, plugin, composable? This resonates again as a config option many modules might want to consume.

ovasileva set the point value for this task to 3.Aug 10 2023, 5:58 PM

This issue title is specific about Vector but per T343476#9067392 it seems the problem might also exist for Minerva and other skins

Yes, we would apply this to all skins, not just Vector. If we're going to teleport a dialog, I think it should be teleported to the same place in all skins. Each skin may need to style the teleport target slightly differently, but the teleport target itself should exist in all skins, be in the same place, and have the same ID.

  • Are there any use cases in MediaWiki where a dialog shouldn't be teleported, or should be teleported to a different place than the mw-modal-target div?

It's hard to predict these use cases; @Volker_E mentions about dialogs on top of dialogs; I think this can be achieved with teleported dialogs (maybe I'm wrong) and it seems like a very valid scenario to account for.

  • By default (if no teleport target is provided, this would only happen outside of MW), would the dialog teleport to the bottom of the <body>, or not teleport at all?

If that was the case I suggest to not teleport at all or make the teleport target a required prop. Note that teleporting to <body> could lead to unpredictable behavior with SSR, see vuejs.org/guide/scaling-up/ssr.html#teleports.

  • We think it might make more sense to always teleport, for consistency, so that the MW default case and the non-MW default case are more similar to each other

What about a companion component <CdxTeleportableDialog> so developers can pick depending on their needs. MW ( createMWApp() ) could provide a default target and skins could overwrite it if needed.

  • Should there be a teleport-target prop to allow overriding the teleport target on a per-dialog basis?

Maybe

  • Maybe we should only add this if and when someone needs it?

Exactly

  • If the dialog teleports by default, should there be a way to prevent it from teleporting?
    • Maybe this could be implemented by setting the teleport target to null, either via the prop (if we add one) or via provide()?

Would the disable prop work for this?

Yes, I think that's what we would use for this in practice. Vue doesn't let you pass null or another invalid value for <teleport>'s to prop, unless you also set the disabled prop. So we'd do something like <teleport :to="teleportTarget" :disabled="!teleportTarget">.

@egardner From the code example, is there any way to move const teleportTarget = require( 'mediawiki.page.modalTarget' ); somewhere behind .createMwApp, plugin, composable? This resonates again as a config option many modules might want to consume.

We could provide() it in createMwApp, and then have people inject() it when they use a dialog (or, in option 2, inject it in Codex itself).

As part of @Volker_E's work on T342395, he recommended that dialogs should always be teleported, so that aria-hidden and inert can safely be applied to the rest of the page.

In part for that reason (and in part because it would make dialogs easier to use in MediaWiki), I propose that we go with option 2: teleport all dialogs within Codex. Specifically, we would:

  • Teleport all dialogs to the provide()d teleport target, or if one is not specified, to the bottom of the <body>
  • Document the fact that this default behavior doesn't work well with SSR; recommend that when SSR is used, a teleport target should be provided
  • In Vue.createMwApp() in MediaWiki, create the modal target div (once) and provide() it (for every Vue app), so that all Codex dialogs in MediaWiki will automatically use it
    • This means we don't need a separate mediawiki.page.modalTarget module, because users of Codex dialogs don't need to access the modal target themselves
  • In Vue.createMwApp() in MediaWiki, create the modal target div (once) and provide() it (for every Vue app), so that all Codex dialogs in MediaWiki will automatically use it
    • This means we don't need a separate mediawiki.page.modalTarget module, because users of Codex dialogs don't need to access the modal target themselves

I still think it might be good to have a separate module for mediawiki.page.modalTarget which our Codex or Vue RL module can depend on, and then all MW-hosted Vue apps can provide() this target to all children as you describe. Keeping the code that adds the target div in a separate module would allow other code (not just Vue or Codex) to use this element as well, and skins may wish to style it so that any content placed here inherits the standard body styles.

Sure, that seems fine to me, as long as there are use cases for other code. I don't think skins wanting to style it is a reason we'd need a separate module, because they have to hard-code the ID in their CSS anyway, and that style rule could live in a different (more general-purpose) module. But if we had other use cases for JS code wanting to add things to this div, then it makes sense to have a module for it.

CCiufo-WMF changed the task status from Open to In Progress.Aug 16 2023, 7:19 PM
CCiufo-WMF assigned this task to egardner.
CCiufo-WMF changed the point value for this task from 3 to 5.
CCiufo-WMF moved this task from Following to Design-Systems-Sprint-6 on the Design-System-Team board.

Change 949603 had a related patch set uploaded (by Eric Gardner; author: Eric Gardner):

[design/codex@main] [Breaking] Dialog: incorporate Vue's <teleport> feature

https://gerrit.wikimedia.org/r/949603

Change 949603 merged by jenkins-bot:

[design/codex@main] [Breaking] Dialog: incorporate Vue's <teleport> feature

https://gerrit.wikimedia.org/r/949603

Change 952493 had a related patch set uploaded (by Eric Gardner; author: Eric Gardner):

[mediawiki/skins/Vector@master] [WIP] Apply .vector-body typography styes to #mw-teleport-target

https://gerrit.wikimedia.org/r/952493

Change 952493 had a related patch set uploaded (by Catrope; author: Eric Gardner):

[mediawiki/skins/Vector@master] Add skinStyles for mediawiki.page.teleportTarget

https://gerrit.wikimedia.org/r/952493

Change 945825 merged by jenkins-bot:

[mediawiki/core@master] Add "mediawiki.page.teleportTarget" module to core

https://gerrit.wikimedia.org/r/945825

Change 952493 merged by jenkins-bot:

[mediawiki/skins/Vector@master] Add skinStyles for mediawiki.page.teleportTarget

https://gerrit.wikimedia.org/r/952493

Current status of this task: complete pending release.

  • Codex has been updated so that the Dialog component relies on Vue's <teleport> feature, and will look for a provided CdxTeleportTarget value (a DOM element or ID selector) to use as the default teleport target; if this is not provided <body> will be used, or the user can pass in a target prop for a specific dialog; finally, the Teleport behavior can be disabled by passing a renderInPlace: true prop. This is a breaking change and will be released this week.
  • MediaWiki core has been updated to add a new mediawiki.page.teleportTarget resourceModule which adds a new element to the page that can be used as a teleport target. The vue module in MW depends on this new module, and the createMwApp method we've added to Vue will automatically take this new teleport target element and provide it so that all on-wiki Vue apps can rely on this feature automatically. This means that on-wiki use of Codex Dialog component should not need a target prop in most situations. This patch will ride the train this week.
  • Vector has been updated to add some additional styling to the mediawiki.page.modalTarget element using skinStyles – the main change is that Vector's font-size value for body-text will now also be applied to this modal target element, so that Dialogs teleported here have the same font-size as the page body text.

It's possible that some other body-text styles from Vector may need to be applied to teleported dialogs. It's also possible that other skins may need to do something similar to what was done in Vector. If that ends up being the case, just follow what was done in https://gerrit.wikimedia.org/r/952493 and apply whatever skinStyles you need to #mw-teleport-target.

Change 952939 had a related patch set uploaded (by Eric Gardner; author: Eric Gardner):

[mediawiki/core@master] Resources: Move teleportTarget module into mediawiki.page.ready

https://gerrit.wikimedia.org/r/952939

Change 952950 had a related patch set uploaded (by Eric Gardner; author: Eric Gardner):

[mediawiki/skins/Vector@master] Apply teleport target styles to mediawiki.page.ready module

https://gerrit.wikimedia.org/r/952950

Change 952939 merged by jenkins-bot:

[mediawiki/core@master] Resources: Move teleportTarget module into mediawiki.page.ready

https://gerrit.wikimedia.org/r/952939

Change 952950 merged by jenkins-bot:

[mediawiki/skins/Vector@master] Apply teleport target styles to mediawiki.page.ready module

https://gerrit.wikimedia.org/r/952950

Change 953351 had a related patch set uploaded (by Catrope; author: Catrope):

[mediawiki/core@master] Update Codex from v0.17.0 to v0.18.0

https://gerrit.wikimedia.org/r/953351

Change 953351 merged by jenkins-bot:

[mediawiki/core@master] Update Codex from v0.17.0 to v0.18.0

https://gerrit.wikimedia.org/r/953351

Change 946637 abandoned by Eric Gardner:

[design/codex@main] [Proof of concept] Dialog: Automatically teleport to provided target

Reason:

https://gerrit.wikimedia.org/r/946637