Skip to content

aria warning is triggered when the bootstrap modal is closed #1553

Open
@Calabashmc

Description

@Calabashmc

When using suneditor instances on bootstrap 5.3 modals an aria warning is triggered when the modal is closed:

Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.
Element with focus: <div.se-wrapper-inner se-wrapper-wysiwyg sun-editor-editable>

To Reproduce
Steps to reproduce the behavior:
create a boostrap modal with a textarea and instantiate suneditor

Expected behavior
Modal closes without aria warning

Desktop (please complete the following information):

  • OS: [e.g. iOS] Ubuntu 25.04
  • Browser [e.g. chrome, safari] Chrome
  • Version [e.g. 22] 136.0.7103.113 (Official Build) (64-bit)

Additional context
I am using suneditor with flask-wtf textareafield, e.g.

resolution_notes = TextAreaField(
        id="resolution-notes",
        render_kw={"class": "editor"}
    )

which is rendered in the bootstrap 5.3 modal (jina2 template) like so:

<div class="modal fade" id="resolution-modal" data-bs-backdrop="static" tabindex="-1">
    <div class="modal-dialog modal-lg">
        <div class="modal-content h-auto">
            <div class="modal-header">
                <h5 class="modal-title">Resolution Notes</h5>
            </div>
            <div class="modal-body">
                {{ render_field(form.resolution_code_id) }}
                <hr>
                {{ form.resolution_notes }}
            </div>
            <div class="modal-footer">
                <button id="modal-cancel-resolution" type="button" class="btn btn-danger live">Cancel</button>
                <button id="modal-add-resolution-btn" type="button" class="btn btn-primary live">Add Resolution</button>
            </div>
        </div>
    </div>
</div>

This is my script for managing suneditor instances:

import {showFormButtons, showSwal} from './form-classes/form-utils.js';

class SunEditorClass {
    constructor(containerIds, sunEditorOptions) {
        this.status = document.getElementById('status');
        this.sunEditorOptions = sunEditorOptions;
        this.ticketNumber = document.getElementById('ticket-number');
        this.ticketType = document.getElementById('ticket-type');

        this.normalButtonList = [
            // default button lists can be set for different screen sizes
            ['fullScreen'],
            ['fontSize'],
            ['formatBlock'],
            ['bold', 'underline', 'italic'],
            ['fontColor', 'hiliteColor'],
            ['removeFormat'],
            ['outdent', 'indent'],
            ['align', 'horizontalRule', 'list'],
            ['table', 'image'],
        ];


        this.closedOptions = {
            buttonList: [...this.normalButtonList],
            width: '100%',
            resizeEnable: false,
            resizingBar: false,
        };

        this.timelineOptions = {
            autoResize: true,
            buttonList: [['saveBtn'], ...this.normalButtonList],
            fontSize: [
                12,
                14,
                16,
                24,
            ],
            defaultStyle: 'font-size: 12pt;',
            fontSizeUnit: 'pt',
            hideToolbar: true,
            minHeight: '250px',
            maxHeight: '600px',
            plugins: [this.saveBtnPlugin],  // Add the plugin
            resizeEnable: false,
            resizingBar: false,
            showPathLabel: false,
            width: '100%',
        }

        this.normalOptions = {
            height: 'auto',
            minHeight: '250px',
            autoResize: true,
            width: '100%',
            fontSize: [
                12,
                14,
                16,
                24,
            ],
            defaultStyle: 'font-size: 12pt;',
            fontSizeUnit: 'pt',
            fullScreenOffset: 150,
            buttonList: this.normalButtonList,
            imageAccept: '.jpg, .png, .gif',
        };

        // Map to store sunEditor instances, similar to tomSelectInstances
        this.sunEditorInstances = new Map();
        this.journalInstances = new Map(); // Map to store journal sunEditor instances
    }


    createSunEditor(id) {
        const area = document.getElementById(id);
        let options;

        if (area.classList.contains('timeline-editor')) {
            const saveBtnPlugin = {
                name: 'saveBtn',
                display: 'command',
                innerHTML: '<i class="bi bi-floppy text-bg-success"></i>',
                buttonClass: '',
                add: function (core, targetElement) {
                    const context = core.context;
                    context.saveBtn = context.saveBtn || {};
                    context.saveBtn.targetButton = targetElement;
                    context.saveBtn.editorId = id; // Capture the current editor's ID
                    setTimeout(() => {
                        new bootstrap.Tooltip(targetElement, {
                            title: 'Save Changes',
                            placement: 'bottom',
                        });
                    }, 100);
                },
                action: () => { // Use arrow function to bind to the class instance
                    this.saveNote(id); // Pass the current editor's ID
                }
            };

            options = {...this.timelineOptions};
            options.buttonList = [['saveBtn'], ...this.normalButtonList];
            options.plugins = [saveBtnPlugin];

        } else {
            options = {...this.normalOptions};
        }

        if (this.status) {
            if (this.status.value === 'closed') {
                options = {...this.closedOptions};
            }
        }

        if (this.sunEditorInstances.has(id)) {
            return;  // editor already exists
        }

        const editorInstance = SUNEDITOR.create(id, options);
        // Store the instance in the map, using the ID as the key
        this.sunEditorInstances.set(id, editorInstance);
        this.setupListeners(area);


        if (area.classList.contains('timeline-editor')) {
            this.journalInstances.set(id, editorInstance);
            editorInstance.readOnly(true);
        }

        if (id === 'resolution-notes') {
            editorInstance.toolbar.show();
        }

        return editorInstance;
    }

    setupListeners(area) {
        const editor = this.sunEditorInstances.get(area.id);
        if (!area.classList.contains('timeline-editor')) {
            editor.onInput = () => {
                // only show form buttons for non-timeline suneditors because they have flask-wtf fields associated
                showFormButtons();
            }
        } else {
            editor.onBlur = async () => {
                if (editor.core.isReadOnly) {
                    return;
                }

                if (area.classList.contains('timeline-editor')) {
                    const response = await showSwal(
                        'Changes not saved',
                        'Save updates to the work note?',
                        'question'
                    );

                    if (response.isConfirmed) {
                        await this.saveNote(area.id)
                        editor.readOnly(true);
                        editor.toolbar.hide();
                    } else {
                        editor.core.focus();
                    }
                }
            }

            editor.onChange = () => {
                this.changeContent = true;

            }
        }
    }

    async setUpMultipleSunEditors(editorClass) {
        const textAreas = document.querySelectorAll(editorClass);

        let textAreaIds = await Array.from(textAreas).map(function (editor) {
            return editor.id;
        });


        textAreaIds.forEach((id, index) => {
            this.createSunEditor(id);
        });
    }

    checkSunEditorForContent(editorInstanceId) {
        const editorContents = editorInstanceId.getContents()
        const editorImages = editorInstanceId.getImagesInfo()
        const isEmpty = editorContents.replace(/<[^>]+>/g, '').trim();
        // return true if empty
        return isEmpty !== '' || editorImages.length !== 0;
    }

    clearSunEditorContent(id) {
        this.sunEditorInstances.get(id)?.setContents('');
    }

    // Method to disable all sunEditor instances
    disableAllEditors() {
        this.sunEditorInstances.forEach((editor, key) => {
            if (this.journalInstances.has(key)) {
                return;
            }
            editor.readOnly(true);
        });
    }


// Method to enable all sunEditor instances
    enableAllEditors() {
        this.sunEditorInstances.forEach(editor => {
            editor.readOnly(false);
            editor.toolbar.show();
            editor.setOptions({
                resizingBar: true,
            });
            this.sunEditorInstances.forEach(editor => {
                editor.setOptions(this.normalOptions);
            });
        });
    }

// Method to enable one sunEditor instances
    enableSingleEditor(editorId) {
        const editorInstance = this.sunEditorInstances.get(editorId);
        if (editorInstance) {
            editorInstance.readOnly(false);
            editorInstance.toolbar.show();
            editorInstance.core.focus();
        }
    }

    disableSingleEditor(editorId) {
        const editorInstance = this.sunEditorInstances.get(editorId);
        if (editorInstance) {
            editorInstance.readOnly(true);
            editorInstance.toolbar.show();
            editorInstance.core.blur();
        }
    }

    destroySunEditor(id) {
        if (this.sunEditorInstances.has(id)) {
            const editor = this.sunEditorInstances.get(id);
            editor.destroy();
            this.sunEditorInstances.delete(id);
            this.journalInstances.delete(id);
        }
    }


    async saveNote(editorId) {
        const editor = this.sunEditorInstances.get(editorId);
        const updatedContent = editor.getContents();
        editor.toolbar.hide();
        editor.readOnly(true);

        const apiArgs = {
            'record_id': editor.core.context.element.originElement.dataset.recordId,
            'ticket_number': this.ticketNumber.value,
            'ticket_type': this.ticketType.value,
            'updated_note': updatedContent
        };


        try {
            const response = await fetch('/api/update-worknote/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(apiArgs),
            });

            const result = await response.json();
            if (!response.ok) {
                await showSwal('Error', result['error'], 'error');
            }
        } catch (error) {
            console.error('Failed to update note:', error);
        }
    }
}

export const sunEditorClass = new SunEditorClass();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions