Skip to content

Commit 1ea6d34

Browse files
crisbetotinayuangao
authored andcommitted
fix(dialog): avoid subpixel rendering issues and refactor GlobalPositionStrategy (#1962)
* fix(dialog): avoid subpixel rendering issues and refactor GlobalPositionStrategy Refactors the `GlobalPositionStrategy` to use flexbox for positioning. This avoids the subpixel rendering issues that come with using transforms for centering. Fixes #932. * Make the `dispose` method required. * Add extra comments regarding the flex wrapper. * Fix failing unit tests. * Fix the unit tests after the recent changes.
1 parent deb940f commit 1ea6d34

10 files changed

+172
-116
lines changed

src/lib/core/overlay/_overlay.scss

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88

99
// TODO(jelbourn): change from the `md` prefix to something else for everything in the toolkit.
1010

11-
// The overlay-container is an invisible element which contains all individual overlays.
12-
.md-overlay-container {
13-
position: fixed;
14-
11+
.md-overlay-container, .md-global-overlay-wrapper {
1512
// Disable events from being captured on the overlay container.
1613
pointer-events: none;
1714

@@ -20,9 +17,24 @@
2017
left: 0;
2118
height: 100%;
2219
width: 100%;
20+
}
21+
22+
// The overlay-container is an invisible element which contains all individual overlays.
23+
.md-overlay-container {
24+
position: fixed;
2325
z-index: $md-z-index-overlay-container;
2426
}
2527

28+
// We use an extra wrapper element in order to use make the overlay itself a flex item.
29+
// This makes centering the overlay easy without running into the subpixel rendering
30+
// problems tied to using `transform` and without interfering with the other position
31+
// strategies.
32+
.md-global-overlay-wrapper {
33+
display: flex;
34+
position: absolute;
35+
z-index: $md-z-index-overlay;
36+
}
37+
2638
// A single overlay pane.
2739
.md-overlay-pane {
2840
position: absolute;

src/lib/core/overlay/overlay-ref.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export class OverlayRef implements PortalHost {
4343
}
4444

4545
dispose(): void {
46+
if (this._state.positionStrategy) {
47+
this._state.positionStrategy.dispose();
48+
}
49+
4650
this._detachBackdrop();
4751
this._portalHost.dispose();
4852
}

src/lib/core/overlay/overlay.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,5 +251,6 @@ class FakePositionStrategy implements PositionStrategy {
251251
return Promise.resolve();
252252
}
253253

254+
dispose() {}
254255
}
255256

src/lib/core/overlay/position/connected-position-strategy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export class ConnectedPositionStrategy implements PositionStrategy {
5959
return this._preferredPositions;
6060
}
6161

62+
/**
63+
* To be used to for any cleanup after the element gets destroyed.
64+
*/
65+
dispose() { }
66+
6267
/**
6368
* Updates the position of the overlay element, using whichever preferred position relative
6469
* to the origin fits on-screen.

src/lib/core/overlay/position/global-position-strategy.spec.ts

Lines changed: 81 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,45 @@ describe('GlobalPositonStrategy', () => {
1313
beforeEach(() => {
1414
element = document.createElement('div');
1515
strategy = new GlobalPositionStrategy();
16+
document.body.appendChild(element);
1617
});
1718

18-
it('should set explicit (top, left) position to the element', fakeAsyncTest(() => {
19-
strategy.top('10px').left('40%').apply(element);
19+
afterEach(() => {
20+
strategy.dispose();
21+
});
22+
23+
it('should position the element to the (top, left) with an offset', fakeAsyncTest(() => {
24+
strategy.top('10px').left('40px').apply(element);
2025

2126
flushMicrotasks();
2227

23-
expect(element.style.top).toBe('10px');
24-
expect(element.style.left).toBe('40%');
25-
expect(element.style.bottom).toBe('');
26-
expect(element.style.right).toBe('');
28+
let elementStyle = element.style;
29+
let parentStyle = (element.parentNode as HTMLElement).style;
30+
31+
expect(elementStyle.marginTop).toBe('10px');
32+
expect(elementStyle.marginLeft).toBe('40px');
33+
expect(elementStyle.marginBottom).toBe('');
34+
expect(elementStyle.marginRight).toBe('');
35+
36+
expect(parentStyle.justifyContent).toBe('flex-start');
37+
expect(parentStyle.alignItems).toBe('flex-start');
2738
}));
2839

29-
it('should set explicit (bottom, right) position to the element', fakeAsyncTest(() => {
40+
it('should position the element to the (bottom, right) with an offset', fakeAsyncTest(() => {
3041
strategy.bottom('70px').right('15em').apply(element);
3142

3243
flushMicrotasks();
3344

34-
expect(element.style.top).toBe('');
35-
expect(element.style.left).toBe('');
36-
expect(element.style.bottom).toBe('70px');
37-
expect(element.style.right).toBe('15em');
45+
let elementStyle = element.style;
46+
let parentStyle = (element.parentNode as HTMLElement).style;
47+
48+
expect(elementStyle.marginTop).toBe('');
49+
expect(elementStyle.marginLeft).toBe('');
50+
expect(elementStyle.marginBottom).toBe('70px');
51+
expect(elementStyle.marginRight).toBe('15em');
52+
53+
expect(parentStyle.justifyContent).toBe('flex-end');
54+
expect(parentStyle.alignItems).toBe('flex-end');
3855
}));
3956

4057
it('should overwrite previously applied positioning', fakeAsyncTest(() => {
@@ -44,61 +61,85 @@ describe('GlobalPositonStrategy', () => {
4461
strategy.top('10px').left('40%').apply(element);
4562
flushMicrotasks();
4663

47-
expect(element.style.top).toBe('10px');
48-
expect(element.style.left).toBe('40%');
49-
expect(element.style.bottom).toBe('');
50-
expect(element.style.right).toBe('');
51-
expect(element.style.transform).not.toContain('translate');
64+
let elementStyle = element.style;
65+
let parentStyle = (element.parentNode as HTMLElement).style;
66+
67+
expect(elementStyle.marginTop).toBe('10px');
68+
expect(elementStyle.marginLeft).toBe('40%');
69+
expect(elementStyle.marginBottom).toBe('');
70+
expect(elementStyle.marginRight).toBe('');
71+
72+
expect(parentStyle.justifyContent).toBe('flex-start');
73+
expect(parentStyle.alignItems).toBe('flex-start');
5274

5375
strategy.bottom('70px').right('15em').apply(element);
5476

5577
flushMicrotasks();
5678

57-
expect(element.style.top).toBe('');
58-
expect(element.style.left).toBe('');
59-
expect(element.style.bottom).toBe('70px');
60-
expect(element.style.right).toBe('15em');
61-
expect(element.style.transform).not.toContain('translate');
79+
expect(element.style.marginTop).toBe('');
80+
expect(element.style.marginLeft).toBe('');
81+
expect(element.style.marginBottom).toBe('70px');
82+
expect(element.style.marginRight).toBe('15em');
83+
84+
expect(parentStyle.justifyContent).toBe('flex-end');
85+
expect(parentStyle.alignItems).toBe('flex-end');
6286
}));
6387

6488
it('should center the element', fakeAsyncTest(() => {
6589
strategy.centerHorizontally().centerVertically().apply(element);
6690

6791
flushMicrotasks();
6892

69-
expect(element.style.top).toBe('50%');
70-
expect(element.style.left).toBe('50%');
71-
expect(element.style.transform).toContain('translateX(-50%)');
72-
expect(element.style.transform).toContain('translateY(-50%)');
93+
let parentStyle = (element.parentNode as HTMLElement).style;
94+
95+
expect(parentStyle.justifyContent).toBe('center');
96+
expect(parentStyle.alignItems).toBe('center');
7397
}));
7498

7599
it('should center the element with an offset', fakeAsyncTest(() => {
76100
strategy.centerHorizontally('10px').centerVertically('15px').apply(element);
77101

78102
flushMicrotasks();
79103

80-
expect(element.style.top).toBe('50%');
81-
expect(element.style.left).toBe('50%');
82-
expect(element.style.transform).toContain('translateX(-50%)');
83-
expect(element.style.transform).toContain('translateX(10px)');
84-
expect(element.style.transform).toContain('translateY(-50%)');
85-
expect(element.style.transform).toContain('translateY(15px)');
104+
let elementStyle = element.style;
105+
let parentStyle = (element.parentNode as HTMLElement).style;
106+
107+
expect(elementStyle.marginLeft).toBe('10px');
108+
expect(elementStyle.marginTop).toBe('15px');
109+
110+
expect(parentStyle.justifyContent).toBe('center');
111+
expect(parentStyle.alignItems).toBe('center');
112+
}));
113+
114+
it('should make the element position: static', fakeAsyncTest(() => {
115+
strategy.apply(element);
116+
117+
flushMicrotasks();
118+
119+
expect(element.style.position).toBe('static');
86120
}));
87121

88-
it('should default the element to position: absolute', fakeAsyncTest(() => {
122+
it('should wrap the element in a `md-global-overlay-wrapper`', fakeAsyncTest(() => {
89123
strategy.apply(element);
90124

91125
flushMicrotasks();
92126

93-
expect(element.style.position).toBe('absolute');
127+
let parent = element.parentNode as HTMLElement;
128+
129+
expect(parent.classList.contains('md-global-overlay-wrapper')).toBe(true);
94130
}));
95131

96-
it('should make the element position: fixed', fakeAsyncTest(() => {
97-
strategy.fixed().apply(element);
132+
133+
it('should remove the parent wrapper from the DOM', fakeAsync(() => {
134+
strategy.apply(element);
98135

99136
flushMicrotasks();
100137

101-
expect(element.style.position).toBe('fixed');
138+
expect(document.body.contains(element.parentNode)).toBe(true);
139+
140+
strategy.dispose();
141+
142+
expect(document.body.contains(element.parentNode)).toBe(false);
102143
}));
103144

104145
it('should set the element width', fakeAsync(() => {
@@ -122,17 +163,17 @@ describe('GlobalPositonStrategy', () => {
122163

123164
flushMicrotasks();
124165

125-
expect(element.style.left).toBe('0px');
126-
expect(element.style.transform).toBe('');
166+
expect(element.style.marginLeft).toBe('0px');
167+
expect((element.parentNode as HTMLElement).style.justifyContent).toBe('flex-start');
127168
}));
128169

129170
it('should reset the vertical position and offset when the height is 100%', fakeAsync(() => {
130171
strategy.centerVertically().height('100%').apply(element);
131172

132173
flushMicrotasks();
133174

134-
expect(element.style.top).toBe('0px');
135-
expect(element.style.transform).toBe('');
175+
expect(element.style.marginTop).toBe('0px');
176+
expect((element.parentNode as HTMLElement).style.alignItems).toBe('flex-start');
136177
}));
137178
});
138179

0 commit comments

Comments
 (0)