Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit 981ec9b

Browse files
authoredJan 10, 2020
fix(floatinglabel): Estimate hidden scroll width (#5448)
In some cases, the floating label needs to immediately know its width. This creates problems if the floating label is instantiated inside a display: none; parent element, like a hidden dialog. To resolve that, we provide a helper method that estimates the width of an element if hidden. If visible, it computes the true width.
1 parent 19f8724 commit 981ec9b

File tree

6 files changed

+49
-3
lines changed

6 files changed

+49
-3
lines changed
 

‎packages/mdc-dom/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Function Signature | Description
3434
--- | ---
3535
`closest(element: Element, selector: string) => ?Element` | Returns the ancestor of the given element matching the given selector (which may be the element itself if it matches), or `null` if no matching ancestor is found.
3636
`matches(element: Element, selector: string) => boolean` | Returns true if the given element matches the given CSS selector.
37+
`estimateScrollWidth(element: Element) => number` | Returns the true optical width of the element if visible or an estimation if hidden by a parent element with `display: none;`.
3738

3839
### Event Functions
3940

‎packages/mdc-dom/ponyfill.ts

+27
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,30 @@ export function matches(element: Element, selector: string): boolean {
4747
|| element.msMatchesSelector;
4848
return nativeMatches.call(element, selector);
4949
}
50+
51+
/**
52+
* Used to compute the estimated scroll width of elements. When an element is
53+
* hidden due to display: none; being applied to a parent element, the width is
54+
* returned as 0. However, the element will have a true width once no longer
55+
* inside a display: none context. This method computes an estimated width when
56+
* the element is hidden or returns the true width when the element is visble.
57+
* @param {Element} element the element whose width to estimate
58+
*/
59+
export function estimateScrollWidth(element: Element): number {
60+
// Check the offsetParent. If the element inherits display: none from any
61+
// parent, the offsetParent property will be null (see
62+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent).
63+
// This check ensures we only clone the node when necessary.
64+
const htmlEl = element as HTMLElement;
65+
if (htmlEl.offsetParent !== null) {
66+
return htmlEl.scrollWidth;
67+
}
68+
69+
const clone = htmlEl.cloneNode(true) as HTMLElement;
70+
clone.style.setProperty('position', 'absolute');
71+
clone.style.setProperty('transform', 'translate(-9999px, -9999px)');
72+
document.documentElement.appendChild(clone);
73+
const scrollWidth = clone.scrollWidth;
74+
document.documentElement.removeChild(clone);
75+
return scrollWidth;
76+
}

‎packages/mdc-floating-label/component.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
*/
2323

2424
import {MDCComponent} from '@material/base/component';
25+
import {estimateScrollWidth} from '@material/dom/ponyfill';
2526
import {MDCFloatingLabelAdapter} from './adapter';
2627
import {MDCFloatingLabelFoundation} from './foundation';
2728

@@ -59,7 +60,7 @@ export class MDCFloatingLabel extends MDCComponent<MDCFloatingLabelFoundation> {
5960
const adapter: MDCFloatingLabelAdapter = {
6061
addClass: (className) => this.root_.classList.add(className),
6162
removeClass: (className) => this.root_.classList.remove(className),
62-
getWidth: () => this.root_.scrollWidth,
63+
getWidth: () => estimateScrollWidth(this.root_),
6364
registerInteractionHandler: (evtType, handler) => this.listen(evtType, handler),
6465
deregisterInteractionHandler: (evtType, handler) => this.unlisten(evtType, handler),
6566
};

‎packages/mdc-floating-label/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"dependencies": {
2525
"@material/animation": "^4.0.0",
2626
"@material/base": "^4.0.0",
27+
"@material/dom": "^4.0.0",
2728
"@material/feature-targeting": "^4.0.0",
2829
"@material/rtl": "^4.0.0",
2930
"@material/theme": "^4.0.0",

‎packages/mdc-textfield/test/component.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ describe('MDCTextField', () => {
284284
const component = new MDCTextField(root);
285285
const adapter = (component.getDefaultFoundation() as any).adapter_;
286286
expect(adapter.hasClass('foo')).toBe(false);
287-
expect(adapter.getLabelWidth()).toEqual(0);
287+
expect(adapter.getLabelWidth()).toBeGreaterThan(0);
288288
expect(() => adapter.floatLabel).not.toThrow();
289289
});
290290

‎test/unit/mdc-dom/ponyfill.test.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {assert} from 'chai';
2525
import bel from 'bel';
2626
import td from 'testdouble';
2727

28-
import {closest, matches} from '../../../packages/mdc-dom/ponyfill.ts';
28+
import {closest, matches, estimateScrollWidth} from '../../../packages/mdc-dom/ponyfill.ts';
2929

3030
suite('MDCDom - ponyfill');
3131

@@ -86,3 +86,19 @@ test('#matches supports vendor prefixes', () => {
8686
assert.isTrue(matches({webkitMatchesSelector: () => true}, ''));
8787
assert.isTrue(matches({msMatchesSelector: () => true}, ''));
8888
});
89+
90+
test('#estimateScrollWidth returns the default width when the element is not hidden', () => {
91+
const root = bel`<span>
92+
<span id="i0" style="width:10px;"></span>
93+
</span>`;
94+
const el = root.querySelector('#i0');
95+
assert.strictEqual(estimateScrollWidth(el), 10);
96+
});
97+
98+
test('#estimateScrollWidth returns the estimated width when the element is hidden', () => {
99+
const root = bel`<span style="display:none;">
100+
<span id="i0" style="width:10px;"></span>
101+
</span>`;
102+
const el = root.querySelector('#i0');
103+
assert.strictEqual(estimateScrollWidth(el), 10);
104+
});

0 commit comments

Comments
 (0)