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

feat: support objects and references as Web Component props #5957

Merged
merged 3 commits into from
Jun 25, 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
10 changes: 5 additions & 5 deletions packages/cli/src/scripts/create-wrappers/AttributesRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ function mapWebComponentTypeToTsType(type: string) {
return primitive;
}
switch (type) {
case 'HTMLElement | string | undefined':
case 'HTMLElement | string':
// opener props only accept strings as prop types
return 'string';
// case 'HTMLElement | string | undefined':
// case 'HTMLElement | string':
// // opener props only accept strings as prop types
// return 'string';

default:
if (!loggedTypes.has(type)) {
Expand Down Expand Up @@ -70,7 +70,7 @@ export class AttributesRenderer extends AbstractRenderer {
type = mapWebComponentTypeToTsType(type);

const references = attribute.type?.references;
const isEnum = references != null && references?.length > 0;
const isEnum = references != null && references?.length > 0 && attribute._ui5validator !== 'Object';

if (isEnum) {
type += ` | keyof typeof ${type}`;
Expand Down
8 changes: 1 addition & 7 deletions packages/cli/src/scripts/create-wrappers/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ import { WebComponentWrapper } from './WebComponentWrapper.js';
const WITH_WEB_COMPONENT_IMPORT_PATH = process.env.WITH_WEB_COMPONENT_IMPORT_PATH ?? '@ui5/webcomponents-react';

function filterAttributes(member: CEM.ClassField | CEM.ClassMethod): member is CEM.ClassField {
return (
member.kind === 'field' &&
member.privacy === 'public' &&
!member.readonly &&
!member.static &&
member._ui5validator !== 'Object'
);
return member.kind === 'field' && member.privacy === 'public' && !member.readonly && !member.static;
}

interface Options {
Expand Down
57 changes: 53 additions & 4 deletions packages/main/src/internal/withWebComponent.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import {
setCustomElementsScopingSuffix,
setCustomElementsScopingRules
setCustomElementsScopingRules,
setCustomElementsScopingSuffix
} from '@ui5/webcomponents-base/dist/CustomElementsScope.js';
import { useReducer, useState } from 'react';
import { Bar, Button, Switch } from '../webComponents/index.js';
import { useReducer, useRef, useState } from 'react';
import type { ButtonDomRef } from '../webComponents/index.js';
import { Bar, Button, Popover, Switch } from '../webComponents/index.js';

describe('withWebComponent', () => {
// reset scoping
afterEach(function () {
if (this.currentTest.title === 'scoping') {
// it's not possible to pass an empty string to `setCustomElementsScopingSuffix`
setCustomElementsScopingRules({ include: [/^ui5-/], exclude: [/.*/] });
}
});

it('Unmount Event Handlers correctly after prop update', () => {
const custom = cy.spy().as('custom');
const nativePassedThrough = cy.spy().as('nativePassedThrough');
Expand Down Expand Up @@ -172,4 +181,44 @@ describe('withWebComponent', () => {
cy.get('ui5-button').should('be.visible');
cy.get('ui5-button-ui5-wcr').should('not.exist');
});

it('pass objects & refs as props', () => {
const PopoverComponent = () => {
const btnRef = useRef(null);
const [open, setOpen] = useState(false);
return (
<>
<Button
ref={btnRef}
onClick={() => {
setOpen((prev) => !prev);
}}
>
Opener
</Button>
<Popover
open={open}
opener={btnRef.current}
onClose={() => {
setOpen(false);
}}
>
Popover Content
</Popover>
</>
);
};

cy.mount(<Button accessibilityAttributes={{ expanded: 'true' }}>Test</Button>);
cy.findByText('Test').should('have.attr', 'ui5-button');
cy.wait(500);
cy.contains<ButtonDomRef>('Test').then(([$button]) => {
expect($button.accessibilityAttributes).to.deep.equal({ expanded: 'true' });
});

cy.mount(<PopoverComponent />);
cy.get('[ui5-popover]').should('exist').should('not.be.visible');
cy.findByText('Opener').click();
cy.get('[ui5-popover]').should('be.visible');
});
});
20 changes: 19 additions & 1 deletion packages/main/src/internal/withWebComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { camelToKebabCase, capitalizeFirstLetter, kebabToCamelCase } from './uti

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

const isPrimitiveAttribute = (value: unknown): boolean => {
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
MarcusNotheis marked this conversation as resolved.
Show resolved Hide resolved
};

type EventHandler = (event: CustomEvent<unknown>) => void;

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

// regular props (no booleans, no slots and no events)
const regularProps = regularProperties.reduce((acc, name) => {
if (rest.hasOwnProperty(name)) {
if (rest.hasOwnProperty(name) && isPrimitiveAttribute(rest[name])) {
return { ...acc, [camelToKebabCase(name)]: rest[name] };
}
return acc;
Expand Down Expand Up @@ -158,6 +162,20 @@ export const withWebComponent = <Props extends Record<string, any>, RefType = Ui
});
}
}, [Component, waitForDefine, isDefined]);

const propsToApply = regularProperties.map((prop) => ({ name: prop, value: props[prop] }));
useEffect(() => {
void customElements.whenDefined(Component as unknown as string).then(() => {
for (const prop of propsToApply) {
if (prop.value != null && !isPrimitiveAttribute(prop.value)) {
if (ref.current) {
ref.current[prop.name] = prop.value;
}
}
}
});
}, [Component, ...propsToApply]);

if (waitForDefine && !isDefined) {
return null;
}
Expand Down
27 changes: 14 additions & 13 deletions packages/main/src/webComponents/Avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ import { withWebComponent } from '../../internal/withWebComponent.js';
import type { CommonProps, Ui5DomRef, UI5WCSlotsNode } from '../../types/index.js';

interface AvatarAttributes {
/**
* Defines the additional accessibility attributes that will be applied to the component.
* The following field is supported:
*
* - **hasPopup**: Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by the button.
* Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`.
*
* **Note:** Available since [v2.0.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v2.0.0) of **@ui5/webcomponents**.
* @default {}
*/
accessibilityAttributes?: AvatarAccessibilityAttributes;

/**
* Defines the text alternative of the component.
* If not provided a default text alternative will be set, if present.
Expand Down Expand Up @@ -95,18 +107,7 @@ interface AvatarAttributes {
size?: AvatarSize | keyof typeof AvatarSize;
}

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

interface AvatarPropTypes extends AvatarAttributes, Omit<CommonProps, keyof AvatarAttributes | 'badge' | 'children'> {
/**
Expand Down Expand Up @@ -152,7 +153,7 @@ interface AvatarPropTypes extends AvatarAttributes, Omit<CommonProps, keyof Avat
*/
const Avatar = withWebComponent<AvatarPropTypes, AvatarDomRef>(
'ui5-avatar',
['accessibleName', 'colorScheme', 'fallbackIcon', 'icon', 'initials', 'shape', 'size'],
['accessibilityAttributes', 'accessibleName', 'colorScheme', 'fallbackIcon', 'icon', 'initials', 'shape', 'size'],
['disabled', 'interactive'],
['badge'],
[],
Expand Down
21 changes: 11 additions & 10 deletions packages/main/src/webComponents/AvatarGroup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,6 @@ import { withWebComponent } from '../../internal/withWebComponent.js';
import type { CommonProps, Ui5CustomEvent, Ui5DomRef, UI5WCSlotsNode } from '../../types/index.js';

interface AvatarGroupAttributes {
/**
* Defines the mode of the `AvatarGroup`.
* @default "Group"
*/
type?: AvatarGroupType | keyof typeof AvatarGroupType;
}

interface AvatarGroupDomRef extends Required<AvatarGroupAttributes>, Ui5DomRef {
/**
* Defines the additional accessibility attributes that will be applied to the component.
* The following field is supported:
Expand All @@ -29,9 +21,18 @@ interface AvatarGroupDomRef extends Required<AvatarGroupAttributes>, Ui5DomRef {
* Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`.
*
* **Note:** Available since [v2.0.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v2.0.0) of **@ui5/webcomponents**.
* @default {}
*/
accessibilityAttributes: AvatarGroupAccessibilityAttributes;
accessibilityAttributes?: AvatarGroupAccessibilityAttributes;

/**
* Defines the mode of the `AvatarGroup`.
* @default "Group"
*/
type?: AvatarGroupType | keyof typeof AvatarGroupType;
}

interface AvatarGroupDomRef extends Required<AvatarGroupAttributes>, Ui5DomRef {
/**
* Returns an array containing the `AvatarColorScheme` values that correspond to the avatars in the component.
*/
Expand Down Expand Up @@ -140,7 +141,7 @@ interface AvatarGroupPropTypes
*/
const AvatarGroup = withWebComponent<AvatarGroupPropTypes, AvatarGroupDomRef>(
'ui5-avatar-group',
['type'],
['accessibilityAttributes', 'type'],
[],
['overflowButton'],
['click', 'overflow'],
Expand Down
49 changes: 30 additions & 19 deletions packages/main/src/webComponents/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ import { withWebComponent } from '../../internal/withWebComponent.js';
import type { CommonProps, Ui5DomRef } from '../../types/index.js';

interface ButtonAttributes {
/**
* Defines the additional accessibility attributes that will be applied to the component.
* The following fields are supported:
*
* - **expanded**: Indicates whether the button, or another grouping element it controls, is currently expanded or collapsed.
* Accepts the following string values: `true` or `false`
*
* - **hasPopup**: Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by the button.
* Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`.
*
* - **controls**: Identifies the element (or elements) whose contents or presence are controlled by the button element.
* Accepts a lowercase string value.
*
* **Note:** Available since [v1.2.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v1.2.0) of **@ui5/webcomponents**.
* @default {}
*/
accessibilityAttributes?: ButtonAccessibilityAttributes;

/**
* Defines the accessible ARIA name of the component.
* @default undefined
Expand Down Expand Up @@ -99,24 +117,7 @@ interface ButtonAttributes {
type?: ButtonType | keyof typeof ButtonType;
}

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

interface ButtonPropTypes extends ButtonAttributes, Omit<CommonProps, keyof ButtonAttributes | 'children' | 'onClick'> {
/**
Expand Down Expand Up @@ -159,7 +160,17 @@ interface ButtonPropTypes extends ButtonAttributes, Omit<CommonProps, keyof Butt
*/
const Button = withWebComponent<ButtonPropTypes, ButtonDomRef>(
'ui5-button',
['accessibleName', 'accessibleNameRef', 'accessibleRole', 'design', 'endIcon', 'icon', 'tooltip', 'type'],
[
'accessibilityAttributes',
'accessibleName',
'accessibleNameRef',
'accessibleRole',
'design',
'endIcon',
'icon',
'tooltip',
'type'
],
['disabled', 'submits'],
[],
['click'],
Expand Down
13 changes: 2 additions & 11 deletions packages/main/src/webComponents/ColorPalettePopover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface ColorPalettePopoverAttributes {
* **Note:** Available since [v1.21.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v1.21.0) of **@ui5/webcomponents**.
* @default undefined
*/
opener?: string;
opener?: HTMLElement | string | undefined;

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

interface ColorPalettePopoverDomRef extends Omit<Required<ColorPalettePopoverAttributes>, 'opener'>, Ui5DomRef {
/**
* Defines the ID or DOM Reference of the element that the popover is shown at.
* 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.
* You can only set the `opener` attribute to a DOM Reference when using JavaScript.
*
* **Note:** Available since [v1.21.0](https://github.com/SAP/ui5-webcomponents/releases/tag/v1.21.0) of **@ui5/webcomponents**.
*/
opener: HTMLElement | string | undefined;
}
interface ColorPalettePopoverDomRef extends Required<ColorPalettePopoverAttributes>, Ui5DomRef {}

interface ColorPalettePopoverPropTypes
extends ColorPalettePopoverAttributes,
Expand Down
Loading
Loading