Skip to content

Commit

Permalink
Merge pull request #110 from GavinJoyce/dialog-scroll-locking
Browse files Browse the repository at this point in the history
fix(dialog): lock scroll when the dialog is open
  • Loading branch information
alexlafroscia authored Nov 12, 2021
2 parents c3335bc + d847f3f commit 319f244
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 2 deletions.
1 change: 1 addition & 0 deletions addon/components/dialog.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
}}
{{this.handleEscapeKey @isOpen this.onClose}}
{{this.lockWindowScroll}}
{{did-insert (fn this.dialogStackProvider.push this)}}
{{will-destroy (fn this.dialogStackProvider.remove this)}}
>
Expand Down
32 changes: 31 additions & 1 deletion addon/components/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import { modifier } from 'ember-modifier';

import type DialogStackProvider from 'ember-headlessui/services/dialog-stack-provider';

function getPortalRoot() {
/**
* Expose the element that the `Dialog` should be "slotted" into
*
* This is exported _only_ for testing purposes; do not consider this API to be public
*
* @private
*/
export function getPortalRoot() {
const { rootElement } = getOwnConfig();

// If we looked up a `rootElement` config at build-time, use that; otherwise use the body
Expand Down Expand Up @@ -52,6 +59,29 @@ export default class DialogComponent extends Component<Args> {
}
);

lockWindowScroll = modifier(() => {
// Opt-out of some other dialog already locked scrolling
if (this.dialogStackProvider.dialogIsOpen) {
return;
}

let overflow = this.$portalRoot.style.overflow;
let paddingRight = this.$portalRoot.style.paddingRight;

// Setting `overflow: hidden` will suddenly hide the scroll bar on the window, which can cause horizontal
// layout shifting when the `Dialog` becomes open
// By applying the width of the scroll bar as padding, we can avoid that layout shift from happening
let scrollbarWidth = window.innerWidth - this.$portalRoot.clientWidth;

this.$portalRoot.style.overflow = 'hidden';
this.$portalRoot.style.paddingRight = `${scrollbarWidth}px`;

return () => {
this.$portalRoot.style.overflow = overflow;
this.$portalRoot.style.paddingRight = paddingRight;
};
});

constructor(owner: unknown, args: Args) {
super(owner, args);

Expand Down
4 changes: 4 additions & 0 deletions addon/services/dialog-stack-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ interface WithGuid {
export default class DialogStackProvider extends Service {
stack: string[] = [];

get dialogIsOpen() {
return this.stack.length !== 0;
}

@action
hasOpenChild(dialog: WithGuid) {
return this.stack[this.stack.length - 1] !== dialog.guid;
Expand Down
46 changes: 45 additions & 1 deletion tests/integration/components/dialog-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { module, test, todo } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

import userEvent from '@testing-library/user-event';
import { getPortalRoot } from 'ember-headlessui/components/dialog';
import { Keys } from 'ember-headlessui/utils/keyboard';

import {
Expand Down Expand Up @@ -233,7 +234,50 @@ module('Integration | Component | <Dialog>', function (hooks) {
async function () {}
);

todo('it should add a scroll lock to the html tag', async function () {});
test('it should add a scroll lock to the html tag', async function (assert) {
const portalRoot = getPortalRoot();
this.set('isOpen', false);

await render(hbs`
<button id="trigger" type="button" {{on "click" (set this "isOpen" true)}}>
Trigger
</button>
<Dialog
class="relative bg-blue-500"
@isOpen={{this.isOpen}}
@onClose={{set this "isOpen" false}}
as |d|
>
<d.Overlay data-test-overlay>Hello</d.Overlay>
<div tabindex="0"></div>
</Dialog>
`);

assert
.dom(portalRoot)
.doesNotHaveStyle(
{ overflow: 'hidden' },
'The page is not initially "locked"'
);

await click(document.getElementById('trigger'));

assert
.dom(portalRoot)
.hasStyle(
{ overflow: 'hidden' },
'The page becomes "locked" when the dialog is open'
);

await click('[data-test-overlay]');

assert
.dom(portalRoot)
.doesNotHaveStyle(
{ overflow: 'hidden' },
'The page is "unlocked" when the dialog closes'
);
});
});

module('Dialog.Overlay', function () {
Expand Down

0 comments on commit 319f244

Please sign in to comment.