Skip to content

Dunk.dirty dialog#6798

Open
duncanuszkay-d2l wants to merge 6 commits intomainfrom
dunk.dirty-dialog
Open

Dunk.dirty dialog#6798
duncanuszkay-d2l wants to merge 6 commits intomainfrom
dunk.dirty-dialog

Conversation

@duncanuszkay-d2l
Copy link
Copy Markdown
Contributor

@duncanuszkay-d2l duncanuszkay-d2l commented Apr 14, 2026

https://desire2learn.atlassian.net/browse/NTNGL-5471?atlOrigin=eyJpIjoiODJjZWQ3NzZmNjE0NDc1MTgxOTEwMTY5MGUzNDNkZTAiLCJwIjoiaiJ9

We're adding a dirty state to the table component to act as an intermediate state between clean and loading while the user changes their selections.

I've split this PR into two commits:

  1. Move from a shown boolean to a dataState enum, to allow the addition of a third state
  2. Add in the dirty state dialog

Demo

https://d2l.slack.com/archives/C0PHG3QB0/p1775842223156609

@duncanuszkay-d2l duncanuszkay-d2l changed the base branch from main to dunk.misc-table-changes April 14, 2026 14:43
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for the PR! 🎉

We've deployed an automatic preview for this PR - you can see your changes here:

URL https://live.d2l.dev/prs/BrightspaceUI/core/pr-6798/

Note

The build needs to finish before your changes are deployed.
Changes to the PR will automatically update the instance.

@duncanuszkay-d2l duncanuszkay-d2l force-pushed the dunk.dirty-dialog branch 3 times, most recently from 4cd674b to a373718 Compare April 16, 2026 17:43
@duncanuszkay-d2l duncanuszkay-d2l force-pushed the dunk.misc-table-changes branch from 033b515 to 00c1358 Compare April 16, 2026 18:27
Base automatically changed from dunk.misc-table-changes to main April 17, 2026 15:55
Comment on lines -122 to -127

if (changedProperties.has('shown') && (
(reduceMotion && this._state === 'shown') || (!reduceMotion && this._state === 'showing')
)) {
this.#centerLoadingSpinner();
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced this check with a simpler one- rather than checking to see if we're in the first state after hidden, requiring us to check reduceMotion to see which that first state should be, instead we're just checking if the last state is hidden.

Comment on lines +6 to +8
const overlayStyles = css`
.action-slot::slotted(*) {
display: inline;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These styles are replicated from the simple empty state component

} else if (oldState === 'loading' && newState === 'clean') {
this.#setLiveArea(this.localize('components.backdrop-loading.loadingCompleteAnnouncement'));
} else if (newState === 'dirty') {
this.#setLiveArea(this.#renderDirtyOverlay(), { delay: DIRTY_ANNOUNCEMENT_DELAY });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid complexities in coordinating the live region announcement and the display state of the visual overlay, I've opted to render the same content into the offscreen component. This results in an announcement that matches what you would get if the dirty overlay itself was in an aria-live component.

} else if (oldState === 'loading' && newState === 'clean') {
this.#setLiveArea(this.localize('components.backdrop-loading.loadingCompleteAnnouncement'));
} else if (newState === 'dirty') {
this.#setLiveArea(this.#renderDirtyOverlay(), { delay: DIRTY_ANNOUNCEMENT_DELAY });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed some differences in screen reader behavior here that I feel are worth mentioning:

NVDA: Announces the text before the button, then the button text, with no pause. E.G. "Your data is out of date you should refresh your filters refresh"

Narrator: Communicates that there's a group with text and a button, E.G. "Group, Your data is out of date your should refresh your filters, button, refresh"

The latter I assume is more what we want, but ultimately the component is surfacing the necessary info to NVDA so I'm not sure if there's anything actionable here. Have we dealt with similar issues elsewhere in the Corel library before?

@duncanuszkay-d2l duncanuszkay-d2l force-pushed the dunk.dirty-dialog branch 5 times, most recently from 54c986a to 7903c49 Compare April 17, 2026 19:11
@duncanuszkay-d2l duncanuszkay-d2l marked this pull request as ready for review April 17, 2026 19:48
@duncanuszkay-d2l duncanuszkay-d2l requested a review from a team as a code owner April 17, 2026 19:48
Comment thread components/backdrop/backdrop-dirty-overlay.js Outdated
Comment thread components/backdrop/backdrop-loading.js Outdated
Comment thread components/backdrop/backdrop-loading.js Outdated
@duncanuszkay-d2l
Copy link
Copy Markdown
Contributor Author

@GZolla Ready for another look 👍 Thanks!

Comment thread components/backdrop/backdrop-loading.js Outdated
},
/**
* Used to identify content that the backdrop should make inert
* @type {boolean}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't notice this before

Suggested change
* @type {boolean}
* @type {string}

Comment thread components/table/table-wrapper.js Outdated
}
}

willUpdate() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could use the PropertyRequiredMixin and a validator like in here.

<div style="position:relative">
<slot id="table-slot" @slotchange="${this._handleSlotChange}"></slot>
<d2l-backdrop-loading for="table-slot" ?shown=${this.loading}></d2l-backdrop-loading>
<d2l-backdrop-loading @d2l-backdrop-dirty-overlay-action=${this._handleDirtyButton} for="table-slot" dataState=${this.dataState} dirty-text="${this.dirtyText}" dirty-button-text="${this.dirtyButtonText}"></d2l-backdrop-loading>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a separate PR but I noticed that the scroll wrapper is not made inert meaning that it will still be focusable and scrollable, resulting in unexpected behavior. Maybe the backdrop should move to target the scroll wrapper instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, interesting- I'll dig deeper into that after this merges 👍

}

d2l-backdrop-dirty-overlay {
background-color: var(--d2l-theme-backdrop-dialog-color);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming that it would no longer use a custom variable and the background would not match the table's controls background

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would not match the table's controls background

I think that's fine, at least for now. I assume we could always override this value on the table side if we decide that's important.

Copy link
Copy Markdown
Contributor

@GZolla GZolla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's looking good, just missing the PropertyRequiredMixin for the validators to work

reflect: true,
attribute: 'dirty-text',
required: {
validator: (_value, elem, hasValue) => {hasValue || elem.dataState !== 'dirty';}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
validator: (_value, elem, hasValue) => {hasValue || elem.dataState !== 'dirty';}
validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty'

reflect: true,
attribute: 'dirty-button-text',
required: {
validator: (_value, elem, hasValue) => {hasValue || elem.dataState !== 'dirty';}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
validator: (_value, elem, hasValue) => {hasValue || elem.dataState !== 'dirty';}
validator: (_value, elem, hasValue) => hasValue || elem.dataState !== 'dirty'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants