Skip to content

Commit

Permalink
fix(floatinglabel): Estimate hidden scroll width (material-components…
Browse files Browse the repository at this point in the history
…#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.
  • Loading branch information
patrickrodee authored Jan 10, 2020
1 parent 19f8724 commit 981ec9b
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/mdc-dom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Function Signature | Description
--- | ---
`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.
`matches(element: Element, selector: string) => boolean` | Returns true if the given element matches the given CSS selector.
`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;`.

### Event Functions

Expand Down
27 changes: 27 additions & 0 deletions packages/mdc-dom/ponyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,30 @@ export function matches(element: Element, selector: string): boolean {
|| element.msMatchesSelector;
return nativeMatches.call(element, selector);
}

/**
* Used to compute the estimated scroll width of elements. When an element is
* hidden due to display: none; being applied to a parent element, the width is
* returned as 0. However, the element will have a true width once no longer
* inside a display: none context. This method computes an estimated width when
* the element is hidden or returns the true width when the element is visble.
* @param {Element} element the element whose width to estimate
*/
export function estimateScrollWidth(element: Element): number {
// Check the offsetParent. If the element inherits display: none from any
// parent, the offsetParent property will be null (see
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent).
// This check ensures we only clone the node when necessary.
const htmlEl = element as HTMLElement;
if (htmlEl.offsetParent !== null) {
return htmlEl.scrollWidth;
}

const clone = htmlEl.cloneNode(true) as HTMLElement;
clone.style.setProperty('position', 'absolute');
clone.style.setProperty('transform', 'translate(-9999px, -9999px)');
document.documentElement.appendChild(clone);
const scrollWidth = clone.scrollWidth;
document.documentElement.removeChild(clone);
return scrollWidth;
}
3 changes: 2 additions & 1 deletion packages/mdc-floating-label/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/

import {MDCComponent} from '@material/base/component';
import {estimateScrollWidth} from '@material/dom/ponyfill';
import {MDCFloatingLabelAdapter} from './adapter';
import {MDCFloatingLabelFoundation} from './foundation';

Expand Down Expand Up @@ -59,7 +60,7 @@ export class MDCFloatingLabel extends MDCComponent<MDCFloatingLabelFoundation> {
const adapter: MDCFloatingLabelAdapter = {
addClass: (className) => this.root_.classList.add(className),
removeClass: (className) => this.root_.classList.remove(className),
getWidth: () => this.root_.scrollWidth,
getWidth: () => estimateScrollWidth(this.root_),
registerInteractionHandler: (evtType, handler) => this.listen(evtType, handler),
deregisterInteractionHandler: (evtType, handler) => this.unlisten(evtType, handler),
};
Expand Down
1 change: 1 addition & 0 deletions packages/mdc-floating-label/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"@material/animation": "^4.0.0",
"@material/base": "^4.0.0",
"@material/dom": "^4.0.0",
"@material/feature-targeting": "^4.0.0",
"@material/rtl": "^4.0.0",
"@material/theme": "^4.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/mdc-textfield/test/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ describe('MDCTextField', () => {
const component = new MDCTextField(root);
const adapter = (component.getDefaultFoundation() as any).adapter_;
expect(adapter.hasClass('foo')).toBe(false);
expect(adapter.getLabelWidth()).toEqual(0);
expect(adapter.getLabelWidth()).toBeGreaterThan(0);
expect(() => adapter.floatLabel).not.toThrow();
});

Expand Down
18 changes: 17 additions & 1 deletion test/unit/mdc-dom/ponyfill.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {assert} from 'chai';
import bel from 'bel';
import td from 'testdouble';

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

suite('MDCDom - ponyfill');

Expand Down Expand Up @@ -86,3 +86,19 @@ test('#matches supports vendor prefixes', () => {
assert.isTrue(matches({webkitMatchesSelector: () => true}, ''));
assert.isTrue(matches({msMatchesSelector: () => true}, ''));
});

test('#estimateScrollWidth returns the default width when the element is not hidden', () => {
const root = bel`<span>
<span id="i0" style="width:10px;"></span>
</span>`;
const el = root.querySelector('#i0');
assert.strictEqual(estimateScrollWidth(el), 10);
});

test('#estimateScrollWidth returns the estimated width when the element is hidden', () => {
const root = bel`<span style="display:none;">
<span id="i0" style="width:10px;"></span>
</span>`;
const el = root.querySelector('#i0');
assert.strictEqual(estimateScrollWidth(el), 10);
});

0 comments on commit 981ec9b

Please sign in to comment.