Skip to content

Commit 19a29aa

Browse files
feat: update to UI5 Web Components 2.5.0 (#6696)
Co-authored-by: Lukas Harbarth <[email protected]>
1 parent 0715e49 commit 19a29aa

File tree

56 files changed

+2943
-513
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2943
-513
lines changed

.storybook/custom-element-manifests/fiori.json

Lines changed: 907 additions & 147 deletions
Large diffs are not rendered by default.

.storybook/custom-element-manifests/main.json

Lines changed: 810 additions & 113 deletions
Large diffs are not rendered by default.

.storybook/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { StorybookConfig } from '@storybook/react-vite';
21
import { dirname, join } from 'path';
2+
import type { StorybookConfig } from '@storybook/react-vite';
33
import remarkGfm from 'remark-gfm';
44
import { isChromatic } from './utils';
55

.storybook/manager-head.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
#data-display-table::after,
5656
#inputs-tokenizer::after,
5757
#layouts-floorplans-form::after,
58+
#modals-popovers-usermenu::after,
5859
#charts-timelinechart::after {
5960
content: '(experimental)';
6061
overflow: hidden;

config/version-info.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,6 @@
4646
"2.1.2": "2.0.0",
4747
"2.2.0": "2.1.0",
4848
"2.3.0": "2.2.0",
49-
"2.4.0": "2.4.0"
49+
"2.4.0": "2.4.0",
50+
"2.5.0": "2.5.0"
5051
}

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@
4040
"@storybook/react": "8.4.5",
4141
"@storybook/react-vite": "8.4.5",
4242
"@storybook/theming": "8.4.5",
43-
"@ui5/webcomponents": "2.4.0",
44-
"@ui5/webcomponents-compat": "2.4.0",
45-
"@ui5/webcomponents-fiori": "2.4.0",
46-
"@ui5/webcomponents-icons": "2.4.0",
43+
"@ui5/webcomponents": "2.5.0",
44+
"@ui5/webcomponents-compat": "2.5.0",
45+
"@ui5/webcomponents-fiori": "2.5.0",
46+
"@ui5/webcomponents-icons": "2.5.0",
4747
"react": "^18.3.1",
4848
"react-dom": "^18.3.1",
4949
"remark-gfm": "^4.0.0",
@@ -65,7 +65,7 @@
6565
"@types/node": "^22.0.0",
6666
"@types/react": "^18.3.4",
6767
"@types/react-dom": "^18.3.0",
68-
"@ui5/webcomponents-tools": "2.4.0",
68+
"@ui5/webcomponents-tools": "2.5.0",
6969
"@vitejs/plugin-react": "^4.2.0",
7070
"chromatic": "^11.0.0",
7171
"cssnano": "^7.0.0",

packages/base/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
},
3232
"peerDependencies": {
3333
"@types/react": "*",
34-
"@ui5/webcomponents-base": "~2.4.0",
34+
"@ui5/webcomponents-base": "~2.5.0",
3535
"react": "^18 || ^19"
3636
},
3737
"peerDependenciesMeta": {

packages/charts/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
"recharts": "2.14.1"
4040
},
4141
"peerDependencies": {
42-
"@ui5/webcomponents-react": "~2.4.0",
43-
"@ui5/webcomponents-react-base": "~2.4.0",
42+
"@ui5/webcomponents-react": "~2.5.0",
43+
"@ui5/webcomponents-react-base": "~2.5.0",
4444
"react": "^18"
4545
},
4646
"publishConfig": {

packages/compat/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
"peerDependencies": {
4242
"@types/react": "*",
4343
"@types/react-dom": "*",
44-
"@ui5/webcomponents-compat": "~2.4.0",
45-
"@ui5/webcomponents-react": "~2.4.0",
44+
"@ui5/webcomponents-compat": "~2.5.0",
45+
"@ui5/webcomponents-react": "~2.5.0",
4646
"react": "^18 || ^19",
4747
"react-dom": "^18 || ^19"
4848
},

packages/cypress-commands/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,18 @@
2323
"clean": "rimraf dist api.json"
2424
},
2525
"peerDependencies": {
26+
"@ui5/webcomponents": "~2.5.0",
27+
"@ui5/webcomponents-base": "~2.5.0",
2628
"cypress": "^12.0.0 || ^13.0.0"
2729
},
30+
"peerDependenciesMeta": {
31+
"@ui5/webcomponents": {
32+
"optional": true
33+
},
34+
"@ui5/webcomponents-base": {
35+
"optional": true
36+
}
37+
},
2838
"publishConfig": {
2939
"access": "public"
3040
},

packages/cypress-commands/src/commands.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type UI5Element from '@ui5/webcomponents-base/dist/UI5Element.js';
2+
13
declare global {
24
// eslint-disable-next-line @typescript-eslint/no-namespace
35
namespace Cypress {
@@ -9,12 +11,14 @@ declare global {
911
* @example cy.get('[ui5-input]').typeIntoUi5Input('Hello World');
1012
*/
1113
typeIntoUi5Input(text: string, options?: Partial<TypeOptions>): Chainable<Element>;
14+
1215
/**
1316
* Clears a value from ui5-webcomponent that offers a typeable input field.
1417
*
1518
* @example cy.get('[ui5-input]').clearUi5Input();
1619
*/
1720
clearUi5Input(options?: Partial<ClearOptions>): Chainable<Element>;
21+
1822
/**
1923
* Types a value with a delay into an ui5-webcomponent that offers a typeable input field.
2024
*
@@ -189,30 +193,24 @@ Cypress.Commands.add('clickUi5SelectOption', { prevSubject: 'element' }, (subjec
189193
});
190194

191195
Cypress.Commands.add('clickDropdownMenuItemByText', { prevSubject: 'element' }, (subject, text, options = {}) => {
192-
cy.wrap(subject)
193-
.find('[ui5-responsive-popover]')
194-
.then(($popover) => {
195-
cy.wrap($popover).should('have.attr', 'open');
196-
// necessary as otherwise focusing the ui5-li is flaky
197-
cy.wait(300);
198-
cy.wrap($popover)
199-
.contains(text)
200-
.then(($li) => {
201-
$li.get(0).focus();
202-
cy.wrap($li)
203-
.find('li')
204-
.click({ force: true, ...options });
205-
});
206-
});
196+
cy.wrap(subject).find('[ui5-responsive-popover]').should('have.attr', 'open');
197+
cy.wrap(subject).then(([$dropdown]) => {
198+
switch (true) {
199+
case $dropdown.hasAttribute('ui5-select'):
200+
cy.wrap($dropdown).contains(text).clickDropdownMenuItem(options);
201+
break;
202+
default:
203+
cy.wrap($dropdown).get(`[text="${text}"]`).clickDropdownMenuItem(options);
204+
break;
205+
}
206+
});
207207
});
208208

209209
Cypress.Commands.add('clickDropdownMenuItem', { prevSubject: 'element' }, (subject, options = {}) => {
210-
cy.wrap(subject).then(($option) => {
211-
// @ts-expect-error: ui5-webcomponent types are not bundled in
212-
const domRef = $option.get(0).getDomRef();
213-
cy.wrap(domRef)
214-
.find('li')
215-
.click({ force: true, ...options });
210+
cy.wrap(subject).then(([$option]) => {
211+
const domRef = ($option as UI5Element).getDomRef();
212+
cy.wrap(domRef).focus();
213+
cy.wrap(domRef).click(options);
216214
});
217215
});
218216

packages/cypress-commands/test/UI5WebComponentsChild.cy.tsx

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -194,72 +194,95 @@ describe('UI5 Web Components - Child Commands', () => {
194194
cy.get('@select').should('have.been.calledTwice');
195195
});
196196

197-
it('clickDropdownMenuItemByText', () => {
197+
describe('clickDropdownMenuItemByText', () => {
198198
const selectItemText =
199199
'This very long item should be selected by first focusing it and then pressing it. A focus is applied first, because otherwise it wouldnt be visible. Strangely, longer items tend to result in occasional test failures compared to smaller ones, which is why this item has this text.';
200-
const changeSpy = cy.spy().as('change');
201-
let callCounter = 1;
200+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
201+
let changeSpy = (e: unknown) => {};
202+
const handler = (e: unknown) => {
203+
changeSpy(e);
204+
};
202205
const components = [
203-
<ComboBox key="ui5-combobox" onSelectionChange={changeSpy}>
206+
<Select key="ui5-select" onChange={handler}>
207+
{...new Array(5).fill(<Option value="Item">Value</Option>)}
208+
<Option value={selectItemText} data-testid="selectItem">
209+
{selectItemText}
210+
</Option>
211+
</Select>,
212+
<ComboBox key="ui5-combobox" onSelectionChange={handler}>
204213
{...new Array(30).fill(<ComboBoxItem text="Item" />)}
205214
<ComboBoxItem text={selectItemText} />
206215
</ComboBox>,
207-
<MultiComboBox key="ui5-multi-combobox" onSelectionChange={changeSpy}>
216+
<MultiComboBox key="ui5-multi-combobox" onSelectionChange={handler}>
208217
{...new Array(30).fill(<MultiComboBoxItem text="Item" />)}
209218
<MultiComboBoxItem text={selectItemText} />
210219
</MultiComboBox>
211220
];
212221

213222
components.forEach((component) => {
214-
cy.mount(component);
215-
cy.get(`[${component.key}]`).openDropDownByClick();
216-
cy.get(`[${component.key}]`).clickDropdownMenuItemByText(selectItemText);
223+
it(component.key, () => {
224+
changeSpy = cy.spy().as('change');
225+
cy.mount(component);
226+
cy.get(`[${component.key}]`).openDropDownByClick();
227+
cy.get(`[${component.key}]`).clickDropdownMenuItemByText(selectItemText);
217228

218-
switch (component.key) {
219-
case 'ui5-combobox':
220-
cy.get(`[${component.key}]`).should('have.value', selectItemText);
221-
break;
222-
case 'ui5-multi-combobox':
223-
cy.get(`[${component.key}]`).find('[ui5-token]').contains(selectItemText);
224-
break;
225-
}
229+
switch (component.key) {
230+
case 'ui5-combobox':
231+
case 'ui5-select':
232+
cy.get(`[${component.key}]`).should('have.value', selectItemText);
233+
break;
234+
case 'ui5-multi-combobox':
235+
cy.get(`[${component.key}]`).find('[ui5-token]').contains(selectItemText);
236+
break;
237+
}
226238

227-
cy.get('@change').should('have.callCount', callCounter);
228-
callCounter++;
229-
cy.wait(200);
239+
cy.get('@change').should('have.been.calledOnce');
240+
cy.wait(200);
241+
});
230242
});
231243
});
232244

233-
it('clickDropDownMenuItem', () => {
245+
describe('clickDropDownMenuItem', () => {
234246
const selectItemText = 'Select me';
235-
const changeSpy = cy.spy().as('change');
236-
let callCounter = 1;
247+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
248+
let changeSpy = (e: unknown) => {};
249+
const handler = (e: unknown) => {
250+
changeSpy(e);
251+
};
237252
const components = [
238-
<ComboBox key="ui5-combobox" onSelectionChange={changeSpy}>
253+
<Select key="ui5-select" onChange={handler}>
254+
{...new Array(5).fill(<Option value="Item">Value</Option>)}
255+
<Option value={selectItemText} data-testid="selectItem">
256+
{selectItemText}
257+
</Option>
258+
</Select>,
259+
<ComboBox key="ui5-combobox" onSelectionChange={handler}>
239260
{...new Array(5).fill(<ComboBoxItem text="Item" />)}
240261
<ComboBoxItem text={selectItemText} data-testid="selectItem" />
241262
</ComboBox>,
242-
<MultiComboBox key="ui5-multi-combobox" onSelectionChange={changeSpy}>
263+
<MultiComboBox key="ui5-multi-combobox" onSelectionChange={handler}>
243264
{...new Array(5).fill(<MultiComboBoxItem text="Item" />)}
244265
<MultiComboBoxItem text={selectItemText} data-testid="selectItem" />
245266
</MultiComboBox>
246267
];
247268

248269
components.forEach((component) => {
249-
cy.mount(component);
250-
cy.get(`[${component.key}]`).openDropDownByClick();
251-
cy.get('[ui5-responsive-popover][open]').should('be.visible');
252-
cy.get(`[data-testid="selectItem"]`).clickDropdownMenuItem();
270+
it(component.key, () => {
271+
changeSpy = cy.spy().as('change');
272+
cy.mount(component);
273+
cy.get(`[${component.key}]`).openDropDownByClick();
274+
cy.get('[ui5-responsive-popover][open]').should('exist');
275+
cy.get(`[data-testid="selectItem"]`).clickDropdownMenuItem();
253276

254-
if (component.key === 'ui5-combobox') {
255-
cy.get(`[${component.key}]`).should('have.value', selectItemText);
256-
} else if (component.key === 'ui5-multi-combobox') {
257-
cy.get(`[${component.key}]`).find('[ui5-token]').contains(selectItemText);
258-
}
277+
if (component.key === 'ui5-combobox' || component.key === 'ui5-select') {
278+
cy.get(`[${component.key}]`).should('have.value', selectItemText);
279+
} else if (component.key === 'ui5-multi-combobox') {
280+
cy.get(`[${component.key}]`).find('[ui5-token]').contains(selectItemText);
281+
}
259282

260-
cy.get('@change').should('have.callCount', callCounter);
261-
callCounter++;
262-
cy.wait(200);
283+
cy.get('@change').should('have.been.calledOnce');
284+
cy.wait(200);
285+
});
263286
});
264287
});
265288

packages/main/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@
5757
"peerDependencies": {
5858
"@types/react": "*",
5959
"@types/react-dom": "*",
60-
"@ui5/webcomponents": "~2.4.0",
61-
"@ui5/webcomponents-base": "~2.4.0",
62-
"@ui5/webcomponents-fiori": "~2.4.0",
63-
"@ui5/webcomponents-icons": "~2.4.0",
60+
"@ui5/webcomponents": "~2.5.0",
61+
"@ui5/webcomponents-base": "~2.5.0",
62+
"@ui5/webcomponents-fiori": "~2.5.0",
63+
"@ui5/webcomponents-icons": "~2.5.0",
6464
"react": "^18 || ^19",
6565
"react-dom": "^18 || ^19"
6666
},

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

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
'use client';
22

3-
import type { RefObject } from 'react';
3+
import { useIsomorphicLayoutEffect, useSyncRef } from '@ui5/webcomponents-react-base';
4+
import type { MutableRefObject } from 'react';
45
import { createRef, useSyncExternalStore } from 'react';
56
import { createPortal } from 'react-dom';
67
import { getRandomId } from '../../internal/getRandomId.js';
8+
import type { IModal } from '../../internal/ModalStore.js';
79
import { ModalStore } from '../../internal/ModalStore.js';
810
import type {
911
DialogDomRef,
@@ -22,7 +24,7 @@ import type { MessageBoxPropTypes } from '../MessageBox/index.js';
2224
import { MessageBox } from '../MessageBox/index.js';
2325

2426
type ModalReturnType<DomRef> = {
25-
ref: RefObject<DomRef>;
27+
ref: MutableRefObject<DomRef>;
2628
};
2729

2830
type ClosableModalReturnType<DomRef> = ModalReturnType<DomRef> & {
@@ -72,7 +74,6 @@ function showPopoverFn(
7274
Component: Popover,
7375
props: {
7476
...props,
75-
7677
open: true,
7778
onClose: (event) => {
7879
if (typeof props.onClose === 'function') {
@@ -218,6 +219,24 @@ function showToastFn(props: ToastPropTypes, container?: Element | DocumentFragme
218219
};
219220
}
220221

222+
// todo: remove this once it's possible initializing popovers with `open=true` again
223+
function ModalComponent({ modal }: { modal: IModal }) {
224+
const [componentRef, modalsRef] = useSyncRef(modal.ref);
225+
useIsomorphicLayoutEffect(() => {
226+
const modalElement = modalsRef.current as PopoverDomRef;
227+
if (modalElement) {
228+
requestAnimationFrame(() => {
229+
modalElement.open = true;
230+
});
231+
}
232+
}, []);
233+
234+
const { open: _0, ...props } = modal.props;
235+
236+
// @ts-expect-error: ref is supported by all supported modals
237+
return <modal.Component {...props} ref={componentRef} data-id={modal.id} />;
238+
}
239+
221240
/**
222241
* Utility class for opening modals in an imperative way.
223242
*
@@ -238,14 +257,9 @@ export function Modals() {
238257
{modals.map((modal) => {
239258
if (modal?.Component) {
240259
if (modal.container) {
241-
return createPortal(
242-
// @ts-expect-error: ref is supported by all supported modals
243-
<modal.Component {...modal.props} ref={modal.ref} key={modal.id} data-id={modal.id} />,
244-
modal.container
245-
);
260+
return createPortal(<ModalComponent modal={modal} key={modal.id} />, modal.container);
246261
}
247-
// @ts-expect-error: ref is supported by all supported modals
248-
return <modal.Component {...modal.props} ref={modal.ref} key={modal.id} data-id={modal.id} />;
262+
return <ModalComponent modal={modal} key={modal.id} />;
249263
}
250264
})}
251265
</>

packages/main/src/internal/ModalStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function getStyleStoreSymbol() {
1212
return Symbol.for(`@ui5/webcomponents-react/Modals-${getCurrentRuntimeIndex()}`);
1313
}
1414

15-
type IModal = {
15+
export type IModal = {
1616
Component: ComponentType;
1717
props: Record<string, unknown>;
1818
ref: RefObject<unknown> | RefCallback<unknown>;

0 commit comments

Comments
 (0)