Skip to content
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
13 changes: 10 additions & 3 deletions modules/react/collection/lib/useOverflowListModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,16 @@ export function getHiddenIds(
let shouldAddGap = false;

if (selectedIds !== 'all' && selectedIds.length) {
if (items.length) {
// If selectedIds[0] is not in items, use the first id from items
selectedKey = items.find(item => item.id === selectedIds[0]) ? selectedIds[0] : items[0].id;
const candidate = selectedIds[0];
const fallbackId = items?.[0]?.id;
// Only pin own cached sizes so unmeasured ids and inherited names cannot yield NaN.
if (Object.prototype.hasOwnProperty.call(itemSizeCache, candidate)) {
selectedKey = candidate;
} else if (
fallbackId !== undefined &&
Object.prototype.hasOwnProperty.call(itemSizeCache, fallbackId)
) {
selectedKey = fallbackId;
}
}

Expand Down
45 changes: 45 additions & 0 deletions modules/react/collection/spec/useOverflowModel.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,50 @@ describe('useOverflowModel', () => {
).toEqual(hiddenIds);
});
});

it('should keep the selected item visible when items use a custom id field instead of `id`', () => {
expect(
getHiddenIds(250, 0, overflowTargeSize, itemSizeCache, ['second'], [
{index: 0, value: {contextId: 'first'}, textValue: 'First'},
{index: 1, value: {contextId: 'second'}, textValue: 'Second'},
{index: 2, value: {contextId: 'third'}, textValue: 'Third'},
{index: 3, value: {contextId: 'fourth'}, textValue: 'Fourth'},
] as any)
).toEqual(['first', 'third', 'fourth']);
});

const items = [
{id: 'first', value: 'first', index: 0, textValue: 'first'},
{id: 'second', value: 'second', index: 1, textValue: 'second'},
{id: 'third', value: 'third', index: 2, textValue: 'third'},
{id: 'fourth', value: 'fourth', index: 3, textValue: 'fourth'},
];

it('should not pin an unmeasured selected id when the first item is cached', () => {
expect(
getHiddenIds(
250,
0,
overflowTargeSize,
{first: 100, second: 150, third: 200},
['fourth'],
items
)
).toEqual(['second', 'third']);
});

it('should leave the selected item unpinned when neither the selected id nor the first item is cached', () => {
expect(
getHiddenIds(250, 0, overflowTargeSize, {second: 150, third: 200}, ['fourth'], items)
).toEqual(['third']);
});

it('should ignore inherited prototype identifiers such as toString', () => {
expect(getHiddenIds(250, 0, overflowTargeSize, itemSizeCache, ['toString'], items)).toEqual([
'second',
'third',
'fourth',
]);
});
});
});
35 changes: 20 additions & 15 deletions modules/react/tabs/lib/useTabsModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,26 @@ export const useTabsModel = createModelHook({
unregisterPanel: panels.events.unregisterItem,
};

const menu = useMenuModel(
useMenuModel.mergeConfig(config.menuConfig, {
id: `menu-${model.state.id}`,
items: overflowItems,
nonInteractiveIds: state.nonInteractiveIds.filter(key => !state.hiddenIds.includes(key)),
onSelect(data) {
menu.events.hide();
events.select(data);
},
onShow() {
// Always select the first item when the menu is opened
menu.events.goToFirst();
},
})
);
const mergedMenuConfig = useMenuModel.mergeConfig(config.menuConfig, {
id: `menu-${model.state.id}`,
items: overflowItems,
// `getId`/`getTextValue` aren't callbacks or guards, so `mergeConfig` would let these
// unconditionally clobber a `menuConfig.getId`/`getTextValue` override. Fall back to the
// top-level Tabs config only when the caller hasn't set one on `menuConfig` directly.
getId: config.menuConfig?.getId || getId,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is the real change, this was returning undefined

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.

Do we want to add a Cypress test with a custom getId & overflow menu scenario?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I update the story and we have a test for OverFlowTabs in cypress, so by changing the story, it should still pass!

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.

Ah ok. I'm still trying to figure out when we add tests for specific scenarios in Cypress tests vs just having them in Storybook.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Its confusing sometimes we use storybook for cypress

getTextValue: config.menuConfig?.getTextValue || config.getTextValue,
nonInteractiveIds: state.nonInteractiveIds.filter(key => !state.hiddenIds.includes(key)),
onSelect(data) {
menu.events.hide();
events.select(data);
},
onShow() {
// Always select the first item when the menu is opened
menu.events.goToFirst();
},
});

const menu = useMenuModel(mergedMenuConfig);

return {
...model,
Expand Down
17 changes: 9 additions & 8 deletions modules/react/tabs/stories/examples/OverflowTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@ import {px2rem} from '@workday/canvas-kit-styling';
import {system} from '@workday/canvas-tokens-web';

type MyTabItem = {
id: string;
contextId: string;
text: React.ReactNode;
contents: string;
};

export const OverflowTabs = () => {
const [items] = React.useState<MyTabItem[]>([
{id: 'first', text: 'First Tab', contents: 'Contents of First Tab'},
{id: 'second', text: 'Second Tab', contents: 'Contents of Second Tab'},
{id: 'third', text: 'Third Tab', contents: 'Contents of Third Tab'},
{id: 'fourth', text: 'Fourth Tab', contents: 'Contents of Fourth Tab'},
{id: 'fifth', text: 'Fifth Tab', contents: 'Contents of Fifth Tab'},
{id: 'sixth', text: 'Sixth Tab', contents: 'Contents of Sixth Tab'},
{id: 'seventh', text: 'Seventh Tab', contents: 'Contents of Seventh Tab'},
{contextId: 'first', text: 'First Tab', contents: 'Contents of First Tab'},
{contextId: 'second', text: 'Second Tab', contents: 'Contents of Second Tab'},
{contextId: 'third', text: 'Third Tab', contents: 'Contents of Third Tab'},
{contextId: 'fourth', text: 'Fourth Tab', contents: 'Contents of Fourth Tab'},
{contextId: 'fifth', text: 'Fifth Tab', contents: 'Contents of Fifth Tab'},
{contextId: 'sixth', text: 'Sixth Tab', contents: 'Contents of Sixth Tab'},
{contextId: 'seventh', text: 'Seventh Tab', contents: 'Contents of Seventh Tab'},
]);
const model = useTabsModel({
items,
getId: (item: MyTabItem) => item.contextId,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed this story to match a real work example

});
const [containerWidth, setContainerWidth] = React.useState('100%');
return (
Expand Down