Repository navigation
fix: let CoreTextField shrink inside fixed-width parents - #23
Conversation
CoreTextField's inner TextFieldWrapper only received flexGrow, and Valdi's flexShrink defaults to 0. On web the underlying input's intrinsic width (~200px) therefore escaped narrower parents, causing hidden horizontal page overflow (Snapchat#21). - Pass flexShrink=1 and minWidth=0 to the inner TextFieldWrapper so it can shrink below its intrinsic width when the parent is constrained. - Add a containerStyle view model property, merged over the default container style, so callers can size the component root. - Add regression tests for both behaviors.
|
@clholgat thoughts? |
beaucollins
left a comment
There was a problem hiding this comment.
Thanks for the contribution and the accompanying tests!
There's one concern around the performance of instantiating new Style objects during an onRender.
| const baseFont = TextStyleFont.BODY_EMPHASIS; | ||
| <view | ||
| style={styles.container} | ||
| style={viewModel.containerStyle ? Style.merge(styles.container, viewModel.containerStyle) : styles.container} |
There was a problem hiding this comment.
Creating or merging a Style during onRender() defeats the reuse optimization provided by Style.
See: https://github.com/Snapchat/Valdi/blob/main/docs/docs/core-styling.md#performance-considerations
Since the merged style depends only on viewModel.containerStyle, derive it in onViewModelUpdate() and retain it as a component instance property.
See: https://github.com/Snapchat/Valdi/blob/main/docs/docs/core-component.md#lifecycle
class CoreTextField extends StatefulComponent<CoreTextFieldViewModel, InternalState> {
private mergedContainerStyle: Style<View> = styles.container;
onViewModelUpdate(previousViewModel?: Readonly<CoreTextFieldViewModel>): void {
const containerStyle = this.viewModel.containerStyle;
if (
previousViewModel &&
previousViewModel.containerStyle === containerStyle
) {
return;
}
this.mergedContainerStyle = containerStyle
? Style.merge(styles.container, containerStyle)
: styles.container;
}
onRender(): void {
<view style={this.mergedContainerStyle}>
{/* ... */}
</view>;
}
}This still pays the cost of constructing a new Style whenever containerStyle changes, but avoids paying it on every render. An unrelated view-model update with the same containerStyle retains the cached merged style. This assumes the caller also provides a stable containerStyle instance.
Also note that the directly supplied backgroundColor, borderWidth, and borderColor attributes take precedence over style. Values for those properties in containerStyle will therefore be ignored.
If that behavior is intentional, the API should communicate it by excluding those properties:
type ContainerStyle = Omit<
View,
'backgroundColor' | 'borderColor' | 'borderWidth'
>;
containerStyle?: Style<ContainerStyle>;There was a problem hiding this comment.
Thanks for the review and the doc links! Both are addressed in eda22ee:
- The merge now happens in
onViewModelUpdate()and is cached inmergedContainerStyle, so it's only rebuilt when thecontainerStyleinstance changes, andonRender()reuses the sameStyle. I also added a note to the doc comment that callers should create the style once. - Went with the
Omittype, since those three are driven by the field's state:containerStyleis nowStyle<Omit<View, 'backgroundColor' | 'borderColor' | 'borderWidth'>>.
Separately, on the question in the description: should widgets tests use valdiIt going forward? Happy to migrate the rest of CoreTextFieldTest.ts in a follow-up :)
There was a problem hiding this comment.
Thanks! I'll get this reviewed.
Re valdiIt, yes that's the correct helper to use. composerIt is deprecated due to a naming change.
There was a problem hiding this comment.
Also worth noting this PR has me working through an API improvement that would allow for Style use that performs overrides on the native side.
So something like this:
<view style={[someStyle, someStyleOverride]}>
{/* ... */}
</view>Which means you no longer would have to worry about state management and merging styles.
There was a problem hiding this comment.
That's sick! No more merging/caching in the component would definitely be easier... Happy to swap CoreTextField to style={[styles.container, viewModel.containerStyle]} once that's merged. I'd be interested in looking at the valdiIt test migration too if that's available in the future!
Merge containerStyle in onViewModelUpdate and keep the result, so the Style is only rebuilt when containerStyle changes instead of on every render. Exclude backgroundColor, borderColor and borderWidth from the containerStyle type, since the container sets them as attributes from the field's state and attributes take precedence over style.
Fixes #21.
Root cause
CoreTextFieldpasses onlyflexGrow={1}to its innerTextFieldWrapper, and Valdi'sflexShrinkdefaults to0. On web, the underlyingvaldi-input's intrinsic width is the HTML input's default (~200px at typical fonts), so the field cannot shrink below that and escapes narrower fixed-width parents — hidden horizontal page overflow even when the parent visually clips. The view model also exposed no way to size the container from the caller.I verified the geometry in Chromium with a DOM reproduction of the rendered structure: with
flex-shrink: 0the input renders ~177px inside a 96px parent; with shrink allowed it fits at 62px.Changes
flexShrink={1}andminWidth={0}to the innerTextFieldWrapper(it already forwards both) so the field shrinks below its intrinsic width when the parent is constrained.containerStyleview model property, merged over the default container style viaStyle.merge, so callers can size the component root.Testing
All five CI targets pass locally (
widgets,navigation,navigation_internal,playground,valdi_standalone_ui). I also verified the new tests fail when the fix is reverted.Question
The new tests use
valdiItsincecreateComponentis marked@deprecated, while the existing tests in this file still usecreateComponent. IsvaldiItthe preferred direction for widgets tests going forward? Happy to migrate the rest ofCoreTextFieldTest.tshere or in a follow-up if so.