diff --git a/.github/workflows/next-latest.yml b/.github/workflows/next-latest.yml index 2873ea3dc..c6b6abe5f 100644 --- a/.github/workflows/next-latest.yml +++ b/.github/workflows/next-latest.yml @@ -1,4 +1,4 @@ -name: Next.js Latest +name: Next.js Latest with Gluestack UI on: push: @@ -33,7 +33,7 @@ jobs: needs: check-next-version if: ${{ needs.check-next-version.outputs.should_run == 'true' || github.event_name == 'push' || github.event_name == 'pull_request' }} runs-on: ubuntu-latest - name: Next.js latest + name: Next.js latest with Gluestack UI steps: - uses: actions/checkout@v3 @@ -67,32 +67,930 @@ jobs: working-directory: test-app run: | cat < next.config.ts - import originalConfig from './next.config.original.ts'; - - const nextConfig = { - ...originalConfig, - webpack(config, { isServer }) { - config.resolve.alias['react-native'] = 'react-native-web'; - - // Add support for JSX and ensure React Native CSS works - if (!isServer) { - config.module.rules.push({ - test: /\.(js|jsx|ts|tsx)$/, - use: ['babel-loader'], - exclude: /node_modules/, + import { withGluestackUI } from "@gluestack/ui-next-adapter"; + const nextConfig = { + transpilePackages: ["nativewind", "react-native-css-interop"], + }; + export default withGluestackUI(nextConfig); + EOT + - name: Copy data to src/app/registry.tsx + working-directory: test-app + run: | + cat < src/app/registry.tsx + "use client"; + import React, { useRef, useState } from "react"; + import { useServerInsertedHTML } from "next/navigation"; + import { StyleRegistry, createStyleRegistry } from "styled-jsx"; + // @ts-ignore + import { AppRegistry } from "react-native-web"; + import { flush } from "@gluestack-ui/nativewind-utils/flush"; + + export default function StyledJsxRegistry({ + children, + }: { + children: React.ReactNode; + }) { + // Only create stylesheet once with lazy initial state + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + const [jsxStyleRegistry] = useState(() => createStyleRegistry()); + const isServerInserted = useRef(false); + + useServerInsertedHTML(() => { + AppRegistry.registerComponent("Main", () => "main"); + const { getStyleElement } = AppRegistry.getApplication("Main"); + if (!isServerInserted.current) { + isServerInserted.current = true; + const styles = [getStyleElement(), jsxStyleRegistry.styles(), flush()]; + jsxStyleRegistry.flush(); + return <>{styles}; + } + }); + + return {children}; + } + EOT + - name: Copy data to components/ui/gluestack-ui-provider/index.web.tsx + working-directory: test-app + run: | + cat < components/ui/gluestack-ui-provider/index.web.tsx + + "use client"; + import React, { useEffect, useLayoutEffect } from "react"; + import { config } from "./config"; + import { OverlayProvider } from "@gluestack-ui/overlay"; + import { ToastProvider } from "@gluestack-ui/toast"; + import { setFlushStyles } from "@gluestack-ui/nativewind-utils/flush"; + import { script } from "./script"; + + const variableStyleTagId = "nativewind-style"; + const createStyle = (styleTagId: string) => { + const style = document.createElement("style"); + style.id = styleTagId; + style.appendChild(document.createTextNode("")); + return style; + }; + + export const useSafeLayoutEffect = + typeof window !== "undefined" ? useLayoutEffect : useEffect; + + export function GluestackUIProvider({ + mode = "light", + ...props + }: { + mode?: "light" | "dark" | "system"; + children?: React.ReactNode; + }) { + let cssVariablesWithMode = ``; + Object.keys(config).forEach((configKey) => { + cssVariablesWithMode += + configKey === "dark" ? `\n .dark {\n ` : `\n:root {\n`; + const cssVariables = Object.keys( + config[configKey as keyof typeof config] + ).reduce((acc: string, curr: string) => { + acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `; + return acc; + }, ""); + cssVariablesWithMode += `${cssVariables} \n}`; }); - } - return config; - }, - typescript: { - ignoreBuildErrors: true, - }, - }; + setFlushStyles(cssVariablesWithMode); + + const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => { + script(e.matches ? "dark" : "light"); + }, []); + + useSafeLayoutEffect(() => { + if (mode !== "system") { + const documentElement = document.documentElement; + if (documentElement) { + documentElement.classList.add(mode); + documentElement.classList.remove(mode === "light" ? "dark" : "light"); + documentElement.style.colorScheme = mode; + } + } + }, [mode]); + + useSafeLayoutEffect(() => { + if (mode !== "system") return; + const media = window.matchMedia("(prefers-color-scheme: dark)"); + + media.addListener(handleMediaQuery); - export default nextConfig; + return () => media.removeListener(handleMediaQuery); + }, [handleMediaQuery]); + + useSafeLayoutEffect(() => { + if (typeof window !== "undefined") { + const documentElement = document.documentElement; + if (documentElement) { + const head = documentElement.querySelector("head"); + let style = head?.querySelector(`[id='${variableStyleTagId}']`); + if (!style) { + style = createStyle(variableStyleTagId); + style.innerHTML = cssVariablesWithMode; + if (head) head.appendChild(style); + } + } + } + }, []); + + return ( + <> + + {props.children} + + + ); + } EOT + - name: Create patches folder + working-directory: test-app + run: | + mkdir -p patches + - name: Create react-native-web patch file + working-directory: test-app/patches + run: | + cat < react-native-web+0.19.13.patch + diff --git a/node_modules/react-native-web/dist/cjs/exports/AppRegistry/renderApplication.js b/node_modules/react-native-web/dist/cjs/exports/AppRegistry/renderApplication.js + index 0c0cb2f..83fd94b 100644 + --- a/node_modules/react-native-web/dist/cjs/exports/AppRegistry/renderApplication.js + +++ b/node_modules/react-native-web/dist/cjs/exports/AppRegistry/renderApplication.js + @@ -1,6 +1,5 @@ + "use strict"; + + -var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; + var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; + exports.__esModule = true; + exports.default = renderApplication; + @@ -8,7 +7,7 @@ exports.getApplication = getApplication; + var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); + var _AppContainer = _interopRequireDefault(require("./AppContainer")); + var _invariant = _interopRequireDefault(require("fbjs/lib/invariant")); + -var _render = _interopRequireWildcard(require("../render")); + +var _render = require("../render"); + var _StyleSheet = _interopRequireDefault(require("../StyleSheet")); + var _react = _interopRequireDefault(require("react")); + /** + @@ -24,9 +23,8 @@ var _react = _interopRequireDefault(require("react")); + function renderApplication(RootComponent, WrapperComponent, callback, options) { + var shouldHydrate = options.hydrate, + initialProps = options.initialProps, + - mode = options.mode, + rootTag = options.rootTag; + - var renderFn = shouldHydrate ? mode === 'concurrent' ? _render.hydrate : _render.hydrateLegacy : mode === 'concurrent' ? _render.render : _render.default; + + var renderFn = shouldHydrate ? _render.hydrate : _render.render; + (0, _invariant.default)(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag); + return renderFn(/*#__PURE__*/_react.default.createElement(_AppContainer.default, { + WrapperComponent: WrapperComponent, + diff --git a/node_modules/react-native-web/dist/cjs/exports/render/index.js b/node_modules/react-native-web/dist/cjs/exports/render/index.js + index b41ee11..18d9b2f 100644 + --- a/node_modules/react-native-web/dist/cjs/exports/render/index.js + +++ b/node_modules/react-native-web/dist/cjs/exports/render/index.js + @@ -10,15 +10,10 @@ + + 'use client'; + + -var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; + exports.__esModule = true; + -exports.default = renderLegacy; + +exports.default = render; + exports.hydrate = hydrate; + -exports.hydrateLegacy = hydrateLegacy; + -exports.render = render; + -var _reactDom = require("react-dom"); + var _client = require("react-dom/client"); + -var _unmountComponentAtNode = _interopRequireDefault(require("../unmountComponentAtNode")); + var _dom = require("../StyleSheet/dom"); + function hydrate(element, root) { + (0, _dom.createSheet)(root); + @@ -30,21 +25,3 @@ function render(element, root) { + reactRoot.render(element); + return reactRoot; + } + \ No newline at end of file + -function hydrateLegacy(element, root, callback) { + - (0, _dom.createSheet)(root); + - (0, _reactDom.hydrate)(element, root, callback); + - return { + - unmount: function unmount() { + - return (0, _unmountComponentAtNode.default)(root); + - } + - }; + -} + -function renderLegacy(element, root, callback) { + - (0, _dom.createSheet)(root); + - (0, _reactDom.render)(element, root, callback); + - return { + - unmount: function unmount() { + - return (0, _unmountComponentAtNode.default)(root); + - } + - }; + -} + \ No newline at end of file + diff --git a/node_modules/react-native-web/dist/cjs/exports/unmountComponentAtNode/index.js b/node_modules/react-native-web/dist/cjs/exports/unmountComponentAtNode/index.js + index 3ea3964..e740204 100644 + --- a/node_modules/react-native-web/dist/cjs/exports/unmountComponentAtNode/index.js + +++ b/node_modules/react-native-web/dist/cjs/exports/unmountComponentAtNode/index.js + @@ -1,8 +1,7 @@ + "use strict"; + + exports.__esModule = true; + -exports.default = void 0; + -var _reactDom = require("react-dom"); + +exports.default = unmountComponentAtNode; + /** + * Copyright (c) Nicolas Gallagher. + * + @@ -11,5 +10,9 @@ var _reactDom = require("react-dom"); + * + * + */ + -var _default = exports.default = _reactDom.unmountComponentAtNode; + + + +function unmountComponentAtNode(rootTag) { + + rootTag.unmount(); + + return true; + +} + module.exports = exports.default; + \ No newline at end of file + diff --git a/node_modules/react-native-web/dist/exports/AppRegistry/renderApplication.js b/node_modules/react-native-web/dist/exports/AppRegistry/renderApplication.js + index b53dff6..c56c1dc 100644 + --- a/node_modules/react-native-web/dist/exports/AppRegistry/renderApplication.js + +++ b/node_modules/react-native-web/dist/exports/AppRegistry/renderApplication.js + @@ -11,15 +11,14 @@ import _extends from "@babel/runtime/helpers/extends"; + + import AppContainer from './AppContainer'; + import invariant from 'fbjs/lib/invariant'; + -import renderLegacy, { hydrateLegacy, render, hydrate } from '../render'; + +import { render, hydrate } from '../render'; + import StyleSheet from '../StyleSheet'; + import React from 'react'; + export default function renderApplication(RootComponent, WrapperComponent, callback, options) { + var shouldHydrate = options.hydrate, + initialProps = options.initialProps, + - mode = options.mode, + rootTag = options.rootTag; + - var renderFn = shouldHydrate ? mode === 'concurrent' ? hydrate : hydrateLegacy : mode === 'concurrent' ? render : renderLegacy; + + var renderFn = shouldHydrate ? hydrate : render; + invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag); + return renderFn(/*#__PURE__*/React.createElement(AppContainer, { + WrapperComponent: WrapperComponent, + diff --git a/node_modules/react-native-web/dist/exports/AppRegistry/renderApplication.js.flow b/node_modules/react-native-web/dist/exports/AppRegistry/renderApplication.js.flow + index b9df2af..2b671ba 100644 + --- a/node_modules/react-native-web/dist/exports/AppRegistry/renderApplication.js.flow + +++ b/node_modules/react-native-web/dist/exports/AppRegistry/renderApplication.js.flow + @@ -11,7 +11,7 @@ + import type { ComponentType, Node } from 'react'; + import AppContainer from './AppContainer'; + import invariant from 'fbjs/lib/invariant'; + -import renderLegacy, { hydrateLegacy, render, hydrate } from '../render'; + +import { render, hydrate } from '../render'; + import StyleSheet from '../StyleSheet'; + import React from 'react'; + export type Application = { + @@ -20,7 +20,6 @@ export type Application = { + declare export default function renderApplication(RootComponent: ComponentType, WrapperComponent?: ?ComponentType<*>, callback?: () => void, options: { + hydrate: boolean, + initialProps: Props, + - mode: 'concurrent' | 'legacy', + rootTag: any, + }): Application; + declare export function getApplication(RootComponent: ComponentType, initialProps: Object, WrapperComponent?: ?ComponentType<*>): {| + diff --git a/node_modules/react-native-web/dist/exports/render/index.js b/node_modules/react-native-web/dist/exports/render/index.js + index aa91a2a..8f9a14d 100644 + --- a/node_modules/react-native-web/dist/exports/render/index.js + +++ b/node_modules/react-native-web/dist/exports/render/index.js + @@ -9,35 +9,15 @@ + + 'use client'; + + -import { hydrate as domLegacyHydrate, render as domLegacyRender } from 'react-dom'; + import { createRoot as domCreateRoot, hydrateRoot as domHydrateRoot } from 'react-dom/client'; + -import unmountComponentAtNode from '../unmountComponentAtNode'; + import { createSheet } from '../StyleSheet/dom'; + export function hydrate(element, root) { + createSheet(root); + return domHydrateRoot(root, element); + } + -export function render(element, root) { + +export default function render(element, root) { + createSheet(root); + var reactRoot = domCreateRoot(root); + reactRoot.render(element); + return reactRoot; + } + \ No newline at end of file + -export function hydrateLegacy(element, root, callback) { + - createSheet(root); + - domLegacyHydrate(element, root, callback); + - return { + - unmount: function unmount() { + - return unmountComponentAtNode(root); + - } + - }; + -} + -export default function renderLegacy(element, root, callback) { + - createSheet(root); + - domLegacyRender(element, root, callback); + - return { + - unmount: function unmount() { + - return unmountComponentAtNode(root); + - } + - }; + -} + \ No newline at end of file + diff --git a/node_modules/react-native-web/dist/exports/render/index.js.flow b/node_modules/react-native-web/dist/exports/render/index.js.flow + index 1bd771e..729d57d 100644 + --- a/node_modules/react-native-web/dist/exports/render/index.js.flow + +++ b/node_modules/react-native-web/dist/exports/render/index.js.flow + @@ -9,11 +9,7 @@ + + 'use client'; + + -import { hydrate as domLegacyHydrate, render as domLegacyRender } from 'react-dom'; + import { createRoot as domCreateRoot, hydrateRoot as domHydrateRoot } from 'react-dom/client'; + -import unmountComponentAtNode from '../unmountComponentAtNode'; + import { createSheet } from '../StyleSheet/dom'; + declare export function hydrate(element: any, root: any): any; + -declare export function render(element: any, root: any): any; + -declare export function hydrateLegacy(element: any, root: any, callback: any): any; + -declare export default function renderLegacy(element: any, root: any, callback: any): any; + \ No newline at end of file + +declare export default function render(element: any, root: any): any; + \ No newline at end of file + diff --git a/node_modules/react-native-web/dist/exports/unmountComponentAtNode/index.js b/node_modules/react-native-web/dist/exports/unmountComponentAtNode/index.js + index 925051c..cea4dee 100644 + --- a/node_modules/react-native-web/dist/exports/unmountComponentAtNode/index.js + +++ b/node_modules/react-native-web/dist/exports/unmountComponentAtNode/index.js + @@ -7,5 +7,7 @@ + * + */ + + -import { unmountComponentAtNode } from 'react-dom'; + -export default unmountComponentAtNode; + \ No newline at end of file + +export default function unmountComponentAtNode(rootTag) { + + rootTag.unmount(); + + return true; + +} + \ No newline at end of file + diff --git a/node_modules/react-native-web/dist/exports/unmountComponentAtNode/index.js.flow b/node_modules/react-native-web/dist/exports/unmountComponentAtNode/index.js.flow + index b950090..90ec151 100644 + --- a/node_modules/react-native-web/dist/exports/unmountComponentAtNode/index.js.flow + +++ b/node_modules/react-native-web/dist/exports/unmountComponentAtNode/index.js.flow + @@ -6,6 +6,4 @@ + * + * @noflow + */ + - + -import { unmountComponentAtNode } from 'react-dom'; + -export default unmountComponentAtNode; + \ No newline at end of file + +declare export default function unmountComponentAtNode(rootTag: any): any; + \ No newline at end of file + diff --git a/node_modules/react-native-web/dist/modules/addEventListener/__tests__/index-test.js.flow b/node_modules/react-native-web/dist/modules/addEventListener/__tests__/index-test.js.flow + new file mode 100644 + index 0000000..9eb6144 + --- /dev/null + +++ b/node_modules/react-native-web/dist/modules/addEventListener/__tests__/index-test.js.flow + @@ -0,0 +1,191 @@ + +/** + + * Copyright (c) Meta Platforms, Inc. and affiliates. + + * + + * This source code is licensed under the MIT license found in the + + * LICENSE file in the root directory of this source tree. + + * + + * @flow strict-local + + */ + + + +import * as React from 'react'; + +import { act, render } from '@testing-library/react'; + +import { createEventTarget } from 'dom-event-testing-library'; + +import { addEventListener } from '..'; + +describe('addEventListener', () => { + + describe('addEventListener()', () => { + + test('event dispatched on target', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(): any; + + render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('event dispatched on parent', () => { + + const listener = jest.fn(); + + const listenerCapture = jest.fn(); + + const targetRef = React.createRef(); + + const parentRef = React.createRef(); + + declare function Component(): any; + + render(); + + const parent = createEventTarget(parentRef.current); + + act(() => { + + parent.click(); + + }); + + expect(listener).toBeCalledTimes(0); + + expect(listenerCapture).toBeCalledTimes(0); + + }); + + test('event dispatched on child', () => { + + const log = []; + + const listener = jest.fn(() => { + + log.push('bubble'); + + }); + + const listenerCapture = jest.fn(() => { + + log.push('capture'); + + }); + + const targetRef = React.createRef(); + + const childRef = React.createRef(); + + declare function Component(): any; + + render(); + + const child = createEventTarget(childRef.current); + + act(() => { + + child.click(); + + }); + + expect(listenerCapture).toBeCalledTimes(1); + + expect(listener).toBeCalledTimes(1); + + expect(log).toEqual(['capture', 'bubble']); + + }); + + test('event dispatched on text node', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + const childRef = React.createRef(); + + declare function Component(): any; + + render(); + + const text = createEventTarget(childRef.current.firstChild); + + act(() => { + + text.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('listener can be attached to document', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(arg0: any): any; + + render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('listener can be attached to window ', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(arg0: any): any; + + render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('custom event dispatched on target', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(): any; + + render(); + + act(() => { + + const event = new CustomEvent('magic-event', { + + bubbles: true + + }); + + targetRef.current.dispatchEvent(event); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('listeners can be set on multiple targets simultaneously', () => { + + const log = []; + + const targetRef = React.createRef(); + + const parentRef = React.createRef(); + + const childRef = React.createRef(); + + const listener = jest.fn(e => { + + log.push(['bubble', e.currentTarget.id]); + + }); + + const listenerCapture = jest.fn(e => { + + log.push(['capture', e.currentTarget.id]); + + }); + + declare function Component(): any; + + render(); + + const child = createEventTarget(childRef.current); + + act(() => { + + child.click(); + + }); + + expect(listenerCapture).toBeCalledTimes(2); + + expect(listener).toBeCalledTimes(2); + + expect(log).toEqual([['capture', 'parent'], ['capture', 'target'], ['bubble', 'target'], ['bubble', 'parent']]); + + }); + + test('listeners are specific to each event handle', () => { + + const log = []; + + const targetRef = React.createRef(); + + const childRef = React.createRef(); + + const listener = jest.fn(e => { + + log.push(['bubble', 'target']); + + }); + + const listenerAlt = jest.fn(e => { + + log.push(['bubble', 'target-alt']); + + }); + + const listenerCapture = jest.fn(e => { + + log.push(['capture', 'target']); + + }); + + const listenerCaptureAlt = jest.fn(e => { + + log.push(['capture', 'target-alt']); + + }); + + declare function Component(): any; + + render(); + + const child = createEventTarget(childRef.current); + + act(() => { + + child.click(); + + }); + + expect(listenerCapture).toBeCalledTimes(1); + + expect(listenerCaptureAlt).toBeCalledTimes(1); + + expect(listener).toBeCalledTimes(1); + + expect(listenerAlt).toBeCalledTimes(1); + + expect(log).toEqual([['capture', 'target'], ['capture', 'target-alt'], ['bubble', 'target'], ['bubble', 'target-alt']]); + + }); + + }); + + describe('stopPropagation and stopImmediatePropagation', () => { + + test('stopPropagation works as expected', () => { + + const childListener = jest.fn(e => { + + e.stopPropagation(); + + }); + + const targetListener = jest.fn(); + + const targetRef = React.createRef(); + + const childRef = React.createRef(); + + declare function Component(): any; + + render(); + + const child = createEventTarget(childRef.current); + + act(() => { + + child.click(); + + }); + + expect(childListener).toBeCalledTimes(1); + + expect(targetListener).toBeCalledTimes(0); + + }); + + test('stopImmediatePropagation works as expected', () => { + + const firstListener = jest.fn(e => { + + e.stopImmediatePropagation(); + + }); + + const secondListener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(): any; + + render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(firstListener).toBeCalledTimes(1); + + expect(secondListener).toBeCalledTimes(0); + + }); + + }); + +}); + \ No newline at end of file + diff --git a/node_modules/react-native-web/dist/modules/addEventListener/__tests__/index-test.node.js.flow b/node_modules/react-native-web/dist/modules/addEventListener/__tests__/index-test.node.js.flow + new file mode 100644 + index 0000000..c1805b7 + --- /dev/null + +++ b/node_modules/react-native-web/dist/modules/addEventListener/__tests__/index-test.node.js.flow + @@ -0,0 +1,21 @@ + +/** + + * Copyright (c) Meta Platforms, Inc. and affiliates. + + * + + * This source code is licensed under the MIT license found in the + + * LICENSE file in the root directory of this source tree. + + * + + * @flow strict-local + + */ + + + +import * as React from 'react'; + +import * as ReactDOMServer from 'react-dom/server'; + +import { addEventListener } from '..'; + +describe('addEventListener', () => { + + test('can render correctly using ReactDOMServer', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(): any; + + const output = ReactDOMServer.renderToString(); + + expect(output).toBe('
'); + + }); + +}); + \ No newline at end of file + diff --git a/node_modules/react-native-web/dist/modules/useEvent/__tests__/index-test.js.flow b/node_modules/react-native-web/dist/modules/useEvent/__tests__/index-test.js.flow + new file mode 100644 + index 0000000..9b57364 + --- /dev/null + +++ b/node_modules/react-native-web/dist/modules/useEvent/__tests__/index-test.js.flow + @@ -0,0 +1,247 @@ + +/** + + * Copyright (c) Meta Platforms, Inc. and affiliates. + + * + + * This source code is licensed under the MIT license found in the + + * LICENSE file in the root directory of this source tree. + + * + + * @flow strict-local + + */ + + + +import * as React from 'react'; + +import { act, render } from '@testing-library/react'; + +import { createEventTarget } from 'dom-event-testing-library'; + +import useEvent from '..'; + +describe('use-event', () => { + + describe('setListener()', () => { + + test('event dispatched on target', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(): any; + + render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('event dispatched on parent', () => { + + const listener = jest.fn(); + + const listenerCapture = jest.fn(); + + const targetRef = React.createRef(); + + const parentRef = React.createRef(); + + declare function Component(): any; + + render(); + + const parent = createEventTarget(parentRef.current); + + act(() => { + + parent.click(); + + }); + + expect(listener).toBeCalledTimes(0); + + expect(listenerCapture).toBeCalledTimes(0); + + }); + + test('event dispatched on child', () => { + + const log = []; + + const listener = jest.fn(() => { + + log.push('bubble'); + + }); + + const listenerCapture = jest.fn(() => { + + log.push('capture'); + + }); + + const targetRef = React.createRef(); + + const childRef = React.createRef(); + + declare function Component(): any; + + render(); + + const child = createEventTarget(childRef.current); + + act(() => { + + child.click(); + + }); + + expect(listenerCapture).toBeCalledTimes(1); + + expect(listener).toBeCalledTimes(1); + + expect(log).toEqual(['capture', 'bubble']); + + }); + + test('event dispatched on text node', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + const childRef = React.createRef(); + + declare function Component(): any; + + render(); + + const text = createEventTarget(childRef.current.firstChild); + + act(() => { + + text.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('listener can be attached to document ', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(arg0: any): any; + + render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('listener can be attached to window ', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(arg0: any): any; + + render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('listener is replaceable', () => { + + const listener = jest.fn(); + + const listenerAlt = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(arg0: any): any; + + const { + + rerender + + } = render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + rerender(); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + expect(listenerAlt).toBeCalledTimes(1); + + }); + + test('listener is removed when value is null', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(arg0: any): any; + + const { + + unmount + + } = render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(1); + + + + // this should unset the listener + + unmount(); + + listener.mockClear(); + + act(() => { + + target.click(); + + }); + + expect(listener).toBeCalledTimes(0); + + }); + + test('custom event dispatched on target', () => { + + const listener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(): any; + + render(); + + act(() => { + + const event = new CustomEvent('magic-event', { + + bubbles: true + + }); + + targetRef.current.dispatchEvent(event); + + }); + + expect(listener).toBeCalledTimes(1); + + }); + + test('listeners can be set on multiple targets simultaneously', () => { + + const log = []; + + const targetRef = React.createRef(); + + const parentRef = React.createRef(); + + const childRef = React.createRef(); + + const listener = jest.fn(e => { + + log.push(['bubble', e.currentTarget.id]); + + }); + + const listenerCapture = jest.fn(e => { + + log.push(['capture', e.currentTarget.id]); + + }); + + declare function Component(): any; + + render(); + + const child = createEventTarget(childRef.current); + + act(() => { + + child.click(); + + }); + + expect(listenerCapture).toBeCalledTimes(2); + + expect(listener).toBeCalledTimes(2); + + expect(log).toEqual([['capture', 'parent'], ['capture', 'target'], ['bubble', 'target'], ['bubble', 'parent']]); + + }); + + test('listeners are specific to each event handle', () => { + + const log = []; + + const targetRef = React.createRef(); + + const childRef = React.createRef(); + + const listener = jest.fn(e => { + + log.push(['bubble', 'target']); + + }); + + const listenerAlt = jest.fn(e => { + + log.push(['bubble', 'target-alt']); + + }); + + const listenerCapture = jest.fn(e => { + + log.push(['capture', 'target']); + + }); + + const listenerCaptureAlt = jest.fn(e => { + + log.push(['capture', 'target-alt']); + + }); + + declare function Component(): any; + + render(); + + const child = createEventTarget(childRef.current); + + act(() => { + + child.click(); + + }); + + expect(listenerCapture).toBeCalledTimes(1); + + expect(listenerCaptureAlt).toBeCalledTimes(1); + + expect(listener).toBeCalledTimes(1); + + expect(listenerAlt).toBeCalledTimes(1); + + expect(log).toEqual([['capture', 'target'], ['capture', 'target-alt'], ['bubble', 'target'], ['bubble', 'target-alt']]); + + }); + + }); + + describe('cleanup', () => { + + test('removes all listeners for given event type from targets', () => { + + const clickListener = jest.fn(); + + declare function Component(): any; + + const { + + unmount + + } = render(); + + unmount(); + + const target = createEventTarget(document); + + act(() => { + + target.click(); + + }); + + expect(clickListener).toBeCalledTimes(0); + + }); + + }); + + describe('stopPropagation and stopImmediatePropagation', () => { + + test('stopPropagation works as expected', () => { + + const childListener = jest.fn(e => { + + e.stopPropagation(); + + }); + + const targetListener = jest.fn(); + + const targetRef = React.createRef(); + + const childRef = React.createRef(); + + declare function Component(): any; + + render(); + + const child = createEventTarget(childRef.current); + + act(() => { + + child.click(); + + }); + + expect(childListener).toBeCalledTimes(1); + + expect(targetListener).toBeCalledTimes(0); + + }); + + test('stopImmediatePropagation works as expected', () => { + + const firstListener = jest.fn(e => { + + e.stopImmediatePropagation(); + + }); + + const secondListener = jest.fn(); + + const targetRef = React.createRef(); + + declare function Component(): any; + + render(); + + const target = createEventTarget(targetRef.current); + + act(() => { + + target.click(); + + }); + + expect(firstListener).toBeCalledTimes(1); + + expect(secondListener).toBeCalledTimes(0); + + }); + + }); + +}); + \ No newline at end of file + diff --git a/node_modules/react-native-web/dist/modules/useStable/__tests__/index-test.js.flow b/node_modules/react-native-web/dist/modules/useStable/__tests__/index-test.js.flow + new file mode 100644 + index 0000000..49edfc6 + --- /dev/null + +++ b/node_modules/react-native-web/dist/modules/useStable/__tests__/index-test.js.flow + @@ -0,0 +1,54 @@ + +/** + + * Copyright (c) Meta Platforms, Inc. and affiliates. + + * + + * This source code is licensed under the MIT license found in the + + * LICENSE file in the root directory of this source tree. + + * + + * @flow + + */ + + + +import * as React from 'react'; + +import { render } from '@testing-library/react'; + +import useStable from '..'; + +describe('useStable', () => { + + let spy = {}; + + declare var TestComponent: (arg0: any) => React.Node; + + beforeEach(() => { + + spy = {}; + + }); + + test('correctly sets the initial value', () => { + + declare var initialValueCallback: () => any; + + render(); + + expect(spy.value).toBe(5); + + }); + + test('does not change the value', () => { + + let counter = 0; + + declare var initialValueCallback: () => any; + + const { + + rerender + + } = render(); + + expect(spy.value).toBe(0); + + rerender(); + + expect(spy.value).toBe(0); + + }); + + test('only calls the callback once', () => { + + let counter = 0; + + declare var initialValueCallback: () => any; + + const { + + rerender + + } = render(); + + expect(counter).toBe(1); + + rerender(); + + expect(counter).toBe(1); + + }); + + test('does not change the value if the callback initially returns null', () => { + + let counter = 0; + + declare var initialValueCallback: () => any; + + const { + + rerender + + } = render(); + + expect(spy.value).toBe(null); + + rerender(); + + expect(spy.value).toBe(null); + + }); + +}); + \ No newline at end of file + EOT - name: Add Button component working-directory: test-app run: | @@ -103,8 +1001,7 @@ jobs: ButtonSpinner, ButtonIcon, ButtonGroup, - } from "@/components/ui/button" - + } from "@/components/ui/button"; export default function Home() { return (
@@ -112,10 +1009,24 @@ jobs: Hello World!
- ) + ); } EOT + - name: Add postinstall script to package.json + working-directory: test-app + run: | + jq '.scripts.postinstall = "patch-package"' package.json > tmp.$$.json && mv tmp.$$.json package.json + + - name: Run postinstall to apply patches + working-directory: test-app + run: npm run postinstall + + - name: Install Babel dependencies + working-directory: test-app + run: | + npm install @babel/preset-react babel-loader --save-dev + - name: Install dependencies and clean cache working-directory: test-app run: |