Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ListGroup open close handling #369

Merged
merged 3 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
72 changes: 24 additions & 48 deletions src/components/ListGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function ListGroup(props) {
id,
items,
label,
shouldOpen = true,
shouldOpen = false,
translate = translateFallback
} = props;

Expand All @@ -55,69 +55,35 @@ export default function ListGroup(props) {

const [ open, setOpen ] = useLayoutState(
[ 'groups', id, 'open' ],
false
shouldOpen
);

const [ sticky, setSticky ] = useState(false);

const onShow = useCallback(() => setOpen(true), [ setOpen ]);

const [ localItems, setLocalItems ] = useState([]);
const [ newlyAddedItemIds, setNewlyAddedItemIds ] = useState([]);

// Flag to mark that add button was clicked in the last render cycle
const [ addTriggered, setAddTriggered ] = useState(false);

const prevElement = usePrevious(element);

const elementChanged = element !== prevElement;
const shouldHandleEffects = !elementChanged && shouldOpen;
const toggleOpen = useCallback(() => setOpen(!open), [ open ]);

// (0) delay setting items
//
// We need to this to align the render cycles of items
// with the detection of newly added items.
// This is important, because the autoOpen property can
// only set per list item on its very first render.
useEffect(() => {
setLocalItems(items);
}, [ items ]);
const openItemIds = (element === prevElement && open && addTriggered)
? getNewItemIds(items, localItems)
: [];

// (1) handle auto opening when items were added
// reset local state after items changed
useEffect(() => {

// reset addTriggered flag
setLocalItems(items);
setAddTriggered(false);

if (shouldHandleEffects && localItems) {
if (addTriggered) {
const previousItemIds = localItems.map(item => item.id);
const currentItemsIds = items.map(item => item.id);
const newItemIds = currentItemsIds.filter(itemId => !previousItemIds.includes(itemId));

// open if not open, configured and triggered by add button
//
// TODO(marstamm): remove once we refactor layout handling for listGroups.
// Ideally, opening should be handled as part of the `add` callback and
// not be a concern for the ListGroup component.
if (!open && shouldOpen && newItemIds.length > 0) {
toggleOpen();
}

setNewlyAddedItemIds(newItemIds);
} else {

// ignore newly added items that do not result from a triggered add
setNewlyAddedItemIds([]);
}
}
}, [ items, open, shouldHandleEffects, addTriggered, localItems ]);
}, [ items ]);

// set css class when group is sticky to top
useStickyIntersectionObserver(groupRef, 'div.bio-properties-panel-scroll-container', setSticky);

const toggleOpen = () => setOpen(!open);

const hasItems = !!items.length;

const propertiesPanelContext = {
Expand All @@ -127,6 +93,8 @@ export default function ListGroup(props) {

const handleAddClick = e => {
setAddTriggered(true);
setOpen(true);

add(e);
};

Expand All @@ -142,8 +110,7 @@ export default function ListGroup(props) {

// also check if the error is nested, e.g. for name-value entries
return item.entries.some(entry => allErrors[entry.id]);
}
);
});


return <div class="bio-properties-panel-group" data-group-id={ 'group-' + id } ref={ groupRef }>
Expand Down Expand Up @@ -224,16 +191,17 @@ export default function ListGroup(props) {
<PropertiesPanelContext.Provider value={ propertiesPanelContext }>

{
localItems.map((item, index) => {
items.map((item, index) => {
if (!item) {
return;
}

const { id } = item;

// if item was added, open it
// Existing items will not be affected as autoOpen is only applied on first render
const autoOpen = newlyAddedItemIds.includes(item.id);
// existing items will not be affected as autoOpen
// is only applied on first render
const autoOpen = openItemIds.includes(item.id);

return (
<ListItem
Expand All @@ -249,4 +217,12 @@ export default function ListGroup(props) {
</PropertiesPanelContext.Provider>
</div>
</div>;
}


function getNewItemIds(newItems, oldItems) {
const newIds = newItems.map(item => item.id);
const oldIds = oldItems.map(item => item.id);

return newIds.filter(itemId => !oldIds.includes(itemId));
}
2 changes: 1 addition & 1 deletion src/hooks/useLayoutState.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function useLayoutState(path, defaultValue) {

const layoutForKey = getLayoutForKey(path, defaultValue);

const setState = useCallback ((newValue) => {
const setState = useCallback((newValue) => {
setLayoutForKey(path, newValue);
}, [ setLayoutForKey ]);

Expand Down
81 changes: 48 additions & 33 deletions test/spec/components/ListGroup.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,28 @@ describe('<ListGroup>', function() {

describe('open', function() {

function expectItemOpen(container, item, expected) {
const itemEl = domQuery(`[data-entry-id="${item.id || item}"]`, container);

expect(domClasses(itemEl).has('open'), `[data-entry-id="${item.id || item}"] open=${expected}`).to.eql(expected);
}

function expectListOpen(container, expected) {
const list = domQuery('.bio-properties-panel-list', container);

expect(domClasses(list).has('open'), `.bio-properties-panel-list open=${expected}`).to.eql(expected);
}

function triggerAdd(container) {

const addButton = domQuery('.bio-properties-panel-add-entry', container);

return act(() => {
addButton.click();
});
}


it('should open on adding new item per default', async function() {

// given
Expand Down Expand Up @@ -621,50 +643,51 @@ describe('<ListGroup>', function() {
container
} = render(<Component />, parentContainer);

const list = domQuery('.bio-properties-panel-list', container);
const addButton = domQuery('.bio-properties-panel-add-entry', container);

// assume
expect(domClasses(list).has('open')).to.be.false;

expectListOpen(container, false);

// when
await act(() => {
addButton.click();
});
await triggerAdd(container);

// then
const newItem = domQuery('[data-entry-id="item-2"]', container);
const oldItem = domQuery('[data-entry-id="item-1"]', container);
expectItemOpen(container, 'item-1', false);
expectItemOpen(container, 'item-2', true);

expect(domClasses(newItem).has('open')).to.be.true;
expect(domClasses(oldItem).has('open')).to.be.false;
expect(domClasses(list).has('open')).to.be.true;
expectListOpen(container, true);
});


it('should open on adding new item in the middle', async function() {

// given
const newItems = [
let initialItems = [
{
id: 'item-1',
label: 'Item 1'
},
{
id: 'item-2',
label: 'Item 2'
}
];

let newItems = [
{
id: 'item-1',
label: 'Item 1'
},
{
id: 'item-3',
label: 'Item 3'
},
{
id: 'item-2',
label: 'Item 2'
}
];

const items = [ newItems[0], newItems[2] ];


const Component = () => {
const [ testItems, setTestItems ] = useState(items);
const [ testItems, setTestItems ] = useState(initialItems);

const add = () => {
setTestItems(newItems);
Expand All @@ -675,28 +698,20 @@ describe('<ListGroup>', function() {

const {
container
} = render(<Component />, parentContainer);

const list = domQuery('.bio-properties-panel-list', container);
const addButton = domQuery('.bio-properties-panel-add-entry', container);
} = render(<Component />, { container: parentContainer });

// assume
expect(domClasses(list).has('open')).to.be.false;
expectListOpen(container, false);

// when
await act(() => {
addButton.click();
});
await triggerAdd(container);

// then
const newItem = domQuery('[data-entry-id="item-2"]', container);
const oldItem = domQuery('[data-entry-id="item-1"]', container);
const otherOldItem = domQuery('[data-entry-id="item-3"]', container);
expectListOpen(container, true);

expect(domClasses(newItem).has('open')).to.be.true;
expect(domClasses(oldItem).has('open')).to.be.false;
expect(domClasses(otherOldItem).has('open')).to.be.false;
expect(domClasses(list).has('open')).to.be.true;
expectItemOpen(container, 'item-1', false);
expectItemOpen(container, 'item-3', true);
expectItemOpen(container, 'item-2', false);
});


Expand Down