Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions apps/automated/src/ui/label/label-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,82 @@ export class LabelTest extends testModule.UITest<Label> {
}
}

public test_maxWidth_caps_label_measured_width() {
const label = this.testView;
label.horizontalAlignment = 'left';
label.verticalAlignment = 'top';
// An explicit width larger than maxWidth: the used width is min(width, maxWidth).
label.width = 300;
label.maxWidth = 100;

this.waitUntilTestElementLayoutIsValid();

TKUnit.assertAreClose(label.getMeasuredWidth(), Utils.layout.toDevicePixels(100), 1, 'maxWidth should cap the label measured width to 100.');
}

public test_maxHeight_caps_label_measured_height() {
const label = this.testView;
label.horizontalAlignment = 'left';
label.verticalAlignment = 'top';
label.height = 300;
label.maxHeight = 100;

this.waitUntilTestElementLayoutIsValid();

TKUnit.assertAreClose(label.getMeasuredHeight(), Utils.layout.toDevicePixels(100), 1, 'maxHeight should cap the label measured height to 100.');
}

public test_maxWidth_larger_than_content_does_not_stretch_label() {
// A maximum larger than the natural content width must leave the label unaffected.
const label = this.testView;
label.horizontalAlignment = 'left';
label.verticalAlignment = 'top';
label.text = 'i';
label.fontSize = 9;
label.maxWidth = 1000;

this.waitUntilTestElementLayoutIsValid();

TKUnit.assertTrue(label.getMeasuredWidth() < Utils.layout.toDevicePixels(1000), 'A large maxWidth should not stretch the label to the maximum.');
}

public test_maxWidth_clamps_long_text_label() {
// The common responsive case: a wrapped label whose natural text is wider than maxWidth.
const label = this.testView;
label.horizontalAlignment = 'left';
label.verticalAlignment = 'top';
label.textWrap = true;
label.text = 'This is a fairly long label text that would otherwise be much wider than one hundred dips';
label.maxWidth = 100;

this.waitUntilTestElementLayoutIsValid();

const eps = Utils.layout.toDevicePixels(1);
TKUnit.assertTrue(label.getMeasuredWidth() <= Utils.layout.toDevicePixels(100) + eps, 'maxWidth should keep a long-text label within the maximum width.');
}

public test_maxWidth_percent_resolves_against_parent() {
const label = new Label();
label.text = 'Label';
label.horizontalAlignment = 'left';
label.verticalAlignment = 'top';
// Explicit width wider than the resolved 50% cap so the clamp is deterministic.
label.width = 300;
label.maxWidth = { value: 0.5, unit: '%' };

const host = new StackLayout();
host.width = 200;
host.height = 200;
host.addChild(label);

const mainPage = helper.getCurrentPage();
mainPage.content = host;
TKUnit.waitUntilReady(() => host.isLoaded && host.isLayoutValid);

// 50% of the 200-dip parent = 100 dip.
TKUnit.assertAreClose(label.getMeasuredWidth(), Utils.layout.toDevicePixels(100), 2, 'maxWidth percent should resolve to 50% of the parent width (100).');
}

public test_Set_TextWrap_Native() {
const testLabel = this.testView;
testLabel.text = 'this is very very very very very very very very very very very very very very very very very very very very very very very long text';
Expand Down
56 changes: 51 additions & 5 deletions apps/automated/src/ui/layouts/flexbox-layout-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1526,11 +1526,57 @@ export const testMinHeight_works_as_lower_bound_shrink_to = test(activity_minhei
closeEnough(height(flexbox), height(text1) + height(text2) + height(text3) + height(text4));
});

// We do not support maxWidth/maxHeight
// omit: testMaxWidth_initial_width_more_than_maxWidth
// omit: testMaxWidth_works_as_upper_bound_expand_to
// omit: testMaxHeight_initial_height_more_than_maxHeight
// omit: testMaxHeight_works_as_lower_bound_expand_to
let activity_maxwidth_test = () =>
getViews(
`<FlexboxLayout iosOverflowSafeArea="false" id="flexbox" width="400" height="400" backgroundColor="gray">
<Label id="text1" horizontalAlignment="left" verticalAlignment="top" width="200" text="1" maxWidth="100" backgroundColor="red" />
<Label id="text2" horizontalAlignment="left" verticalAlignment="top" width="200" text="2" maxWidth="100" backgroundColor="green" flexGrow="1" />
</FlexboxLayout>`,
);

export const testMaxWidth_initial_width_more_than_maxWidth = test(activity_maxwidth_test, noop, ({ root, flexbox, text1, text2 }) => {
closeEnough(width(text1), dipToDp(100));
closeEnough(width(text2), dipToDp(100));
});

let activity_maxwidth_upper_bound_test = () =>
getViews(
`<FlexboxLayout iosOverflowSafeArea="false" id="flexbox" width="400" height="400" flexWrap="${FlexWrap.NOWRAP}" backgroundColor="gray">
<Label id="text1" width="50" verticalAlignment="top" text="1" maxWidth="100" flexGrow="1" backgroundColor="red" />
<Label id="text2" width="50" verticalAlignment="top" text="2" flexGrow="1" backgroundColor="green" />
</FlexboxLayout>`,
);

export const testMaxWidth_works_as_upper_bound_expand_to = test(activity_maxwidth_upper_bound_test, noop, ({ root, flexbox, text1, text2 }) => {
closeEnough(width(text1), dipToDp(100));
closeEnough(width(text2), width(flexbox) - dipToDp(100));
});

let activity_maxheight_test = () =>
getViews(
`<FlexboxLayout iosOverflowSafeArea="false" id="flexbox" width="400" height="400" flexDirection="${FlexDirection.COLUMN}" backgroundColor="gray">
<Label id="text1" horizontalAlignment="left" verticalAlignment="top" height="200" text="1" maxHeight="100" backgroundColor="red" />
<Label id="text2" horizontalAlignment="left" verticalAlignment="top" height="200" text="2" maxHeight="100" flexGrow="1" backgroundColor="green" />
</FlexboxLayout>`,
);

export const testMaxHeight_initial_height_more_than_maxHeight = test(activity_maxheight_test, noop, ({ root, flexbox, text1, text2 }) => {
closeEnough(height(text1), dipToDp(100));
closeEnough(height(text2), dipToDp(100));
});

let activity_maxheight_upper_bound_test = () =>
getViews(
`<FlexboxLayout iosOverflowSafeArea="false" id="flexbox" width="400" height="400" flexDirection="${FlexDirection.COLUMN}" flexWrap="${FlexWrap.NOWRAP}" backgroundColor="gray">
<Label id="text1" horizontalAlignment="left" height="50" text="1" maxHeight="100" flexGrow="1" backgroundColor="red" />
<Label id="text2" horizontalAlignment="left" height="50" text="2" flexGrow="1" backgroundColor="green" />
</FlexboxLayout>`,
);

export const testMaxHeight_works_as_upper_bound_expand_to = test(activity_maxheight_upper_bound_test, noop, ({ root, flexbox, text1, text2 }) => {
closeEnough(height(text1), dipToDp(100));
closeEnough(height(text2), height(flexbox) - dipToDp(100));
});

let activity_views_visibility_gone = () =>
getViews(
Expand Down
29 changes: 29 additions & 0 deletions apps/automated/src/ui/layouts/stack-layout-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,35 @@ export class StackLayoutTest extends testModule.UITest<StackLayout> {
TKUnit.assertEqual(this.rootLayout.getMeasuredHeight(), Math.max(this.btn1.getMeasuredHeight(), this.btn2.getMeasuredHeight()), 'Layout getMeasuredHeight should be Max of children getMeasuredHeight');
}

public test_maxWidth_caps_child_measured_width() {
this.btn1.width = 300;
this.btn1.maxWidth = 100;

this.waitUntilTestElementLayoutIsValid();

TKUnit.assertAreClose(this.btn1.getMeasuredWidth(), utils.layout.toDevicePixels(100), 1, 'maxWidth should cap the child measured width to 100.');
}

public test_maxHeight_caps_child_measured_height() {
this.btn1.height = 300;
this.btn1.maxHeight = 100;

this.waitUntilTestElementLayoutIsValid();

TKUnit.assertAreClose(this.btn1.getMeasuredHeight(), utils.layout.toDevicePixels(100), 1, 'maxHeight should cap the child measured height to 100.');
}

public test_maxWidth_percent_resolves_against_parent() {
this.rootLayout.width = 200;
this.btn1.width = 300;
this.btn1.maxWidth = { value: 0.5, unit: '%' };

this.waitUntilTestElementLayoutIsValid();

// 50% of the 200-dip parent = 100 dip.
TKUnit.assertAreClose(this.btn1.getMeasuredWidth(), utils.layout.toDevicePixels(100), 2, 'maxWidth percent should resolve to 50% of the parent width (100).');
}

public test_Padding_Vertical() {
this.rootLayout.width = { value: 300, unit: 'px' };
this.rootLayout.height = { value: 300, unit: 'px' };
Expand Down
32 changes: 32 additions & 0 deletions apps/automated/src/ui/styling/style-properties-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,30 @@ export function test_setting_minHeight_dip_property_from_CSS_is_applied_to_Style
test_property_from_CSS_is_applied_to_style('minHeight', 'min-height', 200, '200dip', true);
}

export function test_setting_maxWidth_property_from_CSS_is_applied_to_Style() {
test_property_from_CSS_is_applied_to_style('maxWidth', 'max-width', 200, '200', true);
}

export function test_setting_maxWidth_dip_property_from_CSS_is_applied_to_Style() {
test_property_from_CSS_is_applied_to_style('maxWidth', 'max-width', 200, '200dip', true);
}

export function test_setting_maxWidth_percent_property_from_CSS_is_applied_to_Style() {
test_property_from_CSS_is_applied_to_style('maxWidth', 'max-width', { value: 0.5, unit: '%' }, '50%', true);
}

export function test_setting_maxHeight_property_from_CSS_is_applied_to_Style() {
test_property_from_CSS_is_applied_to_style('maxHeight', 'max-height', 200, '200', true);
}

export function test_setting_maxHeight_dip_property_from_CSS_is_applied_to_Style() {
test_property_from_CSS_is_applied_to_style('maxHeight', 'max-height', 200, '200dip', true);
}

export function test_setting_maxHeight_percent_property_from_CSS_is_applied_to_Style() {
test_property_from_CSS_is_applied_to_style('maxHeight', 'max-height', { value: 0.5, unit: '%' }, '50%', true);
}

export function test_setting_verticalAlignment_property_from_CSS_is_applied_to_Style() {
test_property_from_CSS_is_applied_to_style('verticalAlignment', 'vertical-align', 'bottom');
}
Expand Down Expand Up @@ -214,6 +238,14 @@ export function test_minHeight_property_is_synced_in_style_and_view() {
test_property_is_synced_in_style_and_view('minHeight', 200);
}

export function test_maxWidth_property_is_synced_in_style_and_view() {
test_property_is_synced_in_style_and_view('maxWidth', 200);
}

export function test_maxHeight_property_is_synced_in_style_and_view() {
test_property_is_synced_in_style_and_view('maxHeight', 200);
}

export function test_verticalAlignment_property_is_synced_in_style_and_view() {
test_property_is_synced_in_style_and_view('verticalAlignment', 'bottom');
}
Expand Down
Binary file modified packages/core/platforms/android/widgets-release.aar
Binary file not shown.
5 changes: 5 additions & 0 deletions packages/core/ui/core/view-base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,8 @@ export abstract class ViewBase extends Observable {

public effectiveMinWidth: number;
public effectiveMinHeight: number;
public effectiveMaxWidth: number;
public effectiveMaxHeight: number;
public effectiveWidth: number;
public effectiveHeight: number;
public effectiveMarginTop: number;
Expand Down Expand Up @@ -1550,6 +1552,9 @@ ViewBase.prototype._oldBottom = 0;

ViewBase.prototype.effectiveMinWidth = 0;
ViewBase.prototype.effectiveMinHeight = 0;
// Infinity means unconstrained, so Math.min(measured, effectiveMaxWidth) is a no-op when unset.
ViewBase.prototype.effectiveMaxWidth = Number.POSITIVE_INFINITY;
ViewBase.prototype.effectiveMaxHeight = Number.POSITIVE_INFINITY;
ViewBase.prototype.effectiveWidth = 0;
ViewBase.prototype.effectiveHeight = 0;
ViewBase.prototype.effectiveMarginTop = 0;
Expand Down
24 changes: 23 additions & 1 deletion packages/core/ui/core/view/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ShowModalOptions, hiddenProperty } from '../view-base';
import { isCssWideKeyword } from '../properties/property-shared';
import { EventData } from '../../../data/observable';

import { perspectiveProperty, visibilityProperty, opacityProperty, horizontalAlignmentProperty, verticalAlignmentProperty, minWidthProperty, minHeightProperty, widthProperty, heightProperty, marginLeftProperty, marginTopProperty, marginRightProperty, marginBottomProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty, androidElevationProperty, androidDynamicElevationOffsetProperty } from '../../styling/style-properties';
import { perspectiveProperty, visibilityProperty, opacityProperty, horizontalAlignmentProperty, verticalAlignmentProperty, minWidthProperty, minHeightProperty, maxWidthProperty, maxHeightProperty, widthProperty, heightProperty, marginLeftProperty, marginTopProperty, marginRightProperty, marginBottomProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty, androidElevationProperty, androidDynamicElevationOffsetProperty } from '../../styling/style-properties';
import { CoreTypes } from '../../../core-types';

import { Background, BackgroundClearFlags, refreshBorderDrawable } from '../../styling/background';
Expand Down Expand Up @@ -2000,3 +2000,25 @@ createNativePercentLengthProperty({
return org.nativescript.widgets.ViewHelper.setMinHeight;
},
});

createNativePercentLengthProperty({
setter: maxWidthProperty.setNative,
auto: -1, // -1 means unconstrained (no maximum).
get setPixels() {
return org.nativescript.widgets.ViewHelper.setMaxWidth;
},
get setPercent() {
return org.nativescript.widgets.ViewHelper.setMaxWidthPercent;
},
});

createNativePercentLengthProperty({
setter: maxHeightProperty.setNative,
auto: -1, // -1 means unconstrained (no maximum).
get setPixels() {
return org.nativescript.widgets.ViewHelper.setMaxHeight;
},
get setPercent() {
return org.nativescript.widgets.ViewHelper.setMaxHeightPercent;
},
});
16 changes: 16 additions & 0 deletions packages/core/ui/core/view/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,22 @@ export abstract class View extends ViewCommon {
*/
minHeight: CoreTypes.LengthType;

/**
* Gets or sets the maximum width the view may grow to. Accepts a fixed length
* or a percentage of the available width. Defaults to 'auto' (no maximum).
*
* @nsProperty
*/
maxWidth: CoreTypes.PercentLengthType;

/**
* Gets or sets the maximum height the view may grow to. Accepts a fixed length
* or a percentage of the available height. Defaults to 'auto' (no maximum).
*
* @nsProperty
*/
maxHeight: CoreTypes.PercentLengthType;

/**
* Gets or sets the desired width of the view.
*
Expand Down
9 changes: 7 additions & 2 deletions packages/core/ui/core/view/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,13 @@ export class View extends ViewCommon {
nativeHeight = nativeSize.height;
}

const measureWidth = Math.max(nativeWidth, this.effectiveMinWidth);
const measureHeight = Math.max(nativeHeight, this.effectiveMinHeight);
let measureWidth = Math.max(nativeWidth, this.effectiveMinWidth);
let measureHeight = Math.max(nativeHeight, this.effectiveMinHeight);

// Clamp to max-width/max-height (effectiveMax* is Infinity when unconstrained).
// Applied after min so that max wins when min > max, matching CSS.
measureWidth = Math.min(measureWidth, this.effectiveMaxWidth);
measureHeight = Math.min(measureHeight, this.effectiveMaxHeight);

const widthAndState = View.resolveSizeAndState(measureWidth, width, widthMode, 0);
const heightAndState = View.resolveSizeAndState(measureHeight, height, heightMode, 0);
Expand Down
33 changes: 33 additions & 0 deletions packages/core/ui/core/view/view-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ type InteractiveTransitionState = { began?: boolean; cancelled?: boolean; option
// TODO: remove once we fully switch to the new event system
const warnedEvent = new Set<string>();

// Resolves a max-width/max-height style value to an effective device-pixel constraint.
// 'auto' (the default) or an unresolvable percent (parent size unknown) means "no maximum",
// represented as Infinity so that Math.min(measured, effectiveMax) becomes a no-op.
function resolveEffectiveMax(value: CoreTypes.PercentLengthType, availableSize: number): number {
if (value == null || value === 'auto') {
return Number.POSITIVE_INFINITY;
}
// A percent cannot be resolved when the parent size is unspecified (availableSize < 0);
// treat it as unconstrained rather than collapsing the view to ~0.
if (typeof value === 'object' && (value as CoreTypes.LengthPercentUnit).unit === '%' && availableSize < 0) {
return Number.POSITIVE_INFINITY;
}
const resolved = PercentLength.toDevicePixels(value, Number.POSITIVE_INFINITY, availableSize);
// Safety net for any other unresolved/bogus negative result.
return resolved < 0 ? Number.POSITIVE_INFINITY : resolved;
}

export abstract class ViewCommon extends ViewBase {
public static layoutChangedEvent = 'layoutChanged';
public static shownModallyEvent = 'shownModally';
Expand Down Expand Up @@ -765,6 +782,20 @@ export abstract class ViewCommon extends ViewBase {
this.style.minHeight = value;
}

get maxWidth(): CoreTypes.PercentLengthType {
return this.style.maxWidth;
}
set maxWidth(value: CoreTypes.PercentLengthType) {
this.style.maxWidth = value;
}

get maxHeight(): CoreTypes.PercentLengthType {
return this.style.maxHeight;
}
set maxHeight(value: CoreTypes.PercentLengthType) {
this.style.maxHeight = value;
}

get width(): CoreTypes.PercentLengthType {
return this.style.width;
}
Expand Down Expand Up @@ -1225,12 +1256,14 @@ export abstract class ViewCommon extends ViewBase {
const availableWidth = parentWidthMeasureMode === layout.UNSPECIFIED ? -1 : parentWidthMeasureSize;

this.effectiveWidth = PercentLength.toDevicePixels(style.width, -2, availableWidth);
this.effectiveMaxWidth = resolveEffectiveMax(style.maxWidth, availableWidth);
this.effectiveMarginLeft = PercentLength.toDevicePixels(style.marginLeft, 0, availableWidth);
this.effectiveMarginRight = PercentLength.toDevicePixels(style.marginRight, 0, availableWidth);

const availableHeight = parentHeightMeasureMode === layout.UNSPECIFIED ? -1 : parentHeightMeasureSize;

this.effectiveHeight = PercentLength.toDevicePixels(style.height, -2, availableHeight);
this.effectiveMaxHeight = resolveEffectiveMax(style.maxHeight, availableHeight);
this.effectiveMarginTop = PercentLength.toDevicePixels(style.marginTop, 0, availableHeight);
this.effectiveMarginBottom = PercentLength.toDevicePixels(style.marginBottom, 0, availableHeight);
}
Expand Down
Loading
Loading