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 [showListBoxPopover, setShowListBoxPopover] = useState(false);
let Component = null;
let ChildComponent = null;
let selection;
let displayName = '';
const isPinnedItem = !!field.isPinned && isPinFieldEnabled;
const handleShowListBoxPopover = (e) => {
@@ -41,17 +43,8 @@ export default function OneField({
};
if (isPinnedItem) {
Component = (
<PinItem
field={field}
api={api}
showListBoxPopover={showListBoxPopover}
alignTo={alignTo}
skipHandleShowListBoxPopover={skipHandleShowListBoxPopover}
handleShowListBoxPopover={handleShowListBoxPopover}
handleCloseShowListBoxPopover={handleCloseShowListBoxPopover}
/>
);
displayName = field.qName || field.qField;
ChildComponent = <PinItem displayName={displayName} />;
} else {
selection = field.selections[stateIx];
if (typeof selection.qTotal === 'undefined') {
@@ -159,39 +152,47 @@ export default function OneField({
))}
</div>
);
ChildComponent = (
<>
{Header}
{Icon}
{SegmentsIndicator}
</>
);
}
}
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}
>
{Header}
{Icon}
{SegmentsIndicator}
{ChildComponent}
{showListBoxPopover && (
<ListBoxPopover
alignTo={alignTo}
show={showListBoxPopover}
close={handleCloseShowListBoxPopover}
app={api.model}
fieldName={selection.qField}
stateName={field.states[stateIx]}
fieldName={isPinnedItem ? field.qField : selection.qField}
stateName={isPinnedItem ? undefined : field.states[stateIx]}
/>
)}
</Grid>
);
}
}
return moreAlignTo ? (
<ListBoxPopover

View File

@@ -1,51 +1,11 @@
import React from 'react';
import { Grid2, 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;
import { Typography } from '@mui/material';
function PinItem({ displayName }) {
return (
<Grid2
container
gap={1}
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) => (
<Grid
item
key={`${s.states.join('::')}::${s.id ?? s.qField ?? s.name}`}
key={`${s.states.join('::')}::${s.qField ?? s.name}`}
style={{
position: 'relative',
maxWidth: '240px',

View File

@@ -1,214 +1,57 @@
import React, { act } from 'react';
import * as ReactTestRenderer from 'react-test-renderer';
import * as NebulaThemeModule from '@nebula.js/ui/theme';
import { Typography } from '@mui/material';
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 />', () => {
let renderer;
beforeEach(() => {
jest.spyOn(NebulaThemeModule, 'useTheme').mockImplementation(() => ({
palette: {
background: { paper: '#ffffff' },
action: { hover: '#f5f5f5' },
divider: '#e0e0e0',
},
}));
});
afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
if (renderer) {
renderer.unmount();
}
});
test('should render field name from qName when available', () => {
const field = {
qName: 'Product',
qField: 'product_field',
};
const api = {
model: {},
};
test('should render display name', () => {
act(() => {
renderer = ReactTestRenderer.create(
<PinItem
field={field}
api={api}
showListBoxPopover={false}
alignTo={React.createRef()}
skipHandleShowListBoxPopover
handleShowListBoxPopover={jest.fn()}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
renderer = ReactTestRenderer.create(<PinItem displayName="Product" />);
});
const output = renderer.toJSON();
expect(JSON.stringify(output)).toContain('Product');
});
test('should render field name from qField as fallback when qName is not available', () => {
const field = {
qField: 'product_field',
};
const api = {
model: {},
};
test('should render Typography component with correct styles', () => {
act(() => {
renderer = ReactTestRenderer.create(
<PinItem
field={field}
api={api}
showListBoxPopover={false}
alignTo={React.createRef()}
skipHandleShowListBoxPopover
handleShowListBoxPopover={jest.fn()}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
renderer = ReactTestRenderer.create(<PinItem displayName="Test Field" />);
});
const output = renderer.toJSON();
expect(JSON.stringify(output)).toContain('product_field');
const typography = renderer.root.findByType(Typography);
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', () => {
const handleShowListBoxPopover = jest.fn();
const field = {
qName: 'Product',
qField: 'product_field',
};
const api = {
model: {},
};
test('should render empty string when displayName is empty', () => {
act(() => {
renderer = ReactTestRenderer.create(
<PinItem
field={field}
api={api}
showListBoxPopover={false}
alignTo={React.createRef()}
skipHandleShowListBoxPopover={false}
handleShowListBoxPopover={handleShowListBoxPopover}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
renderer = ReactTestRenderer.create(<PinItem displayName="" />);
});
// Verify handler hasn't been called yet
expect(handleShowListBoxPopover).not.toHaveBeenCalled();
const typography = renderer.root.findByType(Typography);
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
test('should handle undefined displayName', () => {
act(() => {
pinItemElement.props.onClick();
renderer = ReactTestRenderer.create(<PinItem displayName={undefined} />);
});
// Verify handler was called
expect(handleShowListBoxPopover).toHaveBeenCalledTimes(1);
});
test('should not call handleShowListBoxPopover when skipHandleShowListBoxPopover is true', () => {
const handleShowListBoxPopover = jest.fn();
const field = {
qName: 'Product',
qField: 'product_field',
};
const api = {
model: {},
};
act(() => {
renderer = ReactTestRenderer.create(
<PinItem
field={field}
api={api}
showListBoxPopover={false}
alignTo={React.createRef()}
skipHandleShowListBoxPopover
handleShowListBoxPopover={handleShowListBoxPopover}
handleCloseShowListBoxPopover={jest.fn()}
/>
);
});
expect(handleShowListBoxPopover).not.toHaveBeenCalled();
});
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');
const typography = renderer.root.findByType(Typography);
expect(typography.props.children).toBeUndefined();
});
});