Skip to content

Commit

Permalink
fix(scrollviewer): Fix invalid measure (scroll flickering) of TextBox…
Browse files Browse the repository at this point in the history
…View when directly nested in ScrollViewer
  • Loading branch information
dr1rrb committed Oct 29, 2020
1 parent 449cdcb commit 73db47a
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 101 deletions.
1 change: 1 addition & 0 deletions src/Uno.UI/WasmScripts/Uno.UI.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ declare namespace Uno.UI {
private static MAX_HEIGHT;
private measureElement;
private measureViewInternal;
private createUnconstrainedStyle;
scrollTo(pParams: number): boolean;
rawPixelsToBase64EncodeImage(dataPtr: number, width: number, height: number): string;
/**
Expand Down
96 changes: 52 additions & 44 deletions src/Uno.UI/WasmScripts/Uno.UI.js
Original file line number Diff line number Diff line change
Expand Up @@ -1382,10 +1382,11 @@ var Uno;
const elementStyle = element.style;
const elementClasses = element.className;
const originalStyleCssText = elementStyle.cssText;
const unconstrainedStyleCssText = this.createUnconstrainedStyle(elementStyle, maxWidth, maxHeight);
let parentElement = null;
let parentElementWidthHeight = null;
let unconnectedRoot = null;
let cleanupUnconnectedRoot = function (owner) {
let cleanupUnconnectedRoot = (owner) => {
if (unconnectedRoot !== null) {
owner.removeChild(unconnectedRoot);
}
Expand All @@ -1402,55 +1403,18 @@ var Uno;
}
this.containerElement.appendChild(unconnectedRoot);
}
// As per W3C css-transform spec:
// https://www.w3.org/TR/css-transforms-1/#propdef-transform
//
// > For elements whose layout is governed by the CSS box model, any value other than none
// > for the transform property also causes the element to establish a containing block for
// > all descendants.Its padding box will be used to layout for all of its
// > absolute - position descendants, fixed - position descendants, and descendant fixed
// > background attachments.
//
// We use this feature to allow an measure of text without being influenced by the bounds
// of the viewport. We just need to temporary set both the parent width & height to a very big value.
parentElement = element.parentElement;
parentElementWidthHeight = { width: parentElement.style.width, height: parentElement.style.height };
parentElement.style.width = WindowManager.MAX_WIDTH;
parentElement.style.height = WindowManager.MAX_HEIGHT;
const updatedStyles = {};
for (let i = 0; i < elementStyle.length; i++) {
const key = elementStyle[i];
updatedStyles[key] = elementStyle.getPropertyValue(key);
}
if (updatedStyles.hasOwnProperty("width")) {
delete updatedStyles.width;
}
if (updatedStyles.hasOwnProperty("height")) {
delete updatedStyles.height;
}
// This is required for an unconstrained measure (otherwise the parents size is taken into account)
updatedStyles.position = "fixed";
updatedStyles["max-width"] = Number.isFinite(maxWidth) ? maxWidth + "px" : "none";
updatedStyles["max-height"] = Number.isFinite(maxHeight) ? maxHeight + "px" : "none";
let updatedStyleString = "";
for (let key in updatedStyles) {
if (updatedStyles.hasOwnProperty(key)) {
updatedStyleString += key + ": " + updatedStyles[key] + "; ";
}
}
// We use a string to prevent the browser to update the element between
// each style assignation. This way, the browser will update the element only once.
elementStyle.cssText = updatedStyleString;
if (element instanceof HTMLImageElement) {
elementStyle.cssText = unconstrainedStyleCssText;
const imgElement = element;
return [imgElement.naturalWidth, imgElement.naturalHeight];
}
else if (element instanceof HTMLInputElement) {
elementStyle.cssText = unconstrainedStyleCssText;
const inputElement = element;
cleanupUnconnectedRoot(this.containerElement);
// Create a temporary element that will contain the input's content
var textOnlyElement = document.createElement("p");
textOnlyElement.style.cssText = updatedStyleString;
textOnlyElement.style.cssText = unconstrainedStyleCssText;
textOnlyElement.innerText = inputElement.value;
textOnlyElement.className = elementClasses;
unconnectedRoot = textOnlyElement;
Expand All @@ -1465,10 +1429,12 @@ var Uno;
cleanupUnconnectedRoot(this.containerElement);
// Create a temporary element that will contain the input's content
var textOnlyElement = document.createElement("p");
textOnlyElement.style.cssText = updatedStyleString;
textOnlyElement.style.cssText = unconstrainedStyleCssText;
textOnlyElement.style.whiteSpace = "pre"; // Make sure to preserve space for measure, especially the ending new line!
// If the input is null or empty, add a no-width character to force the paragraph to take up one line height
textOnlyElement.innerText = !inputElement.value ? "\u200B" : inputElement.value;
textOnlyElement.className = elementClasses;
// The trailing new lines are going to be ignored for measure, so we also append no-width char at the end.
textOnlyElement.innerText = inputElement.value ? (inputElement.value + "\u200B") : "\u200B";
textOnlyElement.className = elementClasses; // Note: Here we will have the uno-textBoxView class name
unconnectedRoot = textOnlyElement;
this.containerElement.appendChild(unconnectedRoot);
var textSize = this.measureElement(textOnlyElement);
Expand All @@ -1478,6 +1444,22 @@ var Uno;
return [width, height];
}
else {
elementStyle.cssText = unconstrainedStyleCssText;
// As per W3C css-transform spec:
// https://www.w3.org/TR/css-transforms-1/#propdef-transform
//
// > For elements whose layout is governed by the CSS box model, any value other than none
// > for the transform property also causes the element to establish a containing block for
// > all descendants.Its padding box will be used to layout for all of its
// > absolute - position descendants, fixed - position descendants, and descendant fixed
// > background attachments.
//
// We use this feature to allow an measure of text without being influenced by the bounds
// of the viewport. We just need to temporary set both the parent width & height to a very big value.
parentElement = element.parentElement;
parentElementWidthHeight = { width: parentElement.style.width, height: parentElement.style.height };
parentElement.style.width = WindowManager.MAX_WIDTH;
parentElement.style.height = WindowManager.MAX_HEIGHT;
return this.measureElement(element);
}
}
Expand All @@ -1490,6 +1472,32 @@ var Uno;
cleanupUnconnectedRoot(this.containerElement);
}
}
createUnconstrainedStyle(elementStyle, maxWidth, maxHeight) {
const updatedStyles = {};
for (let i = 0; i < elementStyle.length; i++) {
const key = elementStyle[i];
updatedStyles[key] = elementStyle.getPropertyValue(key);
}
if (updatedStyles.hasOwnProperty("width")) {
delete updatedStyles.width;
}
if (updatedStyles.hasOwnProperty("height")) {
delete updatedStyles.height;
}
// This is required for an unconstrained measure (otherwise the parents size is taken into account)
updatedStyles.position = "fixed";
updatedStyles["max-width"] = Number.isFinite(maxWidth) ? maxWidth + "px" : "none";
updatedStyles["max-height"] = Number.isFinite(maxHeight) ? maxHeight + "px" : "none";
let updatedStyleString = "";
for (let key in updatedStyles) {
if (updatedStyles.hasOwnProperty(key)) {
updatedStyleString += key + ": " + updatedStyles[key] + "; ";
}
}
// We use a string to prevent the browser to update the element between
// each style assignation. This way, the browser will update the element only once.
return updatedStyleString;
}
scrollTo(pParams) {
const params = WindowManagerScrollToOptionsParams.unmarshal(pParams);
const elt = this.getView(params.HtmlId);
Expand Down
122 changes: 65 additions & 57 deletions src/Uno.UI/ts/WindowManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1391,11 +1391,13 @@ namespace Uno.UI {
const elementStyle = element.style;
const elementClasses = element.className;
const originalStyleCssText = elementStyle.cssText;
const unconstrainedStyleCssText = this.createUnconstrainedStyle(elementStyle, maxWidth, maxHeight);

let parentElement: HTMLElement = null;
let parentElementWidthHeight: { width: string, height: string } = null;
let unconnectedRoot: HTMLElement = null;

let cleanupUnconnectedRoot = function (owner: HTMLDivElement) {
let cleanupUnconnectedRoot = (owner: HTMLDivElement) => {
if (unconnectedRoot !== null) {
owner.removeChild(unconnectedRoot);
}
Expand All @@ -1416,66 +1418,19 @@ namespace Uno.UI {
this.containerElement.appendChild(unconnectedRoot);
}

// As per W3C css-transform spec:
// https://www.w3.org/TR/css-transforms-1/#propdef-transform
//
// > For elements whose layout is governed by the CSS box model, any value other than none
// > for the transform property also causes the element to establish a containing block for
// > all descendants.Its padding box will be used to layout for all of its
// > absolute - position descendants, fixed - position descendants, and descendant fixed
// > background attachments.
//
// We use this feature to allow an measure of text without being influenced by the bounds
// of the viewport. We just need to temporary set both the parent width & height to a very big value.

parentElement = element.parentElement;
parentElementWidthHeight = { width: parentElement.style.width, height: parentElement.style.height };
parentElement.style.width = WindowManager.MAX_WIDTH;
parentElement.style.height = WindowManager.MAX_HEIGHT;

const updatedStyles = <any>{};

for (let i = 0; i < elementStyle.length; i++) {
const key = elementStyle[i];
updatedStyles[key] = elementStyle.getPropertyValue(key);
}

if (updatedStyles.hasOwnProperty("width")) {
delete updatedStyles.width;
}
if (updatedStyles.hasOwnProperty("height")) {
delete updatedStyles.height;
}

// This is required for an unconstrained measure (otherwise the parents size is taken into account)
updatedStyles.position = "fixed";
updatedStyles["max-width"] = Number.isFinite(maxWidth) ? maxWidth + "px" : "none";
updatedStyles["max-height"] = Number.isFinite(maxHeight) ? maxHeight + "px" : "none";

let updatedStyleString = "";

for (let key in updatedStyles) {
if (updatedStyles.hasOwnProperty(key)) {
updatedStyleString += key + ": " + updatedStyles[key] + "; ";
}
}

// We use a string to prevent the browser to update the element between
// each style assignation. This way, the browser will update the element only once.
elementStyle.cssText = updatedStyleString;

if (element instanceof HTMLImageElement) {
elementStyle.cssText = unconstrainedStyleCssText;
const imgElement = element as HTMLImageElement;
return [imgElement.naturalWidth, imgElement.naturalHeight];
}
else if (element instanceof HTMLInputElement) {
} else if (element instanceof HTMLInputElement) {
elementStyle.cssText = unconstrainedStyleCssText;
const inputElement = element as HTMLInputElement;

cleanupUnconnectedRoot(this.containerElement);

// Create a temporary element that will contain the input's content
var textOnlyElement = document.createElement("p") as HTMLParagraphElement;
textOnlyElement.style.cssText = updatedStyleString;
textOnlyElement.style.cssText = unconstrainedStyleCssText;
textOnlyElement.innerText = inputElement.value;
textOnlyElement.className = elementClasses;

Expand All @@ -1494,11 +1449,13 @@ namespace Uno.UI {

// Create a temporary element that will contain the input's content
var textOnlyElement = document.createElement("p") as HTMLParagraphElement;
textOnlyElement.style.cssText = updatedStyleString;
textOnlyElement.style.cssText = unconstrainedStyleCssText;
textOnlyElement.style.whiteSpace = "pre"; // Make sure to preserve space for measure, especially the ending new line!

// If the input is null or empty, add a no-width character to force the paragraph to take up one line height
textOnlyElement.innerText = !inputElement.value ? "\u200B" : inputElement.value;
textOnlyElement.className = elementClasses;
// The trailing new lines are going to be ignored for measure, so we also append no-width char at the end.
textOnlyElement.innerText = inputElement.value ? (inputElement.value + "\u200B") : "\u200B";
textOnlyElement.className = elementClasses; // Note: Here we will have the uno-textBoxView class name

unconnectedRoot = textOnlyElement;
this.containerElement.appendChild(unconnectedRoot);
Expand All @@ -1509,8 +1466,26 @@ namespace Uno.UI {
const width = Math.min(textSize[0], maxWidth);
var height = Math.min(textSize[1], maxHeight);
return [width, height];
}
else {
} else {
elementStyle.cssText = unconstrainedStyleCssText;

// As per W3C css-transform spec:
// https://www.w3.org/TR/css-transforms-1/#propdef-transform
//
// > For elements whose layout is governed by the CSS box model, any value other than none
// > for the transform property also causes the element to establish a containing block for
// > all descendants.Its padding box will be used to layout for all of its
// > absolute - position descendants, fixed - position descendants, and descendant fixed
// > background attachments.
//
// We use this feature to allow an measure of text without being influenced by the bounds
// of the viewport. We just need to temporary set both the parent width & height to a very big value.

parentElement = element.parentElement;
parentElementWidthHeight = { width: parentElement.style.width, height: parentElement.style.height };
parentElement.style.width = WindowManager.MAX_WIDTH;
parentElement.style.height = WindowManager.MAX_HEIGHT;

return this.measureElement(element);
}
}
Expand All @@ -1526,6 +1501,39 @@ namespace Uno.UI {
}
}

private createUnconstrainedStyle(elementStyle: CSSStyleDeclaration, maxWidth: number, maxHeight: number): string {
const updatedStyles = <any>{};

for (let i = 0; i < elementStyle.length; i++) {
const key = elementStyle[i];
updatedStyles[key] = elementStyle.getPropertyValue(key);
}

if (updatedStyles.hasOwnProperty("width")) {
delete updatedStyles.width;
}
if (updatedStyles.hasOwnProperty("height")) {
delete updatedStyles.height;
}

// This is required for an unconstrained measure (otherwise the parents size is taken into account)
updatedStyles.position = "fixed";
updatedStyles["max-width"] = Number.isFinite(maxWidth) ? maxWidth + "px" : "none";
updatedStyles["max-height"] = Number.isFinite(maxHeight) ? maxHeight + "px" : "none";

let updatedStyleString = "";

for (let key in updatedStyles) {
if (updatedStyles.hasOwnProperty(key)) {
updatedStyleString += key + ": " + updatedStyles[key] + "; ";
}
}

// We use a string to prevent the browser to update the element between
// each style assignation. This way, the browser will update the element only once.
return updatedStyleString;
}

public scrollTo(pParams: number): boolean {

const params = WindowManagerScrollToOptionsParams.unmarshal(pParams);
Expand Down

0 comments on commit 73db47a

Please sign in to comment.