From ceae04c5f95dbbe489076ae73be7f9bead80ab9e Mon Sep 17 00:00:00 2001 From: Leo Largillet Date: Mon, 17 Jul 2023 17:42:34 +0200 Subject: [PATCH 1/2] fix(input): set cursor to not-allowed when disabled --- .../src/components/stencil-generated/index.ts | 10 + .../react-component-lib/createComponent.tsx | 104 +++++++++ .../createOverlayComponent.tsx | 162 ++++++++++++++ .../react-component-lib/index.ts | 2 + .../react-component-lib/interfaces.ts | 34 +++ .../react-component-lib/utils/attachProps.ts | 114 ++++++++++ .../react-component-lib/utils/case.ts | 8 + .../react-component-lib/utils/dev.ts | 14 ++ .../react-component-lib/utils/index.tsx | 57 +++++ .../src/components/stencil-generated/index.ts | 15 ++ .../vue-component-lib/utils.ts | 198 ++++++++++++++++++ .../src/components/osds-input/osds-input.scss | 17 +- 12 files changed, 727 insertions(+), 8 deletions(-) create mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/index.ts create mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createComponent.tsx create mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createOverlayComponent.tsx create mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/index.ts create mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/interfaces.ts create mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/attachProps.ts create mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/case.ts create mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/dev.ts create mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/index.tsx create mode 100644 packages/stencil/components/form-field/vue/src/components/stencil-generated/index.ts create mode 100644 packages/stencil/components/form-field/vue/src/components/stencil-generated/vue-component-lib/utils.ts diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/index.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/index.ts new file mode 100644 index 0000000000..df32f8d746 --- /dev/null +++ b/packages/stencil/components/form-field/react/src/components/stencil-generated/index.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +/* tslint:disable */ +/* auto-generated react proxies */ +import { createReactComponent } from './react-component-lib'; + +import type { JSX } from '@ovhcloud/ods-stencil/components/form-field/custom-elements'; + +import { defineCustomElement as defineOsdsFormField } from '@ovhcloud/ods-stencil/components/form-field/custom-elements/osds-form-field.js'; + +export const OsdsFormField = /*@__PURE__*/createReactComponent('osds-form-field', undefined, undefined, defineOsdsFormField); diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createComponent.tsx b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createComponent.tsx new file mode 100644 index 0000000000..a5e3c37092 --- /dev/null +++ b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createComponent.tsx @@ -0,0 +1,104 @@ +import React, { createElement } from 'react'; + +import { + attachProps, + createForwardRef, + dashToPascalCase, + isCoveredByReact, + mergeRefs, +} from './utils'; + +export interface HTMLStencilElement extends HTMLElement { + componentOnReady(): Promise; +} + +interface StencilReactInternalProps extends React.HTMLAttributes { + forwardedRef: React.RefObject; + ref?: React.Ref; +} + +export const createReactComponent = < + PropType, + ElementType extends HTMLStencilElement, + ContextStateType = {}, + ExpandedPropsTypes = {} +>( + tagName: string, + ReactComponentContext?: React.Context, + manipulatePropsFunction?: ( + originalProps: StencilReactInternalProps, + propsToPass: any, + ) => ExpandedPropsTypes, + defineCustomElement?: () => void, +) => { + if (defineCustomElement !== undefined) { + defineCustomElement(); + } + + const displayName = dashToPascalCase(tagName); + const ReactComponent = class extends React.Component> { + componentEl!: ElementType; + + setComponentElRef = (element: ElementType) => { + this.componentEl = element; + }; + + constructor(props: StencilReactInternalProps) { + super(props); + } + + componentDidMount() { + this.componentDidUpdate(this.props); + } + + componentDidUpdate(prevProps: StencilReactInternalProps) { + attachProps(this.componentEl, this.props, prevProps); + } + + render() { + const { children, forwardedRef, style, className, ref, ...cProps } = this.props; + + let propsToPass = Object.keys(cProps).reduce((acc, name) => { + if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { + const eventName = name.substring(2).toLowerCase(); + if (typeof document !== 'undefined' && isCoveredByReact(eventName)) { + (acc as any)[name] = (cProps as any)[name]; + } + } else { + (acc as any)[name] = (cProps as any)[name]; + } + return acc; + }, {}); + + if (manipulatePropsFunction) { + propsToPass = manipulatePropsFunction(this.props, propsToPass); + } + + const newProps: Omit, 'forwardedRef'> = { + ...propsToPass, + ref: mergeRefs(forwardedRef, this.setComponentElRef), + style, + }; + + /** + * We use createElement here instead of + * React.createElement to work around a + * bug in Vite (https://github.com/vitejs/vite/issues/6104). + * React.createElement causes all elements to be rendered + * as instead of the actual Web Component. + */ + return createElement(tagName, newProps, children); + } + + static get displayName() { + return displayName; + } + }; + + // If context was passed to createReactComponent then conditionally add it to the Component Class + if (ReactComponentContext) { + ReactComponent.contextType = ReactComponentContext; + } + + return createForwardRef(ReactComponent, displayName); +}; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createOverlayComponent.tsx b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createOverlayComponent.tsx new file mode 100644 index 0000000000..3203c0deaf --- /dev/null +++ b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createOverlayComponent.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { OverlayEventDetail } from './interfaces'; +import { + StencilReactForwardedRef, + attachProps, + dashToPascalCase, + defineCustomElement, + setRef, +} from './utils'; + +interface OverlayElement extends HTMLElement { + present: () => Promise; + dismiss: (data?: any, role?: string | undefined) => Promise; +} + +export interface ReactOverlayProps { + children?: React.ReactNode; + isOpen: boolean; + onDidDismiss?: (event: CustomEvent) => void; + onDidPresent?: (event: CustomEvent) => void; + onWillDismiss?: (event: CustomEvent) => void; + onWillPresent?: (event: CustomEvent) => void; +} + +export const createOverlayComponent = < + OverlayComponent extends object, + OverlayType extends OverlayElement +>( + tagName: string, + controller: { create: (options: any) => Promise }, + customElement?: any +) => { + defineCustomElement(tagName, customElement); + + const displayName = dashToPascalCase(tagName); + const didDismissEventName = `on${displayName}DidDismiss`; + const didPresentEventName = `on${displayName}DidPresent`; + const willDismissEventName = `on${displayName}WillDismiss`; + const willPresentEventName = `on${displayName}WillPresent`; + + type Props = OverlayComponent & + ReactOverlayProps & { + forwardedRef?: StencilReactForwardedRef; + }; + + let isDismissing = false; + + class Overlay extends React.Component { + overlay?: OverlayType; + el!: HTMLDivElement; + + constructor(props: Props) { + super(props); + if (typeof document !== 'undefined') { + this.el = document.createElement('div'); + } + this.handleDismiss = this.handleDismiss.bind(this); + } + + static get displayName() { + return displayName; + } + + componentDidMount() { + if (this.props.isOpen) { + this.present(); + } + } + + componentWillUnmount() { + if (this.overlay) { + this.overlay.dismiss(); + } + } + + handleDismiss(event: CustomEvent>) { + if (this.props.onDidDismiss) { + this.props.onDidDismiss(event); + } + setRef(this.props.forwardedRef, null) + } + + shouldComponentUpdate(nextProps: Props) { + // Check if the overlay component is about to dismiss + if (this.overlay && nextProps.isOpen !== this.props.isOpen && nextProps.isOpen === false) { + isDismissing = true; + } + + return true; + } + + async componentDidUpdate(prevProps: Props) { + if (this.overlay) { + attachProps(this.overlay, this.props, prevProps); + } + + if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) { + this.present(prevProps); + } + if (this.overlay && prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) { + await this.overlay.dismiss(); + isDismissing = false; + + /** + * Now that the overlay is dismissed + * we need to render again so that any + * inner components will be unmounted + */ + this.forceUpdate(); + } + } + + async present(prevProps?: Props) { + const { + children, + isOpen, + onDidDismiss, + onDidPresent, + onWillDismiss, + onWillPresent, + ...cProps + } = this.props; + const elementProps = { + ...cProps, + ref: this.props.forwardedRef, + [didDismissEventName]: this.handleDismiss, + [didPresentEventName]: (e: CustomEvent) => + this.props.onDidPresent && this.props.onDidPresent(e), + [willDismissEventName]: (e: CustomEvent) => + this.props.onWillDismiss && this.props.onWillDismiss(e), + [willPresentEventName]: (e: CustomEvent) => + this.props.onWillPresent && this.props.onWillPresent(e), + }; + + this.overlay = await controller.create({ + ...elementProps, + component: this.el, + componentProps: {}, + }); + + setRef(this.props.forwardedRef, this.overlay); + attachProps(this.overlay, elementProps, prevProps); + + await this.overlay.present(); + } + + render() { + /** + * Continue to render the component even when + * overlay is dismissing otherwise component + * will be hidden before animation is done. + */ + return ReactDOM.createPortal(this.props.isOpen || isDismissing ? this.props.children : null, this.el); + } + } + + return React.forwardRef((props, ref) => { + return ; + }); +}; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/index.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/index.ts new file mode 100644 index 0000000000..85e81ad196 --- /dev/null +++ b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/index.ts @@ -0,0 +1,2 @@ +export { createReactComponent } from './createComponent'; +export { createOverlayComponent } from './createOverlayComponent'; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/interfaces.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/interfaces.ts new file mode 100644 index 0000000000..92e5389c88 --- /dev/null +++ b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/interfaces.ts @@ -0,0 +1,34 @@ +// General types important to applications using stencil built components +export interface EventEmitter { + emit: (data?: T) => CustomEvent; +} + +export interface StyleReactProps { + class?: string; + className?: string; + style?: { [key: string]: any }; +} + +export interface OverlayEventDetail { + data?: T; + role?: string; +} + +export interface OverlayInterface { + el: HTMLElement; + animated: boolean; + keyboardClose: boolean; + overlayIndex: number; + presented: boolean; + + enterAnimation?: any; + leaveAnimation?: any; + + didPresent: EventEmitter; + willPresent: EventEmitter; + willDismiss: EventEmitter; + didDismiss: EventEmitter; + + present(): Promise; + dismiss(data?: any, role?: string): Promise; +} diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/attachProps.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/attachProps.ts new file mode 100644 index 0000000000..de2cc499b2 --- /dev/null +++ b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/attachProps.ts @@ -0,0 +1,114 @@ +import { camelToDashCase } from './case'; + +export const attachProps = (node: HTMLElement, newProps: any, oldProps: any = {}) => { + // some test frameworks don't render DOM elements, so we test here to make sure we are dealing with DOM first + if (node instanceof Element) { + // add any classes in className to the class list + const className = getClassName(node.classList, newProps, oldProps); + if (className !== '') { + node.className = className; + } + + Object.keys(newProps).forEach((name) => { + if ( + name === 'children' || + name === 'style' || + name === 'ref' || + name === 'class' || + name === 'className' || + name === 'forwardedRef' + ) { + return; + } + if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { + const eventName = name.substring(2); + const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); + + if (!isCoveredByReact(eventNameLc)) { + syncEvent(node, eventNameLc, newProps[name]); + } + } else { + (node as any)[name] = newProps[name]; + const propType = typeof newProps[name]; + if (propType === 'string') { + node.setAttribute(camelToDashCase(name), newProps[name]); + } + } + }); + } +}; + +export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: any) => { + const newClassProp: string = newProps.className || newProps.class; + const oldClassProp: string = oldProps.className || oldProps.class; + // map the classes to Maps for performance + const currentClasses = arrayToMap(classList); + const incomingPropClasses = arrayToMap(newClassProp ? newClassProp.split(' ') : []); + const oldPropClasses = arrayToMap(oldClassProp ? oldClassProp.split(' ') : []); + const finalClassNames: string[] = []; + // loop through each of the current classes on the component + // to see if it should be a part of the classNames added + currentClasses.forEach((currentClass) => { + if (incomingPropClasses.has(currentClass)) { + // add it as its already included in classnames coming in from newProps + finalClassNames.push(currentClass); + incomingPropClasses.delete(currentClass); + } else if (!oldPropClasses.has(currentClass)) { + // add it as it has NOT been removed by user + finalClassNames.push(currentClass); + } + }); + incomingPropClasses.forEach((s) => finalClassNames.push(s)); + return finalClassNames.join(' '); +}; + +/** + * Checks if an event is supported in the current execution environment. + * @license Modernizr 3.0.0pre (Custom Build) | MIT + */ +export const isCoveredByReact = (eventNameSuffix: string) => { + if (typeof document === 'undefined') { + return true; + } else { + const eventName = 'on' + eventNameSuffix; + let isSupported = eventName in document; + + if (!isSupported) { + const element = document.createElement('div'); + element.setAttribute(eventName, 'return;'); + isSupported = typeof (element as any)[eventName] === 'function'; + } + + return isSupported; + } +}; + +export const syncEvent = ( + node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined } }, + eventName: string, + newEventHandler?: (e: Event) => any +) => { + const eventStore = node.__events || (node.__events = {}); + const oldEventHandler = eventStore[eventName]; + + // Remove old listener so they don't double up. + if (oldEventHandler) { + node.removeEventListener(eventName, oldEventHandler); + } + + // Bind new listener. + node.addEventListener( + eventName, + (eventStore[eventName] = function handler(e: Event) { + if (newEventHandler) { + newEventHandler.call(this, e); + } + }) + ); +}; + +const arrayToMap = (arr: string[] | DOMTokenList) => { + const map = new Map(); + (arr as string[]).forEach((s: string) => map.set(s, s)); + return map; +}; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/case.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/case.ts new file mode 100644 index 0000000000..047704f13d --- /dev/null +++ b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/case.ts @@ -0,0 +1,8 @@ +export const dashToPascalCase = (str: string) => + str + .toLowerCase() + .split('-') + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join(''); +export const camelToDashCase = (str: string) => + str.replace(/([A-Z])/g, (m: string) => `-${m[0].toLowerCase()}`); diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/dev.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/dev.ts new file mode 100644 index 0000000000..cc6f9ed081 --- /dev/null +++ b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/dev.ts @@ -0,0 +1,14 @@ +export const isDevMode = () => { + return process && process.env && process.env.NODE_ENV === 'development'; +}; + +const warnings: { [key: string]: boolean } = {}; + +export const deprecationWarning = (key: string, message: string) => { + if (isDevMode()) { + if (!warnings[key]) { + console.warn(message); + warnings[key] = true; + } + } +}; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/index.tsx b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/index.tsx new file mode 100644 index 0000000000..821d067433 --- /dev/null +++ b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/index.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import type { StyleReactProps } from '../interfaces'; + +export type StencilReactExternalProps = PropType & + Omit, 'style'> & + StyleReactProps; + +// This will be replaced with React.ForwardedRef when react-output-target is upgraded to React v17 +export type StencilReactForwardedRef = ((instance: T | null) => void) | React.MutableRefObject | null; + +export const setRef = (ref: StencilReactForwardedRef | React.Ref | undefined, value: any) => { + if (typeof ref === 'function') { + ref(value) + } else if (ref != null) { + // Cast as a MutableRef so we can assign current + (ref as React.MutableRefObject).current = value + } +}; + +export const mergeRefs = ( + ...refs: (StencilReactForwardedRef | React.Ref | undefined)[] +): React.RefCallback => { + return (value: any) => { + refs.forEach(ref => { + setRef(ref, value) + }) + } +}; + +export const createForwardRef = ( + ReactComponent: any, + displayName: string, +) => { + const forwardRef = ( + props: StencilReactExternalProps, + ref: StencilReactForwardedRef, + ) => { + return ; + }; + forwardRef.displayName = displayName; + + return React.forwardRef(forwardRef); +}; + +export const defineCustomElement = (tagName: string, customElement: any) => { + if ( + customElement !== undefined && + typeof customElements !== 'undefined' && + !customElements.get(tagName) + ) { + customElements.define(tagName, customElement); + } +} + +export * from './attachProps'; +export * from './case'; diff --git a/packages/stencil/components/form-field/vue/src/components/stencil-generated/index.ts b/packages/stencil/components/form-field/vue/src/components/stencil-generated/index.ts new file mode 100644 index 0000000000..9c17ef3537 --- /dev/null +++ b/packages/stencil/components/form-field/vue/src/components/stencil-generated/index.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +/* tslint:disable */ +/* auto-generated vue proxies */ +import { defineContainer } from './vue-component-lib/utils'; + +import type { JSX } from '@ovhcloud/ods-stencil/components/form-field/custom-elements'; + +import { defineCustomElement as defineOsdsFormField } from '@ovhcloud/ods-stencil/components/form-field/custom-elements/osds-form-field.js'; + + +export const OsdsFormField = /*@__PURE__*/ defineContainer('osds-form-field', defineOsdsFormField, [ + 'error', + 'flex' +]); + diff --git a/packages/stencil/components/form-field/vue/src/components/stencil-generated/vue-component-lib/utils.ts b/packages/stencil/components/form-field/vue/src/components/stencil-generated/vue-component-lib/utils.ts new file mode 100644 index 0000000000..e48debacfa --- /dev/null +++ b/packages/stencil/components/form-field/vue/src/components/stencil-generated/vue-component-lib/utils.ts @@ -0,0 +1,198 @@ +import { VNode, defineComponent, getCurrentInstance, h, inject, ref, Ref } from 'vue'; + +export interface InputProps { + modelValue?: T; +} + +const UPDATE_VALUE_EVENT = 'update:modelValue'; +const MODEL_VALUE = 'modelValue'; +const ROUTER_LINK_VALUE = 'routerLink'; +const NAV_MANAGER = 'navManager'; +const ROUTER_PROP_PREFIX = 'router'; + +/** + * Starting in Vue 3.1.0, all properties are + * added as keys to the props object, even if + * they are not being used. In order to correctly + * account for both value props and v-model props, + * we need to check if the key exists for Vue <3.1.0 + * and then check if it is not undefined for Vue >= 3.1.0. + * See https://github.com/vuejs/vue-next/issues/3889 + */ +const EMPTY_PROP = Symbol(); +const DEFAULT_EMPTY_PROP = { default: EMPTY_PROP }; + +interface NavManager { + navigate: (options: T) => void; +} + +const getComponentClasses = (classes: unknown) => { + return (classes as string)?.split(' ') || []; +}; + +const getElementClasses = (ref: Ref, componentClasses: Set, defaultClasses: string[] = []) => { + return [ ...Array.from(ref.value?.classList || []), ...defaultClasses ] + .filter((c: string, i, self) => !componentClasses.has(c) && self.indexOf(c) === i); +}; + +/** +* Create a callback to define a Vue component wrapper around a Web Component. +* +* @prop name - The component tag name (i.e. `ion-button`) +* @prop componentProps - An array of properties on the +* component. These usually match up with the @Prop definitions +* in each component's TSX file. +* @prop customElement - An option custom element instance to pass +* to customElements.define. Only set if `includeImportCustomElements: true` in your config. +* @prop modelProp - The prop that v-model binds to (i.e. value) +* @prop modelUpdateEvent - The event that is fired from your Web Component when the value changes (i.e. ionChange) +* @prop externalModelUpdateEvent - The external event to fire from your Vue component when modelUpdateEvent fires. This is used for ensuring that v-model references have been +* correctly updated when a user's event callback fires. +*/ +export const defineContainer = ( + name: string, + defineCustomElement: any, + componentProps: string[] = [], + modelProp?: string, + modelUpdateEvent?: string, + externalModelUpdateEvent?: string +) => { + /** + * Create a Vue component wrapper around a Web Component. + * Note: The `props` here are not all properties on a component. + * They refer to whatever properties are set on an instance of a component. + */ + + if (defineCustomElement !== undefined) { + defineCustomElement(); + } + + const Container = defineComponent>((props: any, { attrs, slots, emit }) => { + let modelPropValue = props[modelProp]; + const containerRef = ref(); + const classes = new Set(getComponentClasses(attrs.class)); + const onVnodeBeforeMount = (vnode: VNode) => { + // Add a listener to tell Vue to update the v-model + if (vnode.el) { + const eventsNames = Array.isArray(modelUpdateEvent) ? modelUpdateEvent : [modelUpdateEvent]; + eventsNames.forEach((eventName: string) => { + vnode.el!.addEventListener(eventName.toLowerCase(), (e: Event) => { + modelPropValue = (e?.target as any)[modelProp]; + emit(UPDATE_VALUE_EVENT, modelPropValue); + + /** + * We need to emit the change event here + * rather than on the web component to ensure + * that any v-model bindings have been updated. + * Otherwise, the developer will listen on the + * native web component, but the v-model will + * not have been updated yet. + */ + if (externalModelUpdateEvent) { + emit(externalModelUpdateEvent, e); + } + }); + }); + } + }; + + const currentInstance = getCurrentInstance(); + const hasRouter = currentInstance?.appContext?.provides[NAV_MANAGER]; + const navManager: NavManager | undefined = hasRouter ? inject(NAV_MANAGER) : undefined; + const handleRouterLink = (ev: Event) => { + const { routerLink } = props; + if (routerLink === EMPTY_PROP) return; + + if (navManager !== undefined) { + let navigationPayload: any = { event: ev }; + for (const key in props) { + const value = props[key]; + if (props.hasOwnProperty(key) && key.startsWith(ROUTER_PROP_PREFIX) && value !== EMPTY_PROP) { + navigationPayload[key] = value; + } + } + + navManager.navigate(navigationPayload); + } else { + console.warn('Tried to navigate, but no router was found. Make sure you have mounted Vue Router.'); + } + } + + return () => { + modelPropValue = props[modelProp]; + + getComponentClasses(attrs.class).forEach(value => { + classes.add(value); + }); + + const oldClick = props.onClick; + const handleClick = (ev: Event) => { + if (oldClick !== undefined) { + oldClick(ev); + } + if (!ev.defaultPrevented) { + handleRouterLink(ev); + } + } + + let propsToAdd: any = { + ref: containerRef, + class: getElementClasses(containerRef, classes), + onClick: handleClick, + onVnodeBeforeMount: (modelUpdateEvent) ? onVnodeBeforeMount : undefined + }; + + /** + * We can use Object.entries here + * to avoid the hasOwnProperty check, + * but that would require 2 iterations + * where as this only requires 1. + */ + for (const key in props) { + const value = props[key]; + if (props.hasOwnProperty(key) && value !== EMPTY_PROP) { + propsToAdd[key] = value; + } + } + + if (modelProp) { + /** + * If form value property was set using v-model + * then we should use that value. + * Otherwise, check to see if form value property + * was set as a static value (i.e. no v-model). + */ + if (props[MODEL_VALUE] !== EMPTY_PROP) { + propsToAdd = { + ...propsToAdd, + [modelProp]: props[MODEL_VALUE] + } + } else if (modelPropValue !== EMPTY_PROP) { + propsToAdd = { + ...propsToAdd, + [modelProp]: modelPropValue + } + } + } + + return h(name, propsToAdd, slots.default && slots.default()); + } + }); + + Container.displayName = name; + + Container.props = { + [ROUTER_LINK_VALUE]: DEFAULT_EMPTY_PROP + }; + + componentProps.forEach(componentProp => { + Container.props[componentProp] = DEFAULT_EMPTY_PROP; + }); + + if (modelProp) { + Container.props[MODEL_VALUE] = DEFAULT_EMPTY_PROP; + Container.emits = [UPDATE_VALUE_EVENT, externalModelUpdateEvent]; + } + + return Container; +}; diff --git a/packages/stencil/components/input/src/components/osds-input/osds-input.scss b/packages/stencil/components/input/src/components/osds-input/osds-input.scss index 0caac49da0..0c97d21fd2 100644 --- a/packages/stencil/components/input/src/components/osds-input/osds-input.scss +++ b/packages/stencil/components/input/src/components/osds-input/osds-input.scss @@ -40,6 +40,15 @@ input { } } +[disabled] { + @include osds-input-on-selected-host() { + opacity: .5; + cursor: not-allowed; + } + + cursor: not-allowed; +} + /* Chrome, Safari, Edge, Opera */ input[type=number], input[type=number]::-webkit-outer-spin-button, @@ -49,14 +58,6 @@ input[type=number]::-webkit-inner-spin-button { margin: 0; } -[disabled] { - @include osds-input-on-selected-host() { - opacity: .5; - cursor: not-allowed; - pointer-events: none; - } -} - // apply the theme template for the component @include ods-theme-component() { @include osds-input-theme-color(); From 29960128eec5cabd70adf925a94cfd524b6a066d Mon Sep 17 00:00:00 2001 From: Leo Largillet Date: Tue, 18 Jul 2023 14:49:40 +0200 Subject: [PATCH 2/2] fix(input): addressing changes --- .../src/components/stencil-generated/index.ts | 10 - .../react-component-lib/createComponent.tsx | 104 --------- .../createOverlayComponent.tsx | 162 -------------- .../react-component-lib/index.ts | 2 - .../react-component-lib/interfaces.ts | 34 --- .../react-component-lib/utils/attachProps.ts | 114 ---------- .../react-component-lib/utils/case.ts | 8 - .../react-component-lib/utils/dev.ts | 14 -- .../react-component-lib/utils/index.tsx | 57 ----- .../src/components/stencil-generated/index.ts | 15 -- .../vue-component-lib/utils.ts | 198 ------------------ 11 files changed, 718 deletions(-) delete mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/index.ts delete mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createComponent.tsx delete mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createOverlayComponent.tsx delete mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/index.ts delete mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/interfaces.ts delete mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/attachProps.ts delete mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/case.ts delete mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/dev.ts delete mode 100644 packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/index.tsx delete mode 100644 packages/stencil/components/form-field/vue/src/components/stencil-generated/index.ts delete mode 100644 packages/stencil/components/form-field/vue/src/components/stencil-generated/vue-component-lib/utils.ts diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/index.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/index.ts deleted file mode 100644 index df32f8d746..0000000000 --- a/packages/stencil/components/form-field/react/src/components/stencil-generated/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ -/* auto-generated react proxies */ -import { createReactComponent } from './react-component-lib'; - -import type { JSX } from '@ovhcloud/ods-stencil/components/form-field/custom-elements'; - -import { defineCustomElement as defineOsdsFormField } from '@ovhcloud/ods-stencil/components/form-field/custom-elements/osds-form-field.js'; - -export const OsdsFormField = /*@__PURE__*/createReactComponent('osds-form-field', undefined, undefined, defineOsdsFormField); diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createComponent.tsx b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createComponent.tsx deleted file mode 100644 index a5e3c37092..0000000000 --- a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createComponent.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { createElement } from 'react'; - -import { - attachProps, - createForwardRef, - dashToPascalCase, - isCoveredByReact, - mergeRefs, -} from './utils'; - -export interface HTMLStencilElement extends HTMLElement { - componentOnReady(): Promise; -} - -interface StencilReactInternalProps extends React.HTMLAttributes { - forwardedRef: React.RefObject; - ref?: React.Ref; -} - -export const createReactComponent = < - PropType, - ElementType extends HTMLStencilElement, - ContextStateType = {}, - ExpandedPropsTypes = {} ->( - tagName: string, - ReactComponentContext?: React.Context, - manipulatePropsFunction?: ( - originalProps: StencilReactInternalProps, - propsToPass: any, - ) => ExpandedPropsTypes, - defineCustomElement?: () => void, -) => { - if (defineCustomElement !== undefined) { - defineCustomElement(); - } - - const displayName = dashToPascalCase(tagName); - const ReactComponent = class extends React.Component> { - componentEl!: ElementType; - - setComponentElRef = (element: ElementType) => { - this.componentEl = element; - }; - - constructor(props: StencilReactInternalProps) { - super(props); - } - - componentDidMount() { - this.componentDidUpdate(this.props); - } - - componentDidUpdate(prevProps: StencilReactInternalProps) { - attachProps(this.componentEl, this.props, prevProps); - } - - render() { - const { children, forwardedRef, style, className, ref, ...cProps } = this.props; - - let propsToPass = Object.keys(cProps).reduce((acc, name) => { - if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { - const eventName = name.substring(2).toLowerCase(); - if (typeof document !== 'undefined' && isCoveredByReact(eventName)) { - (acc as any)[name] = (cProps as any)[name]; - } - } else { - (acc as any)[name] = (cProps as any)[name]; - } - return acc; - }, {}); - - if (manipulatePropsFunction) { - propsToPass = manipulatePropsFunction(this.props, propsToPass); - } - - const newProps: Omit, 'forwardedRef'> = { - ...propsToPass, - ref: mergeRefs(forwardedRef, this.setComponentElRef), - style, - }; - - /** - * We use createElement here instead of - * React.createElement to work around a - * bug in Vite (https://github.com/vitejs/vite/issues/6104). - * React.createElement causes all elements to be rendered - * as instead of the actual Web Component. - */ - return createElement(tagName, newProps, children); - } - - static get displayName() { - return displayName; - } - }; - - // If context was passed to createReactComponent then conditionally add it to the Component Class - if (ReactComponentContext) { - ReactComponent.contextType = ReactComponentContext; - } - - return createForwardRef(ReactComponent, displayName); -}; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createOverlayComponent.tsx b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createOverlayComponent.tsx deleted file mode 100644 index 3203c0deaf..0000000000 --- a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/createOverlayComponent.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { OverlayEventDetail } from './interfaces'; -import { - StencilReactForwardedRef, - attachProps, - dashToPascalCase, - defineCustomElement, - setRef, -} from './utils'; - -interface OverlayElement extends HTMLElement { - present: () => Promise; - dismiss: (data?: any, role?: string | undefined) => Promise; -} - -export interface ReactOverlayProps { - children?: React.ReactNode; - isOpen: boolean; - onDidDismiss?: (event: CustomEvent) => void; - onDidPresent?: (event: CustomEvent) => void; - onWillDismiss?: (event: CustomEvent) => void; - onWillPresent?: (event: CustomEvent) => void; -} - -export const createOverlayComponent = < - OverlayComponent extends object, - OverlayType extends OverlayElement ->( - tagName: string, - controller: { create: (options: any) => Promise }, - customElement?: any -) => { - defineCustomElement(tagName, customElement); - - const displayName = dashToPascalCase(tagName); - const didDismissEventName = `on${displayName}DidDismiss`; - const didPresentEventName = `on${displayName}DidPresent`; - const willDismissEventName = `on${displayName}WillDismiss`; - const willPresentEventName = `on${displayName}WillPresent`; - - type Props = OverlayComponent & - ReactOverlayProps & { - forwardedRef?: StencilReactForwardedRef; - }; - - let isDismissing = false; - - class Overlay extends React.Component { - overlay?: OverlayType; - el!: HTMLDivElement; - - constructor(props: Props) { - super(props); - if (typeof document !== 'undefined') { - this.el = document.createElement('div'); - } - this.handleDismiss = this.handleDismiss.bind(this); - } - - static get displayName() { - return displayName; - } - - componentDidMount() { - if (this.props.isOpen) { - this.present(); - } - } - - componentWillUnmount() { - if (this.overlay) { - this.overlay.dismiss(); - } - } - - handleDismiss(event: CustomEvent>) { - if (this.props.onDidDismiss) { - this.props.onDidDismiss(event); - } - setRef(this.props.forwardedRef, null) - } - - shouldComponentUpdate(nextProps: Props) { - // Check if the overlay component is about to dismiss - if (this.overlay && nextProps.isOpen !== this.props.isOpen && nextProps.isOpen === false) { - isDismissing = true; - } - - return true; - } - - async componentDidUpdate(prevProps: Props) { - if (this.overlay) { - attachProps(this.overlay, this.props, prevProps); - } - - if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) { - this.present(prevProps); - } - if (this.overlay && prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) { - await this.overlay.dismiss(); - isDismissing = false; - - /** - * Now that the overlay is dismissed - * we need to render again so that any - * inner components will be unmounted - */ - this.forceUpdate(); - } - } - - async present(prevProps?: Props) { - const { - children, - isOpen, - onDidDismiss, - onDidPresent, - onWillDismiss, - onWillPresent, - ...cProps - } = this.props; - const elementProps = { - ...cProps, - ref: this.props.forwardedRef, - [didDismissEventName]: this.handleDismiss, - [didPresentEventName]: (e: CustomEvent) => - this.props.onDidPresent && this.props.onDidPresent(e), - [willDismissEventName]: (e: CustomEvent) => - this.props.onWillDismiss && this.props.onWillDismiss(e), - [willPresentEventName]: (e: CustomEvent) => - this.props.onWillPresent && this.props.onWillPresent(e), - }; - - this.overlay = await controller.create({ - ...elementProps, - component: this.el, - componentProps: {}, - }); - - setRef(this.props.forwardedRef, this.overlay); - attachProps(this.overlay, elementProps, prevProps); - - await this.overlay.present(); - } - - render() { - /** - * Continue to render the component even when - * overlay is dismissing otherwise component - * will be hidden before animation is done. - */ - return ReactDOM.createPortal(this.props.isOpen || isDismissing ? this.props.children : null, this.el); - } - } - - return React.forwardRef((props, ref) => { - return ; - }); -}; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/index.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/index.ts deleted file mode 100644 index 85e81ad196..0000000000 --- a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { createReactComponent } from './createComponent'; -export { createOverlayComponent } from './createOverlayComponent'; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/interfaces.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/interfaces.ts deleted file mode 100644 index 92e5389c88..0000000000 --- a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/interfaces.ts +++ /dev/null @@ -1,34 +0,0 @@ -// General types important to applications using stencil built components -export interface EventEmitter { - emit: (data?: T) => CustomEvent; -} - -export interface StyleReactProps { - class?: string; - className?: string; - style?: { [key: string]: any }; -} - -export interface OverlayEventDetail { - data?: T; - role?: string; -} - -export interface OverlayInterface { - el: HTMLElement; - animated: boolean; - keyboardClose: boolean; - overlayIndex: number; - presented: boolean; - - enterAnimation?: any; - leaveAnimation?: any; - - didPresent: EventEmitter; - willPresent: EventEmitter; - willDismiss: EventEmitter; - didDismiss: EventEmitter; - - present(): Promise; - dismiss(data?: any, role?: string): Promise; -} diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/attachProps.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/attachProps.ts deleted file mode 100644 index de2cc499b2..0000000000 --- a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/attachProps.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { camelToDashCase } from './case'; - -export const attachProps = (node: HTMLElement, newProps: any, oldProps: any = {}) => { - // some test frameworks don't render DOM elements, so we test here to make sure we are dealing with DOM first - if (node instanceof Element) { - // add any classes in className to the class list - const className = getClassName(node.classList, newProps, oldProps); - if (className !== '') { - node.className = className; - } - - Object.keys(newProps).forEach((name) => { - if ( - name === 'children' || - name === 'style' || - name === 'ref' || - name === 'class' || - name === 'className' || - name === 'forwardedRef' - ) { - return; - } - if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { - const eventName = name.substring(2); - const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); - - if (!isCoveredByReact(eventNameLc)) { - syncEvent(node, eventNameLc, newProps[name]); - } - } else { - (node as any)[name] = newProps[name]; - const propType = typeof newProps[name]; - if (propType === 'string') { - node.setAttribute(camelToDashCase(name), newProps[name]); - } - } - }); - } -}; - -export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: any) => { - const newClassProp: string = newProps.className || newProps.class; - const oldClassProp: string = oldProps.className || oldProps.class; - // map the classes to Maps for performance - const currentClasses = arrayToMap(classList); - const incomingPropClasses = arrayToMap(newClassProp ? newClassProp.split(' ') : []); - const oldPropClasses = arrayToMap(oldClassProp ? oldClassProp.split(' ') : []); - const finalClassNames: string[] = []; - // loop through each of the current classes on the component - // to see if it should be a part of the classNames added - currentClasses.forEach((currentClass) => { - if (incomingPropClasses.has(currentClass)) { - // add it as its already included in classnames coming in from newProps - finalClassNames.push(currentClass); - incomingPropClasses.delete(currentClass); - } else if (!oldPropClasses.has(currentClass)) { - // add it as it has NOT been removed by user - finalClassNames.push(currentClass); - } - }); - incomingPropClasses.forEach((s) => finalClassNames.push(s)); - return finalClassNames.join(' '); -}; - -/** - * Checks if an event is supported in the current execution environment. - * @license Modernizr 3.0.0pre (Custom Build) | MIT - */ -export const isCoveredByReact = (eventNameSuffix: string) => { - if (typeof document === 'undefined') { - return true; - } else { - const eventName = 'on' + eventNameSuffix; - let isSupported = eventName in document; - - if (!isSupported) { - const element = document.createElement('div'); - element.setAttribute(eventName, 'return;'); - isSupported = typeof (element as any)[eventName] === 'function'; - } - - return isSupported; - } -}; - -export const syncEvent = ( - node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined } }, - eventName: string, - newEventHandler?: (e: Event) => any -) => { - const eventStore = node.__events || (node.__events = {}); - const oldEventHandler = eventStore[eventName]; - - // Remove old listener so they don't double up. - if (oldEventHandler) { - node.removeEventListener(eventName, oldEventHandler); - } - - // Bind new listener. - node.addEventListener( - eventName, - (eventStore[eventName] = function handler(e: Event) { - if (newEventHandler) { - newEventHandler.call(this, e); - } - }) - ); -}; - -const arrayToMap = (arr: string[] | DOMTokenList) => { - const map = new Map(); - (arr as string[]).forEach((s: string) => map.set(s, s)); - return map; -}; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/case.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/case.ts deleted file mode 100644 index 047704f13d..0000000000 --- a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/case.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const dashToPascalCase = (str: string) => - str - .toLowerCase() - .split('-') - .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) - .join(''); -export const camelToDashCase = (str: string) => - str.replace(/([A-Z])/g, (m: string) => `-${m[0].toLowerCase()}`); diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/dev.ts b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/dev.ts deleted file mode 100644 index cc6f9ed081..0000000000 --- a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/dev.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const isDevMode = () => { - return process && process.env && process.env.NODE_ENV === 'development'; -}; - -const warnings: { [key: string]: boolean } = {}; - -export const deprecationWarning = (key: string, message: string) => { - if (isDevMode()) { - if (!warnings[key]) { - console.warn(message); - warnings[key] = true; - } - } -}; diff --git a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/index.tsx b/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/index.tsx deleted file mode 100644 index 821d067433..0000000000 --- a/packages/stencil/components/form-field/react/src/components/stencil-generated/react-component-lib/utils/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; - -import type { StyleReactProps } from '../interfaces'; - -export type StencilReactExternalProps = PropType & - Omit, 'style'> & - StyleReactProps; - -// This will be replaced with React.ForwardedRef when react-output-target is upgraded to React v17 -export type StencilReactForwardedRef = ((instance: T | null) => void) | React.MutableRefObject | null; - -export const setRef = (ref: StencilReactForwardedRef | React.Ref | undefined, value: any) => { - if (typeof ref === 'function') { - ref(value) - } else if (ref != null) { - // Cast as a MutableRef so we can assign current - (ref as React.MutableRefObject).current = value - } -}; - -export const mergeRefs = ( - ...refs: (StencilReactForwardedRef | React.Ref | undefined)[] -): React.RefCallback => { - return (value: any) => { - refs.forEach(ref => { - setRef(ref, value) - }) - } -}; - -export const createForwardRef = ( - ReactComponent: any, - displayName: string, -) => { - const forwardRef = ( - props: StencilReactExternalProps, - ref: StencilReactForwardedRef, - ) => { - return ; - }; - forwardRef.displayName = displayName; - - return React.forwardRef(forwardRef); -}; - -export const defineCustomElement = (tagName: string, customElement: any) => { - if ( - customElement !== undefined && - typeof customElements !== 'undefined' && - !customElements.get(tagName) - ) { - customElements.define(tagName, customElement); - } -} - -export * from './attachProps'; -export * from './case'; diff --git a/packages/stencil/components/form-field/vue/src/components/stencil-generated/index.ts b/packages/stencil/components/form-field/vue/src/components/stencil-generated/index.ts deleted file mode 100644 index 9c17ef3537..0000000000 --- a/packages/stencil/components/form-field/vue/src/components/stencil-generated/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ -/* auto-generated vue proxies */ -import { defineContainer } from './vue-component-lib/utils'; - -import type { JSX } from '@ovhcloud/ods-stencil/components/form-field/custom-elements'; - -import { defineCustomElement as defineOsdsFormField } from '@ovhcloud/ods-stencil/components/form-field/custom-elements/osds-form-field.js'; - - -export const OsdsFormField = /*@__PURE__*/ defineContainer('osds-form-field', defineOsdsFormField, [ - 'error', - 'flex' -]); - diff --git a/packages/stencil/components/form-field/vue/src/components/stencil-generated/vue-component-lib/utils.ts b/packages/stencil/components/form-field/vue/src/components/stencil-generated/vue-component-lib/utils.ts deleted file mode 100644 index e48debacfa..0000000000 --- a/packages/stencil/components/form-field/vue/src/components/stencil-generated/vue-component-lib/utils.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { VNode, defineComponent, getCurrentInstance, h, inject, ref, Ref } from 'vue'; - -export interface InputProps { - modelValue?: T; -} - -const UPDATE_VALUE_EVENT = 'update:modelValue'; -const MODEL_VALUE = 'modelValue'; -const ROUTER_LINK_VALUE = 'routerLink'; -const NAV_MANAGER = 'navManager'; -const ROUTER_PROP_PREFIX = 'router'; - -/** - * Starting in Vue 3.1.0, all properties are - * added as keys to the props object, even if - * they are not being used. In order to correctly - * account for both value props and v-model props, - * we need to check if the key exists for Vue <3.1.0 - * and then check if it is not undefined for Vue >= 3.1.0. - * See https://github.com/vuejs/vue-next/issues/3889 - */ -const EMPTY_PROP = Symbol(); -const DEFAULT_EMPTY_PROP = { default: EMPTY_PROP }; - -interface NavManager { - navigate: (options: T) => void; -} - -const getComponentClasses = (classes: unknown) => { - return (classes as string)?.split(' ') || []; -}; - -const getElementClasses = (ref: Ref, componentClasses: Set, defaultClasses: string[] = []) => { - return [ ...Array.from(ref.value?.classList || []), ...defaultClasses ] - .filter((c: string, i, self) => !componentClasses.has(c) && self.indexOf(c) === i); -}; - -/** -* Create a callback to define a Vue component wrapper around a Web Component. -* -* @prop name - The component tag name (i.e. `ion-button`) -* @prop componentProps - An array of properties on the -* component. These usually match up with the @Prop definitions -* in each component's TSX file. -* @prop customElement - An option custom element instance to pass -* to customElements.define. Only set if `includeImportCustomElements: true` in your config. -* @prop modelProp - The prop that v-model binds to (i.e. value) -* @prop modelUpdateEvent - The event that is fired from your Web Component when the value changes (i.e. ionChange) -* @prop externalModelUpdateEvent - The external event to fire from your Vue component when modelUpdateEvent fires. This is used for ensuring that v-model references have been -* correctly updated when a user's event callback fires. -*/ -export const defineContainer = ( - name: string, - defineCustomElement: any, - componentProps: string[] = [], - modelProp?: string, - modelUpdateEvent?: string, - externalModelUpdateEvent?: string -) => { - /** - * Create a Vue component wrapper around a Web Component. - * Note: The `props` here are not all properties on a component. - * They refer to whatever properties are set on an instance of a component. - */ - - if (defineCustomElement !== undefined) { - defineCustomElement(); - } - - const Container = defineComponent>((props: any, { attrs, slots, emit }) => { - let modelPropValue = props[modelProp]; - const containerRef = ref(); - const classes = new Set(getComponentClasses(attrs.class)); - const onVnodeBeforeMount = (vnode: VNode) => { - // Add a listener to tell Vue to update the v-model - if (vnode.el) { - const eventsNames = Array.isArray(modelUpdateEvent) ? modelUpdateEvent : [modelUpdateEvent]; - eventsNames.forEach((eventName: string) => { - vnode.el!.addEventListener(eventName.toLowerCase(), (e: Event) => { - modelPropValue = (e?.target as any)[modelProp]; - emit(UPDATE_VALUE_EVENT, modelPropValue); - - /** - * We need to emit the change event here - * rather than on the web component to ensure - * that any v-model bindings have been updated. - * Otherwise, the developer will listen on the - * native web component, but the v-model will - * not have been updated yet. - */ - if (externalModelUpdateEvent) { - emit(externalModelUpdateEvent, e); - } - }); - }); - } - }; - - const currentInstance = getCurrentInstance(); - const hasRouter = currentInstance?.appContext?.provides[NAV_MANAGER]; - const navManager: NavManager | undefined = hasRouter ? inject(NAV_MANAGER) : undefined; - const handleRouterLink = (ev: Event) => { - const { routerLink } = props; - if (routerLink === EMPTY_PROP) return; - - if (navManager !== undefined) { - let navigationPayload: any = { event: ev }; - for (const key in props) { - const value = props[key]; - if (props.hasOwnProperty(key) && key.startsWith(ROUTER_PROP_PREFIX) && value !== EMPTY_PROP) { - navigationPayload[key] = value; - } - } - - navManager.navigate(navigationPayload); - } else { - console.warn('Tried to navigate, but no router was found. Make sure you have mounted Vue Router.'); - } - } - - return () => { - modelPropValue = props[modelProp]; - - getComponentClasses(attrs.class).forEach(value => { - classes.add(value); - }); - - const oldClick = props.onClick; - const handleClick = (ev: Event) => { - if (oldClick !== undefined) { - oldClick(ev); - } - if (!ev.defaultPrevented) { - handleRouterLink(ev); - } - } - - let propsToAdd: any = { - ref: containerRef, - class: getElementClasses(containerRef, classes), - onClick: handleClick, - onVnodeBeforeMount: (modelUpdateEvent) ? onVnodeBeforeMount : undefined - }; - - /** - * We can use Object.entries here - * to avoid the hasOwnProperty check, - * but that would require 2 iterations - * where as this only requires 1. - */ - for (const key in props) { - const value = props[key]; - if (props.hasOwnProperty(key) && value !== EMPTY_PROP) { - propsToAdd[key] = value; - } - } - - if (modelProp) { - /** - * If form value property was set using v-model - * then we should use that value. - * Otherwise, check to see if form value property - * was set as a static value (i.e. no v-model). - */ - if (props[MODEL_VALUE] !== EMPTY_PROP) { - propsToAdd = { - ...propsToAdd, - [modelProp]: props[MODEL_VALUE] - } - } else if (modelPropValue !== EMPTY_PROP) { - propsToAdd = { - ...propsToAdd, - [modelProp]: modelPropValue - } - } - } - - return h(name, propsToAdd, slots.default && slots.default()); - } - }); - - Container.displayName = name; - - Container.props = { - [ROUTER_LINK_VALUE]: DEFAULT_EMPTY_PROP - }; - - componentProps.forEach(componentProp => { - Container.props[componentProp] = DEFAULT_EMPTY_PROP; - }); - - if (modelProp) { - Container.props[MODEL_VALUE] = DEFAULT_EMPTY_PROP; - Container.emits = [UPDATE_VALUE_EVENT, externalModelUpdateEvent]; - } - - return Container; -};