>(child) && !!child.props.slot;
});
// Component without slot attribute should go to the overlay.
diff --git a/packages/react-components/src/renderers/useRenderer.ts b/packages/react-components/src/renderers/useRenderer.ts
index 4f3e55bb..3d73f492 100644
--- a/packages/react-components/src/renderers/useRenderer.ts
+++ b/packages/react-components/src/renderers/useRenderer.ts
@@ -45,7 +45,7 @@ export function useRenderer(
convert?: (props: Slice, 1>) => PropsWithChildren,
config?: RendererConfig,
): UseRendererResult {
- const [map, update] = useReducer>(rendererReducer, initialState);
+ const [map, update] = useReducer(rendererReducer, initialState);
const renderer = useCallback(
((...args: Parameters) => {
if (config?.renderMode === 'microtask') {
diff --git a/packages/react-components/src/utils/addOrUpdateEventListener.ts b/packages/react-components/src/utils/addOrUpdateEventListener.ts
new file mode 100644
index 00000000..c72e9538
--- /dev/null
+++ b/packages/react-components/src/utils/addOrUpdateEventListener.ts
@@ -0,0 +1,30 @@
+const listenedEvents = new WeakMap>();
+
+export default function addOrUpdateEventListener(
+ node: Element,
+ event: string,
+ listener: ((event: Event) => void) | undefined,
+) {
+ let nodeEvents = listenedEvents.get(node);
+ if (nodeEvents === undefined) {
+ nodeEvents = new Map();
+ listenedEvents.set(node, nodeEvents);
+ }
+
+ let handler = nodeEvents.get(event);
+ if (listener !== undefined) {
+ if (handler === undefined) {
+ // If necessary, add listener and track handler
+ handler = { handleEvent: listener };
+ node.addEventListener(event, handler);
+ nodeEvents.set(event, handler);
+ } else {
+ // Otherwise just update the listener with new value
+ handler.handleEvent = listener;
+ }
+ } else if (handler !== undefined) {
+ // Remove listener if one exists and value is undefined
+ node.removeEventListener(event, handler);
+ nodeEvents.delete(event);
+ }
+}
diff --git a/packages/react-components/src/utils/createComponent.ts b/packages/react-components/src/utils/createComponent.ts
index 74fe3659..20caf940 100644
--- a/packages/react-components/src/utils/createComponent.ts
+++ b/packages/react-components/src/utils/createComponent.ts
@@ -1,7 +1,8 @@
-import { createComponent as _createComponent, type EventName } from '@lit/react';
import type { ThemePropertyMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
import type React from 'react';
-import type { RefAttributes } from 'react';
+import { createElement, useLayoutEffect, useRef, type RefAttributes } from 'react';
+import useMergedRefs from './useMergedRefs.js';
+import addOrUpdateEventListener from './addOrUpdateEventListener.js';
declare const __VERSION__: string;
@@ -28,8 +29,11 @@ window.Vaadin.registrations.push({
version: __VERSION__,
});
-// TODO: Remove when types from @lit-labs/react are exported
-export type EventNames = Record;
+export type EventName = string & {
+ __eventType: T;
+};
+
+export type EventNames = Record;
type Constructor = { new (): T; name: string };
type PolymerConstructor = Constructor & { _properties: Record };
type Options = Readonly<{
@@ -79,30 +83,56 @@ type AllWebComponentProps = I
export type WebComponentProps = Partial>;
-// We need a separate declaration here; otherwise, the TypeScript fails into the
-// endless loop trying to resolve the typings.
export function createComponent(
options: Options,
-): (props: WebComponentProps & RefAttributes) => React.ReactElement | null;
-
-export function createComponent(options: Options): any {
- const { elementClass } = options;
-
- return _createComponent(
- '_properties' in elementClass
- ? {
- ...options,
- // TODO: improve or remove the Polymer workaround
- // 'createComponent' relies on key presence on the custom element class,
- // but Polymer defines properties on the prototype when the first element
- // is created. Workaround: pass a mock object with properties in
- // the prototype.
- elementClass: {
- // @ts-expect-error: it is a specific workaround for Polymer classes.
- name: elementClass.name,
- prototype: { ...elementClass._properties, hidden: Boolean },
- },
+): (props: WebComponentProps & RefAttributes) => React.ReactElement {
+ const { tagName, events: eventsMap } = options;
+
+ return (props) => {
+ const innerRef = useRef(null);
+ const finalRef = useMergedRefs(innerRef, props.ref);
+ const prevEventsRef = useRef(new Set());
+
+ // Option 1 (no initial property events):
+ useLayoutEffect(() => {
+ if (eventsMap) {
+ const events = new Set(Object.keys(props).filter((event) => eventsMap[event]));
+ events.forEach((event) => {
+ addOrUpdateEventListener(innerRef.current!, eventsMap[event], props[event]);
+ });
+
+ prevEventsRef.current.forEach((event) => {
+ if (!events.has(event)) {
+ addOrUpdateEventListener(innerRef.current!, eventsMap[event], undefined);
+ }
+ });
+
+ prevEventsRef.current = events;
+ }
+ });
+
+ useLayoutEffect(() => {
+ return () => {
+ if (eventsMap) {
+ prevEventsRef.current.forEach((event) => {
+ addOrUpdateEventListener(innerRef.current!, eventsMap[event], undefined);
+ });
}
- : options,
- );
+ };
+ }, []);
+
+ const finalProps = Object.fromEntries(Object.entries(props).filter(([key]) => !eventsMap?.[key]));
+
+ // Option 2 (initial property events are fired):
+ // const finalProps = Object.fromEntries(
+ // Object.entries(props).map(([key, value]) => {
+ // if (eventsMap?.[key]) {
+ // return [`on${eventsMap[key]}`, value];
+ // }
+ // return [key, value];
+ // })
+ // );
+
+ return createElement(tagName, { ...finalProps, ref: finalRef });
+ };
}
diff --git a/packages/react-components/src/utils/useMergedRefs.ts b/packages/react-components/src/utils/useMergedRefs.ts
index 3a02cc50..dea3dc53 100644
--- a/packages/react-components/src/utils/useMergedRefs.ts
+++ b/packages/react-components/src/utils/useMergedRefs.ts
@@ -1,6 +1,8 @@
-import { type ForwardedRef, type RefCallback, useCallback } from 'react';
+import { type Ref, type RefCallback, useCallback } from 'react';
-export default function useMergedRefs(...refs: ReadonlyArray>): RefCallback {
+export default function useMergedRefs(
+ ...refs: ReadonlyArray[ | undefined>
+): RefCallback {
return useCallback((element: T) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
diff --git a/scripts/generator.ts b/scripts/generator.ts
index 4cc20581..14922667 100644
--- a/scripts/generator.ts
+++ b/scripts/generator.ts
@@ -35,7 +35,6 @@ const CREATE_COMPONENT_PATH = '$CREATE_COMPONENT_PATH$';
const EVENT_MAP = '$EVENT_MAP$';
const EVENT_MAP_REF_IN_EVENTS = '$EVENT_MAP_REF_IN_EVENTS$';
const EVENTS_DECLARATION = '$EVENTS_DECLARATION$';
-const LIT_REACT_PATH = '@lit/react';
const MODULE_PATH = '$MODULE_PATH$';
type ElementData = Readonly<{
@@ -321,14 +320,13 @@ function generateReactComponent({ name, js }: SchemaHTMLElement, { packageName,
const ast = template(
`
-import type { EventName } from "${LIT_REACT_PATH}";
import {
${COMPONENT_NAME} as ${COMPONENT_NAME}Element
type ${COMPONENT_NAME}EventMap as _${COMPONENT_NAME}EventMap,
${[...new Set(genericElementInfo?.typeConstraints || [])].map((constraint) => `type ${constraint}`)}
} from "${MODULE_PATH}";
import * as React from "react";
-import { createComponent, type WebComponentProps } from "${CREATE_COMPONENT_PATH}";
+import { createComponent, type WebComponentProps, type EventName } from "${CREATE_COMPONENT_PATH}";
export * from "${MODULE_PATH}";
diff --git a/test/Select.spec.tsx b/test/Select.spec.tsx
index 29a20d7b..88387330 100644
--- a/test/Select.spec.tsx
+++ b/test/Select.spec.tsx
@@ -157,13 +157,13 @@ describe('Select', () => {
it(`should be true in the element if ${property} prop is true`, async () => {
render();
const select = await findByQuerySelector('vaadin-select');
- expect(select[property]).to.be.ok;
+ expect(select[property]).to.be.true;
});
it(`should be false in the element if ${property} prop is false`, async () => {
render();
const select = await findByQuerySelector('vaadin-select');
- expect(select[property]).not.to.be.ok;
+ expect(select[property]).to.be.false;
});
});
});
]