Skip to content

Commit 69ed3e0

Browse files
authored
fix(VariantManagement): fix boolean prop handling (#6641)
Fixes #6616
1 parent cc2bca7 commit 69ed3e0

File tree

6 files changed

+165
-30
lines changed

6 files changed

+165
-30
lines changed

packages/main/src/components/VariantManagement/VariantItem.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base';
44
import { clsx } from 'clsx';
55
import { forwardRef, useContext, useEffect } from 'react';
66
import { VariantManagementContext } from '../../internal/VariantManagementContext.js';
7-
import type { InputPropTypes } from '../../webComponents/index.js';
7+
import type { InputPropTypes } from '../../webComponents/Input/index.js';
88
import type { ListItemStandardDomRef, ListItemStandardPropTypes } from '../../webComponents/ListItemStandard/index.js';
99
import { ListItemStandard } from '../../webComponents/ListItemStandard/index.js';
1010
import { classNames, styleData } from './VariantItem.module.css.js';
1111

1212
export interface VariantItemPropTypes extends Pick<ListItemStandardPropTypes, 'accessibleName' | 'selected'> {
1313
/**
1414
* The name of the variant.
15+
*
16+
* __Note:__ Variant names must be unique.
1517
*/
1618
children: string;
1719
/**
@@ -30,6 +32,8 @@ export interface VariantItemPropTypes extends Pick<ListItemStandardPropTypes, 'a
3032
global?: boolean;
3133
/**
3234
* Indicator if it's the default variant.
35+
*
36+
* __Note:__ There should only be __one__ default variant.
3337
*/
3438
isDefault?: boolean;
3539
/**
@@ -90,7 +94,7 @@ const VariantItem = forwardRef<ListItemStandardDomRef, VariantItemPropTypes>((pr
9094

9195
useStylesheet(styleData, VariantItem.displayName);
9296

93-
const { selectVariantItem } = useContext(VariantManagementContext);
97+
const { selectVariantItem, selectedVariant } = useContext(VariantManagementContext);
9498
const [componentRef, consolidatedRef] = useSyncRef<ListItemStandardDomRef>(ref);
9599
useEffect(() => {
96100
if (selected) {
@@ -116,6 +120,7 @@ const VariantItem = forwardRef<ListItemStandardDomRef, VariantItemPropTypes>((pr
116120
data-read-only={readOnly}
117121
data-children={children}
118122
data-hide-delete={hideDelete}
123+
selected={selectedVariant?.children === children}
119124
/>
120125
);
121126
});

packages/main/src/components/VariantManagement/VariantManagement.cy.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import TitleLevel from '@ui5/webcomponents/dist/types/TitleLevel.js';
22
import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js';
33
import { useState } from 'react';
4+
import type { VariantItemPropTypes } from './VariantItem';
45
import { VariantItem } from './VariantItem';
56
import { WithCustomValidation as WithCustomValidationStory } from './VariantManagement.stories';
67
import type { VariantManagementPropTypes } from './index.js';
@@ -434,7 +435,7 @@ describe('VariantManagement', () => {
434435
});
435436

436437
it('Manage Views - render variants', () => {
437-
const variantItems = [
438+
const variantItems: VariantItemPropTypes[] = [
438439
{ rowId: 'Default VariantItem', props: {} },
439440
{ rowId: 'LabelReadOnly', props: { labelReadOnly: true } },
440441
{ rowId: 'Favorite', props: { favorite: true } },
@@ -689,5 +690,62 @@ describe('VariantManagement', () => {
689690
cy.get('@saveView').should('have.been.calledOnce');
690691
});
691692

693+
it('Programatically change selection', () => {
694+
const TestComp = () => {
695+
const [selected, setSelected] = useState('Item1');
696+
return (
697+
<>
698+
<VariantManagement
699+
onSelect={(e) => {
700+
setSelected(e.detail.selectedVariant.children);
701+
}}
702+
>
703+
<VariantItem selected={selected === 'Item1'}>Item1</VariantItem>
704+
<VariantItem selected={selected === 'Item2'}>Item2</VariantItem>
705+
<VariantItem selected={selected === 'Item3'}>Item3</VariantItem>
706+
<VariantItem selected={selected === 'Item4'}>Item4</VariantItem>
707+
<VariantItem selected={selected === 'Item5'}>Item5</VariantItem>
708+
</VariantManagement>
709+
<button
710+
onClick={() => {
711+
setSelected('Item3');
712+
}}
713+
>
714+
Select Item3
715+
</button>
716+
<button
717+
onClick={() => {
718+
setSelected('Item5');
719+
}}
720+
>
721+
Select Item5
722+
</button>
723+
<button
724+
onClick={() => {
725+
setSelected('Item1');
726+
}}
727+
>
728+
Select Item1
729+
</button>
730+
</>
731+
);
732+
};
733+
cy.mount(<TestComp />);
734+
735+
cy.get('[ui5-title]').contains('Item1').should('be.visible');
736+
cy.findByText('Select Item3').click();
737+
cy.get('[ui5-title]').contains('Item3').should('be.visible').click();
738+
cy.get('[ui5-responsive-popover]').should('be.visible');
739+
cy.get('[ui5-list]').contains('Item3').should('have.attr', 'selected', 'selected');
740+
cy.findByText('Item1').click();
741+
cy.get('[ui5-list]').contains('Item3').should('not.have.attr', 'selected');
742+
cy.get('[ui5-list]').contains('Item1').should('have.attr', 'selected', 'selected');
743+
cy.realPress('Escape');
744+
cy.findByText('Select Item5').click();
745+
cy.get('[ui5-title]').contains('Item5').should('be.visible').click();
746+
cy.get('[ui5-list]').contains('Item1').should('not.have.attr', 'selected');
747+
cy.get('[ui5-list]').contains('Item5').should('have.attr', 'selected', 'selected');
748+
});
749+
692750
cypressPassThroughTestsFactory(VariantManagement);
693751
});

packages/main/src/components/VariantManagement/index.tsx

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,57 @@ import searchIcon from '@ui5/webcomponents-icons/dist/search.js';
1212
import { enrichEventWithDetails, useI18nBundle, useStylesheet } from '@ui5/webcomponents-react-base';
1313
import { clsx } from 'clsx';
1414
import type { ComponentElement, ReactElement } from 'react';
15-
import { Children, cloneElement, forwardRef, isValidElement, useCallback, useEffect, useRef, useState } from 'react';
15+
import {
16+
Children,
17+
cloneElement,
18+
forwardRef,
19+
isValidElement,
20+
useCallback,
21+
useEffect,
22+
useRef,
23+
useState,
24+
version as reactVersion
25+
} from 'react';
1626
import { MANAGE, MY_VIEWS, SAVE, SAVE_AS, SEARCH, SEARCH_VARIANT, SELECT_VIEW } from '../../i18n/i18n-defaults.js';
1727
import { stopPropagation } from '../../internal/stopPropagation.js';
1828
import type { SelectedVariant } from '../../internal/VariantManagementContext.js';
1929
import { VariantManagementContext } from '../../internal/VariantManagementContext.js';
20-
import type { ResponsivePopoverDomRef } from '../../webComponents/index.js';
21-
import {
22-
Bar,
23-
Button,
24-
Icon,
25-
IllustratedMessage,
26-
Input,
27-
List,
28-
ResponsivePopover,
29-
Title
30-
} from '../../webComponents/index.js';
30+
import { Bar } from '../../webComponents/Bar/index.js';
31+
import { Button } from '../../webComponents/Button/index.js';
32+
import { Icon } from '../../webComponents/Icon/index.js';
33+
import { IllustratedMessage } from '../../webComponents/IllustratedMessage/index.js';
34+
import { Input } from '../../webComponents/Input/index.js';
35+
import type { ListPropTypes } from '../../webComponents/List/index.js';
36+
import { List } from '../../webComponents/List/index.js';
37+
import type { ListItemStandardDomRef } from '../../webComponents/ListItemStandard/index.js';
38+
import type { ResponsivePopoverDomRef } from '../../webComponents/ResponsivePopover/index.js';
39+
import { ResponsivePopover } from '../../webComponents/ResponsivePopover/index.js';
40+
import { Title } from '../../webComponents/Title/index.js';
3141
import { FlexBox } from '../FlexBox/index.js';
3242
import type { ManageViewsDialogPropTypes } from './ManageViewsDialog.js';
3343
import { ManageViewsDialog } from './ManageViewsDialog.js';
3444
import { SaveViewDialog } from './SaveViewDialog.js';
35-
import type { VariantManagementPropTypes } from './types.js';
45+
import type { SelectedVariantWithStringBool, VariantManagementPropTypes } from './types.js';
3646
import type { VariantItemPropTypes } from './VariantItem.js';
3747
import { classNames, styleData } from './VariantManagement.module.css.js';
3848

49+
const booleanProps = {
50+
favorite: true,
51+
global: true,
52+
isDefault: true,
53+
labelReadOnly: true,
54+
applyAutomatically: true,
55+
readOnly: true,
56+
hideDelete: true
57+
};
58+
3959
/**
4060
* The VariantManagement can be used to manage variants (views). You can use this component to create and maintain personalization changes.
4161
*
4262
* __Note:__ On the user interface, variants are generally referred to as "views".
4363
*
64+
* __Note:__ Each `VariantManagement` component can only have one default and one selected variant.
65+
*
4466
* ### Matching header styles
4567
*
4668
* To ensure consistent header styles for different use-cases of the `VariantManagement`, we recommend setting the following styles to the `ui5-title` component:
@@ -117,14 +139,44 @@ const VariantManagement = forwardRef<HTMLDivElement, VariantManagementPropTypes>
117139
const [popoverOpen, setPopoverOpen] = useState(false);
118140
const [manageViewsDialogOpen, setManageViewsDialogOpen] = useState(false);
119141
const [saveAsDialogOpen, setSaveAsDialogOpen] = useState(false);
120-
const [selectedVariant, setSelectedVariant] = useState<SelectedVariant | undefined>(() => {
142+
const [selectedVariant, setSelectedVariantState] = useState<SelectedVariant | undefined>(() => {
121143
const currentSelectedVariant = safeChildren.find(
122144
(item) => isValidElement(item) && (item as ReactElement<VariantItemPropTypes>).props.selected
123145
) as ComponentElement<any, any>;
124146
if (currentSelectedVariant) {
125147
return { ...currentSelectedVariant.props, variantItem: currentSelectedVariant.ref };
126148
}
127149
});
150+
const setSelectedVariant = (variant: SelectedVariantWithStringBool) => {
151+
if (variant) {
152+
const stringToBoolVariant = Object.entries(variant).reduce((acc, [key, val]) => {
153+
if (booleanProps[key]) {
154+
if (typeof val === 'boolean') {
155+
acc[key] = val;
156+
return acc;
157+
}
158+
if (val === 'false') {
159+
acc[key] = false;
160+
return acc;
161+
}
162+
if (val === 'true') {
163+
acc[key] = true;
164+
return acc;
165+
}
166+
if (reactVersion.startsWith('19') && val === '') {
167+
acc[key] = true;
168+
return acc;
169+
}
170+
}
171+
acc[key] = val;
172+
return acc;
173+
}, {}) as SelectedVariant;
174+
setSelectedVariantState(stringToBoolVariant);
175+
} else {
176+
setSelectedVariantState(variant as SelectedVariant);
177+
}
178+
};
179+
128180
const [selectedSaveViewInputProps, setSelectedSaveViewInputProps] = useState(
129181
selectedVariant?.saveViewInputProps ?? {}
130182
);
@@ -258,8 +310,13 @@ const VariantManagement = forwardRef<HTMLDivElement, VariantManagementPropTypes>
258310
setSelectedSaveViewInputProps(selectedChild?.props.saveViewInputProps ?? {});
259311
}, [selectedVariant, safeChildren]);
260312

261-
const handleVariantItemSelect = (e) => {
262-
setSelectedVariant({ ...e.detail.selectedItems[0].dataset, variantItem: e.detail.selectedItems[0] });
313+
const handleVariantItemSelect: ListPropTypes['onSelectionChange'] = (e) => {
314+
const targetItem = e.detail.targetItem as unknown as ListItemStandardDomRef;
315+
const dataset = targetItem.dataset as unknown as SelectedVariantWithStringBool;
316+
setSelectedVariant({
317+
...dataset,
318+
variantItem: targetItem
319+
});
263320
selectVariantEventRef.current = e;
264321
if (closeOnItemSelect) {
265322
handleClose();
@@ -317,19 +374,14 @@ const VariantManagement = forwardRef<HTMLDivElement, VariantManagementPropTypes>
317374
}
318375
}, [safeChildrenWithFavorites]);
319376

320-
//todo: selectedVariant type needs to be enhanced for React19 (data attributes: true => "", false => "false")
321-
const showSaveBtn =
322-
dirtyState &&
323-
selectedVariant &&
324-
(typeof selectedVariant?.readOnly === 'string'
325-
? selectedVariant.readOnly !== '' && selectedVariant.readOnly === 'false'
326-
: !selectedVariant.readOnly);
377+
const showSaveBtn = dirtyState && selectedVariant && !selectedVariant.readOnly;
327378

328379
return (
329380
<div className={variantManagementClasses} style={style} {...rest} ref={ref}>
330381
<VariantManagementContext.Provider
331382
value={{
332-
selectVariantItem: setSelectedVariant
383+
selectVariantItem: setSelectedVariant,
384+
selectedVariant
333385
}}
334386
>
335387
<FlexBox onClick={disabled ? undefined : handleOpenVariantManagement}>

packages/main/src/components/VariantManagement/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ import type {
1313
} from '../../webComponents/index.js';
1414
import type { VariantItemPropTypes } from './VariantItem.js';
1515

16+
export interface SelectedVariantWithStringBool
17+
extends Omit<
18+
SelectedVariant,
19+
'favorite' | 'global' | 'isDefault' | 'labelReadOnly' | 'applyAutomatically' | 'readOnly' | 'hideDelete'
20+
> {
21+
favorite?: boolean | string;
22+
global?: boolean | string;
23+
isDefault?: boolean | string;
24+
labelReadOnly?: boolean | string;
25+
applyAutomatically?: boolean | string;
26+
readOnly?: boolean | string;
27+
hideDelete?: boolean | string;
28+
}
29+
1630
interface UpdatedVariant extends SelectedVariant {
1731
prevVariant?: VariantItemPropTypes;
1832
}

packages/main/src/internal/VariantManagementContext.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ import { createContext } from 'react';
22
import type { VariantItemPropTypes } from '../components/VariantManagement/VariantItem.js';
33
import type { ListItemStandardDomRef } from '../webComponents/ListItemStandard/index.js';
44

5+
interface VariantManagementContextTypes {
6+
selectVariantItem: (_selectedVariant: SelectedVariant) => void;
7+
selectedVariant: SelectedVariant;
8+
}
9+
510
export interface SelectedVariant extends VariantItemPropTypes {
611
variantItem: ListItemStandardDomRef;
712
}
813

9-
export const VariantManagementContext = createContext({
10-
selectVariantItem: (_selectedVariant: SelectedVariant) => {}
14+
export const VariantManagementContext = createContext<VariantManagementContextTypes>({
15+
selectVariantItem: (_selectedVariant: SelectedVariant) => {},
16+
selectedVariant: undefined
1117
});

packages/main/src/internal/withWebComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export const withWebComponent = <Props extends Record<string, any>, RefType = Ui
168168

169169
useEffect(() => {
170170
if (waitForDefine && !isDefined) {
171-
customElements.whenDefined(Component as unknown as string).then(() => {
171+
void customElements.whenDefined(Component as unknown as string).then(() => {
172172
setIsDefined(true);
173173
definedWebComponents.add(Component);
174174
});

0 commit comments

Comments
 (0)