Skip to content

Commit f41d32b

Browse files
feat: support objects and references as Web Component props (#5957)
This supports setting e.g. `accessibilityAttributes` and the `opener` props in a declarative way --------- Co-authored-by: Lukas Harbarth <[email protected]>
1 parent 5b2ac63 commit f41d32b

File tree

22 files changed

+401
-310
lines changed

22 files changed

+401
-310
lines changed

packages/cli/src/scripts/create-wrappers/AttributesRenderer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ function mapWebComponentTypeToTsType(type: string) {
1818
return primitive;
1919
}
2020
switch (type) {
21-
case 'HTMLElement | string | undefined':
22-
case 'HTMLElement | string':
23-
// opener props only accept strings as prop types
24-
return 'string';
21+
// case 'HTMLElement | string | undefined':
22+
// case 'HTMLElement | string':
23+
// // opener props only accept strings as prop types
24+
// return 'string';
2525

2626
default:
2727
if (!loggedTypes.has(type)) {
@@ -70,7 +70,7 @@ export class AttributesRenderer extends AbstractRenderer {
7070
type = mapWebComponentTypeToTsType(type);
7171

7272
const references = attribute.type?.references;
73-
const isEnum = references != null && references?.length > 0;
73+
const isEnum = references != null && references?.length > 0 && attribute._ui5validator !== 'Object';
7474

7575
if (isEnum) {
7676
type += ` | keyof typeof ${type}`;

packages/cli/src/scripts/create-wrappers/main.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,7 @@ import { WebComponentWrapper } from './WebComponentWrapper.js';
1414
const WITH_WEB_COMPONENT_IMPORT_PATH = process.env.WITH_WEB_COMPONENT_IMPORT_PATH ?? '@ui5/webcomponents-react';
1515

1616
function filterAttributes(member: CEM.ClassField | CEM.ClassMethod): member is CEM.ClassField {
17-
return (
18-
member.kind === 'field' &&
19-
member.privacy === 'public' &&
20-
!member.readonly &&
21-
!member.static &&
22-
member._ui5validator !== 'Object'
23-
);
17+
return member.kind === 'field' && member.privacy === 'public' && !member.readonly && !member.static;
2418
}
2519

2620
interface Options {

packages/main/src/internal/withWebComponent.cy.tsx

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import {
2-
setCustomElementsScopingSuffix,
3-
setCustomElementsScopingRules
2+
setCustomElementsScopingRules,
3+
setCustomElementsScopingSuffix
44
} from '@ui5/webcomponents-base/dist/CustomElementsScope.js';
5-
import { useReducer, useState } from 'react';
6-
import { Bar, Button, Switch } from '../webComponents/index.js';
5+
import { useReducer, useRef, useState } from 'react';
6+
import type { ButtonDomRef } from '../webComponents/index.js';
7+
import { Bar, Button, Popover, Switch } from '../webComponents/index.js';
78

89
describe('withWebComponent', () => {
10+
// reset scoping
11+
afterEach(function () {
12+
if (this.currentTest.title === 'scoping') {
13+
// it's not possible to pass an empty string to `setCustomElementsScopingSuffix`
14+
setCustomElementsScopingRules({ include: [/^ui5-/], exclude: [/.*/] });
15+
}
16+
});
17+
918
it('Unmount Event Handlers correctly after prop update', () => {
1019
const custom = cy.spy().as('custom');
1120
const nativePassedThrough = cy.spy().as('nativePassedThrough');
@@ -172,4 +181,44 @@ describe('withWebComponent', () => {
172181
cy.get('ui5-button').should('be.visible');
173182
cy.get('ui5-button-ui5-wcr').should('not.exist');
174183
});
184+
185+
it('pass objects & refs as props', () => {
186+
const PopoverComponent = () => {
187+
const btnRef = useRef(null);
188+
const [open, setOpen] = useState(false);
189+
return (
190+
<>
191+
<Button
192+
ref={btnRef}
193+
onClick={() => {
194+
setOpen((prev) => !prev);
195+
}}
196+
>
197+
Opener
198+
</Button>
199+
<Popover
200+
open={open}
201+
opener={btnRef.current}
202+
onClose={() => {
203+
setOpen(false);
204+
}}
205+
>
206+
Popover Content
207+
</Popover>
208+
</>
209+
);
210+
};
211+
212+
cy.mount(<Button accessibilityAttributes={{ expanded: 'true' }}>Test</Button>);
213+
cy.findByText('Test').should('have.attr', 'ui5-button');
214+
cy.wait(500);
215+
cy.contains<ButtonDomRef>('Test').then(([$button]) => {
216+
expect($button.accessibilityAttributes).to.deep.equal({ expanded: 'true' });
217+
});
218+
219+
cy.mount(<PopoverComponent />);
220+
cy.get('[ui5-popover]').should('exist').should('not.be.visible');
221+
cy.findByText('Opener').click();
222+
cy.get('[ui5-popover]').should('be.visible');
223+
});
175224
});

packages/main/src/internal/withWebComponent.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import { camelToKebabCase, capitalizeFirstLetter, kebabToCamelCase } from './uti
1010

1111
const createEventPropName = (eventName: string) => `on${capitalizeFirstLetter(kebabToCamelCase(eventName))}`;
1212

13+
const isPrimitiveAttribute = (value: unknown): boolean => {
14+
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
15+
};
16+
1317
type EventHandler = (event: CustomEvent<unknown>) => void;
1418

1519
export interface WithWebComponentPropTypes {
@@ -49,7 +53,7 @@ export const withWebComponent = <Props extends Record<string, any>, RefType = Ui
4953

5054
// regular props (no booleans, no slots and no events)
5155
const regularProps = regularProperties.reduce((acc, name) => {
52-
if (rest.hasOwnProperty(name)) {
56+
if (rest.hasOwnProperty(name) && isPrimitiveAttribute(rest[name])) {
5357
return { ...acc, [camelToKebabCase(name)]: rest[name] };
5458
}
5559
return acc;
@@ -158,6 +162,20 @@ export const withWebComponent = <Props extends Record<string, any>, RefType = Ui
158162
});
159163
}
160164
}, [Component, waitForDefine, isDefined]);
165+
166+
const propsToApply = regularProperties.map((prop) => ({ name: prop, value: props[prop] }));
167+
useEffect(() => {
168+
void customElements.whenDefined(Component as unknown as string).then(() => {
169+
for (const prop of propsToApply) {
170+
if (prop.value != null && !isPrimitiveAttribute(prop.value)) {
171+
if (ref.current) {
172+
ref.current[prop.name] = prop.value;
173+
}
174+
}
175+
}
176+
});
177+
}, [Component, ...propsToApply]);
178+
161179
if (waitForDefine && !isDefined) {
162180
return null;
163181
}

packages/main/src/webComponents/Avatar/index.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ import { withWebComponent } from '../../internal/withWebComponent.js';
1010
import type { CommonProps, Ui5DomRef, UI5WCSlotsNode } from '../../types/index.js';
1111

1212
interface AvatarAttributes {
13+
/**
14+
* Defines the additional accessibility attributes that will be applied to the component.
15+
* The following field is supported:
16+
*
17+
* - **hasPopup**: Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by the button.
18+
* Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`.
19+
*
20+
* **Note:** Available since [v2.0.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v2.0.0) of **@ui5/webcomponents**.
21+
* @default {}
22+
*/
23+
accessibilityAttributes?: AvatarAccessibilityAttributes;
24+
1325
/**
1426
* Defines the text alternative of the component.
1527
* If not provided a default text alternative will be set, if present.
@@ -95,18 +107,7 @@ interface AvatarAttributes {
95107
size?: AvatarSize | keyof typeof AvatarSize;
96108
}
97109

98-
interface AvatarDomRef extends Required<AvatarAttributes>, Ui5DomRef {
99-
/**
100-
* Defines the additional accessibility attributes that will be applied to the component.
101-
* The following field is supported:
102-
*
103-
* - **hasPopup**: Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by the button.
104-
* Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`.
105-
*
106-
* **Note:** Available since [v2.0.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v2.0.0) of **@ui5/webcomponents**.
107-
*/
108-
accessibilityAttributes: AvatarAccessibilityAttributes;
109-
}
110+
interface AvatarDomRef extends Required<AvatarAttributes>, Ui5DomRef {}
110111

111112
interface AvatarPropTypes extends AvatarAttributes, Omit<CommonProps, keyof AvatarAttributes | 'badge' | 'children'> {
112113
/**
@@ -152,7 +153,7 @@ interface AvatarPropTypes extends AvatarAttributes, Omit<CommonProps, keyof Avat
152153
*/
153154
const Avatar = withWebComponent<AvatarPropTypes, AvatarDomRef>(
154155
'ui5-avatar',
155-
['accessibleName', 'colorScheme', 'fallbackIcon', 'icon', 'initials', 'shape', 'size'],
156+
['accessibilityAttributes', 'accessibleName', 'colorScheme', 'fallbackIcon', 'icon', 'initials', 'shape', 'size'],
156157
['disabled', 'interactive'],
157158
['badge'],
158159
[],

packages/main/src/webComponents/AvatarGroup/index.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ import { withWebComponent } from '../../internal/withWebComponent.js';
1313
import type { CommonProps, Ui5CustomEvent, Ui5DomRef, UI5WCSlotsNode } from '../../types/index.js';
1414

1515
interface AvatarGroupAttributes {
16-
/**
17-
* Defines the mode of the `AvatarGroup`.
18-
* @default "Group"
19-
*/
20-
type?: AvatarGroupType | keyof typeof AvatarGroupType;
21-
}
22-
23-
interface AvatarGroupDomRef extends Required<AvatarGroupAttributes>, Ui5DomRef {
2416
/**
2517
* Defines the additional accessibility attributes that will be applied to the component.
2618
* The following field is supported:
@@ -29,9 +21,18 @@ interface AvatarGroupDomRef extends Required<AvatarGroupAttributes>, Ui5DomRef {
2921
* Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`.
3022
*
3123
* **Note:** Available since [v2.0.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v2.0.0) of **@ui5/webcomponents**.
24+
* @default {}
3225
*/
33-
accessibilityAttributes: AvatarGroupAccessibilityAttributes;
26+
accessibilityAttributes?: AvatarGroupAccessibilityAttributes;
3427

28+
/**
29+
* Defines the mode of the `AvatarGroup`.
30+
* @default "Group"
31+
*/
32+
type?: AvatarGroupType | keyof typeof AvatarGroupType;
33+
}
34+
35+
interface AvatarGroupDomRef extends Required<AvatarGroupAttributes>, Ui5DomRef {
3536
/**
3637
* Returns an array containing the `AvatarColorScheme` values that correspond to the avatars in the component.
3738
*/
@@ -140,7 +141,7 @@ interface AvatarGroupPropTypes
140141
*/
141142
const AvatarGroup = withWebComponent<AvatarGroupPropTypes, AvatarGroupDomRef>(
142143
'ui5-avatar-group',
143-
['type'],
144+
['accessibilityAttributes', 'type'],
144145
[],
145146
['overflowButton'],
146147
['click', 'overflow'],

packages/main/src/webComponents/Button/index.tsx

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ import { withWebComponent } from '../../internal/withWebComponent.js';
1010
import type { CommonProps, Ui5DomRef } from '../../types/index.js';
1111

1212
interface ButtonAttributes {
13+
/**
14+
* Defines the additional accessibility attributes that will be applied to the component.
15+
* The following fields are supported:
16+
*
17+
* - **expanded**: Indicates whether the button, or another grouping element it controls, is currently expanded or collapsed.
18+
* Accepts the following string values: `true` or `false`
19+
*
20+
* - **hasPopup**: Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by the button.
21+
* Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`.
22+
*
23+
* - **controls**: Identifies the element (or elements) whose contents or presence are controlled by the button element.
24+
* Accepts a lowercase string value.
25+
*
26+
* **Note:** Available since [v1.2.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v1.2.0) of **@ui5/webcomponents**.
27+
* @default {}
28+
*/
29+
accessibilityAttributes?: ButtonAccessibilityAttributes;
30+
1331
/**
1432
* Defines the accessible ARIA name of the component.
1533
* @default undefined
@@ -99,24 +117,7 @@ interface ButtonAttributes {
99117
type?: ButtonType | keyof typeof ButtonType;
100118
}
101119

102-
interface ButtonDomRef extends Required<ButtonAttributes>, Ui5DomRef {
103-
/**
104-
* Defines the additional accessibility attributes that will be applied to the component.
105-
* The following fields are supported:
106-
*
107-
* - **expanded**: Indicates whether the button, or another grouping element it controls, is currently expanded or collapsed.
108-
* Accepts the following string values: `true` or `false`
109-
*
110-
* - **hasPopup**: Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by the button.
111-
* Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`.
112-
*
113-
* - **controls**: Identifies the element (or elements) whose contents or presence are controlled by the button element.
114-
* Accepts a lowercase string value.
115-
*
116-
* **Note:** Available since [v1.2.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v1.2.0) of **@ui5/webcomponents**.
117-
*/
118-
accessibilityAttributes: ButtonAccessibilityAttributes;
119-
}
120+
interface ButtonDomRef extends Required<ButtonAttributes>, Ui5DomRef {}
120121

121122
interface ButtonPropTypes extends ButtonAttributes, Omit<CommonProps, keyof ButtonAttributes | 'children' | 'onClick'> {
122123
/**
@@ -159,7 +160,17 @@ interface ButtonPropTypes extends ButtonAttributes, Omit<CommonProps, keyof Butt
159160
*/
160161
const Button = withWebComponent<ButtonPropTypes, ButtonDomRef>(
161162
'ui5-button',
162-
['accessibleName', 'accessibleNameRef', 'accessibleRole', 'design', 'endIcon', 'icon', 'tooltip', 'type'],
163+
[
164+
'accessibilityAttributes',
165+
'accessibleName',
166+
'accessibleNameRef',
167+
'accessibleRole',
168+
'design',
169+
'endIcon',
170+
'icon',
171+
'tooltip',
172+
'type'
173+
],
163174
['disabled', 'submits'],
164175
[],
165176
['click'],

packages/main/src/webComponents/ColorPalettePopover/index.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ interface ColorPalettePopoverAttributes {
3131
* **Note:** Available since [v1.21.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v1.21.0) of **@ui5/webcomponents**.
3232
* @default undefined
3333
*/
34-
opener?: string;
34+
opener?: HTMLElement | string | undefined;
3535

3636
/**
3737
* Defines whether the user can choose the default color from a button.
@@ -54,16 +54,7 @@ interface ColorPalettePopoverAttributes {
5454
showRecentColors?: boolean;
5555
}
5656

57-
interface ColorPalettePopoverDomRef extends Omit<Required<ColorPalettePopoverAttributes>, 'opener'>, Ui5DomRef {
58-
/**
59-
* Defines the ID or DOM Reference of the element that the popover is shown at.
60-
* When using this attribute in a declarative way, you must only use the `id` (as a string) of the element at which you want to show the popover.
61-
* You can only set the `opener` attribute to a DOM Reference when using JavaScript.
62-
*
63-
* **Note:** Available since [v1.21.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v1.21.0) of **@ui5/webcomponents**.
64-
*/
65-
opener: HTMLElement | string | undefined;
66-
}
57+
interface ColorPalettePopoverDomRef extends Required<ColorPalettePopoverAttributes>, Ui5DomRef {}
6758

6859
interface ColorPalettePopoverPropTypes
6960
extends ColorPalettePopoverAttributes,

0 commit comments

Comments
 (0)