MDCDialog Uncaught Error: FocusTrap: Element must have at least one focusable child

Viewed 4403

I've a Vue single file component, and I'm trying to use Material Dialog within as follows:

<template>
    <div v-show="open" id="emergency-details-dialog" class="mdc-dialog"
         ref="mainContainer"
    >
        <div class="mdc-dialog__container">
            <div class="mdc-dialog__surface"
                 role="dialog"
                 aria-modal="true"
                 aria-labelledby="my-dialog-title"
                 aria-describaedby="my-dialog-content"
            >
                <div class="mdc-dialog__content" id="my-dialog-content">
                    Discard draft?
                </div>
                <div class="mdc-dialog__actions">
                    <button type="button" class="mdc-button mdc-dialog__button"
                            data-mdc-dialog-action="cancel">
                        <div class="mdc-button__ripple"></div>
                        <span class="mdc-button__label">Cancel</span>
                    </button>
                    <button type="button" class="mdc-button mdc-dialog__button"
                            data-mdc-dialog-action="discard">
                        <div class="mdc-button__ripple"></div>
                        <span class="mdc-button__label">Discard</span>
                    </button>
                </div>
            </div>
        </div>
        <div class="mdc-dialog__scrim"></div>
    </div>
</template>

<script>
import {MDCDialog} from '@material/dialog'

export default {
    name: 'EmergencyContactDialog',
    data() {
        return {
            dialog: null,
        }
    },
    computed: {
        open() {
            const module = 'clientDetails/editableDetails/person/ec/isDialogOpen'
            return this.$store.getters[module]
        },
    },
    watch: {
        open() {
            if (this.open) {
                this.dialog.open()
            } else {
                this.dialog.close()
            }
        },
    },
    mounted() {
        this.dialog = new MDCDialog(this.$refs.mainContainer)
    },
}
</script>

<style scoped>

</style>

However, when I dispatch action to set isDialogOpen to true, I get the following console error:

Uncaught Error: FocusTrap: Element must have at least one focusable child.

Also, this.dialog.focusTrap_.elFocusedBeforeTrapFocus is null. In the project we have another Material Dialog that works well, and it follows almost the same syntax of opening through dispatching vuex actions and calling this.dialog.open(), but that one has this.dialog.focusTrap_.elFocusedBeforeTrapFocus of body element. I don't know if that could be a reason for the error to happen, nor how to set elFocusedBeforeTrapFocus

MDCDialog's README has the following about focusTrap()

The MDCDialog component uses the focus-trap package to handle this. You can use util.createFocusTrapInstance() (see below) to easily create a focus trapping solution for your component code.

I'm just not quite sure how to properly create focusTrap() if that could solve the problem.

1 Answers

Make sure that your dialog (.mdc-dialog) is actually injected in the DOM, and that it contains at least one element a, input, textarea, select, or button with a tabindex attribute set.

For info, here is the function called by trapFocus (from focus-trap.js):

 var FOCUS_SENTINEL_CLASS = 'mdc-dom-focus-sentinel';
[...]
 FocusTrap.prototype.getFocusableElements = function (root) {
        var focusableEls = [].slice.call(root.querySelectorAll('[autofocus], [tabindex], a, input, textarea, select, button'));
        return focusableEls.filter(function (el) {
            var isDisabledOrHidden = el.getAttribute('aria-disabled') === 'true' ||
                el.getAttribute('disabled') != null ||
                el.getAttribute('hidden') != null ||
                el.getAttribute('aria-hidden') === 'true';
            var isTabbableAndVisible = el.tabIndex >= 0 &&
                el.getBoundingClientRect().width > 0 &&
                !el.classList.contains(FOCUS_SENTINEL_CLASS) && !isDisabledOrHidden;
            var isProgrammaticallyHidden = false;
            if (isTabbableAndVisible) {
                var style = getComputedStyle(el);
                isProgrammaticallyHidden =
                    style.display === 'none' || style.visibility === 'hidden';
            }
            return isTabbableAndVisible && !isProgrammaticallyHidden;
        });
    };
Related