Skip to content

Commit 6421196

Browse files
authored
refactor: move withWebComponent and corresponding types & utils to base pkg (#6927)
1 parent 1b07868 commit 6421196

File tree

167 files changed

+819
-767
lines changed

Some content is hidden

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

167 files changed

+819
-767
lines changed

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

+38-1
Original file line numberDiff line numberDiff line change
@@ -1721,7 +1721,44 @@
17211721
{
17221722
"kind": "javascript-module",
17231723
"path": "dist/types/TimelineGrowingMode.js",
1724-
"declarations": [],
1724+
"declarations": [
1725+
{
1726+
"kind": "enum",
1727+
"name": "TimelineGrowingMode",
1728+
"description": "Timeline growing modes.",
1729+
"_ui5privacy": "public",
1730+
"_ui5since": "2.7.0",
1731+
"members": [
1732+
{
1733+
"kind": "field",
1734+
"static": true,
1735+
"privacy": "public",
1736+
"description": "Event `load-more` is fired\nupon pressing a \"More\" button at the end.",
1737+
"default": "Button",
1738+
"name": "Button",
1739+
"readonly": true
1740+
},
1741+
{
1742+
"kind": "field",
1743+
"static": true,
1744+
"privacy": "public",
1745+
"description": "Event `load-more` is fired upon scroll.",
1746+
"default": "Scroll",
1747+
"name": "Scroll",
1748+
"readonly": true
1749+
},
1750+
{
1751+
"kind": "field",
1752+
"static": true,
1753+
"privacy": "public",
1754+
"description": "The growing feature is not enabled.",
1755+
"default": "None",
1756+
"name": "None",
1757+
"readonly": true
1758+
}
1759+
]
1760+
}
1761+
],
17251762
"exports": [
17261763
{
17271764
"kind": "js",

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@
754754
"kind": "field",
755755
"static": true,
756756
"privacy": "public",
757-
"description": "The badge is displayed at the top-end corner of the button.\n\n**Note:** It's highly recommended to use the OverlayText design mode only in cozy density.",
757+
"description": "The badge is displayed at the top-end corner of the button.\n\n**Note:** According to design guidance, the OverlayText design mode is best used in cozy density to avoid potential visual issues in compact.",
758758
"default": "OverlayText",
759759
"name": "OverlayText",
760760
"readonly": true
@@ -5251,7 +5251,7 @@
52515251
"declarations": [
52525252
{
52535253
"kind": "class",
5254-
"description": "The `ui5-button-badge` component defines a badge that appears in the `ui5-button`.\n\n * ### ES6 Module Import\n\n`import \"@ui5/webcomponents/dist/ButtonBadge.js\";`",
5254+
"description": "The `ui5-button-badge` component defines a badge that appears in the `ui5-button`.\n\n### ES6 Module Import\n\n`import \"@ui5/webcomponents/dist/ButtonBadge.js\";`",
52555255
"name": "ButtonBadge",
52565256
"members": [
52575257
{
@@ -5268,7 +5268,7 @@
52685268
]
52695269
},
52705270
"default": "\"AttentionDot\"",
5271-
"description": "Determines where the badge should be placed and how it should be styled.",
5271+
"description": "Defines the badge placement and appearance.\n- **InlineText** - displayed inside the button after its text, and recommended for **compact** density.\n- **OverlayText** - displayed at the top-end corner of the button, and recommended for **cozy** density.\n- **AttentionDot** - displayed at the top-end corner of the button as a dot, and suitable for both **cozy** and **compact** densities.",
52725272
"privacy": "public",
52735273
"_ui5since": "2.7.0"
52745274
},
@@ -5279,14 +5279,14 @@
52795279
"text": "string"
52805280
},
52815281
"default": "\"\"",
5282-
"description": "Defines the text of the component.\n\n**Note:** Text is not needed when the `design` property is set to `AttentionDot`.",
5282+
"description": "Defines the text of the component.\n\n**Note:** Text is not applied when the `design` property is set to `AttentionDot`.",
52835283
"privacy": "public",
52845284
"_ui5since": "2.7.0"
52855285
}
52865286
],
52875287
"attributes": [
52885288
{
5289-
"description": "Determines where the badge should be placed and how it should be styled.",
5289+
"description": "Defines the badge placement and appearance.\n- **InlineText** - displayed inside the button after its text, and recommended for **compact** density.\n- **OverlayText** - displayed at the top-end corner of the button, and recommended for **cozy** density.\n- **AttentionDot** - displayed at the top-end corner of the button as a dot, and suitable for both **cozy** and **compact** densities.",
52905290
"name": "design",
52915291
"default": "\"AttentionDot\"",
52925292
"fieldName": "design",
@@ -5295,7 +5295,7 @@
52955295
}
52965296
},
52975297
{
5298-
"description": "Defines the text of the component.\n\n**Note:** Text is not needed when the `design` property is set to `AttentionDot`.",
5298+
"description": "Defines the text of the component.\n\n**Note:** Text is not applied when the `design` property is set to `AttentionDot`.",
52995299
"name": "text",
53005300
"default": "\"\"",
53015301
"fieldName": "text",

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
"prettier:all": "prettier --write --config ./prettier.config.cjs \"packages/**/*.{js,jsx,ts,tsx,mdx,json,md}\"",
2222
"lint": "eslint packages",
2323
"lerna:version-dryrun": "lerna version --conventional-graduate --no-git-tag-version --no-push",
24-
"wrappers:main": "WITH_WEB_COMPONENT_IMPORT_PATH='../../internal/withWebComponent.js' INTERFACES_IMPORT_PATH='../../types/index.js' node packages/cli/dist/bin/index.js create-wrappers --packageName @ui5/webcomponents --out ./packages/main/src/webComponents --additionalComponentNote 'This is a UI5 Web Component! [Repository](https://github.com/SAP/ui5-webcomponents) | [Documentation](https://sap.github.io/ui5-webcomponents/)'",
25-
"wrappers:fiori": "WITH_WEB_COMPONENT_IMPORT_PATH='../../internal/withWebComponent.js' INTERFACES_IMPORT_PATH='../../types/index.js' node packages/cli/dist/bin/index.js create-wrappers --packageName @ui5/webcomponents-fiori --out ./packages/main/src/webComponents --additionalComponentNote 'This is a UI5 Web Component! [Repository](https://github.com/SAP/ui5-webcomponents) | [Documentation](https://sap.github.io/ui5-webcomponents/)'",
26-
"wrappers:compat": "WITH_WEB_COMPONENT_IMPORT_PATH='@ui5/webcomponents-react/dist/internal/withWebComponent.js' node packages/cli/dist/bin/index.js create-wrappers --packageName @ui5/webcomponents-compat --out ./packages/compat/src/components --additionalComponentNote 'This is a UI5 Web Component! [Repository](https://github.com/SAP/ui5-webcomponents) | [Documentation](https://sap.github.io/ui5-webcomponents/)' && prettier --log-level silent --write ./packages/compat/src/components",
24+
"wrappers:main": "node packages/cli/dist/bin/index.js create-wrappers --packageName @ui5/webcomponents --out ./packages/main/src/webComponents --additionalComponentNote 'This is a UI5 Web Component! [Repository](https://github.com/SAP/ui5-webcomponents) | [Documentation](https://sap.github.io/ui5-webcomponents/)'",
25+
"wrappers:fiori": "node packages/cli/dist/bin/index.js create-wrappers --packageName @ui5/webcomponents-fiori --out ./packages/main/src/webComponents --additionalComponentNote 'This is a UI5 Web Component! [Repository](https://github.com/SAP/ui5-webcomponents) | [Documentation](https://sap.github.io/ui5-webcomponents/)'",
26+
"wrappers:compat": "WITH_WEB_COMPONENT_IMPORT_PATH='@ui5/webcomponents-react-base/dist/wrapper/withWebComponent.js' node packages/cli/dist/bin/index.js create-wrappers --packageName @ui5/webcomponents-compat --out ./packages/compat/src/components --additionalComponentNote 'This is a UI5 Web Component! [Repository](https://github.com/SAP/ui5-webcomponents) | [Documentation](https://sap.github.io/ui5-webcomponents/)' && prettier --log-level silent --write ./packages/compat/src/components",
2727
"create-webcomponents-wrapper": "(cd packages/cli && tsc) && yarn run wrappers:main && yarn run wrappers:fiori && prettier --log-level silent --write ./packages/main/src/webComponents && eslint --fix ./packages/main/src/webComponents/*/index.tsx && yarn run sb:prepare-cem",
2828
"create-webcomponents-wrapper-compat": "(cd packages/cli && tsc) && yarn run wrappers:compat && yarn run sb:prepare-cem && eslint --fix ./packages/compat/src/components/*/index.tsx",
2929
"chromatic": "cross-env STORYBOOK_ENV=chromatic npx chromatic --build-script-name build:storybook",

packages/base/src/Device/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ const handleMobileTimeout = () => {
9696

9797
const handleMobileOrientationResizeChange = (evt) => {
9898
if (evt.type === 'resize') {
99-
// @ts-expect-error: undefined is fine here
10099
if (rInputTagRegex.test(document.activeElement?.tagName) && !bOrientationChange) {
101100
return;
102101
}

packages/base/src/hooks/useStylesheet.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function useStylesheet(styles: string, componentName: string, options?: U
2828
return () => {
2929
if (shouldInject) {
3030
StyleStore.unmountComponent(componentName);
31-
const numberOfMountedComponents = componentsMap.get(componentName)!;
31+
const numberOfMountedComponents = componentsMap.get(componentName);
3232
if (numberOfMountedComponents <= 0) {
3333
removeStyle('data-ui5wcr-component', scopedComponentName);
3434
}

packages/base/src/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import * as hooks from './hooks/index.js';
44
import { I18nStore } from './stores/I18nStore.js';
55
import { StyleStore } from './stores/StyleStore.js';
66
import { ThemingParameters } from './styling/ThemingParameters.js';
7+
import { withWebComponent } from './wrapper/withWebComponent.js';
8+
import type { WithWebComponentPropTypes } from './wrapper/withWebComponent.js';
79

810
export * from './styling/CssSizeVariables.js';
911
export * from './utils/index.js';
1012
export * from './hooks/index.js';
13+
export type * from './types/index.js';
1114

12-
export { I18nStore, StyleStore, ThemingParameters, Device, hooks };
15+
export { I18nStore, StyleStore, ThemingParameters, Device, hooks, withWebComponent };
16+
export type { WithWebComponentPropTypes };
1317
export const version = VersionInfo.version;

packages/base/src/stores/StyleStore.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const StyleStore = {
6767
mountComponent: (componentName: string) => {
6868
const { componentsMap } = getSnapshot();
6969
if (componentsMap.has(componentName)) {
70-
componentsMap.set(componentName, componentsMap.get(componentName)! + 1);
70+
componentsMap.set(componentName, componentsMap.get(componentName) + 1);
7171
} else {
7272
componentsMap.set(componentName, 1);
7373
}
@@ -76,7 +76,7 @@ export const StyleStore = {
7676
unmountComponent: (componentName: string) => {
7777
const { componentsMap } = getSnapshot();
7878
if (componentsMap.has(componentName)) {
79-
componentsMap.set(componentName, componentsMap.get(componentName)! - 1);
79+
componentsMap.set(componentName, componentsMap.get(componentName) - 1);
8080
}
8181
emitChange();
8282
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { CSSProperties, HTMLAttributes } from 'react';
2+
3+
export interface CommonProps<T = HTMLElement> extends Omit<HTMLAttributes<T>, 'dangerouslySetInnerHTML'> {
4+
/**
5+
* Element style which will be appended to the most outer element of a component.
6+
* Use this prop carefully, some css properties might break the component.
7+
*/
8+
style?: CSSProperties;
9+
/**
10+
* CSS Class Name which will be appended to the most outer element of a component.
11+
* Use this prop carefully, overwriting CSS rules might break the component.
12+
*/
13+
className?: string;
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface Ui5CustomEvent<EventTarget = HTMLElement, Detail = never>
2+
extends Omit<CustomEvent<Detail>, 'target' | 'currentTarget'> {
3+
target: EventTarget;
4+
currentTarget: EventTarget | null;
5+
}

packages/base/src/types/Ui5DomRef.ts

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { PropertyValue, SlotValue } from '@ui5/webcomponents-base/dist/UI5ElementMetadata.js';
2+
3+
type ChangeInfo = {
4+
type: 'property' | 'slot';
5+
name: string;
6+
reason?: string;
7+
child?: SlotValue;
8+
target?: Ui5DomRef;
9+
newValue?: PropertyValue;
10+
oldValue?: PropertyValue;
11+
};
12+
13+
type InvalidationInfo = ChangeInfo & { target: Ui5DomRef };
14+
15+
export interface Ui5DomRef extends Omit<HTMLElement, 'focus'> {
16+
/**
17+
* Called every time before the component renders.
18+
* @public
19+
*/
20+
onBeforeRendering(): void;
21+
/**
22+
* Called every time after the component renders.
23+
* @public
24+
*/
25+
onAfterRendering(): void;
26+
/**
27+
* Called on connectedCallback - added to the DOM.
28+
* @public
29+
*/
30+
onEnterDOM(): void;
31+
/**
32+
* Called on disconnectedCallback - removed from the DOM.
33+
* @public
34+
*/
35+
onExitDOM(): void;
36+
/**
37+
* Attach a callback that will be executed whenever the component is invalidated
38+
*
39+
* @param callback
40+
* @public
41+
*/
42+
attachInvalidate(callback: (param: InvalidationInfo) => void): void;
43+
/**
44+
* Detach the callback that is executed whenever the component is invalidated
45+
*
46+
* @param callback
47+
* @public
48+
*/
49+
detachInvalidate(callback: (param: InvalidationInfo) => void): void;
50+
/**
51+
* A callback that is executed each time an already rendered component is invalidated (scheduled for re-rendering)
52+
*
53+
* @param changeInfo An object with information about the change that caused invalidation.
54+
* The object can have the following properties:
55+
* - type: (property|slot) tells what caused the invalidation
56+
* 1) property: a property value was changed either directly or as a result of changing the corresponding attribute
57+
* 2) slot: a slotted node(nodes) changed in one of several ways (see "reason")
58+
*
59+
* - name: the name of the property or slot that caused the invalidation
60+
*
61+
* - reason: (children|textcontent|childchange|slotchange) relevant only for type="slot" only and tells exactly what changed in the slot
62+
* 1) children: immediate children (HTML elements or text nodes) were added, removed or reordered in the slot
63+
* 2) textcontent: text nodes in the slot changed value (or nested text nodes were added or changed value). Can only trigger for slots of "type: Node"
64+
* 3) slotchange: a slot element, slotted inside that slot had its "slotchange" event listener called. This practically means that transitively slotted children changed.
65+
* Can only trigger if the child of a slot is a slot element itself.
66+
* 4) childchange: indicates that a UI5Element child in that slot was invalidated and in turn invalidated the component.
67+
* Can only trigger for slots with "invalidateOnChildChange" metadata descriptor
68+
*
69+
* - newValue: the new value of the property (for type="property" only)
70+
*
71+
* - oldValue: the old value of the property (for type="property" only)
72+
*
73+
* - child the child that was changed (for type="slot" and reason="childchange" only)
74+
*
75+
* @public
76+
*/
77+
onInvalidation(changeInfo: ChangeInfo): void;
78+
/**
79+
* Returns the DOM Element inside the Shadow Root that corresponds to the opening tag in the UI5 Web Component's template
80+
* *Note:* For logical (abstract) elements (items, options, etc...), returns the part of the parent's DOM that represents this option
81+
* Use this method instead of "this.shadowRoot" to read the Shadow DOM, if ever necessary
82+
*
83+
* @public
84+
*/
85+
getDomRef(): HTMLElement | undefined;
86+
87+
/**
88+
* Returns the DOM Element marked with "data-sap-focus-ref" inside the template.
89+
* This is the element that will receive the focus by default.
90+
* @public
91+
*/
92+
getFocusDomRef(): HTMLElement | undefined;
93+
94+
/**
95+
* Waits for dom ref and then returns the DOM Element marked with "data-sap-focus-ref" inside the template.
96+
* This is the element that will receive the focus by default.
97+
* @public
98+
*/
99+
getFocusDomRefAsync(): Promise<HTMLElement | undefined>;
100+
/**
101+
* Set the focus to the element, returned by "getFocusDomRef()" (marked by "data-sap-focus-ref")
102+
* @param focusOptions additional options for the focus
103+
* @public
104+
*/
105+
focus(focusOptions?: FocusOptions): Promise<void>;
106+
/**
107+
*
108+
* @public
109+
* @param name - name of the event
110+
* @param data - additional data for the event
111+
* @param cancelable - true, if the user can call preventDefault on the event object
112+
* @param bubbles - true, if the event bubbles
113+
* @returns false, if the event was cancelled (preventDefault called), true otherwise
114+
*/
115+
fireEvent<T>(name: string, data?: T, cancelable?: boolean, bubbles?: boolean): boolean;
116+
/**
117+
* Returns the actual children, associated with a slot.
118+
* Useful when there are transitive slots in nested component scenarios and you don't want to get a list of the slots, but rather of their content.
119+
* @public
120+
*/
121+
getSlottedNodes<T = Node>(slotName: string): Array<T>;
122+
/**
123+
* Attach a callback that will be executed whenever the component's state is finalized
124+
*
125+
* @param callback
126+
* @public
127+
*/
128+
attachComponentStateFinalized(callback: () => void): void;
129+
/**
130+
* Detach the callback that is executed whenever the component's state is finalized
131+
*
132+
* @param callback
133+
* @public
134+
*/
135+
detachComponentStateFinalized(callback: () => void): void;
136+
/**
137+
* Determines whether the component should be rendered in RTL mode or not.
138+
* Returns: "rtl", "ltr" or undefined
139+
*
140+
* @public
141+
* @default undefined
142+
*/
143+
readonly effectiveDir: string | undefined;
144+
145+
/**
146+
* Used to duck-type UI5 elements without using instanceof
147+
* @public
148+
* @default true
149+
*/
150+
readonly isUI5Element: boolean;
151+
}

packages/base/src/types/index.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { ReactElement, ReactNode, ReactPortal } from 'react';
2+
3+
export type ReducedReactNode = Exclude<ReactNode, string | number | boolean | ReactPortal | Iterable<ReactNode>>;
4+
export type ReducedReactNodeWithBoolean = Exclude<ReactNode, string | number | ReactPortal | Iterable<ReactNode>>;
5+
6+
type InternalUI5WCSlotsNode =
7+
| ReducedReactNode
8+
| Iterable<ReducedReactNode>
9+
| false
10+
| ReactElement /* necessary for React v16 & v17 ReactNode type*/;
11+
12+
export type UI5WCSlotsNode = InternalUI5WCSlotsNode | InternalUI5WCSlotsNode[];
13+
14+
export type Nullable<T> = T | null;
15+
16+
export type { CommonProps } from './CommonProps.js';
17+
export type { Ui5CustomEvent } from './Ui5CustomEvent.js';
18+
export type { Ui5DomRef } from './Ui5DomRef.js';

packages/base/src/utils/index.ts

+23-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const enrichEventWithDetails = <
2323
payload: Detail
2424
): EnrichedEventType<Event, Detail> => {
2525
if (!event) {
26-
return event;
26+
return event as EnrichedEventType<Event, Detail>;
2727
}
2828

2929
// Determine if we need to create a new details object
@@ -41,9 +41,9 @@ export const enrichEventWithDetails = <
4141
});
4242

4343
if (nativeDetail) {
44-
Object.assign(event.detail!, { nativeDetail });
44+
Object.assign(event.detail, { nativeDetail });
4545
}
46-
Object.assign(event.detail!, payload);
46+
Object.assign(event.detail, payload);
4747

4848
return event as EnrichedEventType<Event, Detail>;
4949
};
@@ -55,3 +55,23 @@ export function getUi5TagWithSuffix(baseTagName: string) {
5555

5656
export { debounce } from './debounce.js';
5757
export { throttle } from './throttle.js';
58+
59+
export const capitalizeFirstLetter = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
60+
export const lowercaseFirstLetter = (s: string) => s.charAt(0).toLowerCase() + s.slice(1);
61+
export const camelToKebabCase = (s: string) => s.replace(/([A-Z])/g, (a, b: string) => `-${b.toLowerCase()}`);
62+
export const kebabToCamelCase = (str: string) => str.replace(/([-_]\w)/g, (g) => g[1].toUpperCase());
63+
64+
const SEMVER_REGEX =
65+
/^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
66+
67+
export function parseSemVer(version: string) {
68+
const parsed = SEMVER_REGEX.exec(version).groups;
69+
return {
70+
version,
71+
major: parseInt(parsed.major),
72+
minor: parseInt(parsed.minor),
73+
patch: parseInt(parsed.patch),
74+
prerelease: parsed.prerelease,
75+
buildMetadata: parsed.buildmetadata
76+
};
77+
}

0 commit comments

Comments
 (0)