Skip to content
Closed
Changes from 1 commit
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
39 changes: 25 additions & 14 deletions app/component-library/components/HeaderBase/HeaderBase.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Third party dependencies.
import React, { useCallback, useState } from 'react';
import { View, LayoutChangeEvent } from 'react-native';
import React, { useRef, useState, useLayoutEffect } from 'react';
import { View } from 'react-native';

// External dependencies.
import {
Expand Down Expand Up @@ -42,17 +42,11 @@ const HeaderBase: React.FC<HeaderBaseProps> = ({
const tw = useTailwind();
const insets = useSafeAreaInsets();

const startAccessoryRef = useRef<View>(null);
const endAccessoryRef = useRef<View>(null);
const [startAccessoryWidth, setStartAccessoryWidth] = useState(0);
const [endAccessoryWidth, setEndAccessoryWidth] = useState(0);

const handleStartAccessoryLayout = useCallback((e: LayoutChangeEvent) => {
setStartAccessoryWidth(e.nativeEvent.layout.width);
}, []);

const handleEndAccessoryLayout = useCallback((e: LayoutChangeEvent) => {
setEndAccessoryWidth(e.nativeEvent.layout.width);
}, []);

// Determine alignment and text variant based on variant prop
const isLeftAligned = variant === HeaderBaseVariant.Display;
const textVariant = HEADERBASE_VARIANT_TEXT_VARIANTS[variant];
Expand All @@ -63,6 +57,25 @@ const HeaderBase: React.FC<HeaderBaseProps> = ({
endAccessory || (endButtonIconProps && endButtonIconProps.length > 0);
const hasAnyAccessory = hasStartContent || hasEndContent;

// Measure accessory widths synchronously before paint to eliminate flicker
useLayoutEffect(() => {
if (startAccessoryRef.current) {
startAccessoryRef.current.measure((_x, _y, width, _height) => {
if (width > 0) {
setStartAccessoryWidth(width);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guard width > 0 prevents resetting stale accessory widths

High Severity

The width > 0 guard in both the measure() callback and the onLayout fallback prevents accessory widths from ever resetting to zero. In Compact variant, when one accessory is removed while the other remains, the wrapper still renders (for centering) but contains no children, resulting in a width-0 layout event. The guard blocks this update, so startAccessoryWidth or endAccessoryWidth retains its stale non-zero value. This causes accessoryWrapperWidth via Math.max to use an incorrect measurement, misaligning the title. The previous code had no such guard and unconditionally called setStartAccessoryWidth(e.nativeEvent.layout.width), correctly handling the zero-width case.

Additional Locations (1)

Fix in Cursor Fix in Web

});
}

if (endAccessoryRef.current) {
endAccessoryRef.current.measure((_x, _y, width, _height) => {
if (width > 0) {
setEndAccessoryWidth(width);
}
});
}
}, [hasStartContent, hasEndContent]);
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

// For Compact: render both wrappers if any accessory exists (for centering)
// For Display: only render wrappers if their respective accessory exists
const shouldRenderStartWrapper = isLeftAligned
Expand Down Expand Up @@ -150,9 +163,7 @@ const HeaderBase: React.FC<HeaderBaseProps> = ({
}
{...startAccessoryWrapperProps}
>
<View onLayout={handleStartAccessoryLayout}>
{renderStartContent()}
</View>
<View ref={startAccessoryRef}>{renderStartContent()}</View>
</View>
)}

Expand Down Expand Up @@ -182,7 +193,7 @@ const HeaderBase: React.FC<HeaderBaseProps> = ({
{...endAccessoryWrapperProps}
>
<View
onLayout={handleEndAccessoryLayout}
ref={endAccessoryRef}
style={
hasMultipleEndButtons ? tw.style('flex-row gap-2') : undefined
}
Expand Down
Loading