mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
fix: flickering and disappeared list popover
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user