fix: flickering and disappeared list popover

This commit is contained in:
Jingjing
2025-12-19 13:45:23 +01:00
parent 312ee967fc
commit 015b132c0a
4 changed files with 67 additions and 263 deletions

View File

@@ -25,7 +25,9 @@ export default function OneField({
const theme = useTheme(); const theme = useTheme();
const [showListBoxPopover, setShowListBoxPopover] = useState(false); const [showListBoxPopover, setShowListBoxPopover] = useState(false);
let Component = null; let Component = null;
let ChildComponent = null;
let selection; let selection;
let displayName = '';
const isPinnedItem = !!field.isPinned && isPinFieldEnabled; const isPinnedItem = !!field.isPinned && isPinFieldEnabled;
const handleShowListBoxPopover = (e) => { const handleShowListBoxPopover = (e) => {
@@ -41,17 +43,8 @@ export default function OneField({
}; };
if (isPinnedItem) { if (isPinnedItem) {
Component = ( displayName = field.qName || field.qField;
<PinItem ChildComponent = <PinItem displayName={displayName} />;
field={field}
api={api}
showListBoxPopover={showListBoxPopover}
alignTo={alignTo}
skipHandleShowListBoxPopover={skipHandleShowListBoxPopover}
handleShowListBoxPopover={handleShowListBoxPopover}
handleCloseShowListBoxPopover={handleCloseShowListBoxPopover}
/>
);
} else { } else {
selection = field.selections[stateIx]; selection = field.selections[stateIx];
if (typeof selection.qTotal === 'undefined') { if (typeof selection.qTotal === 'undefined') {
@@ -159,40 +152,48 @@ export default function OneField({
))} ))}
</div> </div>
); );
Component = ( ChildComponent = (
<Grid <>
container
gap={1}
ref={alignTo}
sx={{
backgroundColor: theme.palette.background.paper,
position: 'relative',
cursor: 'pointer',
padding: '4px',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}}
onClick={(skipHandleShowListBoxPopover === false && handleShowListBoxPopover) || null}
>
{Header} {Header}
{Icon} {Icon}
{SegmentsIndicator} {SegmentsIndicator}
{showListBoxPopover && ( </>
<ListBoxPopover
alignTo={alignTo}
show={showListBoxPopover}
close={handleCloseShowListBoxPopover}
app={api.model}
fieldName={selection.qField}
stateName={field.states[stateIx]}
/>
)}
</Grid>
); );
} }
} }
Component = (
<Grid
container
gap={1}
ref={alignTo}
data-testid={isPinnedItem ? `pin-item-${displayName}` : `selection-item-${selection.qField}`}
sx={{
backgroundColor: theme.palette.background.paper,
position: 'relative',
cursor: 'pointer',
padding: '4px',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}}
onClick={(skipHandleShowListBoxPopover === false && handleShowListBoxPopover) || null}
>
{ChildComponent}
{showListBoxPopover && (
<ListBoxPopover
alignTo={alignTo}
show={showListBoxPopover}
close={handleCloseShowListBoxPopover}
app={api.model}
fieldName={isPinnedItem ? field.qField : selection.qField}
stateName={isPinnedItem ? undefined : field.states[stateIx]}
/>
)}
</Grid>
);
return moreAlignTo ? ( return moreAlignTo ? (
<ListBoxPopover <ListBoxPopover
alignTo={alignTo} alignTo={alignTo}

View File

@@ -1,51 +1,11 @@
import React from 'react'; import React from 'react';
import { Grid2, Typography } from '@mui/material'; import { Typography } from '@mui/material';
import { useTheme } from '@nebula.js/ui/theme';
import ListBoxPopover from '../listbox/ListBoxPopover';
function PinItem({
field,
api,
showListBoxPopover,
alignTo,
skipHandleShowListBoxPopover,
handleShowListBoxPopover,
handleCloseShowListBoxPopover,
}) {
const theme = useTheme();
const displayName = field.qName || field.qField;
function PinItem({ displayName }) {
return ( return (
<Grid2 <Typography noWrap style={{ fontSize: '12px', lineHeight: '16px', fontWeight: 600, marginTop: '8px' }}>
container {displayName}
gap={1} </Typography>
ref={alignTo}
data-testid={`pin-item-${displayName}`}
sx={{
backgroundColor: theme.palette.background.paper,
position: 'relative',
cursor: 'pointer',
padding: '4px',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}}
onClick={(skipHandleShowListBoxPopover === false && handleShowListBoxPopover) || null}
>
<Typography noWrap style={{ fontSize: '12px', lineHeight: '16px', fontWeight: 600, marginTop: '8px' }}>
{displayName}
</Typography>
{showListBoxPopover && (
<ListBoxPopover
alignTo={alignTo}
show={showListBoxPopover}
close={handleCloseShowListBoxPopover}
app={api.model}
fieldName={field.qField}
/>
)}
</Grid2>
); );
} }

View File

@@ -157,7 +157,7 @@ export default function SelectedFields({ api, app, halo }) {
{state.items.map((s) => ( {state.items.map((s) => (
<Grid <Grid
item item
key={`${s.states.join('::')}::${s.id ?? s.qField ?? s.name}`} key={`${s.states.join('::')}::${s.qField ?? s.name}`}
style={{ style={{
position: 'relative', position: 'relative',
maxWidth: '240px', maxWidth: '240px',

View File

@@ -1,214 +1,57 @@
import React, { act } from 'react'; import React, { act } from 'react';
import * as ReactTestRenderer from 'react-test-renderer'; import * as ReactTestRenderer from 'react-test-renderer';
import * as NebulaThemeModule from '@nebula.js/ui/theme'; import { Typography } from '@mui/material';
import PinItem from '../PinItem'; import PinItem from '../PinItem';
jest.mock('@nebula.js/ui/theme', () => ({
...jest.requireActual('@nebula.js/ui/theme'),
useTheme: jest.fn(),
}));
jest.mock('../../listbox/ListBoxPopover', () => ({
__esModule: true,
default: () => <div data-testid="listbox-popover">ListBoxPopover</div>,
}));
describe('<PinItem />', () => { describe('<PinItem />', () => {
let renderer; let renderer;
beforeEach(() => {
jest.spyOn(NebulaThemeModule, 'useTheme').mockImplementation(() => ({
palette: {
background: { paper: '#ffffff' },
action: { hover: '#f5f5f5' },
divider: '#e0e0e0',
},
}));
});
afterEach(() => { afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
if (renderer) { if (renderer) {
renderer.unmount(); renderer.unmount();
} }
}); });
test('should render field name from qName when available', () => { test('should render display name', () => {
const field = {
qName: 'Product',
qField: 'product_field',
};
const api = {
model: {},
};
act(() => { act(() => {
renderer = ReactTestRenderer.create( renderer = ReactTestRenderer.create(<PinItem displayName="Product" />);
<PinItem
field={field}
api={api}
showListBoxPopover={false}
alignTo={React.createRef()}
skipHandleShowListBoxPopover
handleShowListBoxPopover={jest.fn()}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
}); });
const output = renderer.toJSON(); const output = renderer.toJSON();
expect(JSON.stringify(output)).toContain('Product'); expect(JSON.stringify(output)).toContain('Product');
}); });
test('should render field name from qField as fallback when qName is not available', () => { test('should render Typography component with correct styles', () => {
const field = {
qField: 'product_field',
};
const api = {
model: {},
};
act(() => { act(() => {
renderer = ReactTestRenderer.create( renderer = ReactTestRenderer.create(<PinItem displayName="Test Field" />);
<PinItem
field={field}
api={api}
showListBoxPopover={false}
alignTo={React.createRef()}
skipHandleShowListBoxPopover
handleShowListBoxPopover={jest.fn()}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
}); });
const output = renderer.toJSON(); const typography = renderer.root.findByType(Typography);
expect(JSON.stringify(output)).toContain('product_field'); expect(typography).toBeDefined();
expect(typography.props.noWrap).toBe(true);
expect(typography.props.style).toMatchObject({
fontSize: '12px',
lineHeight: '16px',
fontWeight: 600,
marginTop: '8px',
});
}); });
test('should call handleShowListBoxPopover when clicked and skipHandleShowListBoxPopover is false', () => { test('should render empty string when displayName is empty', () => {
const handleShowListBoxPopover = jest.fn();
const field = {
qName: 'Product',
qField: 'product_field',
};
const api = {
model: {},
};
act(() => { act(() => {
renderer = ReactTestRenderer.create( renderer = ReactTestRenderer.create(<PinItem displayName="" />);
<PinItem
field={field}
api={api}
showListBoxPopover={false}
alignTo={React.createRef()}
skipHandleShowListBoxPopover={false}
handleShowListBoxPopover={handleShowListBoxPopover}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
}); });
// Verify handler hasn't been called yet const typography = renderer.root.findByType(Typography);
expect(handleShowListBoxPopover).not.toHaveBeenCalled(); expect(typography.props.children).toBe('');
// Find the clickable element by data-testid
const pinItemElement = renderer.root.findByProps({ 'data-testid': 'pin-item-Product' });
expect(pinItemElement).toBeDefined();
// Simulate click
act(() => {
pinItemElement.props.onClick();
});
// Verify handler was called
expect(handleShowListBoxPopover).toHaveBeenCalledTimes(1);
}); });
test('should not call handleShowListBoxPopover when skipHandleShowListBoxPopover is true', () => { test('should handle undefined displayName', () => {
const handleShowListBoxPopover = jest.fn();
const field = {
qName: 'Product',
qField: 'product_field',
};
const api = {
model: {},
};
act(() => { act(() => {
renderer = ReactTestRenderer.create( renderer = ReactTestRenderer.create(<PinItem displayName={undefined} />);
<PinItem
field={field}
api={api}
showListBoxPopover={false}
alignTo={React.createRef()}
skipHandleShowListBoxPopover
handleShowListBoxPopover={handleShowListBoxPopover}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
}); });
expect(handleShowListBoxPopover).not.toHaveBeenCalled(); const typography = renderer.root.findByType(Typography);
}); expect(typography.props.children).toBeUndefined();
test('should render ListBoxPopover when showListBoxPopover is true', () => {
const field = {
qName: 'Product',
qField: 'product_field',
};
const api = {
model: {},
};
const alignToRef = React.createRef();
act(() => {
renderer = ReactTestRenderer.create(
<PinItem
field={field}
api={api}
showListBoxPopover
alignTo={alignToRef}
skipHandleShowListBoxPopover
handleShowListBoxPopover={jest.fn()}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
});
const output = renderer.toJSON();
const serialized = JSON.stringify(output);
expect(serialized).toContain('ListBoxPopover');
});
test('should not render ListBoxPopover when showListBoxPopover is false', () => {
const field = {
qName: 'Product',
qField: 'product_field',
};
const api = {
model: {},
};
const alignToRef = React.createRef();
act(() => {
renderer = ReactTestRenderer.create(
<PinItem
field={field}
api={api}
showListBoxPopover={false}
alignTo={alignToRef}
skipHandleShowListBoxPopover
handleShowListBoxPopover={jest.fn()}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
});
const output = renderer.toJSON();
const serialized = JSON.stringify(output);
expect(serialized).not.toContain('ListBoxPopover');
}); });
}); });