From 9c8947e4ac1d46d092c0a4eede1bcf7f6e72d768 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:28:52 -0600 Subject: [PATCH 01/21] Add support for mutation signals --- .changeset/wicked-tomatoes-grin.md | 7 + .vscode/launch.json | 32 +- .../signals-integration-tests/.babelrc | 7 + .../signals-integration-tests/.eslintignore | 1 + .../signals-integration-tests/package.json | 8 + .../playwright.config.ts | 5 +- .../src/helpers/playwright-utils.ts | 2 +- .../signals-integration-tests/src/shims.d.ts | 1 + .../tests/custom-elements/components/App.tsx | 23 + .../custom-elements/components/Button.css | 42 + .../custom-elements/components/Button.tsx | 7 + .../custom-elements/components/Checkbox.css | 99 + .../custom-elements/components/Checkbox.tsx | 26 + .../custom-elements/components/ComboBox.css | 125 + .../custom-elements/components/ComboBox.tsx | 52 + .../custom-elements/components/Dialog.css | 17 + .../custom-elements/components/Dialog.tsx | 7 + .../tests/custom-elements/components/Form.css | 32 + .../tests/custom-elements/components/Form.tsx | 7 + .../custom-elements/components/ListBox.css | 223 ++ .../custom-elements/components/ListBox.tsx | 20 + .../custom-elements/components/Modal.css | 84 + .../custom-elements/components/Modal.tsx | 7 + .../custom-elements/components/Popover.css | 87 + .../custom-elements/components/Popover.tsx | 26 + .../custom-elements/components/Select.css | 139 ++ .../custom-elements/components/Select.tsx | 58 + .../custom-elements/components/Switch.css | 74 + .../custom-elements/components/Switch.tsx | 19 + .../tests/custom-elements/components/Tabs.css | 102 + .../tests/custom-elements/components/Tabs.tsx | 7 + .../custom-elements/components/TextField.css | 49 + .../custom-elements/components/TextField.tsx | 35 + .../custom-elements/components/theme.css | 127 + .../custom-elements/custom-select.test.ts | 37 + .../custom-elements/custom-textfield.test.ts | 43 + .../src/tests/custom-elements/index-page.ts | 7 + .../tests/custom-elements/index.bundle.tsx | 12 + .../src/tests/custom-elements/index.html | 14 + .../src/tests/performance/index-page.ts | 7 + .../src/tests/performance/index.html | 14 + .../src/tests/performance/memory-leak.test.ts | 97 + .../src/tests/signals-vanilla/index.bundle.ts | 8 + .../tests/signals-vanilla/signals-bundle.ts | 16 - .../signals-vanilla/signals-ingestion.test.ts | 9 +- .../signals-integration-tests/tsconfig.json | 1 + .../webpack.config.ts | 28 +- packages/signals/signals-runtime/package.json | 2 +- .../src/__tests__/signals-runtime.test.ts | 7 +- .../src/web/web-signals-types.ts | 38 +- packages/signals/signals/package.json | 2 +- .../src/core/buffer/__tests__/buffer.test.ts | 45 +- .../src/core/client/__tests__/redact.test.ts | 54 +- .../signals/signals/src/core/client/redact.ts | 56 +- .../signals/signals/src/core/emitter/index.ts | 13 +- .../__tests__/clean-text.test.ts} | 2 +- .../signal-generators/dom-gen/change-gen.ts | 212 ++ .../{ => dom-gen}/dom-gen.ts | 144 +- .../core/signal-generators/dom-gen/helpers.ts | 7 + .../core/signal-generators/dom-gen/index.ts | 20 + .../dom-gen/mutation-observer.ts | 302 +++ .../__tests__/network-generator.test.ts | 42 +- .../network-gen/network-signals-filter.ts | 23 +- .../src/core/signal-generators/register.ts | 6 +- .../src/core/signal-generators/types.ts | 3 +- .../signals/src/core/signals/settings.ts | 16 +- .../signals/src/core/signals/signals.ts | 8 +- .../lib/debounce/__tests__/debounce.test.ts | 48 + .../signals/signals/src/lib/debounce/index.ts | 42 + .../src/test-helpers/mocks/factories.ts | 13 + .../signals/signals/src/types/settings.ts | 37 + yarn.lock | 2200 +++++++++++++++-- 72 files changed, 4845 insertions(+), 347 deletions(-) create mode 100644 .changeset/wicked-tomatoes-grin.md create mode 100644 packages/signals/signals-integration-tests/.babelrc create mode 100644 packages/signals/signals-integration-tests/.eslintignore create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/App.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/theme.css create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/index-page.ts create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/index.bundle.tsx create mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/index.html create mode 100644 packages/signals/signals-integration-tests/src/tests/performance/index-page.ts create mode 100644 packages/signals/signals-integration-tests/src/tests/performance/index.html create mode 100644 packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts create mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.bundle.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-bundle.ts rename packages/signals/signals/src/core/signal-generators/{__tests__/dom-gen-helpers.test.ts => dom-gen/__tests__/clean-text.test.ts} (97%) create mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts rename packages/signals/signals/src/core/signal-generators/{ => dom-gen}/dom-gen.ts (65%) create mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts create mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/index.ts create mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts create mode 100644 packages/signals/signals/src/lib/debounce/__tests__/debounce.test.ts create mode 100644 packages/signals/signals/src/lib/debounce/index.ts create mode 100644 packages/signals/signals/src/test-helpers/mocks/factories.ts diff --git a/.changeset/wicked-tomatoes-grin.md b/.changeset/wicked-tomatoes-grin.md new file mode 100644 index 000000000..e1c8d8329 --- /dev/null +++ b/.changeset/wicked-tomatoes-grin.md @@ -0,0 +1,7 @@ +--- +'@segment/analytics-signals': minor +'@segment/analytics-signals-runtime': minor +--- + +- Add support for interaction signals for custom components and elements with contenteditable property +- Allow custom disallow list to override network signals, even if same domain. diff --git a/.vscode/launch.json b/.vscode/launch.json index 248b1f89d..c046791af 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,16 +6,10 @@ "request": "launch", "name": "Jest Current File", "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": [ - "--testTimeout=100000", - "--findRelatedTests", - "${relativeFile}" - ], + "args": ["--testTimeout=100000", "--findRelatedTests", "${relativeFile}"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "type": "node", @@ -30,9 +24,17 @@ ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] + }, + { + "name": "Run Jest Tests for Current Package", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--testTimeout=100000"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "cwd": "${fileDirname}" }, { "type": "node", @@ -47,9 +49,7 @@ "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "program": "${workspaceFolder}/node_modules/jest/bin/jest", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "type": "node", @@ -64,9 +64,7 @@ "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "program": "${workspaceFolder}/node_modules/jest/bin/jest", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "name": "ts-node Current File", diff --git a/packages/signals/signals-integration-tests/.babelrc b/packages/signals/signals-integration-tests/.babelrc new file mode 100644 index 000000000..701fada78 --- /dev/null +++ b/packages/signals/signals-integration-tests/.babelrc @@ -0,0 +1,7 @@ +{ + "presets": [ + "@babel/preset-env", + "@babel/preset-react", + "@babel/preset-typescript" + ] +} \ No newline at end of file diff --git a/packages/signals/signals-integration-tests/.eslintignore b/packages/signals/signals-integration-tests/.eslintignore new file mode 100644 index 000000000..64dbbc4d9 --- /dev/null +++ b/packages/signals/signals-integration-tests/.eslintignore @@ -0,0 +1 @@ +playwright-report/ diff --git a/packages/signals/signals-integration-tests/package.json b/packages/signals/signals-integration-tests/package.json index 7cdef675d..734d53649 100644 --- a/packages/signals/signals-integration-tests/package.json +++ b/packages/signals/signals-integration-tests/package.json @@ -9,6 +9,9 @@ ".": "yarn run -T turbo run --filter=@internal/signals-integration-tests...", "build": "webpack", "test": "playwright test", + "test:vanilla": "playwright test src/tests/vanilla", + "test:perf": "playwright test src/tests/performance", + "test:custom": "playwright test src/tests/custom", "watch": "webpack -w", "lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'", "concurrently": "yarn run -T concurrently", @@ -25,8 +28,13 @@ "@playwright/test": "^1.28.1", "@segment/analytics-next": "workspace:^", "@segment/analytics-signals": "workspace:^", + "@types/react": "^18.0.0", + "@types/react-dom": "^18", "globby": "^11.0.2", "http-server": "14.1.1", + "react": "^18.0.0", + "react-aria-components": "^1.5.0", + "react-dom": "^18.0.0", "tslib": "^2.4.1", "webpack": "^5.76.0", "webpack-cli": "^4.8.0" diff --git a/packages/signals/signals-integration-tests/playwright.config.ts b/packages/signals/signals-integration-tests/playwright.config.ts index fe45d2455..4ba30e097 100644 --- a/packages/signals/signals-integration-tests/playwright.config.ts +++ b/packages/signals/signals-integration-tests/playwright.config.ts @@ -31,11 +31,14 @@ const config: PlaywrightTestConfig = { /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: [['html', { open: 'never' }]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on', + launchOptions: { + args: ['--enable-precise-memory-info', '--js-flags=--expose-gc'], + }, }, /* Configure projects for major browsers */ diff --git a/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts b/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts index d8335fd77..f06bb8333 100644 --- a/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts +++ b/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts @@ -19,7 +19,7 @@ export function waitForCondition( resolve() } else if (Date.now() - startTime >= timeout) { clearInterval(interval) - reject(new Error(errorMessage)) + reject(new Error(`${errorMessage}. Timeout: ${timeout}ms`)) } } catch (error) { clearInterval(interval) diff --git a/packages/signals/signals-integration-tests/src/shims.d.ts b/packages/signals/signals-integration-tests/src/shims.d.ts index bda651867..5b1f53835 100644 --- a/packages/signals/signals-integration-tests/src/shims.d.ts +++ b/packages/signals/signals-integration-tests/src/shims.d.ts @@ -5,5 +5,6 @@ declare global { interface Window { analytics: AnalyticsBrowser signalsPlugin: SignalsPlugin + SignalsPlugin: typeof SignalsPlugin } } diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/App.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/App.tsx new file mode 100644 index 000000000..0f736a132 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/App.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { TextField } from './TextField' +import { Select, SelectItem } from './Select' + +export const App: React.FC = () => { + return ( +
+
+

TextField

+ +
+
+

Select

+ +
+
+ ) +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.css new file mode 100644 index 000000000..6ce5422fe --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.css @@ -0,0 +1,42 @@ +@import "./theme.css"; + +.react-aria-Button { + color: var(--text-color); + background: var(--button-background); + border: 1px solid var(--border-color); + border-radius: 4px; + appearance: none; + vertical-align: middle; + font-size: 1rem; + text-align: center; + margin: 0; + outline: none; + padding: 6px 10px; + text-decoration: none; + + &[data-pressed] { + box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1); + background: var(--button-background-pressed); + border-color: var(--border-color-pressed); + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + &[data-disabled]{ + border-color: var(--border-color-disabled); + color: var(--text-color-disabled); + } +} + +@keyframes toggle { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.tsx new file mode 100644 index 000000000..85824ea50 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import { Button as RACButton, ButtonProps } from 'react-aria-components' +import './Button.css' + +export function Button(props: ButtonProps) { + return +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.css new file mode 100644 index 000000000..47226a65e --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.css @@ -0,0 +1,99 @@ +@import "./theme.css"; + +.react-aria-Checkbox { + --selected-color: var(--highlight-background); + --selected-color-pressed: var(--highlight-background-pressed); + --checkmark-color: var(--highlight-foreground); + + display: flex; + align-items: center; + gap: 0.571rem; + font-size: 1.143rem; + color: var(--text-color); + forced-color-adjust: none; + + .checkbox { + width: 1.143rem; + height: 1.143rem; + border: 2px solid var(--border-color); + border-radius: 4px; + transition: all 200ms; + display: flex; + align-items: center; + justify-content: center; + } + + svg { + width: 1rem; + height: 1rem; + fill: none; + stroke: var(--checkmark-color); + stroke-width: 3px; + stroke-dasharray: 22px; + stroke-dashoffset: 66; + transition: all 200ms; + } + + &[data-pressed] .checkbox { + border-color: var(--border-color-pressed); + } + + &[data-focus-visible] .checkbox { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + &[data-selected], + &[data-indeterminate] { + .checkbox { + border-color: var(--selected-color); + background: var(--selected-color); + } + + &[data-pressed] .checkbox { + border-color: var(--selected-color-pressed); + background: var(--selected-color-pressed); + } + + svg { + stroke-dashoffset: 44; + } + } + + &[data-indeterminate] { + & svg { + stroke: none; + fill: var(--checkmark-color); + } + } + + &[data-invalid] { + .checkbox { + --checkmark-color: var(--gray-50); + border-color: var(--invalid-color); + } + + &[data-pressed] .checkbox { + border-color: var(--invalid-color-pressed); + } + + &[data-selected], + &[data-indeterminate] { + .checkbox { + background: var(--invalid-color); + } + + &[data-pressed] .checkbox { + background: var(--invalid-color-pressed); + } + } + } + + &[data-disabled] { + color: var(--text-color-disabled); + + .checkbox { + border-color: var(--border-color-disabled); + } + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.tsx new file mode 100644 index 000000000..a6aad2c68 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.tsx @@ -0,0 +1,26 @@ +import { Checkbox as AriaCheckbox, CheckboxProps } from 'react-aria-components' +import React from 'react' +import './Checkbox.css' + +export function Checkbox({ children, ...props }: CheckboxProps) { + return ( + + {({ isIndeterminate }) => ( + <> +
+ +
+ {children} + + )} +
+ ) +} + +export { Checkbox as MyCheckbox } diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.css new file mode 100644 index 000000000..7377d73fc --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.css @@ -0,0 +1,125 @@ +@import './Checkbox.css'; +@import './ListBox.css'; +@import './Popover.css'; +@import './Form.css'; +@import './Button.css'; +@import "./theme.css"; + +.react-aria-ComboBox { + color: var(--text-color); + + .react-aria-Input { + margin: 0; + font-size: 1.072rem; + background: var(--field-background); + color: var(--field-text-color); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 0.286rem 2rem 0.286rem 0.571rem; + vertical-align: middle; + + &[data-focused] { + outline: none; + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + } + + .react-aria-Button { + background: var(--highlight-background); + color: var(--highlight-foreground); + forced-color-adjust: none; + border-radius: 4px; + border: none; + margin-left: -1.714rem; + width: 1.429rem; + height: 1.429rem; + padding: 0; + font-size: 0.857rem; + cursor: default; + + &[data-pressed] { + box-shadow: none; + background: var(--highlight-background); + } + } +} + +.react-aria-Popover[data-trigger=ComboBox] { + width: var(--trigger-width); + + .react-aria-ListBox { + display: block; + width: unset; + max-height: inherit; + min-height: unset; + border: none; + + .react-aria-Header { + padding-left: 1.571rem; + } + } + + .react-aria-ListBoxItem { + padding: 0.286rem 0.571rem 0.286rem 1.571rem; + + &[data-focus-visible] { + outline: none; + } + + &[data-selected] { + font-weight: 600; + background: unset; + color: var(--text-color); + + &::before { + content: '✓'; + content: '✓' / ''; + alt: ' '; + position: absolute; + top: 4px; + left: 4px; + } + } + + &[data-focused], + &[data-pressed] { + background: var(--highlight-background); + color: var(--highlight-foreground); + } + } +} + +.react-aria-ListBoxItem[href] { + text-decoration: none; + cursor: pointer; +} + +.react-aria-ComboBox { + .react-aria-Input { + &[data-disabled] { + border-color: var(--border-color-disabled); + } + } + + .react-aria-Button { + &[data-disabled] { + background: var(--border-color-disabled); + } + } + + .react-aria-Input { + &[data-invalid]:not([data-focused]) { + border-color: var(--invalid-color); + } + } + + .react-aria-FieldError { + font-size: 12px; + color: var(--invalid-color); + } + + [slot=description] { + font-size: 12px; + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.tsx new file mode 100644 index 000000000..5d76259ab --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.tsx @@ -0,0 +1,52 @@ +import { + Button, + ComboBox as AriaComboBox, + ComboBoxProps as AriaComboBoxProps, + FieldError, + Input, + Label, + ListBox, + ListBoxItem, + ListBoxItemProps, + Popover, + Text, + ValidationResult, +} from 'react-aria-components' +import React from 'react' + +import './ComboBox.css' + +export interface ComboBoxProps + extends Omit, 'children'> { + label?: string + description?: string | null + errorMessage?: string | ((validation: ValidationResult) => string) + children: React.ReactNode | ((item: T) => React.ReactNode) +} + +export function ComboBox({ + label, + description, + errorMessage, + children, + ...props +}: ComboBoxProps) { + return ( + + +
+ + +
+ {description && {description}} + {errorMessage} + + {children} + +
+ ) +} + +export function ComboBoxItem(props: ListBoxItemProps) { + return +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.css new file mode 100644 index 000000000..a5051aeb2 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.css @@ -0,0 +1,17 @@ +@import "./theme.css"; +@import './Button.css'; +@import './TextField.css'; +@import './Modal.css'; + +.react-aria-Dialog { + outline: none; + padding: 30px; + max-height: inherit; + box-sizing: border-box; + overflow: auto; + + .react-aria-Heading[slot=title] { + line-height: 1em; + margin-top: 0; + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.tsx new file mode 100644 index 000000000..40d5a58c2 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.tsx @@ -0,0 +1,7 @@ +import { Dialog as RACDialog, DialogProps } from 'react-aria-components' +import './Dialog.css' +import React from 'react' + +export function Dialog(props: DialogProps) { + return +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.css new file mode 100644 index 000000000..37c01bfd6 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.css @@ -0,0 +1,32 @@ +@import "./theme.css"; +@import './TextField.css'; +@import './Button.css'; + +.react-aria-Form { + display: flex; + flex-direction: column; + align-items: start; + gap: 8px; +} + +.react-aria-Form [role=alert] { + border: 2px solid var(--invalid-color); + background: var(--overlay-background); + border-radius: 6px; + padding: 12px; + max-width: 250px; + outline: none; + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + h3 { + margin-top: 0; + } + + p { + margin-bottom: 0; + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.tsx new file mode 100644 index 000000000..6b5d38d1a --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import { Form as RACForm, FormProps } from 'react-aria-components' +import './Form.css' + +export function Form(props: FormProps) { + return +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.css new file mode 100644 index 000000000..3784cbf7d --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.css @@ -0,0 +1,223 @@ +@import './Checkbox.css'; +@import "./theme.css"; + +.react-aria-ListBox { + display: flex; + flex-direction: column; + max-height: inherit; + overflow: auto; + padding: 2px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--overlay-background); + forced-color-adjust: none; + outline: none; + width: 250px; + max-height: 300px; + min-height: 100px; + box-sizing: border-box; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } +} + +.react-aria-ListBoxItem { + margin: 2px; + padding: 0.286rem 0.571rem; + border-radius: 6px; + outline: none; + cursor: default; + color: var(--text-color); + font-size: 1.072rem; + position: relative; + display: flex; + flex-direction: column; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &[data-selected] { + background: var(--highlight-background); + color: var(--highlight-foreground); + + &[data-focus-visible] { + outline-color: var(--highlight-foreground); + outline-offset: -4px; + } + } +} + +.react-aria-ListBoxItem[href] { + text-decoration: none; + cursor: pointer; + -webkit-touch-callout: none; +} + +.react-aria-ListBox { + .react-aria-ListBoxSection:not(:first-child) { + margin-top: 12px; + } + + .react-aria-Header { + font-size: 1.143rem; + font-weight: bold; + padding: 0 0.714rem; + } +} + +.react-aria-ListBoxItem { + [slot=label] { + font-weight: bold; + } + + [slot=description] { + font-size: small; + } +} + +.react-aria-ListBox[data-orientation=horizontal], +.react-aria-ListBox[data-layout=grid] { + flex-direction: row; + width: fit-content; + max-width: 100%; + padding: 4px; + + .react-aria-ListBoxItem { + position: relative; + margin: 0; + padding: 4px; + + & img { + object-fit: cover; + aspect-ratio: 1/1; + max-width: 150px; + margin-bottom: 4px; + border-radius: 4px; + transition: box-shadow 200ms; + } + + &[data-hovered] { + & img { + box-shadow: 0 0 8px rgb(from slateblue r g b / 0.5); + } + } + + &[data-selected] { + background: none; + color: inherit; + + & img { + box-shadow: 0 0 12px rgb(from slateblue r g b / 0.8); + } + + &:after { + content: '✓'; + content: '✓' / ''; + alt: ' '; + position: absolute; + top: 8px; + right: 8px; + background: var(--highlight-background); + border: 2px solid var(--highlight-foreground); + color: var(--highlight-foreground); + width: 22px; + height: 22px; + border-radius: 22px; + box-sizing: border-box; + font-size: 14px; + line-height: 1em; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 8px rgb(0 0 0 / .5); + } + } + } +} + +.react-aria-ListBox[data-layout=grid] { + display: grid; + grid-template-columns: 1fr 1fr; + scrollbar-gutter: stable; +} + +.react-aria-ListBox[data-layout=grid][data-orientation=horizontal] { + width: 100%; + max-width: none; + display: grid; + grid-auto-flow: column; + grid-template-rows: 58px 58px; + grid-template-columns: none; + grid-auto-columns: 250px; + max-height: 200px; + gap: 8px; + + .react-aria-ListBoxItem { + display: grid; + grid-template-areas: "image ." + "image title" + "image description" + "image ."; + grid-template-columns: auto 1fr; + grid-template-rows: 1fr auto auto 1fr; + column-gap: 8px; + + & img { + width: 50px; + height: 50px; + grid-area: image; + margin-bottom: 0; + } + + [slot=label] { + grid-area: title; + } + + [slot=description] { + grid-area: description; + } + } +} + +.react-aria-ListBoxItem { + &[data-disabled] { + color: var(--text-color-disabled); + } +} + +.react-aria-ListBox { + &[data-empty] { + align-items: center; + justify-content: center; + font-style: italic; + } +} + +.react-aria-ListBoxItem { + &[data-dragging] { + opacity: 0.6; + } +} + +.react-aria-DropIndicator[data-drop-target] { + outline: 1px solid var(--highlight-background); +} + +.react-aria-ListBox[data-drop-target] { + outline: 2px solid var(--highlight-background); + outline-offset: -1px; + background: var(--highlight-overlay) +} + +.react-aria-ListBoxItem[data-drop-target] { + outline: 2px solid var(--highlight-background); + background: var(--highlight-overlay) +} + +.react-aria-DropIndicator[data-drop-target] { + outline: 1px solid var(--highlight-background); +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.tsx new file mode 100644 index 000000000..086b7d7e5 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { + ListBox as AriaListBox, + ListBoxItem as AriaListBoxItem, + ListBoxItemProps, + ListBoxProps, +} from 'react-aria-components' + +import './ListBox.css' + +export function ListBox({ + children, + ...props +}: ListBoxProps) { + return {children} +} + +export function ListBoxItem(props: ListBoxItemProps) { + return +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.css new file mode 100644 index 000000000..f8fec1164 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.css @@ -0,0 +1,84 @@ +@import './Button.css'; +@import './TextField.css'; +@import "./theme.css"; + +.react-aria-ModalOverlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: var(--visual-viewport-height); + background: rgba(0 0 0 / .5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + + &[data-entering] { + animation: modal-fade 200ms; + } + + &[data-exiting] { + animation: modal-fade 150ms reverse ease-in; + } +} + +.react-aria-Modal { + box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); + border-radius: 6px; + background: var(--overlay-background); + color: var(--text-color); + border: 1px solid var(--gray-400); + outline: none; + max-width: 300px; + + &[data-entering] { + animation: modal-zoom 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + + .react-aria-TextField { + margin-bottom: 8px; + } +} + +@keyframes modal-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes modal-zoom { + from { + transform: scale(0.8); + } + + to { + transform: scale(1); + } +} + +@keyframes mymodal-blur { + from { + background: rgba(45 0 0 / 0); + backdrop-filter: blur(0); + } + + to { + background: rgba(45 0 0 / .3); + backdrop-filter: blur(10px); + } +} + +@keyframes mymodal-slide { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.tsx new file mode 100644 index 000000000..9cfce9871 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.tsx @@ -0,0 +1,7 @@ +import { Modal as RACModal, ModalOverlayProps } from 'react-aria-components' +import './Modal.css' +import React from 'react' + +export function Modal(props: ModalOverlayProps) { + return +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.css new file mode 100644 index 000000000..fd060cd39 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.css @@ -0,0 +1,87 @@ +@import './Button.css'; +@import './Dialog.css'; +@import './Switch.css'; +@import "./theme.css"; + +.react-aria-Popover { + --background-color: var(--overlay-background); + + border: 1px solid var(--border-color); + box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); + border-radius: 6px; + background: var(--background-color); + color: var(--text-color); + outline: none; + max-width: 250px; + + .react-aria-OverlayArrow svg { + display: block; + fill: var(--background-color); + stroke: var(--border-color); + stroke-width: 1px; + } + + &[data-placement=top] { + --origin: translateY(8px); + + &:has(.react-aria-OverlayArrow) { + margin-bottom: 6px; + } + } + + &[data-placement=bottom] { + --origin: translateY(-8px); + + &:has(.react-aria-OverlayArrow) { + margin-top: 6px; + } + + .react-aria-OverlayArrow svg { + transform: rotate(180deg); + } + } + + &[data-placement=right] { + --origin: translateX(-8px); + + &:has(.react-aria-OverlayArrow) { + margin-left: 6px; + } + + .react-aria-OverlayArrow svg { + transform: rotate(90deg); + } + } + + &[data-placement=left] { + --origin: translateX(8px); + + &:has(.react-aria-OverlayArrow) { + margin-right: 6px; + } + + .react-aria-OverlayArrow svg { + transform: rotate(-90deg); + } + } + + &[data-entering] { + animation: popover-slide 200ms; + } + + &[data-exiting] { + animation: popover-slide 200ms reverse ease-in; + } +} + +@keyframes popover-slide { + from { + transform: var(--origin); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.tsx new file mode 100644 index 000000000..622627563 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { + Dialog, + OverlayArrow, + Popover as AriaPopover, + PopoverProps as AriaPopoverProps, +} from 'react-aria-components' + +import './Popover.css' + +export interface PopoverProps extends Omit { + children: React.ReactNode +} + +export function Popover({ children, ...props }: PopoverProps) { + return ( + + + + + + + {children} + + ) +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.css new file mode 100644 index 000000000..ec6765e2b --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.css @@ -0,0 +1,139 @@ +@import './ListBox.css'; +@import './Popover.css'; +@import './Button.css'; +@import './Form.css'; +@import "./theme.css"; + +.react-aria-Select { + color: var(--text-color); + + .react-aria-Button { + box-shadow: 0 1px 2px rgba(0 0 0 / 0.1); + border-radius: 6px; + font-size: 1.072rem; + padding: 0.286rem 0.286rem 0.286rem 0.571rem; + display: flex; + align-items: center; + max-width: 250px; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + } + + .react-aria-SelectValue { + &[data-placeholder] { + font-style: italic; + color: var(--text-color-placeholder); + } + } + + span[aria-hidden] { + width: 1.5rem; + line-height: 1.375rem; + margin-left: 1rem; + padding: 1px; + background: var(--highlight-background); + color: var(--highlight-foreground); + forced-color-adjust: none; + border-radius: 4px; + font-size: 0.857rem; + } +} + +.react-aria-Popover[data-trigger=Select] { + min-width: var(--trigger-width); + + .react-aria-ListBox { + display: block; + width: unset; + max-height: inherit; + min-height: unset; + border: none; + + .react-aria-Header { + padding-left: 1.571rem; + } + } + + .react-aria-ListBoxItem { + padding: 0.286rem 0.571rem 0.286rem 1.571rem; + + &[data-focus-visible] { + outline: none; + } + + &[data-selected] { + font-weight: 600; + background: unset; + color: var(--text-color); + + &::before { + content: '✓'; + content: '✓' / ''; + alt: ' '; + position: absolute; + top: 4px; + left: 4px; + } + } + + &[data-focused], + &[data-pressed] { + background: var(--highlight-background); + color: var(--highlight-foreground); + } + } +} + +.react-aria-ListBoxItem[href] { + text-decoration: none; + cursor: pointer; +} + +.react-aria-Select { + .react-aria-SelectValue { + [slot=description] { + display: none; + } + } + + .react-aria-Button { + &[data-disabled] { + border-color: var(--border-color-disabled); + color: var(--text-color-disabled); + span[aria-hidden] { + background: var(--border-color-disabled); + color: var(--text-color-disabled); + } + + .react-aria-SelectValue { + &[data-placeholder] { + color: var(--text-color-disabled); + } + } + } + } +} + +@media (forced-colors: active) { + .react-aria-Select { + .react-aria-Button { + &[data-disabled] span[aria-hidden] { + background: 0 0; + } + } + } +} + +.react-aria-Select { + .react-aria-FieldError { + font-size: 12px; + color: var(--invalid-color); + } + + [slot=description] { + font-size: 12px; + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.tsx new file mode 100644 index 000000000..66b964613 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { + Button, + FieldError, + Label, + ListBox, + ListBoxItem, + ListBoxItemProps, + Popover, + Select as AriaSelect, + SelectProps as AriaSelectProps, + SelectValue, + Text, + ValidationResult, +} from 'react-aria-components' + +import './Select.css' + +export interface SelectProps + extends Omit, 'children'> { + label?: string + description?: string + errorMessage?: string | ((validation: ValidationResult) => string) + items?: Iterable + children: React.ReactNode | ((item: T) => React.ReactNode) +} + +export function Select({ + label, + description, + errorMessage, + children, + items, + ...props +}: SelectProps) { + return ( + + + + {description && {description}} + {errorMessage} + + {children} + + + ) +} + +export { Select as MySelect } + +export function SelectItem(props: ListBoxItemProps) { + return +} + +export { SelectItem as MyItem } diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.css new file mode 100644 index 000000000..adf5fcbcf --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.css @@ -0,0 +1,74 @@ +@import "./theme.css"; + +.react-aria-Switch { + display: flex; + align-items: center; + gap: 0.571rem; + font-size: 1.143rem; + color: var(--text-color); + forced-color-adjust: none; + + .indicator { + width: 2rem; + height: 1.143rem; + border: 2px solid var(--border-color); + background: var(--background-color); + border-radius: 1.143rem; + transition: all 200ms; + + &:before { + content: ''; + display: block; + margin: 0.143rem; + width: 0.857rem; + height: 0.857rem; + background: var(--highlight-background); + border-radius: 16px; + transition: all 200ms; + } + } + + &[data-pressed] .indicator { + border-color: var(--border-color-pressed); + + &:before { + background: var(--highlight-background-pressed); + } + } + + &[data-selected] { + .indicator { + border-color: var(--highlight-background); + background: var(--highlight-background); + + &:before { + background: var(--field-background); + transform: translateX(100%); + } + } + + &[data-pressed] { + .indicator { + border-color: var(--highlight-background-pressed); + background: var(--highlight-background-pressed); + } + } + } + + &[data-focus-visible] .indicator { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + &[data-disabled] { + color: var(--text-color-disabled); + + .indicator { + border-color: var(--border-color-disabled); + + &:before { + background: var(--border-color-disabled); + } + } + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.tsx new file mode 100644 index 000000000..b1b237e12 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.tsx @@ -0,0 +1,19 @@ +import { + Switch as AriaSwitch, + SwitchProps as AriaSwitchProps, +} from 'react-aria-components' +import React from 'react' +import './Switch.css' + +export interface SwitchProps extends Omit { + children: React.ReactNode +} + +export function Switch({ children, ...props }: SwitchProps) { + return ( + +
+ {children} + + ) +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.css new file mode 100644 index 000000000..c18b5f8f7 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.css @@ -0,0 +1,102 @@ +@import './Button.css'; +@import './Link.css'; +@import "./theme.css"; + +.react-aria-Tabs { + display: flex; + color: var(--text-color); + + &[data-orientation=horizontal] { + flex-direction: column; + } +} + +.react-aria-TabList { + display: flex; + + &[data-orientation=horizontal] { + border-bottom: 1px solid var(--border-color); + + .react-aria-Tab { + border-bottom: 3px solid var(--border-color); + } + } +} + +.react-aria-Tab { + padding: 10px; + cursor: default; + outline: none; + position: relative; + color: var(--text-color-base); + transition: color 200ms; + --border-color: transparent; + forced-color-adjust: none; + + &[data-hovered], + &[data-focused] { + color: var(--text-color-hover); + } + + &[data-selected] { + --border-color: var(--highlight-background); + color: var(--text-color); + } + + &[data-disabled] { + color: var(--text-color-disabled); + &[data-selected] { + --border-color: var(--text-color-disabled); + } + } + + &[data-focus-visible]:after { + content: ''; + position: absolute; + inset: 4px; + border-radius: 4px; + border: 2px solid var(--focus-ring-color); + } +} + +.react-aria-TabPanel { + margin-top: 4px; + padding: 10px; + border-radius: 4px; + outline: none; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + } +} + +.react-aria-Tabs { + &[data-orientation=vertical] { + flex-direction: row; + } +} + +.react-aria-TabList { + &[data-orientation=vertical] { + flex-direction: column; + border-inline-end: 1px solid gray; + + .react-aria-Tab { + border-inline-end: 3px solid var(--border-color, transparent); + } + } +} + +.react-aria-Tab { + &[data-disabled] { + color: var(--text-color-disabled); + &[data-selected] { + --border-color: var(--border-color-disabled); + } + } +} + +.react-aria-Tab[href] { + text-decoration: none; + cursor: pointer; +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.tsx new file mode 100644 index 000000000..aa911b9e3 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import { Tabs as RACTabs, TabsProps } from 'react-aria-components' +import './Tabs.css' + +export function Tabs(props: TabsProps) { + return +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.css new file mode 100644 index 000000000..1a84e2d89 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.css @@ -0,0 +1,49 @@ +@import './Button.css'; +@import "./theme.css"; + +.react-aria-TextField { + display: flex; + flex-direction: column; + width: fit-content; + color: var(--text-color); + + .react-aria-Input, + .react-aria-TextArea { + padding: 0.286rem; + margin: 0; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--field-background); + font-size: 1.143rem; + color: var(--field-text-color); + + &[data-focused] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + } + + .react-aria-Input, + .react-aria-TextArea { + &[data-invalid] { + border-color: var(--invalid-color); + } + } + + .react-aria-FieldError { + font-size: 12px; + color: var(--invalid-color); + } + + [slot=description] { + font-size: 12px; + } + + .react-aria-Input, + .react-aria-TextArea { + &[data-disabled] { + border-color: var(--border-color-disabled); + color: var(--text-color-disabled); + } + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.tsx new file mode 100644 index 000000000..f2159b20a --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.tsx @@ -0,0 +1,35 @@ +import { + FieldError, + Input, + Label, + Text, + TextField as AriaTextField, + TextFieldProps as AriaTextFieldProps, +} from 'react-aria-components' + +import './TextField.css' +import React from 'react' + +export interface TextFieldProps extends AriaTextFieldProps { + label?: string + description?: string +} + +export function TextField({ label, description, ...props }: TextFieldProps) { + const [isError, setIsError] = React.useState(false) + return ( + setIsError(v.includes('error'))} + isInvalid={isError} + > + + + {description && {description}} + {isError && "Some error!"} + + ) +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/theme.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/theme.css new file mode 100644 index 000000000..23dab3f8e --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/theme.css @@ -0,0 +1,127 @@ +/* Base styles */ +:root { + font-family: system-ui; + font-size: 14px; + line-height: 1.5; + background: var(--background-color); +} + +/* color themes for dark and light modes, generated with Leonardo. + * Light: https://leonardocolor.io/theme.html?name=Light&config=%7B%22baseScale%22%3A%22Gray%22%2C%22colorScales%22%3A%5B%7B%22name%22%3A%22Gray%22%2C%22colorKeys%22%3A%5B%22%23000000%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Purple%22%2C%22colorKeys%22%3A%5B%22%235e30eb%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Red%22%2C%22colorKeys%22%3A%5B%22%23e32400%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%5D%2C%22lightness%22%3A98%2C%22contrast%22%3A1%2C%22saturation%22%3A100%2C%22formula%22%3A%22wcag2%22%7D */ +:root { + --background-color: #f8f8f8; + --gray-50: #ffffff; + --gray-100: #d0d0d0; + --gray-200: #afafaf; + --gray-300: #8f8f8f; + --gray-400: #717171; + --gray-500: #555555; + --gray-600: #393939; + --purple-100: #d5c9fa; + --purple-200: #b8a3f6; + --purple-300: #997cf2; + --purple-400: #7a54ef; + --purple-500: #582ddc; + --purple-600: #3c1e95; + --red-100: #f7c4ba; + --red-200: #f29887; + --red-300: #eb664d; + --red-400: #de2300; + --red-500: #a81b00; + --red-600: #731200; + --highlight-hover: rgb(0 0 0 / 0.07); + --highlight-pressed: rgb(0 0 0 / 0.15); +} + +/* Dark: https://leonardocolor.io/theme.html?name=Dark&config=%7B%22baseScale%22%3A%22Gray%22%2C%22colorScales%22%3A%5B%7B%22name%22%3A%22Gray%22%2C%22colorKeys%22%3A%5B%22%23000000%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Purple%22%2C%22colorKeys%22%3A%5B%22%235e30eb%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Red%22%2C%22colorKeys%22%3A%5B%22%23e32400%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%5D%2C%22lightness%22%3A11%2C%22contrast%22%3A1%2C%22saturation%22%3A100%2C%22formula%22%3A%22wcag2%22%7D */ +@media (prefers-color-scheme: dark) { + :root { + --background-color: #1d1d1d; + --gray-50: #101010; + --gray-100: #393939; + --gray-200: #4f4f4f; + --gray-300: #686868; + --gray-400: #848484; + --gray-500: #a7a7a7; + --gray-600: #cfcfcf; + --purple-100: #3c1e95; + --purple-200: #522acd; + --purple-300: #6f46ed; + --purple-400: #8e6ef1; + --purple-500: #b099f5; + --purple-600: #d5c8fa; + --red-100: #721200; + --red-200: #9c1900; + --red-300: #cc2000; + --red-400: #e95034; + --red-500: #f08c79; + --red-600: #f7c3ba; + --highlight-hover: rgb(255 255 255 / 0.1); + --highlight-pressed: rgb(255 255 255 / 0.2); + } +} + +/* Semantic colors */ +:root { + --focus-ring-color: var(--purple-400); + --text-color: var(--gray-600); + --text-color-base: var(--gray-500); + --text-color-hover: var(--gray-600); + --text-color-disabled: var(--gray-200); + --text-color-placeholder: var(--gray-400); + --link-color: var(--purple-500); + --link-color-secondary: var(--gray-500); + --link-color-pressed: var(--purple-600); + --border-color: var(--gray-300); + --border-color-hover: var(--gray-400); + --border-color-pressed: var(--gray-400); + --border-color-disabled: var(--gray-100); + --field-background: var(--gray-50); + --field-text-color: var(--gray-600); + --overlay-background: var(--gray-50); + --button-background: var(--gray-50); + --button-background-pressed: var(--background-color); + /* these colors are the same between light and dark themes + * to ensure contrast with the foreground color */ + --highlight-background: #6f46ed; /* purple-300 from dark theme, 3.03:1 against background-color */ + --highlight-background-pressed: #522acd; /* purple-200 from dark theme */ + --highlight-background-invalid: #cc2000; /* red-300 from dark theme */ + --highlight-foreground: white; /* 5.56:1 against highlight-background */ + --highlight-foreground-pressed: #ddd; + --highlight-overlay: rgb(from #6f46ed r g b / 15%); + --invalid-color: var(--red-400); + --invalid-color-pressed: var(--red-500); +} + +/* Windows high contrast mode overrides */ +@media (forced-colors: active) { + :root { + --background-color: Canvas; + --focus-ring-color: Highlight; + --text-color: ButtonText; + --text-color-base: ButtonText; + --text-color-hover: ButtonText; + --text-color-disabled: GrayText; + --text-color-placeholder: ButtonText; + --link-color: LinkText; + --link-color-secondary: LinkText; + --link-color-pressed: LinkText; + --border-color: ButtonBorder; + --border-color-hover: ButtonBorder; + --border-color-pressed: ButtonBorder; + --border-color-disabled: GrayText; + --field-background: Field; + --field-text-color: FieldText; + --overlay-background: Canvas; + --button-background: ButtonFace; + --button-background-pressed: ButtonFace; + --highlight-background: Highlight; + --highlight-background-pressed: Highlight; + --highlight-background-invalid: LinkText; + --highlight-foreground: HighlightText; + --highlight-foreground-pressed: HighlightText; + --invalid-color: LinkText; + --invalid-color-pressed: LinkText; + } +} + diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts new file mode 100644 index 000000000..9851eb719 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test' +import { waitForCondition } from '../../helpers/playwright-utils' +import { IndexPage } from './index-page' +import type { SegmentEvent } from '@segment/analytics-next' + +const basicEdgeFn = `const processSignal = (signal) => {}` + +test('Collecting signals whenever a user selects an item', async ({ page }) => { + const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { + disableSignalsRedaction: true, + enableSignalsIngestion: true, + }) + + const filterClick = (e: SegmentEvent): boolean => { + return ( + e.properties!.data.eventType === 'click' && + e.properties!.data.target.textContent?.includes('Mint') + ) + } + + const waitForInteraction = waitForCondition( + () => { + const events = indexPage.signalsAPI.getEvents('interaction') + return events.some(filterClick) + }, + { errorMessage: 'No interaction signals found' } + ) + await page.click('#select button') + await page.getByRole('option', { name: 'Mint' }).click() + + await waitForInteraction + const signals = indexPage.signalsAPI + .getEvents('interaction') + .filter(filterClick) + + expect(signals).toHaveLength(1) +}) diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts new file mode 100644 index 000000000..827c8b9cf --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test' +import { waitForCondition } from '../../helpers/playwright-utils' +import { IndexPage } from './index-page' + +const basicEdgeFn = `const processSignal = (signal) => {}` + +test('Collecting signals whenever a user enters text input and focuses out', async ({ + page, +}) => { + const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { + disableSignalsRedaction: true, + enableSignalsIngestion: true, + }) + const fillAndConfirm = async (selector: string, text: string) => { + await page.getByTestId(selector).fill(text) + await page.getByTestId(selector).press('Enter') + } + await Promise.all([ + fillAndConfirm('aria-text-field', 'John Doe'), + waitForCondition( + () => indexPage.signalsAPI.getEvents('interaction').length > 0, + { errorMessage: 'No interaction signals found' } + ), + ]) + const interactionSignals = indexPage.signalsAPI.getEvents('interaction') + + const data = expect.objectContaining({ + eventType: 'change', + listener: 'mutation', + change: { + value: 'John Doe', + }, + target: expect.objectContaining({ + attributes: expect.objectContaining({ + type: 'text', + value: 'John Doe', + }), + tagName: 'INPUT', + value: 'John Doe', + }), + }) + expect(interactionSignals[0].properties!.data).toMatchObject(data) +}) diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/index-page.ts b/packages/signals/signals-integration-tests/src/tests/custom-elements/index-page.ts new file mode 100644 index 000000000..57f96df6c --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/index-page.ts @@ -0,0 +1,7 @@ +import { BasePage } from '../../helpers/base-page-object' + +export class IndexPage extends BasePage { + constructor() { + super(`/custom-elements/index.html`) + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/index.bundle.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/index.bundle.tsx new file mode 100644 index 000000000..53deff5e2 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/index.bundle.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { AnalyticsBrowser } from '@segment/analytics-next' +import { SignalsPlugin } from '@segment/analytics-signals' +import { App } from './components/App' + +window.SignalsPlugin = SignalsPlugin +window.analytics = new AnalyticsBrowser() + +const container = document.getElementById('root') +const root = createRoot(container!) +root.render() diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/index.html b/packages/signals/signals-integration-tests/src/tests/custom-elements/index.html new file mode 100644 index 000000000..5133e5283 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/index.html @@ -0,0 +1,14 @@ + + + + + + + + + +
+ + + + diff --git a/packages/signals/signals-integration-tests/src/tests/performance/index-page.ts b/packages/signals/signals-integration-tests/src/tests/performance/index-page.ts new file mode 100644 index 000000000..6b8df50ed --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/performance/index-page.ts @@ -0,0 +1,7 @@ +import { BasePage } from '../../helpers/base-page-object' + +export class IndexPage extends BasePage { + constructor() { + super(`/performance/index.html`) + } +} diff --git a/packages/signals/signals-integration-tests/src/tests/performance/index.html b/packages/signals/signals-integration-tests/src/tests/performance/index.html new file mode 100644 index 000000000..7c113236e --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/performance/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + +
+ + + diff --git a/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts b/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts new file mode 100644 index 000000000..a8ecb23e1 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts @@ -0,0 +1,97 @@ +import { test, expect, Page } from '@playwright/test' +import { IndexPage } from './index-page' +import { sleep } from '@segment/analytics-core' + +declare global { + interface Window { + gc: () => void // chrome specific + } + interface Performance { + memory: { + usedJSHeapSize: number + totalJSHeapSize: number + } + } +} + +const basicEdgeFn = ` + // this is a process signal function + const processSignal = (signal) => { + if (signal.type === 'interaction') { + const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']' + analytics.track(eventName, signal.data) + } + }` + +const checkForMemoryLeak = async (page: Page) => { + const ALLOWED_GROWTH = 1.1 + const getMemoryUsage = (): Promise => + page.evaluate(() => { + return performance.memory.usedJSHeapSize + }) + + const firstMemory = await getMemoryUsage() + + // add nodes + await page.evaluate(() => { + const target = document.getElementById('test-container')! + const NODE_COUNT = 2000 + for (let i = 0; i < NODE_COUNT; i++) { + const newNode = document.createElement('input') + newNode.type = 'text' + newNode.value = Math.random().toString() + target.appendChild(newNode) + } + }) + + // remove all the nodes + await page.evaluate(() => { + const target = document.getElementById('test-container')! + while (target.firstChild) { + target.removeChild(target.firstChild) + } + }) + const inputNodeLength = await page.evaluate( + () => document.querySelectorAll('input').length + ) + expect(inputNodeLength).toBe(0) + + await page.evaluate(() => { + // force run garbage collection: --js-flags="--expose-gc" is required + window.gc() + }) + + await sleep(500) // may not be needed, but just in case. + + const lastMemory = await getMemoryUsage() // Allow some fluctuation, but fail if there's a significant memory increase + const report = `initial: ${firstMemory}, final: ${lastMemory}, allowed growth: ${ALLOWED_GROWTH}` + if (lastMemory > firstMemory * ALLOWED_GROWTH) { + throw new Error(`Memory leak detected! ${report}`) + } else { + console.log('Memory leak test passed!', `initial: ${report}`) + } +} + +test('memory leak test scaffold works', async ({ page }) => { + const htmlContent = ` + + + + Test Page + + +
+ + + ` + await page.setContent(htmlContent) + await page.waitForLoadState('networkidle') + await checkForMemoryLeak(page) +}) + +test('memory leak', async ({ page }) => { + const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) + await indexPage.waitForSignalsApiFlush() + await page.waitForLoadState('networkidle') + await checkForMemoryLeak(page) +}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.bundle.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.bundle.ts new file mode 100644 index 000000000..4680f5acf --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.bundle.ts @@ -0,0 +1,8 @@ +import { AnalyticsBrowser } from '@segment/analytics-next' +import { SignalsPlugin } from '@segment/analytics-signals' + +/** + * Not instantiating the analytics object here, as it will be instantiated in the test + */ +window.SignalsPlugin = SignalsPlugin +window.analytics = new AnalyticsBrowser() diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-bundle.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-bundle.ts deleted file mode 100644 index cb3644b4e..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-bundle.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AnalyticsBrowser } from '@segment/analytics-next' -import { SignalsPlugin } from '@segment/analytics-signals' - -declare global { - interface Window { - analytics: AnalyticsBrowser - SignalsPlugin: typeof SignalsPlugin - signalsPlugin: SignalsPlugin - } -} - -/** - * Not instantiating the analytics object here, as it will be instantiated in the test - */ -;(window as any).SignalsPlugin = SignalsPlugin -;(window as any).analytics = new AnalyticsBrowser() diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts index 962e2fc1a..e0f7262c0 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts @@ -1,5 +1,6 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' +import { waitForCondition } from '../../helpers/playwright-utils' const indexPage = new IndexPage() @@ -56,6 +57,10 @@ test('debug ingestion disabled and sample rate 1 -> will send the signal', async sampleRate: 1, } ) - await indexPage.fillNameInput('John Doe') - expect(indexPage.signalsAPI.getEvents('interaction')).toHaveLength(1) + await Promise.all([ + indexPage.fillNameInput('John Doe'), + waitForCondition( + () => indexPage.signalsAPI.getEvents('interaction').length > 0 + ), + ]) }) diff --git a/packages/signals/signals-integration-tests/tsconfig.json b/packages/signals/signals-integration-tests/tsconfig.json index c669c9424..30cc88da4 100644 --- a/packages/signals/signals-integration-tests/tsconfig.json +++ b/packages/signals/signals-integration-tests/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../../tsconfig.json", "exclude": ["node_modules", "dist"], "compilerOptions": { + "jsx": "react", "module": "esnext", "target": "ES2022", "moduleResolution": "node", diff --git a/packages/signals/signals-integration-tests/webpack.config.ts b/packages/signals/signals-integration-tests/webpack.config.ts index eeccd8ad6..a22f8c2f7 100644 --- a/packages/signals/signals-integration-tests/webpack.config.ts +++ b/packages/signals/signals-integration-tests/webpack.config.ts @@ -3,8 +3,11 @@ import globby from 'globby' import type { Configuration as WebpackConfiguration } from 'webpack' // This config is for bundling fixtures in order to serve the pages that webdriver.io will use in its tests. -const files = globby.sync('src/tests/*/signals-bundle.ts', { cwd: __dirname }) +const files = globby.sync('src/tests/*/index.bundle.{ts,tsx}', { + cwd: __dirname, +}) +// e.g if file is src/tests/signals-vanilla/index.bundle.ts, then entry is { signals-vanilla: src/tests/signals-vanilla/index.bundle.ts } const entries = files.reduce((acc, file) => { const [dirName] = file.split('/').slice(-2) const base = path.basename(dirName) @@ -15,7 +18,7 @@ const entries = files.reduce((acc, file) => { }, {}) const config: WebpackConfiguration = { - mode: 'development', + mode: 'production', devtool: 'source-map', entry: entries, output: { @@ -24,25 +27,22 @@ const config: WebpackConfiguration = { chunkFilename: '[name].chunk.js', clean: true, }, - target: ['web', 'es5'], + target: ['web'], module: { rules: [ { - test: /\.tsx?$/, - use: [ - { - loader: 'ts-loader', - options: { - configFile: 'tsconfig.json', - transpileOnly: true, - }, - }, - ], + test: /\.(ts|tsx)$/, + use: 'babel-loader', + exclude: /node_modules/, + }, + { + test: /\.css$/i, + use: ['style-loader', 'css-loader'], }, ], }, resolve: { - extensions: ['.ts', '.js'], + extensions: ['.ts', '.js', '.tsx'], }, } diff --git a/packages/signals/signals-runtime/package.json b/packages/signals/signals-runtime/package.json index 3370dfc66..87903a0ab 100644 --- a/packages/signals/signals-runtime/package.json +++ b/packages/signals/signals-runtime/package.json @@ -25,7 +25,7 @@ "build:cjs": "yarn tsc -p tsconfig.build.json --outDir ./dist/cjs --module commonjs", "build:global": "node build-signals-runtime-global.js", "assert-generated": "bash scripts/assert-generated.sh", - "watch": "yarn build:esm --watch", + "watch": "rm -rf dist/esm && yarn build:esm --watch", "watch:test": "yarn test --watch", "tsc": "yarn run -T tsc", "eslint": "yarn run -T eslint", diff --git a/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts b/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts index f9d8f243e..16ee82d4f 100644 --- a/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts +++ b/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts @@ -16,7 +16,12 @@ describe(WebSignalsRuntime, () => { signal2 = mockInteractionSignal signal3 = { ...mockInteractionSignal, - data: { eventType: 'change', target: {} }, + data: { + eventType: 'change', + target: {}, + change: {}, + listener: 'onchange', + }, } mockSignals = [signal1, signal2, signal3] signalsRuntime = new WebSignalsRuntime(mockSignals) diff --git a/packages/signals/signals-runtime/src/web/web-signals-types.ts b/packages/signals/signals-runtime/src/web/web-signals-types.ts index 6380011cb..37baf42b4 100644 --- a/packages/signals/signals-runtime/src/web/web-signals-types.ts +++ b/packages/signals/signals-runtime/src/web/web-signals-types.ts @@ -7,27 +7,51 @@ export interface RawSignal extends BaseSignal { data: Data metadata?: Record } - export type InteractionData = ClickData | SubmitData | ChangeData -interface SerializedTarget { +export type ParsedAttributes = { [attributeName: string]: string | null } + +export interface TargetedHTMLElement { + id: string + attributes: ParsedAttributes [key: string]: any } type ClickData = { eventType: 'click' - target: SerializedTarget + target: TargetedHTMLElement } type SubmitData = { eventType: 'submit' - submitter?: SerializedTarget - target: SerializedTarget + submitter?: TargetedHTMLElement + target: TargetedHTMLElement } -type ChangeData = { +export type ChangeData = { eventType: 'change' - [key: string]: unknown + /** + * The target element that changed. + */ + target: TargetedHTMLElement + /** + * The name/type of "listener" that triggered the change. + * Elements can change due to a variety of reasons, such as a mutation, a change event, or a contenteditable change + */ + listener: 'contenteditable' | 'onchange' | 'mutation' + /** + * The change that occurred -- this is a key-value object of the change that occurred + * For mutation listeners, this is the attributes that changed + * For contenteditable listeners, this is the text that changed + * @example + * ```ts + * { checked: true } // onchange + * { value: 'new value' } // onchange / mutation + * {'aria-selected': 'true' } // mutation + * { textContent: 'Sentence1\nSentence2\n' } // contenteditable + * ``` + */ + change: JSONValue } export type InteractionSignal = RawSignal<'interaction', InteractionData> diff --git a/packages/signals/signals/package.json b/packages/signals/signals/package.json index 10a92cf72..380274a32 100644 --- a/packages/signals/signals/package.json +++ b/packages/signals/signals/package.json @@ -32,7 +32,7 @@ "build:bundle": "NODE_ENV=production yarn run webpack", "workerbox": "node scripts/build-workerbox.js", "assert-generated": "sh scripts/assert-workerbox-built.sh", - "watch": "yarn concurrently 'yarn build:bundle --watch' 'yarn build:esm --watch'", + "watch": "rm -rf dist && yarn concurrently 'yarn build:bundle --watch' 'yarn build:esm --watch'", "version": "sh scripts/version.sh", "watch:test": "yarn test --watch", "tsc": "yarn run -T tsc", diff --git a/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts b/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts index 8b639d79e..54a07d6f3 100644 --- a/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts +++ b/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts @@ -1,8 +1,17 @@ import { sleep } from '@segment/analytics-core' +import { createMockTarget } from '../../../test-helpers/mocks/factories' import { range } from '../../../test-helpers/range' import { createInteractionSignal } from '../../../types/factories' import { getSignalBuffer, SignalBuffer } from '../index' +const createMockSignal = () => + createInteractionSignal({ + eventType: 'submit', + target: createMockTarget({ + id: Math.random().toString(), + }), + }) + describe(getSignalBuffer, () => { let buffer: SignalBuffer beforeEach(async () => { @@ -19,7 +28,7 @@ describe(getSignalBuffer, () => { it('should add and clear', async () => { const mockSignal = createInteractionSignal({ eventType: 'submit', - target: {}, + target: createMockTarget({}), }) await buffer.add(mockSignal) await expect(buffer.getAll()).resolves.toEqual([mockSignal]) @@ -28,13 +37,7 @@ describe(getSignalBuffer, () => { }) it('should delete older signals when maxBufferSize is exceeded', async () => { - const signals = range(15).map((_, idx) => - createInteractionSignal({ - idx: idx, - eventType: 'change', - target: {}, - }) - ) + const signals = range(15).map(() => createMockSignal()) for (const signal of signals) { await buffer.add(signal) @@ -46,13 +49,7 @@ describe(getSignalBuffer, () => { }) it('should delete older signals on initialize if current number exceeds maxBufferSize', async () => { - const signals = range(15).map((_, idx) => - createInteractionSignal({ - idx: idx, - eventType: 'change', - target: {}, - }) - ) + const signals = range(15).map((_) => createMockSignal()) for (const signal of signals) { await buffer.add(signal) @@ -69,10 +66,7 @@ describe(getSignalBuffer, () => { }) it('should clear signal buffer if there is a new session according to session storage', async () => { - const mockSignal = createInteractionSignal({ - eventType: 'submit', - target: {}, - }) + const mockSignal = createMockSignal() await buffer.add(mockSignal) await expect(buffer.getAll()).resolves.toEqual([mockSignal]) @@ -92,10 +86,7 @@ describe(getSignalBuffer, () => { }) it('should add and clear', async () => { - const mockSignal = createInteractionSignal({ - eventType: 'submit', - target: {}, - }) + const mockSignal = createMockSignal() await buffer.add(mockSignal) await expect(buffer.getAll()).resolves.toEqual([mockSignal]) await buffer.clear() @@ -103,13 +94,7 @@ describe(getSignalBuffer, () => { }) it('should delete older signals when maxBufferSize is exceeded', async () => { - const signals = range(15).map((_, idx) => - createInteractionSignal({ - idx: idx, - eventType: 'change', - target: {}, - }) - ) + const signals = range(15).map(() => createMockSignal()) for (const signal of signals) { await buffer.add(signal) diff --git a/packages/signals/signals/src/core/client/__tests__/redact.test.ts b/packages/signals/signals/src/core/client/__tests__/redact.test.ts index 7ffe5c626..21959fe52 100644 --- a/packages/signals/signals/src/core/client/__tests__/redact.test.ts +++ b/packages/signals/signals/src/core/client/__tests__/redact.test.ts @@ -1,4 +1,5 @@ import { NetworkSignalMetadata } from '@segment/analytics-signals-runtime' +import { createMockTarget } from '../../../test-helpers/mocks/factories' import * as factories from '../../../types/factories' import { redactJsonValues, redactSignalData } from '../redact' @@ -79,11 +80,58 @@ describe(redactSignalData, () => { it('should redact the value in the "target" property if the type is "interaction"', () => { const signal = factories.createInteractionSignal({ eventType: 'change', - target: { value: 'secret', formData: { password: '123' } }, + change: { value: 'secret' }, + listener: 'onchange', + target: createMockTarget({ + value: 'secret', + formData: { password: '123' }, + }), }) const expected = factories.createInteractionSignal({ eventType: 'change', - target: { value: 'XXX', formData: { password: 'XXX' } }, + change: { value: 'XXX' }, + listener: 'onchange', + target: createMockTarget({ value: 'XXX', formData: { password: 'XXX' } }), + }) + expect(redactSignalData(signal)).toEqual(expected) + }) + + it('should redact attributes in change and in target if the listener is "mutation"', () => { + const signal = factories.createInteractionSignal({ + eventType: 'change', + change: { 'aria-selected': 'value' }, + listener: 'mutation', + target: createMockTarget({ + attributes: { 'aria-selected': 'value', foo: 'value' }, + textContent: 'value', + innerText: 'value', + }), + }) + const expected = factories.createInteractionSignal({ + eventType: 'change', + change: { 'aria-selected': 'XXX' }, + listener: 'mutation', + target: createMockTarget({ + attributes: { 'aria-selected': 'XXX', foo: 'XXX' }, + textContent: 'XXX', + innerText: 'XXX', + }), + }) + expect(redactSignalData(signal)).toEqual(expected) + }) + + it('should redact the textContent and innerText in the "target" property if the listener is "contenteditable"', () => { + const signal = factories.createInteractionSignal({ + eventType: 'change', + listener: 'contenteditable', + change: { textContent: 'secret' }, + target: createMockTarget({ textContent: 'secret', innerText: 'secret' }), + }) + const expected = factories.createInteractionSignal({ + eventType: 'change', + listener: 'contenteditable', + change: { textContent: 'XXX' }, + target: createMockTarget({ textContent: 'XXX', innerText: 'XXX' }), }) expect(redactSignalData(signal)).toEqual(expected) }) @@ -115,7 +163,7 @@ describe(redactSignalData, () => { it('should not mutate the original signal object', () => { const originalSignal = factories.createInteractionSignal({ eventType: 'click', - target: { value: 'sensitiveData' }, + target: createMockTarget({ value: 'sensitiveData' }), }) const originalSignalCopy = JSON.parse(JSON.stringify(originalSignal)) diff --git a/packages/signals/signals/src/core/client/redact.ts b/packages/signals/signals/src/core/client/redact.ts index 6f7a7eb61..b6bc60938 100644 --- a/packages/signals/signals/src/core/client/redact.ts +++ b/packages/signals/signals/src/core/client/redact.ts @@ -1,5 +1,10 @@ import { Signal } from '@segment/analytics-signals-runtime' +/** + * This is a very imperfect redaction. + * Issues: + * - innerText could contain sensitive data, and be leaked depending + */ export const redactSignalData = (signalArg: Signal): Signal => { const signal = structuredClone(signalArg) if (signal.type === 'interaction') { @@ -8,14 +13,63 @@ export const redactSignalData = (signalArg: Signal): Signal => { signal.data.target && typeof signal.data.target === 'object' ) { - if ('value' in signal.data.target) { + if ( + 'value' in signal.data.target && + signal.data.target.value !== undefined + ) { signal.data.target.value = redactJsonValues(signal.data.target.value) } + if ( + 'checked' in signal.data.target && + signal.data.target.checked !== undefined + ) { + signal.data.target.checked = redactJsonValues( + signal.data.target.checked + ) + } + if ('formData' in signal.data.target) { signal.data.target.formData = redactJsonValues( signal.data.target.formData ) } + + if (signal.data.eventType === 'change') { + if ('change' in signal.data) { + signal.data.change = redactJsonValues(signal.data.change) + } + + if (signal.data.listener === 'mutation') { + if ('innerText' in signal.data.target) { + signal.data.target.innerText = redactJsonValues( + signal.data.target.innerText + ) + } + if ('textContent' in signal.data.target) { + signal.data.target.textContent = redactJsonValues( + signal.data.target.textContent + ) + } + if ('attributes' in signal.data.target) { + signal.data.target.attributes = redactJsonValues( + signal.data.target.attributes + ) + } + } + + if (signal.data.listener === 'contenteditable') { + if ('textContent' in signal.data.target) { + signal.data.target.textContent = redactJsonValues( + signal.data.target.textContent + ) + } + if ('innerText' in signal.data.target) { + signal.data.target.innerText = redactJsonValues( + signal.data.target.innerText + ) + } + } + } } } else if (signal.type === 'network') { signal.data = redactJsonValues(signal.data, 2) diff --git a/packages/signals/signals/src/core/emitter/index.ts b/packages/signals/signals/src/core/emitter/index.ts index c9cc705e2..a480721c4 100644 --- a/packages/signals/signals/src/core/emitter/index.ts +++ b/packages/signals/signals/src/core/emitter/index.ts @@ -6,12 +6,23 @@ export interface EmitSignal { emit: (signal: Signal) => void } +const logSignal = (signal: Signal) => { + logger.info( + 'New signal:', + signal.type, + signal.data, + ...(signal.type === 'interaction' && 'change' in signal.data + ? ['change:', JSON.stringify(signal.data.change, null, 2)] + : []) + ) +} + export class SignalEmitter implements EmitSignal { private emitter = new Emitter<{ add: [Signal] }>() private listeners = new Set<(signal: Signal) => void>() emit(signal: Signal) { - logger.info('New signal:', signal.type, signal.data) + logSignal(signal) this.emitter.emit('add', signal) } diff --git a/packages/signals/signals/src/core/signal-generators/__tests__/dom-gen-helpers.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/clean-text.test.ts similarity index 97% rename from packages/signals/signals/src/core/signal-generators/__tests__/dom-gen-helpers.test.ts rename to packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/clean-text.test.ts index a87f58a38..5e7f42a9c 100644 --- a/packages/signals/signals/src/core/signal-generators/__tests__/dom-gen-helpers.test.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/clean-text.test.ts @@ -1,4 +1,4 @@ -import { cleanText } from '../dom-gen' +import { cleanText } from '../helpers' describe(cleanText, () => { test('should remove newline characters', () => { diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts new file mode 100644 index 000000000..39c3666cd --- /dev/null +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts @@ -0,0 +1,212 @@ +import { logger } from '../../../lib/logger' +import { createInteractionSignal } from '../../../types/factories' +import { SignalEmitter } from '../../emitter' +import { SignalGlobalSettings } from '../../signals' +import { SignalGenerator } from '../types' +import { shouldIgnoreElement, parseElement } from './dom-gen' +import { + MutationObservable, + AttributeChangedEvent, + MutationObservableSettings, +} from './mutation-observer' + +export class MutationGeneratorSettings extends MutationObservableSettings {} + +export class MutationChangeGenerator implements SignalGenerator { + id = 'mutation' + private elMutObserver: MutationObservable + /** + * Custom selectors that should be ignored by the mutation observer + * e.g if you have a custom input field that is not a standard input field, you can add it here + */ + customSelectors = [] + constructor(settings: SignalGlobalSettings) { + this.elMutObserver = new MutationObservable(settings.mutationGenerator) + } + + register(emitter: SignalEmitter) { + type NormalizedAttributes = { [attributeName: string]: string | null } + const normalizeAttributes = ( + attributeMutation: AttributeChangedEvent + ): NormalizedAttributes => { + const attributes = + attributeMutation.attributes.reduce( + (acc, { attributeName, newValue }) => { + acc[attributeName] = newValue + return acc + }, + {} + ) + return attributes + } + + const callback = (ev: AttributeChangedEvent) => { + const target = ev.element as HTMLElement | null + if (!target || shouldIgnoreElement(target)) { + return + } + const el = parseElement(ev.element) + emitter.emit( + createInteractionSignal({ + eventType: 'change', + target: el, + listener: 'mutation', + change: normalizeAttributes(ev), + }) + ) + } + this.elMutObserver.subscribe(callback) + return () => this.elMutObserver.cleanup() + } +} + +export class OnChangeGenerator implements SignalGenerator { + id = 'change' + + register(emitter: SignalEmitter) { + /** + * Magic attributes that we use to normalize the API between the mutation listener + * and the onchange listener + */ + type ChangedEvent = { + checked?: boolean + value?: string + files?: string[] + selectedOptions?: { label: string; value: string }[] + } + + /** + * Extract the change from a change event for stateless elistener lements, + * so we can normalize the response between mutation listener changes and onchange listener events + */ + const parseChange = (target: HTMLElement): ChangedEvent | undefined => { + if (target instanceof HTMLSelectElement) { + return { + selectedOptions: Array.from(target.selectedOptions), + } + } + if (target instanceof HTMLTextAreaElement) { + return { value: target.value } + } + if (target instanceof HTMLInputElement) { + if ('value' in target || 'checked' in target) { + if (target.type === 'checkbox' || target.type === 'radio') { + return { checked: target.checked } + } + if (target.type === 'file') { + return { + files: Array.from(target.files ?? []).map((f) => f.name), + } + } + return { value: target.value } + } + } + } + const isHandledByMutationObserver = (el: HTMLElement): boolean => { + // check if the element is stateful -- if it is, we should ignore the onchange event since the mutation observer will pick it up + // input fields where can modify the field through interactions: + const inputTypesWithMutableValue = [ + 'text', + 'password', + 'email', + 'url', + 'tel', + 'number', + 'search', + 'date', + 'time', + 'datetime-local', + 'month', + 'week', + 'color', + 'range', + ] + const type = el.getAttribute('type') + const isInput = el instanceof HTMLInputElement + if ( + isInput && + (type === null || inputTypesWithMutableValue.includes(type)) + ) { + return el.getAttribute('value') !== null + } + return false + } + + // vanilla change events do not trigger dom updates. + const handleOnChangeEvent = (ev: Event) => { + const target = ev.target as HTMLElement | null + if (!target || shouldIgnoreElement(target)) { + return + } + // if the element is an input with a value, we can use mutation observer to get the new value, so we don't send duplicate signals + // this can really only happen with inputs, so we don't need to check for other elements + // This is very hacky -- onChange has different semantics than the value mutation (onchange event only fires when the element loses focus), so it's not a perfect match. + // We're not sure what the tolerance for duplicate-ish signals is since we have both strategies available? + if (isHandledByMutationObserver(target)) { + logger.debug('Ignoring onchange event in stateful element', target) + return + } + + const el = parseElement(target) + const change = parseChange(target) + if (!change) { + logger.debug( + 'No change found on element..., this should not happen', + el + ) + return + } + emitter.emit( + createInteractionSignal({ + eventType: 'change', + listener: 'onchange', + target: el, + change, + }) + ) + } + + document.addEventListener('change', handleOnChangeEvent, true) + return () => { + document.removeEventListener('change', handleOnChangeEvent, true) + } + } +} + +export class ContentEditableChangeGenerator implements SignalGenerator { + id = 'contenteditable' + register(emitter: SignalEmitter) { + const commitChange = (ev: Event) => { + if (!(ev.target instanceof HTMLElement)) { + return + } + const target = ev.target as HTMLElement + const el = parseElement(target) + emitter.emit( + createInteractionSignal({ + eventType: 'change', + listener: 'contenteditable', + target: el, + change: { + textContent: el.textContent || null, + }, + }) + ) + } + + const handleContentEditableChange = (ev: Event) => { + const target = ev.target as HTMLElement | null + const editable = target instanceof HTMLElement && target.isContentEditable + if (!editable) { + return + } + + // normalize so this behaves like a change event on an input field -- so it doesn't fire on every keystroke. + target.addEventListener('blur', commitChange, { once: true }) + } + document.addEventListener('input', handleContentEditableChange, true) + + return () => + document.removeEventListener('input', handleContentEditableChange) + } +} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts similarity index 65% rename from packages/signals/signals/src/core/signal-generators/dom-gen.ts rename to packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts index 94e39c4cb..115e02594 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts @@ -1,16 +1,17 @@ -import { URLChangeObservable } from '../../lib/detect-url-change' -import { logger } from '../../lib/logger' +import { URLChangeObservable } from '../../../lib/detect-url-change' import { createInteractionSignal, createNavigationSignal, -} from '../../types/factories' -import { SignalEmitter } from '../emitter' -import { SignalGenerator } from './types' +} from '../../../types/factories' +import { SignalEmitter } from '../../emitter' +import { SignalGenerator } from '../types' +import { cleanText } from './helpers' +import type { ParsedAttributes } from '@segment/analytics-signals-runtime' interface Label { textContent: string id: string - attributes: Record + attributes: ParsedAttributes } const parseFormData = (data: FormData): Record => { @@ -26,48 +27,66 @@ const parseLabels = ( labels: NodeListOf | null | undefined ): Label[] => { if (!labels) return [] - return [...labels] - .map((label) => ({ - id: label.id, - attributes: parseNodeMap(label.attributes), - textContent: label.textContent ? cleanText(label.textContent) : undefined, - })) - .filter((el): el is Label => Boolean(el.textContent)) + return [...labels].map(parseToLabel).filter((el): el is Label => Boolean(el)) } -const parseNodeMap = (nodeMap: NamedNodeMap): Record => { - return Array.from(nodeMap).reduce((acc, attr) => { - acc[attr.name] = attr.value - return acc - }, {} as Record) +const parseToLabel = (label: HTMLElement): Label => { + const textContent = label.textContent ? cleanText(label.textContent) : '' + return { + id: label.id, + attributes: parseNodeMap(label.attributes), + textContent, + } } -export const cleanText = (str: string): string => { - return str - .replace(/[\r\n\t]+/g, ' ') // Replace newlines and tabs with a space - .replace(/\s\s+/g, ' ') // Replace multiple spaces with a single space - .replace(/\u00A0/g, ' ') // Replace non-breaking spaces with a regular space - .trim() // Trim leading and trailing spaces +const parseNodeMap = (nodeMap: NamedNodeMap): ParsedAttributes => { + return Array.from(nodeMap).reduce((acc, attr) => { + acc[attr.name] = attr.value + return acc + }, {}) } interface ParsedElementBase { - attributes: Record + /** + * The attributes of the element -- this is a key-value object of the attributes of the element + */ + attributes: ParsedAttributes classList: string[] id: string + /** + * The labels associated with this element -- either from the `labels` property or from the `aria-labelledby` attribute + */ labels?: Label[] + /** + * The first label associated with this element -- either from the `labels` property or from the `aria-labelledby` attribute + */ label?: Label name?: string nodeName: string tagName: string title: string type?: string + + /** + * The value of the element -- for inputs, this is the value of the input, for selects, this is the value of the selected option + */ value?: string + /** + * The value content of the element -- this is the value content of the element, stripped of newlines, tabs, and multiple spaces + */ textContent?: string + /** + * The inner value of the element -- this is the value content of the element, stripped of newlines, tabs, and multiple spaces + */ innerText?: string + /** + * The element referenced by the `aria-describedby` attribute + */ + describedBy?: Label } interface ParsedSelectElement extends ParsedElementBase { - selectedOptions: { value: string; text: string }[] + selectedOptions: { label: string; value: string }[] selectedIndex: number } interface ParsedInputElement extends ParsedElementBase { @@ -99,8 +118,26 @@ type AnyParsedElement = | ParsedMediaElement | ParsedElementBase -const parseElement = (el: HTMLElement): AnyParsedElement => { +/** + * Get the element referenced from an type + */ +const getReferencedElement = ( + el: HTMLElement, + attr: string +): HTMLElement | undefined => { + const value = el.getAttribute(attr) + if (!value) return undefined + return document.getElementById(value) ?? undefined +} + +export const parseElement = (el: HTMLElement): AnyParsedElement => { const labels = parseLabels((el as HTMLInputElement).labels) + const labeledBy = getReferencedElement(el, 'aria-labelledby') + const describedBy = getReferencedElement(el, 'aria-describedby') + if (labeledBy) { + const label = parseToLabel(labeledBy) + labels.unshift(label) + } const base: ParsedElementBase = { // adding a bunch of fields that are not on _all_ elements, but are on enough that it's useful to have them here. attributes: parseNodeMap(el.attributes), @@ -116,6 +153,7 @@ const parseElement = (el: HTMLElement): AnyParsedElement => { value: (el as HTMLInputElement).value, textContent: (el.textContent && cleanText(el.textContent)) ?? undefined, innerText: (el.innerText && cleanText(el.innerText)) ?? undefined, + describedBy: (describedBy && parseToLabel(describedBy)) ?? undefined, } if (el instanceof HTMLSelectElement) { @@ -123,7 +161,7 @@ const parseElement = (el: HTMLElement): AnyParsedElement => { ...base, selectedOptions: [...el.selectedOptions].map((option) => ({ value: option.value, - text: option.text, + label: option.label, })), selectedIndex: el.selectedIndex, } @@ -179,8 +217,22 @@ export class ClickSignalsGenerator implements SignalGenerator { } private getClosestClickableElement(el: HTMLElement): HTMLElement | null { - // if you click on a nested element, we want to get the closest clickable ancestor. Useful for things like buttons with nested text or images - return el.closest('button, a, [role="button"], [role="link"]') + // if you click on a nested element, we want to get the closest clickable ancestor. Useful for things like buttons with nested value or images + const selector = [ + 'button', + 'a', + 'option', + '[role="button"]', + '[role="link"]', + '[role="menuitem"]', + '[role="menuitemcheckbox"]', + '[role="menuitemradio"]', + '[role="tab"]', + '[role="option"]', + '[role="switch"]', + '[role="treeitem"]', + ].join(', ') + return el.closest(selector) } } @@ -208,28 +260,11 @@ export class FormSubmitGenerator implements SignalGenerator { } } -export class OnChangeGenerator implements SignalGenerator { - id = 'change' - register(emitter: SignalEmitter) { - const handleChange = (ev: Event) => { - const target = ev.target as HTMLElement | null - if (!target) return - if (target && target instanceof HTMLInputElement) { - if (target.type === 'password') { - logger.debug('Ignoring change event for input', target) - return - } - } - emitter.emit( - createInteractionSignal({ - eventType: 'change', - target: parseElement(target), - }) - ) - } - document.addEventListener('change', handleChange, true) - return () => document.removeEventListener('change', handleChange) +export const shouldIgnoreElement = (el: HTMLElement): boolean => { + if (el instanceof HTMLInputElement) { + return el.type === 'password' } + return false } export class OnNavigationEventGenerator implements SignalGenerator { @@ -272,10 +307,3 @@ export class OnNavigationEventGenerator implements SignalGenerator { } } } - -export const domGenerators = [ - ClickSignalsGenerator, - FormSubmitGenerator, - OnChangeGenerator, - OnNavigationEventGenerator, -] diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts new file mode 100644 index 000000000..4383aa3cb --- /dev/null +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts @@ -0,0 +1,7 @@ +export const cleanText = (str: string): string => { + return str + .replace(/[\r\n\t]+/g, ' ') // Replace newlines and tabs with a space + .replace(/\s\s+/g, ' ') // Replace multiple spaces with a single space + .replace(/\u00A0/g, ' ') // Replace non-breaking spaces with a regular space + .trim() // Trim leading and trailing spaces +} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/index.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/index.ts new file mode 100644 index 000000000..1c2675154 --- /dev/null +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/index.ts @@ -0,0 +1,20 @@ +import { + ClickSignalsGenerator, + FormSubmitGenerator, + OnNavigationEventGenerator, +} from './dom-gen' +import { + MutationChangeGenerator, + OnChangeGenerator, + ContentEditableChangeGenerator, +} from './change-gen' +import { SignalGeneratorClass } from '../types' + +export const domGenerators: SignalGeneratorClass[] = [ + MutationChangeGenerator, + OnChangeGenerator, + ContentEditableChangeGenerator, + ClickSignalsGenerator, + FormSubmitGenerator, + OnNavigationEventGenerator, +] diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts new file mode 100644 index 000000000..02f454f99 --- /dev/null +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -0,0 +1,302 @@ +import { Emitter } from '@segment/analytics-generic-utils' +import { exists } from '../../../lib/exists' +import { debounceWithKey } from '../../../lib/debounce' + +const DEFAULT_OBSERVED_ATTRIBUTES = [ + 'aria-pressed', + 'aria-checked', + 'aria-modal', + 'aria-selected', + 'value', + 'checked', + 'data-selected', +] +const DEFAULT_OBSERVED_TAGS = ['input', 'label', 'option', 'select', 'textarea'] +const DEFAULT_OBSERVED_ROLES = [ + 'button', + 'checkbox', + 'dialog', + 'gridcell', + 'row', + 'searchbox', + 'menuitemcheckbox', + 'menuitemradio', + 'option', + 'radio', + 'scrollbar', + 'slider', + 'spinbutton', + 'switch', + 'tab', + 'treeitem', +] + +type AttributeMutation = { + attributeName: string + newValue: string | null +} +export type AttributeChangedEvent = { + element: HTMLElement + attributes: AttributeMutation[] +} + +export interface MutationObservableSettingsConfig { + extraSelectors?: string[] + pollIntervalMs?: number + debounceMs?: number + emitInputStrategy?: 'debounce-only' | 'blur' // the blur strategy seems to have an issue where it does not alwaus register when the page loads? It's also pretty finicky / manual. + observedRoles?: (defaultObservedRoles: string[]) => string[] + observedTags?: (defaultObservedTags: string[]) => string[] + observedAttributes?: (defaultObservedAttributes: string[]) => string[] +} + +export class MutationObservableSettings { + pollIntervalMs: number + debounceTextInputMs: number + emitInputStrategy: 'debounce-only' | 'blur' + extraSelectors: string[] + observedRoles: string[] + observedTags: string[] + observedAttributes: string[] + constructor(config: MutationObservableSettingsConfig = {}) { + const { + pollIntervalMs = 400, + debounceMs = 1000, + emitInputStrategy = 'debounce-only', + extraSelectors = [], + observedRoles, + observedTags, + observedAttributes, + } = config + if (pollIntervalMs < 300) { + throw new Error('Poll interval must be at least 300ms') + } + if (debounceMs < 100) { + throw new Error('Debounce must be at least 100ms') + } + this.emitInputStrategy = emitInputStrategy + this.pollIntervalMs = pollIntervalMs + this.debounceTextInputMs = debounceMs + this.extraSelectors = extraSelectors + + this.observedRoles = observedRoles + ? observedRoles(DEFAULT_OBSERVED_ROLES) + : DEFAULT_OBSERVED_ROLES + this.observedTags = observedTags + ? observedTags(DEFAULT_OBSERVED_TAGS) + : DEFAULT_OBSERVED_TAGS + this.observedAttributes = observedAttributes + ? observedAttributes(DEFAULT_OBSERVED_ATTRIBUTES) + : DEFAULT_OBSERVED_ATTRIBUTES + } +} + +const shouldDebounce = (el: HTMLElement): boolean => { + const MUTABLE_INPUT_TYPES = new Set([ + 'text', + 'password', + 'email', + 'url', + 'tel', + 'number', + 'search', + 'date', + 'time', + 'datetime-local', + 'month', + 'week', + 'color', + 'range', + null, // same as 'text' + ]) + + const ROLES = new Set(['spinbutton']) + const isInput = + el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement + + const isContentEditable = el.isContentEditable + if (isContentEditable) { + return true + } + if (!isInput) { + return false + } + + const type = el.getAttribute('type') + if (MUTABLE_INPUT_TYPES.has(type)) { + return true + } + const role = el.getAttribute('role') + if (role && ROLES.has(role)) { + return true + } + return false +} + +/** + * This class is responsible for observing changes to elements in the DOM + * This is preferred over monitoring document 'change' events, as it captures changes to custom elements + */ +export class MutationObservable { + private settings: MutationObservableSettings + // Track observed elements to avoid duplicate observers + // WeakSet is used here to allow garbage collection of elements that are no longer in the DOM + private observedElements = new WeakSet() + private emitter = new ElementChangedEmitter() + private listeners = new Set<(event: AttributeChangedEvent) => void>() + + subscribe(fn: (event: AttributeChangedEvent) => void) { + this.listeners.add(fn) + this.emitter.on('attributeChanged', fn) + } + + cleanup() { + this.listeners.forEach((fn) => this.emitter.off('attributeChanged', fn)) + this.listeners.clear() + clearInterval(this.pollTimeout) + } + + private pollTimeout: ReturnType + + constructor( + settings: MutationObservableSettingsConfig | MutationObservableSettings = {} + ) { + this.settings = + settings instanceof MutationObservableSettings + ? settings + : new MutationObservableSettings(settings) + + this.checkForNewElements(this.emitter) + + this.pollTimeout = setInterval( + () => this.checkForNewElements(this.emitter), + this.settings.pollIntervalMs + ) + } + + private shouldEmitEvent(mut: AttributeMutation): boolean { + // Filter out aria-selected events where the new value is false, since there will always be another selected value -- otherwise, checked would/should be used + if (mut.attributeName === 'aria-selected' && mut.newValue === 'false') { + return false + } + return true + } + + private experimentalOnChangeAdapter = new ExperimentalOnChangeEventAdapter() + + private observeElementAttributes( + element: HTMLElement, + attributes: string[], + emitter: ElementChangedEmitter + ) { + const _emitAttributeMutationEvent = (attributes: AttributeMutation[]) => { + emitter.emit('attributeChanged', { + element, + attributes, + }) + } + const addOnBlurListener = (attributeMutations: AttributeMutation[]) => + this.experimentalOnChangeAdapter.onBlur(element, () => + _emitAttributeMutationEvent(attributeMutations) + ) + + const emit = + this.settings.emitInputStrategy === 'blur' + ? addOnBlurListener + : _emitAttributeMutationEvent + + const _emitAttributeMutationEventDebounced = shouldDebounce(element) + ? debounceWithKey( + emit, + // debounce based on the attribute names, so that we can debounce all changes to a single attribute. e.g if attribute "value" changes, that gets debounced, but if another attribute changes, that gets debounced separately + (m) => Object.keys(m.map((m) => m.attributeName)).sort(), + this.settings.debounceTextInputMs + ) + : _emitAttributeMutationEvent + + const cb: MutationCallback = (mutationsList) => { + const attributeMutations = mutationsList + .filter((m) => m.type === 'attributes') + .map((m) => { + const attributeName = m.attributeName + if (!attributeName) return + const newValue = element.getAttribute(attributeName) + const v: AttributeMutation = { + attributeName, + newValue: newValue, + } + return v + }) + .filter(exists) + .filter((event) => this.shouldEmitEvent(event)) + + if (attributeMutations.length) { + _emitAttributeMutationEventDebounced(attributeMutations) + } + } + + const observer = new MutationObserver(cb) + + observer.observe(element, { + attributes: true, + attributeFilter: attributes, + subtree: false, + }) + + this.observedElements.add(element) + } + + private checkForNewElements(emitter: ElementChangedEmitter) { + const allElementSelectors = [ + ...this.settings.observedRoles.map((role) => `[role="${role}"]`), + ...this.settings.observedTags, + ...this.settings.extraSelectors, + ] + allElementSelectors.forEach((selector) => { + const elements = document.querySelectorAll(selector) + elements.forEach((element) => { + if (this.observedElements.has(element)) { + return + } + this.observeElementAttributes( + element as HTMLElement, + this.settings.observedAttributes, + emitter + ) + }) + }) + } +} + +/** + * This class is responsible for normalizing listener behavior so that events are only emitted once -- just like 'change' events + */ +class ExperimentalOnChangeEventAdapter { + private inputListeners: Map = new Map() + private removeListener(element: HTMLElement) { + const oldListener = this.inputListeners.get(element) + if (oldListener) { + element.removeEventListener('blur', oldListener) + } + } + onBlur(element: HTMLElement, cb: () => void) { + this.removeListener(element) + element.addEventListener('blur', cb, { once: true }) // once: true is important here, otherwise we'd get duplicate events if someone clicks out of the input and then back in + // on 'enter' keydown, we also want to emit the event + element.addEventListener( + 'keydown', + (event) => { + if (event.key === 'Enter') { + cb() + } + }, + { once: true } + ) + this.inputListeners.set(element, cb) + } +} + +type EmitterContract = { + attributeChanged: [AttributeChangedEvent] +} +class ElementChangedEmitter extends Emitter {} diff --git a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts b/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts index be53ba44c..4e418cbff 100644 --- a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts +++ b/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts @@ -283,7 +283,7 @@ describe(NetworkGenerator, () => { unregister() }) - it('emits signals for same domain if networkSignalsAllowSameDomain = false', async () => { + it('will not emit signals for same domain if networkSignalsAllowSameDomain = false', async () => { const mockEmitter = { emit: jest.fn() } const networkGenerator = new TestNetworkGenerator({ networkSignalsAllowList: ['foo.com'], @@ -301,7 +301,10 @@ describe(NetworkGenerator, () => { }) await sleep(100) - expect(mockEmitter.emit.mock.calls.length).toBe(0) + let requests = mockEmitter.emit.mock.calls.filter( + (c) => c[0].data.action === 'request' + ) + expect(requests.length).toBe(0) await window.fetch(`http://foo.com/test`, { method: 'POST', @@ -310,7 +313,10 @@ describe(NetworkGenerator, () => { }) await sleep(100) - expect(mockEmitter.emit.mock.calls.length).toBe(2) + requests = mockEmitter.emit.mock.calls.filter( + (c) => c[0].data.action === 'request' + ) + expect(requests.length).toBe(1) unregister() }) @@ -336,6 +342,36 @@ describe(NetworkGenerator, () => { unregister() }) + it('allows an explicit disallow list to override same-domain signals', async () => { + const mockEmitter = { emit: jest.fn() } + const networkGenerator = new TestNetworkGenerator({ + networkSignalsDisallowList: ['/foo'], + }) + const unregister = networkGenerator.register( + mockEmitter as unknown as SignalEmitter + ) + + await window.fetch(`/test/foo`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ key: 'value' }), + }) + + await window.fetch(`/test/bar`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ key: 'value' }), + }) + + await sleep(100) + const requests = mockEmitter.emit.mock.calls + .filter((c) => c[0].data.action === 'request') + .flatMap((c) => c[0].data.url) + expect(requests.length).toBe(1) + expect(requests[0]).toBe('http://localhost/test/bar') + unregister() + }) + it('always disallows segment api network signals', async () => { const mockEmitter = { emit: jest.fn() } const networkGenerator = new TestNetworkGenerator({ diff --git a/packages/signals/signals/src/core/signal-generators/network-gen/network-signals-filter.ts b/packages/signals/signals/src/core/signal-generators/network-gen/network-signals-filter.ts index c71ea59c6..c027626da 100644 --- a/packages/signals/signals/src/core/signal-generators/network-gen/network-signals-filter.ts +++ b/packages/signals/signals/src/core/signal-generators/network-gen/network-signals-filter.ts @@ -65,11 +65,12 @@ export class NetworkSignalsFilterList { ]) } + isDisallowed(url: string): boolean { + return this.disallowed.test(url) || this.disallowedDefaults.test(url) + } + isAllowed(url: string): boolean { - const disallowed = - this.disallowed.test(url) || this.disallowedDefaults.test(url) - const allowed = this.allowed.test(url) - return allowed && !disallowed + return this.allowed.test(url) } getRegexes() { @@ -93,10 +94,16 @@ export class NetworkSignalsFilter { const { networkSignalsFilterList, networkSignalsAllowSameDomain } = this.settings - const passesNetworkFilter = networkSignalsFilterList.isAllowed(url) - const allowedBecauseSameDomain = - networkSignalsAllowSameDomain && isSameDomain(url) - const allowed = passesNetworkFilter || allowedBecauseSameDomain + // anything that is disallowed takes precedence over the allow list. + if (networkSignalsFilterList.isDisallowed(url)) { + return false + } + + const allowed = + // allowed because it's in the allow list + networkSignalsFilterList.isAllowed(url) || + // allowed because it's the same domain + (networkSignalsAllowSameDomain && isSameDomain(url)) return allowed } } diff --git a/packages/signals/signals/src/core/signal-generators/register.ts b/packages/signals/signals/src/core/signal-generators/register.ts index 3aab35b6d..c2f6d08b1 100644 --- a/packages/signals/signals/src/core/signal-generators/register.ts +++ b/packages/signals/signals/src/core/signal-generators/register.ts @@ -1,17 +1,19 @@ import { logger } from '../../lib/logger' import { isClass } from '../../utils/is-class' import { SignalEmitter } from '../emitter' +import { SignalGlobalSettings } from '../signals' import { SignalGeneratorClass, SignalGenerator } from './types' export const registerGenerator = async ( emitter: SignalEmitter, - signalGenerators: (SignalGeneratorClass | SignalGenerator)[] + signalGenerators: (SignalGeneratorClass | SignalGenerator)[], + settings: SignalGlobalSettings ): Promise => { const _register = (gen: SignalGeneratorClass | SignalGenerator) => { logger.debug('Registering generator:', gen.id || (gen as any).name) if (isClass(gen)) { // Check if Gen is a function and has a constructor - return new gen().register(emitter) + return new gen(settings).register(emitter) } else { return gen.register(emitter) } diff --git a/packages/signals/signals/src/core/signal-generators/types.ts b/packages/signals/signals/src/core/signal-generators/types.ts index 65dcb43f2..16a594fbe 100644 --- a/packages/signals/signals/src/core/signal-generators/types.ts +++ b/packages/signals/signals/src/core/signal-generators/types.ts @@ -1,4 +1,5 @@ import type { SignalEmitter } from '../emitter' +import { SignalGlobalSettings } from '../signals' export interface SignalGenerator { /** @@ -15,5 +16,5 @@ export interface SignalGenerator { export interface SignalGeneratorClass { id?: string - new (): SignalGenerator + new (settings: SignalGlobalSettings): SignalGenerator } diff --git a/packages/signals/signals/src/core/signals/settings.ts b/packages/signals/signals/src/core/signals/settings.ts index a7469f6fb..256867620 100644 --- a/packages/signals/signals/src/core/signals/settings.ts +++ b/packages/signals/signals/src/core/signals/settings.ts @@ -6,6 +6,7 @@ import { SandboxSettingsConfig } from '../processor/sandbox' import { NetworkSettingsConfig } from '../signal-generators/network-gen' import { SignalsPluginSettingsConfig } from '../../types' import { WebStorage } from '../../lib/storage/web-storage' +import { MutationGeneratorSettings } from '../signal-generators/dom-gen/change-gen' export type SignalsSettingsConfig = Pick< SignalsPluginSettingsConfig, @@ -20,6 +21,11 @@ export type SignalsSettingsConfig = Pick< | 'networkSignalsDisallowList' | 'networkSignalsAllowSameDomain' | 'signalStorageType' + | 'mutationGenExtraSelectors' + | 'mutationGenObservedRoles' + | 'mutationGenObservedTags' + | 'mutationGenPollInterval' + | 'mutationGenObservedAttributes' > & { signalStorage?: SignalPersistentStorage processSignal?: string @@ -36,7 +42,7 @@ export class SignalGlobalSettings { ingestClient: SignalsIngestSettingsConfig network: NetworkSettingsConfig signalsDebug: SignalsDebugSettings - + mutationGenerator: MutationGeneratorSettings private sampleSuccess = false constructor(settings: SignalsSettingsConfig) { @@ -46,6 +52,14 @@ export class SignalGlobalSettings { ) } + this.mutationGenerator = new MutationGeneratorSettings({ + extraSelectors: settings.mutationGenExtraSelectors, + observedRoles: settings.mutationGenObservedRoles, + observedTags: settings.mutationGenObservedTags, + pollIntervalMs: settings.mutationGenPollInterval, + observedAttributes: settings.mutationGenObservedAttributes, + }) + this.signalsDebug = new SignalsDebugSettings( settings.disableSignalsRedaction, settings.enableSignalsIngestion diff --git a/packages/signals/signals/src/core/signals/signals.ts b/packages/signals/signals/src/core/signals/signals.ts index 7e76ca2e2..a70a3a313 100644 --- a/packages/signals/signals/src/core/signals/signals.ts +++ b/packages/signals/signals/src/core/signals/signals.ts @@ -147,6 +147,12 @@ export class Signals implements ISignals { async registerGenerator( generators: (SignalGeneratorClass | SignalGenerator)[] ): Promise { - this.cleanup.push(await registerGenerator(this.signalEmitter, generators)) + this.cleanup.push( + await registerGenerator( + this.signalEmitter, + generators, + this.globalSettings + ) + ) } } diff --git a/packages/signals/signals/src/lib/debounce/__tests__/debounce.test.ts b/packages/signals/signals/src/lib/debounce/__tests__/debounce.test.ts new file mode 100644 index 000000000..99fc75797 --- /dev/null +++ b/packages/signals/signals/src/lib/debounce/__tests__/debounce.test.ts @@ -0,0 +1,48 @@ +import { debounceWithKey } from '../index' + +jest.useFakeTimers() + +describe(debounceWithKey, () => { + type Callback = (...args: any[]) => void + let cb: jest.Mock + let debouncedCb: Callback + + beforeEach(() => { + cb = jest.fn() + const getKey = (obj: Record) => Object.keys(obj) + debouncedCb = debounceWithKey(cb, getKey, 300) + }) + + test('should call the function after the debounce time', () => { + debouncedCb({ foo: 1, bar: 2 }) + jest.advanceTimersByTime(200) + debouncedCb({ baz: 3 }) + jest.advanceTimersByTime(100) // in time for the first call, but not the second + expect(cb).toHaveBeenCalledTimes(1) + expect(cb).toHaveBeenCalledWith({ foo: 1, bar: 2 }) + debouncedCb({ hello: 1, world: 2 }) // just test that a new call does not reset the timer + jest.advanceTimersByTime(200) + expect(cb).toHaveBeenCalledTimes(2) + expect(cb).toHaveBeenCalledWith({ baz: 3 }) + jest.advanceTimersByTime(100) + expect(cb).toHaveBeenCalledTimes(3) + expect(cb).toHaveBeenCalledWith({ hello: 1, world: 2 }) + }) + + test('should debounce multiple calls with the same key group', () => { + debouncedCb({ foo: 1, bar: 2 }) + debouncedCb({ foo: 1, bar: 3 }) + jest.advanceTimersByTime(300) + expect(cb).toHaveBeenCalledTimes(1) + expect(cb).toHaveBeenCalledWith({ foo: 1, bar: 3 }) + }) + + test('should require the exact same keys for debounce', () => { + debouncedCb({ foo: 1, bar: 2 }) + debouncedCb({ bar: 6 }) + jest.advanceTimersByTime(300) + expect(cb).toHaveBeenCalledTimes(2) + expect(cb).toHaveBeenCalledWith({ foo: 1, bar: 2 }) + expect(cb).toHaveBeenCalledWith({ bar: 6 }) + }) +}) diff --git a/packages/signals/signals/src/lib/debounce/index.ts b/packages/signals/signals/src/lib/debounce/index.ts new file mode 100644 index 000000000..9402a1bf3 --- /dev/null +++ b/packages/signals/signals/src/lib/debounce/index.ts @@ -0,0 +1,42 @@ +export function debounce void>( + func: T, + wait: number +): (...args: Parameters) => void { + let timeoutId: ReturnType | null = null + + return function (...args: Parameters) { + if (timeoutId) { + clearTimeout(timeoutId) + } + timeoutId = setTimeout(() => func(...args), wait) + } +} + +/** + * Debounce with key-based partitioning, so that the debouncing is done per key group. + * @param func The function to debounce + * @param getKey A function that returns a key for the arguments passed to `func` -- the return type must be serializable + */ +export function debounceWithKey( + func: (...args: Args) => void, + getKey: (...args: Args) => object | string, + wait: number +): (...args: Args) => void { + const timers = new Map>() + + return (...args: Args) => { + const key = getKey(...args) + const keyGroup = typeof key === 'object' ? JSON.stringify(key) : key + + if (timers.has(keyGroup)) { + clearTimeout(timers.get(keyGroup)) + } + + const timer = setTimeout(() => { + timers.delete(keyGroup) + func(...args) + }, wait) + + timers.set(keyGroup, timer) + } +} diff --git a/packages/signals/signals/src/test-helpers/mocks/factories.ts b/packages/signals/signals/src/test-helpers/mocks/factories.ts new file mode 100644 index 000000000..3cea8295c --- /dev/null +++ b/packages/signals/signals/src/test-helpers/mocks/factories.ts @@ -0,0 +1,13 @@ +import { TargetedHTMLElement } from '@segment/analytics-signals-runtime' + +export const createMockTarget = ( + partialTarget: Partial = {} +): TargetedHTMLElement => { + return { + attributes: {}, + classList: [], + id: 'test', + labels: [], + ...partialTarget, + } +} diff --git a/packages/signals/signals/src/types/settings.ts b/packages/signals/signals/src/types/settings.ts index 2ecc95508..b656bb236 100644 --- a/packages/signals/signals/src/types/settings.ts +++ b/packages/signals/signals/src/types/settings.ts @@ -81,6 +81,43 @@ export interface SignalsPluginSettingsConfig { * @default 'indexDB' */ signalStorageType?: 'session' | 'indexDB' | undefined + + /** + * Custom selectors that map to components that should be observed for attribute changes (if the default list is not sufficient) + * @example [`[id="bar"]`, `.foo`] + */ + mutationGenExtraSelectors?: string[] + /** + * @example + * // add a role to the roles + * (defaultRoles) => [...defaultRoles, 'grid'] + * // remove a role from the roles + * (defaultRoles) => defaultRoles.filter(role => role !== 'grid') + */ + mutationGenObservedRoles?: (defaultRoles: string[]) => string[] + /** + * @example + * // add a new tag to the tags + * (defaultTags) => [...defaultTags, 'video'] + * // remove a tag from the tags + * (defaultTags) => defaultTags.filter(tag => tag.toLowerCase() !== 'video') + */ + mutationGenObservedTags?: (defaultTags: string[]) => string[] + /** + * How often to poll the DOM for new elements to observe (ms) + * @default 400 + */ + mutationGenPollInterval?: number + /** + * + * Which attributes to observe for changes on the observed elements. This is used for MutationObserver. + * @example + * // add a new attribute to watch for changes + * (defaultAttributes) => [...defaultAttributes, 'aria-label'] + * // remove an attribute from the list of attributes to watch for changes + * (defaultAttributes) => defaultAttributes.filter(attr => attr.toLowerCase() !== 'aria-selected') + */ + mutationGenObservedAttributes?: (defaultAttributes: string[]) => string[] } export type RegexLike = RegExp | string diff --git a/yarn.lock b/yarn.lock index d0ee77451..de925cef3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2849,6 +2849,57 @@ __metadata: languageName: node linkType: hard +"@formatjs/ecma402-abstract@npm:2.3.1": + version: 2.3.1 + resolution: "@formatjs/ecma402-abstract@npm:2.3.1" + dependencies: + "@formatjs/fast-memoize": 2.2.5 + "@formatjs/intl-localematcher": 0.5.9 + decimal.js: 10 + tslib: 2 + checksum: d8f521d696294abe4ca3067fa1fa7338903a3e3dd6e7da4d7ea58d221fd061b661e5406c6a5c531bb887029457e9578cf871e7a47f999bbdeccbf56614e9ce38 + languageName: node + linkType: hard + +"@formatjs/fast-memoize@npm:2.2.5": + version: 2.2.5 + resolution: "@formatjs/fast-memoize@npm:2.2.5" + dependencies: + tslib: 2 + checksum: 1009b9f782c7b8c0530cde86a68d174bc8055bbf3207eb662429016c2f793bef534e950a13d012861826860ec97d0240fd89eda619267a8191270f581c025f1b + languageName: node + linkType: hard + +"@formatjs/icu-messageformat-parser@npm:2.9.7": + version: 2.9.7 + resolution: "@formatjs/icu-messageformat-parser@npm:2.9.7" + dependencies: + "@formatjs/ecma402-abstract": 2.3.1 + "@formatjs/icu-skeleton-parser": 1.8.11 + tslib: 2 + checksum: d388f45780d505c6085a7a6eb2a9d2a5e10625a596ba37e1086625026af268fd34c6f2e0607785405a45e51e8e51e5d41d6b9832fff01716d8ba6576c16c6ea1 + languageName: node + linkType: hard + +"@formatjs/icu-skeleton-parser@npm:1.8.11": + version: 1.8.11 + resolution: "@formatjs/icu-skeleton-parser@npm:1.8.11" + dependencies: + "@formatjs/ecma402-abstract": 2.3.1 + tslib: 2 + checksum: f3d507e56754200431fdcc2d573a4075ed886a9dd5e3e97cdbb7af7bdf44d9101cea4956f23cdb2d5b9dbfeba20fdd40c8286508ad1a4fb5a6f730234562d484 + languageName: node + linkType: hard + +"@formatjs/intl-localematcher@npm:0.5.9": + version: 0.5.9 + resolution: "@formatjs/intl-localematcher@npm:0.5.9" + dependencies: + tslib: 2 + checksum: b9f5aaa2cf42b478163853413edee9e13022b04ac97c6805fbd7591cd8a424d4bb357ed7740dd7149b7c502b2182e28dce986967121ccc721e74f6ff58807c79 + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -3025,8 +3076,13 @@ __metadata: "@playwright/test": ^1.28.1 "@segment/analytics-next": "workspace:^" "@segment/analytics-signals": "workspace:^" + "@types/react": ^18.0.0 + "@types/react-dom": ^18 globby: ^11.0.2 http-server: 14.1.1 + react: ^18.0.0 + react-aria-components: ^1.5.0 + react-dom: ^18.0.0 tslib: ^2.4.1 webpack: ^5.76.0 webpack-cli: ^4.8.0 @@ -3042,6 +3098,43 @@ __metadata: languageName: unknown linkType: soft +"@internationalized/date@npm:^3.6.0": + version: 3.6.0 + resolution: "@internationalized/date@npm:3.6.0" + dependencies: + "@swc/helpers": ^0.5.0 + checksum: 82a66c7d7eef8bc49c4ee5e99ecfa91a4752a3a96296a34c5549fe3fb98c5d37c3688887a253ffb991749d3425f7045c7c6b24c4f98c4929d0ef7f8312fa68ec + languageName: node + linkType: hard + +"@internationalized/message@npm:^3.1.6": + version: 3.1.6 + resolution: "@internationalized/message@npm:3.1.6" + dependencies: + "@swc/helpers": ^0.5.0 + intl-messageformat: ^10.1.0 + checksum: a291d32e797a3694d1279c4fb74f2812991f007b15fbd67e148d2089339a4f3e11b4803eae6f1cc4ae1a1872b39bdcafe30f9bb365accdf5ed2af063e532d00f + languageName: node + linkType: hard + +"@internationalized/number@npm:^3.6.0": + version: 3.6.0 + resolution: "@internationalized/number@npm:3.6.0" + dependencies: + "@swc/helpers": ^0.5.0 + checksum: 764078650ac562a54a22938d6889ed2cb54e411a4c58b098dabc8514572709bbc206f8e44b50bd684600e454b0276c2617ddc6d9a7345521f2896a13b1c085a7 + languageName: node + linkType: hard + +"@internationalized/string@npm:^3.2.5": + version: 3.2.5 + resolution: "@internationalized/string@npm:3.2.5" + dependencies: + "@swc/helpers": ^0.5.0 + checksum: e1ad90f418e8a35f49b6fe91cc91ea5230083808b337feaff60f8a0a8a32ee33895728bc4024cdfe93bf6596b3a3dc72cd5f8b7daba29962fbc68827c816fecd + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -3810,256 +3903,1825 @@ __metadata: languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:12.3.4": - version: 12.3.4 - resolution: "@next/swc-win32-arm64-msvc@npm:12.3.4" - conditions: os=win32 & cpu=arm64 +"@next/swc-win32-arm64-msvc@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-win32-arm64-msvc@npm:12.3.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-win32-ia32-msvc@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-win32-ia32-msvc@npm:12.3.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@next/swc-win32-x64-msvc@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-win32-x64-msvc@npm:12.3.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3": + version: 2.1.8-no-fsevents.3 + resolution: "@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3" + checksum: ee55cc9241aeea7eb94b8a8551bfa4246c56c53bc71ecda0a2104018fcc328ba5723b33686bdf9cc65d4df4ae65e8016b89e0bbdeb94e0309fe91bb9ced42344 + languageName: node + linkType: hard + +"@node-kit/extra.fs@npm:3.2.0": + version: 3.2.0 + resolution: "@node-kit/extra.fs@npm:3.2.0" + checksum: 48781d37ddd45f544774c17fccf31e1bfe648a16354cf8b20b28f0315798d977336a50c2a4cbb421fd9016792a0860cb2254e7450885324e7ace08903176b58b + languageName: node + linkType: hard + +"@node-kit/yarn-workspace-root@npm:^3.2.0": + version: 3.2.0 + resolution: "@node-kit/yarn-workspace-root@npm:3.2.0" + dependencies: + "@node-kit/extra.fs": 3.2.0 + find-up: ^5.0.0 + micromatch: ^4.0.5 + checksum: 18eca9649017f1b419a230909c319d57fe26400d3074685bb89946be30b3eb6670594dc7bb20d1a4d83cb4b991acf9818026b214fb879717f5ca0290ed934c3e + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": 2.0.5 + run-parallel: ^1.1.9 + checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59 + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": 2.1.5 + fastq: ^1.6.0 + checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^2.1.0": + version: 2.1.0 + resolution: "@npmcli/fs@npm:2.1.0" + dependencies: + "@gar/promisify": ^1.1.3 + semver: ^7.3.5 + checksum: 6ec6d678af6da49f9dac50cd882d7f661934dd278972ffbaacde40d9eaa2871292d634000a0cca9510f6fc29855fbd4af433e1adbff90a524ec3eaf140f1219b + languageName: node + linkType: hard + +"@npmcli/move-file@npm:^2.0.0": + version: 2.0.0 + resolution: "@npmcli/move-file@npm:2.0.0" + dependencies: + mkdirp: ^1.0.4 + rimraf: ^3.0.2 + checksum: 1388777b507b0c592d53f41b9d182e1a8de7763bc625fc07999b8edbc22325f074e5b3ec90af79c89d6987fdb2325bc66d59f483258543c14a43661621f841b0 + languageName: node + linkType: hard + +"@npmcli/promise-spawn@npm:^7.0.0": + version: 7.0.0 + resolution: "@npmcli/promise-spawn@npm:7.0.0" + dependencies: + which: ^4.0.0 + checksum: 22a8c4fd4ef2729cf75d13b0b294e8c695e08bdb2143e951288056656091fc5281e8baf330c97a6bc803e6fc09489028bf80dcd787972597ef9fda9a9349fc0f + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f + languageName: node + linkType: hard + +"@playground/next-playground@workspace:playgrounds/next-playground": + version: 0.0.0-use.local + resolution: "@playground/next-playground@workspace:playgrounds/next-playground" + dependencies: + "@builder.io/partytown": ^0.7.4 + "@next/bundle-analyzer": ^12.1.5 + "@segment/analytics-next": "workspace:^" + "@types/faker": ^5.1.2 + "@types/react": ^17.0.37 + eslint-config-next: ^12.1.6 + faker: ^5.1.0 + lodash: ^4.17.21 + next: ^12.1.0 + prismjs: ^1.27.0 + rc-table: ^7.10.0 + react: ^17.0.2 + react-dom: ^17.0.2 + react-json-tree: ^0.13.0 + react-simple-code-editor: ^0.11.0 + source-map-loader: ^3.0.1 + languageName: unknown + linkType: soft + +"@playground/standalone-playground@workspace:playgrounds/standalone-playground": + version: 0.0.0-use.local + resolution: "@playground/standalone-playground@workspace:playgrounds/standalone-playground" + dependencies: + "@segment/analytics-consent-wrapper-onetrust": "workspace:^" + "@segment/analytics-next": "workspace:^" + "@segment/analytics-signals": "workspace:^" + http-server: 14.1.1 + languageName: unknown + linkType: soft + +"@playground/with-vite@workspace:playgrounds/with-vite": + version: 0.0.0-use.local + resolution: "@playground/with-vite@workspace:playgrounds/with-vite" + dependencies: + "@segment/analytics-next": "workspace:^" + "@types/react": ^18 + "@types/react-dom": ^18 + "@vitejs/plugin-react": ^1.3.0 + react: ^18.0.0 + react-dom: ^18.0.0 + typescript: ^4.7.0 + vite: ^2.9.18 + languageName: unknown + linkType: soft + +"@playwright/test@npm:^1.28.1": + version: 1.28.1 + resolution: "@playwright/test@npm:1.28.1" + dependencies: + "@types/node": "*" + playwright-core: 1.28.1 + bin: + playwright: cli.js + checksum: dc39dfdf848171a6c65fc32a9dbc95162684a4a1e3401dd157d7d6822a065d8dcb96b2484fc3b223baea4da774450fddaeaa6d4d21546d17d45f01884fa8d7c5 + languageName: node + linkType: hard + +"@polka/url@npm:^1.0.0-next.9": + version: 1.0.0-next.11 + resolution: "@polka/url@npm:1.0.0-next.11" + checksum: db1626fb6d7167ce2de6223c95f0a5ff8e1e7c56b2e8709f904f219d8fcc7b075de842ea8bf0ed7af9f5bc350b166b286b241636982f10d0f02964f34215a0e0 + languageName: node + linkType: hard + +"@puppeteer/browsers@npm:1.3.0": + version: 1.3.0 + resolution: "@puppeteer/browsers@npm:1.3.0" + dependencies: + debug: 4.3.4 + extract-zip: 2.0.1 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + progress: 2.0.3 + proxy-from-env: 1.1.0 + tar-fs: 2.1.1 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + peerDependencies: + typescript: ">= 4.7.4" + peerDependenciesMeta: + typescript: + optional: true + bin: + browsers: lib/cjs/main-cli.js + checksum: b966546abc56d23e1546a8139a5c10137e7b67c4a7403947518bab27a47a0d8f8a0b30c12108f04014a08e345f7e5d899b174dab3605d46774bd0245295c8789 + languageName: node + linkType: hard + +"@puppeteer/browsers@npm:1.4.6": + version: 1.4.6 + resolution: "@puppeteer/browsers@npm:1.4.6" + dependencies: + debug: 4.3.4 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.0 + tar-fs: 3.0.4 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + peerDependencies: + typescript: ">= 4.7.4" + peerDependenciesMeta: + typescript: + optional: true + bin: + browsers: lib/cjs/main-cli.js + checksum: 29569dd8a8a41737bb0dd40cce6279cfc8764afc6242d2f9d8ae610bed7e466fc77eeb27b9b3ac53dd04927a1a0e26389f282f6ba057210979b36ab455009d64 + languageName: node + linkType: hard + +"@puppeteer/browsers@npm:^1.6.0": + version: 1.7.0 + resolution: "@puppeteer/browsers@npm:1.7.0" + dependencies: + debug: 4.3.4 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.0 + tar-fs: 3.0.4 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + bin: + browsers: lib/cjs/main-cli.js + checksum: 0a2aecc72fb94a8d94246188f94cfaad730d1d372b34df94ca51ff8a94596bf475a0fee162c317a768fa4b2a707bfa8afd582d594958f49e1019effadfe744b6 + languageName: node + linkType: hard + +"@rc-component/context@npm:^1.4.0": + version: 1.4.0 + resolution: "@rc-component/context@npm:1.4.0" + dependencies: + "@babel/runtime": ^7.10.1 + rc-util: ^5.27.0 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 3771237de1e82a453cfff7b5f0ca0dcc370a2838be8ecbfe172c26dec2e94dc2354a8b3061deaff7e633e418fc1b70ce3d10d770603f12dc477fe03f2ada7059 + languageName: node + linkType: hard + +"@react-aria/breadcrumbs@npm:^3.5.19": + version: 3.5.19 + resolution: "@react-aria/breadcrumbs@npm:3.5.19" + dependencies: + "@react-aria/i18n": ^3.12.4 + "@react-aria/link": ^3.7.7 + "@react-aria/utils": ^3.26.0 + "@react-types/breadcrumbs": ^3.7.9 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 2f0ceeeea5a4bf248739d574fd163ce09161f16dfdbf9a7bc5f55b7ac3bdda76bf9e6add8f085b7b70ea2bfd47840c950e03700299eb3755e3ff12724213f6b7 + languageName: node + linkType: hard + +"@react-aria/button@npm:^3.11.0": + version: 3.11.0 + resolution: "@react-aria/button@npm:3.11.0" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/interactions": ^3.22.5 + "@react-aria/toolbar": 3.0.0-beta.11 + "@react-aria/utils": ^3.26.0 + "@react-stately/toggle": ^3.8.0 + "@react-types/button": ^3.10.1 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 4bb57fef1be1834cbcdad13e9b4942f888392ab6806dcb904ca72f7bbc0d35be83e286e4af9301b916802372c19521880883f5ee61d4becad67edd9c7c76bea2 + languageName: node + linkType: hard + +"@react-aria/calendar@npm:^3.6.0": + version: 3.6.0 + resolution: "@react-aria/calendar@npm:3.6.0" + dependencies: + "@internationalized/date": ^3.6.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/live-announcer": ^3.4.1 + "@react-aria/utils": ^3.26.0 + "@react-stately/calendar": ^3.6.0 + "@react-types/button": ^3.10.1 + "@react-types/calendar": ^3.5.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: b7cba0c72f60cec776495820b32a99c8dff29b5cf58719c4c1379a53e58d637f95dccc647cce1b648ec8cfbca6ca4f68f1b1c0d861cd665c3bbdd93dbc60d5cc + languageName: node + linkType: hard + +"@react-aria/checkbox@npm:^3.15.0": + version: 3.15.0 + resolution: "@react-aria/checkbox@npm:3.15.0" + dependencies: + "@react-aria/form": ^3.0.11 + "@react-aria/interactions": ^3.22.5 + "@react-aria/label": ^3.7.13 + "@react-aria/toggle": ^3.10.10 + "@react-aria/utils": ^3.26.0 + "@react-stately/checkbox": ^3.6.10 + "@react-stately/form": ^3.1.0 + "@react-stately/toggle": ^3.8.0 + "@react-types/checkbox": ^3.9.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: edfeae6a037b6432c3a9d3fc56299df965314f2db3ad3fb652ec13ea3fd3a738c0b153dfec19feb52a5540a4255971815124c20aed10fd67067702a4301ef750 + languageName: node + linkType: hard + +"@react-aria/collections@npm:3.0.0-alpha.6": + version: 3.0.0-alpha.6 + resolution: "@react-aria/collections@npm:3.0.0-alpha.6" + dependencies: + "@react-aria/ssr": ^3.9.7 + "@react-aria/utils": ^3.26.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 9495de236b0a9cf1c925a3d39e641ede78d53a394fb9bf75063fece2399d869d36225d7aad8940caac2629f6e388afc963f83e72bd1fbc6466a4464965ca0e12 + languageName: node + linkType: hard + +"@react-aria/color@npm:^3.0.2": + version: 3.0.2 + resolution: "@react-aria/color@npm:3.0.2" + dependencies: + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/numberfield": ^3.11.9 + "@react-aria/slider": ^3.7.14 + "@react-aria/spinbutton": ^3.6.10 + "@react-aria/textfield": ^3.15.0 + "@react-aria/utils": ^3.26.0 + "@react-aria/visually-hidden": ^3.8.18 + "@react-stately/color": ^3.8.1 + "@react-stately/form": ^3.1.0 + "@react-types/color": ^3.0.1 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: a7ed07e8fca53ebabcabe4c193fde3ffcfe9e93a5c9f0810136becfc2c6b074599a8c388437fc1f97820a29ab05b1e42b7a3863d8b411e62447cedb42afb5014 + languageName: node + linkType: hard + +"@react-aria/combobox@npm:^3.11.0": + version: 3.11.0 + resolution: "@react-aria/combobox@npm:3.11.0" + dependencies: + "@react-aria/i18n": ^3.12.4 + "@react-aria/listbox": ^3.13.6 + "@react-aria/live-announcer": ^3.4.1 + "@react-aria/menu": ^3.16.0 + "@react-aria/overlays": ^3.24.0 + "@react-aria/selection": ^3.21.0 + "@react-aria/textfield": ^3.15.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/collections": ^3.12.0 + "@react-stately/combobox": ^3.10.1 + "@react-stately/form": ^3.1.0 + "@react-types/button": ^3.10.1 + "@react-types/combobox": ^3.13.1 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 00c4cc4ef072f2ff453f49dd7fbcd0fffd48960939b4c42bfe688e7c1af13215cf98b13344af252f60d52f9c40c3611c6ab5201402538683b63d2f954c976011 + languageName: node + linkType: hard + +"@react-aria/datepicker@npm:^3.12.0": + version: 3.12.0 + resolution: "@react-aria/datepicker@npm:3.12.0" + dependencies: + "@internationalized/date": ^3.6.0 + "@internationalized/number": ^3.6.0 + "@internationalized/string": ^3.2.5 + "@react-aria/focus": ^3.19.0 + "@react-aria/form": ^3.0.11 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/label": ^3.7.13 + "@react-aria/spinbutton": ^3.6.10 + "@react-aria/utils": ^3.26.0 + "@react-stately/datepicker": ^3.11.0 + "@react-stately/form": ^3.1.0 + "@react-types/button": ^3.10.1 + "@react-types/calendar": ^3.5.0 + "@react-types/datepicker": ^3.9.0 + "@react-types/dialog": ^3.5.14 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 34e203372eb0b9e72402677570cb06ec8126ffb1498f222b193f5830db765fc8a76f84e09b056ef944a4b0d46c0504f5c19a0291ad769d13cd8828dffbd594da + languageName: node + linkType: hard + +"@react-aria/dialog@npm:^3.5.20": + version: 3.5.20 + resolution: "@react-aria/dialog@npm:3.5.20" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/overlays": ^3.24.0 + "@react-aria/utils": ^3.26.0 + "@react-types/dialog": ^3.5.14 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 1f1766f528a031a8d2307c9bc6ece41d4bbbf8312193b33779f026863ce43c3193702f03de72b65d88f93c705d7f20895c8eb6a51e3ff39d620add9b89cd1591 + languageName: node + linkType: hard + +"@react-aria/disclosure@npm:^3.0.0": + version: 3.0.0 + resolution: "@react-aria/disclosure@npm:3.0.0" + dependencies: + "@react-aria/ssr": ^3.9.7 + "@react-aria/utils": ^3.26.0 + "@react-stately/disclosure": ^3.0.0 + "@react-types/button": ^3.10.1 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 11fd140d4ef422d636fa31488f3e39bbba5957d89cd218fe8afa8ccb79d35bb2f142fe879c0e18e5e829edc544fce57d5355bf651c6b789235c7d9bd8664220b + languageName: node + linkType: hard + +"@react-aria/dnd@npm:^3.8.0": + version: 3.8.0 + resolution: "@react-aria/dnd@npm:3.8.0" + dependencies: + "@internationalized/string": ^3.2.5 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/live-announcer": ^3.4.1 + "@react-aria/overlays": ^3.24.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/dnd": ^3.5.0 + "@react-types/button": ^3.10.1 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 284f98c85f6ad29175c7b9960b5f682abc0d7a349668399f86626f3a81fa55c273e25f0a5a75f210d59c4c192fad20cc1a7d4eecced82f9fa8ac14bcb850e0a9 + languageName: node + linkType: hard + +"@react-aria/focus@npm:^3.19.0": + version: 3.19.0 + resolution: "@react-aria/focus@npm:3.19.0" + dependencies: + "@react-aria/interactions": ^3.22.5 + "@react-aria/utils": ^3.26.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + clsx: ^2.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 5109b24a89ba049cf3b9ffc71ad68fedd8d667a8d9a50a41f334d97db01abf22d144b32ff1ca68f76b7067d9a67e27d5cb13989cd92fcd3734e4e509a04c9ad5 + languageName: node + linkType: hard + +"@react-aria/form@npm:^3.0.11": + version: 3.0.11 + resolution: "@react-aria/form@npm:3.0.11" + dependencies: + "@react-aria/interactions": ^3.22.5 + "@react-aria/utils": ^3.26.0 + "@react-stately/form": ^3.1.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 7ea68ad50545178036b003996b8636b8b4dd86921df19703a5609602abb9689cf90689a0038096c8cb46447a1534e25d54d77168e18070832a8d6f42fadecc0b + languageName: node + linkType: hard + +"@react-aria/grid@npm:^3.11.0": + version: 3.11.0 + resolution: "@react-aria/grid@npm:3.11.0" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/live-announcer": ^3.4.1 + "@react-aria/selection": ^3.21.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/collections": ^3.12.0 + "@react-stately/grid": ^3.10.0 + "@react-stately/selection": ^3.18.0 + "@react-types/checkbox": ^3.9.0 + "@react-types/grid": ^3.2.10 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 15ae910876e3fee7e667078fc973c51bdced9eb405189ee49dd0eba6d528a88742b407cf32953743ca797cbf66965aaae6467f7da797a96e8f815b3498066e9d + languageName: node + linkType: hard + +"@react-aria/gridlist@npm:^3.10.0": + version: 3.10.0 + resolution: "@react-aria/gridlist@npm:3.10.0" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/grid": ^3.11.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/selection": ^3.21.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/collections": ^3.12.0 + "@react-stately/list": ^3.11.1 + "@react-stately/tree": ^3.8.6 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 60d82eec9fae0570011b3ce478ac1f66a1b2f9103ed2fe3e788265f78d9dee53cb59ed70f5c6e12446849cb17817a3f754652aebae12890eb6c961b644f3f9c4 + languageName: node + linkType: hard + +"@react-aria/i18n@npm:^3.12.4": + version: 3.12.4 + resolution: "@react-aria/i18n@npm:3.12.4" + dependencies: + "@internationalized/date": ^3.6.0 + "@internationalized/message": ^3.1.6 + "@internationalized/number": ^3.6.0 + "@internationalized/string": ^3.2.5 + "@react-aria/ssr": ^3.9.7 + "@react-aria/utils": ^3.26.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: a3d9593bdef208d8b2c23b78e89b9d7a62f62755ef66a77e7b33919fabcbf232f8e4f90ab1d373776487aef461d3b7650b89c33480f1915a1f23184182b062ae + languageName: node + linkType: hard + +"@react-aria/interactions@npm:^3.22.5": + version: 3.22.5 + resolution: "@react-aria/interactions@npm:3.22.5" + dependencies: + "@react-aria/ssr": ^3.9.7 + "@react-aria/utils": ^3.26.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: f15c8c343ad6f9725a801a2d931a59a9a0302cd4577e542c3d57ebc1296fcdbd2c75f7cd9f36516ff838f3f3afa2ef2414ba0a514d97663043b7ec07ac8a1611 + languageName: node + linkType: hard + +"@react-aria/label@npm:^3.7.13": + version: 3.7.13 + resolution: "@react-aria/label@npm:3.7.13" + dependencies: + "@react-aria/utils": ^3.26.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 9f3dd8d5a49906ac6aa16c2d27a4afb6b84ef76990855b806fd0bd8335b0727ce2f9cfb9fe83c51f75facc53a707ea565d85d40692d81c646f6017eac1997b2e + languageName: node + linkType: hard + +"@react-aria/link@npm:^3.7.7": + version: 3.7.7 + resolution: "@react-aria/link@npm:3.7.7" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/interactions": ^3.22.5 + "@react-aria/utils": ^3.26.0 + "@react-types/link": ^3.5.9 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 9634b30f2104c8a6257cc3ce6dc81818de548649773ee09cd4c2e6fc286de3950a6ab8a9b1f4ee135f014449e0738c41a33f297f339d1e4e0ccddd5763ffc399 + languageName: node + linkType: hard + +"@react-aria/listbox@npm:^3.13.6": + version: 3.13.6 + resolution: "@react-aria/listbox@npm:3.13.6" + dependencies: + "@react-aria/interactions": ^3.22.5 + "@react-aria/label": ^3.7.13 + "@react-aria/selection": ^3.21.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/collections": ^3.12.0 + "@react-stately/list": ^3.11.1 + "@react-types/listbox": ^3.5.3 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: a86e15da3a710d449678f347b3c67a88f9e10713cefc087c57fbe3931d9dc716a6a1e3998b34eaeb7dca0482ba6742c196f43c03ba6d620434d30d3b739d9581 + languageName: node + linkType: hard + +"@react-aria/live-announcer@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-aria/live-announcer@npm:3.4.1" + dependencies: + "@swc/helpers": ^0.5.0 + checksum: 8f8416c30e359729683e05836b66234cb4156f6166bf6ba023bc0fd4408f2679bac59bd8e6639b629e438b2da292839aa8c293575ad30499f95ea650fccf8a1a + languageName: node + linkType: hard + +"@react-aria/menu@npm:^3.16.0": + version: 3.16.0 + resolution: "@react-aria/menu@npm:3.16.0" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/overlays": ^3.24.0 + "@react-aria/selection": ^3.21.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/collections": ^3.12.0 + "@react-stately/menu": ^3.9.0 + "@react-stately/selection": ^3.18.0 + "@react-stately/tree": ^3.8.6 + "@react-types/button": ^3.10.1 + "@react-types/menu": ^3.9.13 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 39896c9a9f221e69c389ae8cafcdbbfca9235a5a3ab04a0ef3dbc86ae050b0547e8ea72a85baac1731cbe99377240ae9585b2de38844ef81bea39976f87cdeef + languageName: node + linkType: hard + +"@react-aria/meter@npm:^3.4.18": + version: 3.4.18 + resolution: "@react-aria/meter@npm:3.4.18" + dependencies: + "@react-aria/progress": ^3.4.18 + "@react-types/meter": ^3.4.5 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: a92c826bf1c88285f4db8aff511ac26eb0381ceba103777ee0bb12681b357d933d07ca3dc45c128d1b266f63a5efe2c534328459201ca299310c9514d8e8f86d + languageName: node + linkType: hard + +"@react-aria/numberfield@npm:^3.11.9": + version: 3.11.9 + resolution: "@react-aria/numberfield@npm:3.11.9" + dependencies: + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/spinbutton": ^3.6.10 + "@react-aria/textfield": ^3.15.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/form": ^3.1.0 + "@react-stately/numberfield": ^3.9.8 + "@react-types/button": ^3.10.1 + "@react-types/numberfield": ^3.8.7 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: da8eae831829d13a777a35e0b394fbe9207768702968c5fd62d2b960facea63e70ed6008258b463565562499e0ff47ca4bd425282c6c6ff3574c58a8eb90d51a + languageName: node + linkType: hard + +"@react-aria/overlays@npm:^3.24.0": + version: 3.24.0 + resolution: "@react-aria/overlays@npm:3.24.0" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/ssr": ^3.9.7 + "@react-aria/utils": ^3.26.0 + "@react-aria/visually-hidden": ^3.8.18 + "@react-stately/overlays": ^3.6.12 + "@react-types/button": ^3.10.1 + "@react-types/overlays": ^3.8.11 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: dfd176d057ccc9441971706977500ee695786ebaac1719b114402b480335cfb7e058e6281cd911a53b7472300ede259eab345e71deb82fd48defd0f35475ef68 + languageName: node + linkType: hard + +"@react-aria/progress@npm:^3.4.18": + version: 3.4.18 + resolution: "@react-aria/progress@npm:3.4.18" + dependencies: + "@react-aria/i18n": ^3.12.4 + "@react-aria/label": ^3.7.13 + "@react-aria/utils": ^3.26.0 + "@react-types/progress": ^3.5.8 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 5445ad6c3db7d9b0f6aec1dbc1cdbd5523cd11285e925702ab79c403b5c161fc8732a2b89508816f879fc44220169c4b5e76dc41b06d1395f20b3c2c54c30932 + languageName: node + linkType: hard + +"@react-aria/radio@npm:^3.10.10": + version: 3.10.10 + resolution: "@react-aria/radio@npm:3.10.10" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/form": ^3.0.11 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/label": ^3.7.13 + "@react-aria/utils": ^3.26.0 + "@react-stately/radio": ^3.10.9 + "@react-types/radio": ^3.8.5 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: a85e0ce83485e1458113547e3f1c6a968e05d374bd39d57cf7f2e5bfea1526e771b379c6b036b6843e6e52fd160def94f48206fe90882ff146932f3b05a6be35 + languageName: node + linkType: hard + +"@react-aria/searchfield@npm:^3.7.11": + version: 3.7.11 + resolution: "@react-aria/searchfield@npm:3.7.11" + dependencies: + "@react-aria/i18n": ^3.12.4 + "@react-aria/textfield": ^3.15.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/searchfield": ^3.5.8 + "@react-types/button": ^3.10.1 + "@react-types/searchfield": ^3.5.10 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 4b0ff36cf1ab44287b62bf4cc41243d90773be95af3aac7e85733d5fd4415fa91a2a86e39d783a4fdd2cff31afa8db2f3021324f68de1f9164ebfac70ba086ac + languageName: node + linkType: hard + +"@react-aria/select@npm:^3.15.0": + version: 3.15.0 + resolution: "@react-aria/select@npm:3.15.0" + dependencies: + "@react-aria/form": ^3.0.11 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/label": ^3.7.13 + "@react-aria/listbox": ^3.13.6 + "@react-aria/menu": ^3.16.0 + "@react-aria/selection": ^3.21.0 + "@react-aria/utils": ^3.26.0 + "@react-aria/visually-hidden": ^3.8.18 + "@react-stately/select": ^3.6.9 + "@react-types/button": ^3.10.1 + "@react-types/select": ^3.9.8 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 9cf3e937995b98a40d8d8896ee38eb39805e67e79419aec451e4a757bb0fbf0df8cebd1699a50c7fa72ae5203e4f581963af6d38becebd20f23b838956b2fe39 + languageName: node + linkType: hard + +"@react-aria/selection@npm:^3.21.0": + version: 3.21.0 + resolution: "@react-aria/selection@npm:3.21.0" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/utils": ^3.26.0 + "@react-stately/selection": ^3.18.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 612673bbc94b32a47788d057a020585b2416c8f4279760355b80c963efe4587f94aaf2655cbd54f8fbad0197e46fc54612a3291b945a5bd518d899e7bb46e9ae + languageName: node + linkType: hard + +"@react-aria/separator@npm:^3.4.4": + version: 3.4.4 + resolution: "@react-aria/separator@npm:3.4.4" + dependencies: + "@react-aria/utils": ^3.26.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 9f12848caf1db60c92c7b83901652f38046849001136ac1de39a6259f7d54b6e5606e23f3ae6a8b2f30030313073b841a0b149ddc2aee93a04c6ee9cfbf93b21 + languageName: node + linkType: hard + +"@react-aria/slider@npm:^3.7.14": + version: 3.7.14 + resolution: "@react-aria/slider@npm:3.7.14" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/label": ^3.7.13 + "@react-aria/utils": ^3.26.0 + "@react-stately/slider": ^3.6.0 + "@react-types/shared": ^3.26.0 + "@react-types/slider": ^3.7.7 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 66ec3f7d6f64efaac822bbe47e328708ab006165f2f7cbbea0c10d2507a6afad695c3ed2bdf0369afa7164eeee35b1216b458b27fa51d7ec4e07ddecc672d325 + languageName: node + linkType: hard + +"@react-aria/spinbutton@npm:^3.6.10": + version: 3.6.10 + resolution: "@react-aria/spinbutton@npm:3.6.10" + dependencies: + "@react-aria/i18n": ^3.12.4 + "@react-aria/live-announcer": ^3.4.1 + "@react-aria/utils": ^3.26.0 + "@react-types/button": ^3.10.1 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: d6290d05b652e6c2401a724766e5ca4d0d4bfecde17cdcd31e7fe44a4b0037b94b8b02977ce85c9e7b7bc8e0684224cd60428c035fd71a24f0df27173f77cba5 + languageName: node + linkType: hard + +"@react-aria/ssr@npm:^3.9.7": + version: 3.9.7 + resolution: "@react-aria/ssr@npm:3.9.7" + dependencies: + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10ad277d8c4db6cf9b546f5800dd084451a4a8173a57b06c6597fd39375526a81f1fb398fe46558d372f8660d33c0a09a2580e0529351d76b2c8938482597b3f + languageName: node + linkType: hard + +"@react-aria/switch@npm:^3.6.10": + version: 3.6.10 + resolution: "@react-aria/switch@npm:3.6.10" + dependencies: + "@react-aria/toggle": ^3.10.10 + "@react-stately/toggle": ^3.8.0 + "@react-types/shared": ^3.26.0 + "@react-types/switch": ^3.5.7 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: e7c39bdd1316fee95065ce6fed51b7cc496f4147656a489ae4aace9c86b8503c36cd04cd9c135893a1d14d3cb95a30bffd503729075ab7fc1db8add7b1a9f00a + languageName: node + linkType: hard + +"@react-aria/table@npm:^3.16.0": + version: 3.16.0 + resolution: "@react-aria/table@npm:3.16.0" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/grid": ^3.11.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/live-announcer": ^3.4.1 + "@react-aria/utils": ^3.26.0 + "@react-aria/visually-hidden": ^3.8.18 + "@react-stately/collections": ^3.12.0 + "@react-stately/flags": ^3.0.5 + "@react-stately/table": ^3.13.0 + "@react-types/checkbox": ^3.9.0 + "@react-types/grid": ^3.2.10 + "@react-types/shared": ^3.26.0 + "@react-types/table": ^3.10.3 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 33f77e1e27dcb5a8bc6c5fe63a737287567d58599e3280406ca26987aac3ee6564479df6070818360644c21902adf3bc87b003997dede6f0afd10b9dfc9945d6 + languageName: node + linkType: hard + +"@react-aria/tabs@npm:^3.9.8": + version: 3.9.8 + resolution: "@react-aria/tabs@npm:3.9.8" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/selection": ^3.21.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/tabs": ^3.7.0 + "@react-types/shared": ^3.26.0 + "@react-types/tabs": ^3.3.11 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 12e19af55286d6299f566b1d31143d18edf0dbfaaf67ebde56b9eecc42668dd01701032bf5fa1bd938da94cacb002a9d8005faf459c5502ceff3aafc4f10ca4a + languageName: node + linkType: hard + +"@react-aria/tag@npm:^3.4.8": + version: 3.4.8 + resolution: "@react-aria/tag@npm:3.4.8" + dependencies: + "@react-aria/gridlist": ^3.10.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/label": ^3.7.13 + "@react-aria/selection": ^3.21.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/list": ^3.11.1 + "@react-types/button": ^3.10.1 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 78075e07f8697bab32ec5d8f75a55ca0618f361188a9725bae1480e0a45c05d695393ff76c0b52fb65fb38f388a47553a6955a40304ceb9acb37017172586f77 + languageName: node + linkType: hard + +"@react-aria/textfield@npm:^3.15.0": + version: 3.15.0 + resolution: "@react-aria/textfield@npm:3.15.0" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/form": ^3.0.11 + "@react-aria/label": ^3.7.13 + "@react-aria/utils": ^3.26.0 + "@react-stately/form": ^3.1.0 + "@react-stately/utils": ^3.10.5 + "@react-types/shared": ^3.26.0 + "@react-types/textfield": ^3.10.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: fb64b469e9e188500f50ae5acdcb1e15236b4c292ab5d0f32beefdc3e26cdce43c9b605ef9a5dcc90cde37909cbac5651e1a0207394d9796c6e59be10200e391 + languageName: node + linkType: hard + +"@react-aria/toggle@npm:^3.10.10": + version: 3.10.10 + resolution: "@react-aria/toggle@npm:3.10.10" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/interactions": ^3.22.5 + "@react-aria/utils": ^3.26.0 + "@react-stately/toggle": ^3.8.0 + "@react-types/checkbox": ^3.9.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 6bb9313691738c0d82d4600a91c232e2c744fb0a3974e97c8593f284a645125eaaafe76e6b4533ad06287077c1eb1dad0776b5e1821f1c7370f204862d256196 + languageName: node + linkType: hard + +"@react-aria/toolbar@npm:3.0.0-beta.11": + version: 3.0.0-beta.11 + resolution: "@react-aria/toolbar@npm:3.0.0-beta.11" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/utils": ^3.26.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 8ac5980c2405f011d1594649cf0c8b5fd82e214357250473275ee2e0dff6e66f1a62812f756a7a65210a2716caecfe3a37a416af99e5485dc9a695c2be9ed1dd + languageName: node + linkType: hard + +"@react-aria/tooltip@npm:^3.7.10": + version: 3.7.10 + resolution: "@react-aria/tooltip@npm:3.7.10" + dependencies: + "@react-aria/focus": ^3.19.0 + "@react-aria/interactions": ^3.22.5 + "@react-aria/utils": ^3.26.0 + "@react-stately/tooltip": ^3.5.0 + "@react-types/shared": ^3.26.0 + "@react-types/tooltip": ^3.4.13 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: a9b00c2d5934a72335cacac99c253555c5cdf8d0e92d06854a622f46d42a24212043d0d0a649869cafd98a757cb2c69f5faffde937ce23cc3aa4dc887948bf3f + languageName: node + linkType: hard + +"@react-aria/tree@npm:3.0.0-beta.2": + version: 3.0.0-beta.2 + resolution: "@react-aria/tree@npm:3.0.0-beta.2" + dependencies: + "@react-aria/gridlist": ^3.10.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/selection": ^3.21.0 + "@react-aria/utils": ^3.26.0 + "@react-stately/tree": ^3.8.6 + "@react-types/button": ^3.10.1 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: a29166a9722688eb21436caaf3fccfaf817aa291ac0333ecb11bdaa6f1e33dd0d83fa8d549a64aee5d6db27992b94668747acb385a191dbcf7cf76c0c8da71c2 + languageName: node + linkType: hard + +"@react-aria/utils@npm:^3.26.0": + version: 3.26.0 + resolution: "@react-aria/utils@npm:3.26.0" + dependencies: + "@react-aria/ssr": ^3.9.7 + "@react-stately/utils": ^3.10.5 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + clsx: ^2.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 8ad5dbfeaf41e04f6ec2b16e7f0a461614f8d0f94a1b8ce5e19a0f09a79cb49774451db485796e46ef62212ad4978c851fc645351fffbef862a48dcde9b9e1a2 + languageName: node + linkType: hard + +"@react-aria/virtualizer@npm:^4.1.0": + version: 4.1.0 + resolution: "@react-aria/virtualizer@npm:4.1.0" + dependencies: + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/utils": ^3.26.0 + "@react-stately/virtualizer": ^4.2.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: c0758442d5de6c054cf2655fba09430603c5c2760a12312b298ff3a6940a6f5d6a1cbc65e2be29cac510662dfcabe560bcf657d768bde314a2d776dc1f8c2a43 + languageName: node + linkType: hard + +"@react-aria/visually-hidden@npm:^3.8.18": + version: 3.8.18 + resolution: "@react-aria/visually-hidden@npm:3.8.18" + dependencies: + "@react-aria/interactions": ^3.22.5 + "@react-aria/utils": ^3.26.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 5169c4d2aea0aebd9135a4fc692f01f1ba58e54de2c9db47a6da4e97e3e6750e14be6beb64718ab1520b878a982523f8056fcf15195247e3ca8624b4e7645d9f + languageName: node + linkType: hard + +"@react-stately/calendar@npm:^3.6.0": + version: 3.6.0 + resolution: "@react-stately/calendar@npm:3.6.0" + dependencies: + "@internationalized/date": ^3.6.0 + "@react-stately/utils": ^3.10.5 + "@react-types/calendar": ^3.5.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 1de64127cef06e3a36ca795981098d389e713cfa21ef9dfcb7c129a50569e0e5008050178629f9d8dcf695eddb8b7a86fcc2e96974764ce2026cbe886d91bf3b + languageName: node + linkType: hard + +"@react-stately/checkbox@npm:^3.6.10": + version: 3.6.10 + resolution: "@react-stately/checkbox@npm:3.6.10" + dependencies: + "@react-stately/form": ^3.1.0 + "@react-stately/utils": ^3.10.5 + "@react-types/checkbox": ^3.9.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 739afb40584a5b73650da1703591511ac36d2b4ef914d024ca0400625bd10500ae977eaabcfb7d8962d8c3ca5740cd4a945e224a145713db1e38292b2b87468b + languageName: node + linkType: hard + +"@react-stately/collections@npm:^3.12.0": + version: 3.12.0 + resolution: "@react-stately/collections@npm:3.12.0" + dependencies: + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 7278224dc5b7a757bcba90454afe2b03209b2ae97954782526226664918c75486ab04e418eef69c575d526135dc257125ab1b23db86a40b844dd8766bc5b3eac + languageName: node + linkType: hard + +"@react-stately/color@npm:^3.8.1": + version: 3.8.1 + resolution: "@react-stately/color@npm:3.8.1" + dependencies: + "@internationalized/number": ^3.6.0 + "@internationalized/string": ^3.2.5 + "@react-aria/i18n": ^3.12.4 + "@react-stately/form": ^3.1.0 + "@react-stately/numberfield": ^3.9.8 + "@react-stately/slider": ^3.6.0 + "@react-stately/utils": ^3.10.5 + "@react-types/color": ^3.0.1 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 9e1aa47f3131c42c17e1f1f708215bfdf10548ffe884e0fd5058ab795b19d437a94578f2eeaeb23d194d6373d947a34284c00b13e95f9023fa16d40c3e7f2da9 + languageName: node + linkType: hard + +"@react-stately/combobox@npm:^3.10.1": + version: 3.10.1 + resolution: "@react-stately/combobox@npm:3.10.1" + dependencies: + "@react-stately/collections": ^3.12.0 + "@react-stately/form": ^3.1.0 + "@react-stately/list": ^3.11.1 + "@react-stately/overlays": ^3.6.12 + "@react-stately/select": ^3.6.9 + "@react-stately/utils": ^3.10.5 + "@react-types/combobox": ^3.13.1 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 90434b4b54572fcf487d53dc33d067e4379884753522ca1d1374c26db017cbd9f2bee0c984bd4c0741b7f77fbdb487e120490997e4bc502288ed1121e0a6e32f + languageName: node + linkType: hard + +"@react-stately/data@npm:^3.12.0": + version: 3.12.0 + resolution: "@react-stately/data@npm:3.12.0" + dependencies: + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: f4e99854f612d1fc1c590a44b938e66dd3bf75303fda0b1e66368ab83ff9e169907a6f71c768d579f69b81a585671a72bef1c2dc6848afe7a439f54b5998643e + languageName: node + linkType: hard + +"@react-stately/datepicker@npm:^3.11.0": + version: 3.11.0 + resolution: "@react-stately/datepicker@npm:3.11.0" + dependencies: + "@internationalized/date": ^3.6.0 + "@internationalized/string": ^3.2.5 + "@react-stately/form": ^3.1.0 + "@react-stately/overlays": ^3.6.12 + "@react-stately/utils": ^3.10.5 + "@react-types/datepicker": ^3.9.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: a1b0126682454ff2ca7c10a18b5a70c783b18e39b5cf82c63484789c8f64ca9e633934be856d79b5faa97867beb3f34e7085b58bcacfac1289c598aca6e4a2a8 + languageName: node + linkType: hard + +"@react-stately/disclosure@npm:^3.0.0": + version: 3.0.0 + resolution: "@react-stately/disclosure@npm:3.0.0" + dependencies: + "@react-stately/utils": ^3.10.5 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: f70a2cceb097c2c97634d66ef682acb1797d2db1b0d3f31a92e2cae55786194727ef921cb6a72ea054ed8400c6de6ba32887aa3d2436ccac3139d95f549f0b61 + languageName: node + linkType: hard + +"@react-stately/dnd@npm:^3.5.0": + version: 3.5.0 + resolution: "@react-stately/dnd@npm:3.5.0" + dependencies: + "@react-stately/selection": ^3.18.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 6fab7824b07b440099fd85affed0db39487db61b1ac7cb52f7e65c5af32d0b79edb1fbca39a41475e1755361d3b0f47443275b5cf79cd8692abfd3fb1c7e09f4 + languageName: node + linkType: hard + +"@react-stately/flags@npm:^3.0.5": + version: 3.0.5 + resolution: "@react-stately/flags@npm:3.0.5" + dependencies: + "@swc/helpers": ^0.5.0 + checksum: 8a2aaacd77bac14ea8e71726350bc30bd252fe5bcd70a72a26da5d433014788e1395ef0c3cb878492de9758e44243fb6470585e697874109c3924e1699a94fc7 + languageName: node + linkType: hard + +"@react-stately/form@npm:^3.1.0": + version: 3.1.0 + resolution: "@react-stately/form@npm:3.1.0" + dependencies: + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: e83eeaee262e770c751a898f12ff5c467954fee687edc9cafa65cfc9b6e1739d5397e0902f134f6a94bb3716295f19e6c98b0048cf7167b78bdb9f77db2ff89a + languageName: node + linkType: hard + +"@react-stately/grid@npm:^3.10.0": + version: 3.10.0 + resolution: "@react-stately/grid@npm:3.10.0" + dependencies: + "@react-stately/collections": ^3.12.0 + "@react-stately/selection": ^3.18.0 + "@react-types/grid": ^3.2.10 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: a90c00019f7264da522c7b82ef9ec637287034976eae091314714fbc32a088054ed3b154c62f467fc3538beadf135285cd6a98b2fe4dd6e29dfbf67938189e87 + languageName: node + linkType: hard + +"@react-stately/layout@npm:^4.1.0": + version: 4.1.0 + resolution: "@react-stately/layout@npm:4.1.0" + dependencies: + "@react-stately/collections": ^3.12.0 + "@react-stately/table": ^3.13.0 + "@react-stately/virtualizer": ^4.2.0 + "@react-types/grid": ^3.2.10 + "@react-types/shared": ^3.26.0 + "@react-types/table": ^3.10.3 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 2ca1df97728833885f09cc3f44fb24d164108c0062b38ad6f5c1d3157720e0d02ff18205d05233b4bf19f63b9059353e7489f00517aefb9b0f6d8168704f013b + languageName: node + linkType: hard + +"@react-stately/list@npm:^3.11.1": + version: 3.11.1 + resolution: "@react-stately/list@npm:3.11.1" + dependencies: + "@react-stately/collections": ^3.12.0 + "@react-stately/selection": ^3.18.0 + "@react-stately/utils": ^3.10.5 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 302fed3798d76da96d6d2a198bee126c8b5967542fd4722fa1b6f740cc33f6df5e1349bdf61353b3e546805107f80eaddd79d39e30611fd150b298c974879abe + languageName: node + linkType: hard + +"@react-stately/menu@npm:^3.9.0": + version: 3.9.0 + resolution: "@react-stately/menu@npm:3.9.0" + dependencies: + "@react-stately/overlays": ^3.6.12 + "@react-types/menu": ^3.9.13 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: b66c76308c609f3b8b5494c8b1fa6e695a05c147d9dcb927af37fcfb70b231ca0f69cb1b42dd8fbdb83df52d9ddda4763bfe784d86693e7c5f9f25fa131a06b1 + languageName: node + linkType: hard + +"@react-stately/numberfield@npm:^3.9.8": + version: 3.9.8 + resolution: "@react-stately/numberfield@npm:3.9.8" + dependencies: + "@internationalized/number": ^3.6.0 + "@react-stately/form": ^3.1.0 + "@react-stately/utils": ^3.10.5 + "@react-types/numberfield": ^3.8.7 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 67187c1b5c7323feeb17966d468141bfba6380e91bf992d10470c1c6aeae0961be0b357e28bb46da23d81619b2eea88284a27fe06d67e6ed975841f7e7a3f153 + languageName: node + linkType: hard + +"@react-stately/overlays@npm:^3.6.12": + version: 3.6.12 + resolution: "@react-stately/overlays@npm:3.6.12" + dependencies: + "@react-stately/utils": ^3.10.5 + "@react-types/overlays": ^3.8.11 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 6be299650614f2a9d3103540eb76c8b049757ebc27c358c86a32ad6e35aff01c19f708877cfa3549b1b1173531d067359336dcfbf3d38ea81f7d63f8ca9dd9a1 + languageName: node + linkType: hard + +"@react-stately/radio@npm:^3.10.9": + version: 3.10.9 + resolution: "@react-stately/radio@npm:3.10.9" + dependencies: + "@react-stately/form": ^3.1.0 + "@react-stately/utils": ^3.10.5 + "@react-types/radio": ^3.8.5 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: b15046c5f38f0ad9cf3bbdc169733dfb901a5532e8dcf2ff71abf112a2378767e5f5b3c628f1d261b2db8f15f771afe972c12d76d0437ed19101c995bc909ab9 + languageName: node + linkType: hard + +"@react-stately/searchfield@npm:^3.5.8": + version: 3.5.8 + resolution: "@react-stately/searchfield@npm:3.5.8" + dependencies: + "@react-stately/utils": ^3.10.5 + "@react-types/searchfield": ^3.5.10 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: cd7d427c744490f6d77387564ebb718091ff0ffcbf164472c92f1f79d5cfafe7def03a0ea9a34175e37fe440184d2992bc9336423a68f846577095063f33702d + languageName: node + linkType: hard + +"@react-stately/select@npm:^3.6.9": + version: 3.6.9 + resolution: "@react-stately/select@npm:3.6.9" + dependencies: + "@react-stately/form": ^3.1.0 + "@react-stately/list": ^3.11.1 + "@react-stately/overlays": ^3.6.12 + "@react-types/select": ^3.9.8 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 41e9ed9df52eaa542d54e81561e23c7899f9a6998a2365dc77d2685e6672a5c8c32ca9331680cacd6b9a6e0092ad8235581d1324b240acc78da7b60512b612c5 + languageName: node + linkType: hard + +"@react-stately/selection@npm:^3.18.0": + version: 3.18.0 + resolution: "@react-stately/selection@npm:3.18.0" + dependencies: + "@react-stately/collections": ^3.12.0 + "@react-stately/utils": ^3.10.5 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 0ae179f7b082bcc472e3ddfa585ed46d5304d1ac21c720d14c394f3030772c510318122fc095801a66b005c2174cfc7ea37298fb929a26993d73194a8bde0324 + languageName: node + linkType: hard + +"@react-stately/slider@npm:^3.6.0": + version: 3.6.0 + resolution: "@react-stately/slider@npm:3.6.0" + dependencies: + "@react-stately/utils": ^3.10.5 + "@react-types/shared": ^3.26.0 + "@react-types/slider": ^3.7.7 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 9c469aeec52d41b03d72a56e024294d9173bec630a5014e3c00fb968b9aba443e2fa1fdfb8b18c972452d018b1478eafe74f391cc5a61e49ac0e121bc6264348 + languageName: node + linkType: hard + +"@react-stately/table@npm:^3.13.0": + version: 3.13.0 + resolution: "@react-stately/table@npm:3.13.0" + dependencies: + "@react-stately/collections": ^3.12.0 + "@react-stately/flags": ^3.0.5 + "@react-stately/grid": ^3.10.0 + "@react-stately/selection": ^3.18.0 + "@react-stately/utils": ^3.10.5 + "@react-types/grid": ^3.2.10 + "@react-types/shared": ^3.26.0 + "@react-types/table": ^3.10.3 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: be03b35ad62ef3e8c95c412c9a9e06a3c7099dc0ab2d1a4e97316f7bce51e414b9e135724b1bac4104b33a1f432fefc958b348901b86e93b237d4e793205d44d + languageName: node + linkType: hard + +"@react-stately/tabs@npm:^3.7.0": + version: 3.7.0 + resolution: "@react-stately/tabs@npm:3.7.0" + dependencies: + "@react-stately/list": ^3.11.1 + "@react-types/shared": ^3.26.0 + "@react-types/tabs": ^3.3.11 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 831bf6f12b055867104eeb3a0d56c0a117202eda4161f585e14a376c74656ba0d5e87b90aac2196e68ce71e9f23ed590e4284e021f5fa63ae32959a3c6af4ddf + languageName: node + linkType: hard + +"@react-stately/toggle@npm:^3.8.0": + version: 3.8.0 + resolution: "@react-stately/toggle@npm:3.8.0" + dependencies: + "@react-stately/utils": ^3.10.5 + "@react-types/checkbox": ^3.9.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 78732515225f2ce7cad352324af808bd7e1437a184f39e1afa54a00d83695e54676e876d61b4bf6c2f43ddf009e819615b9892827a1b455238c216210e4b5377 + languageName: node + linkType: hard + +"@react-stately/tooltip@npm:^3.5.0": + version: 3.5.0 + resolution: "@react-stately/tooltip@npm:3.5.0" + dependencies: + "@react-stately/overlays": ^3.6.12 + "@react-types/tooltip": ^3.4.13 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 3b103fa2c8b0413c50e477225572b5f18d8235b0ddde3913d0e6afe448b126a7580690591d35893f5da07f65ac61c90fc040b755f3f4eda8e4c6e0017800db04 + languageName: node + linkType: hard + +"@react-stately/tree@npm:^3.8.6": + version: 3.8.6 + resolution: "@react-stately/tree@npm:3.8.6" + dependencies: + "@react-stately/collections": ^3.12.0 + "@react-stately/selection": ^3.18.0 + "@react-stately/utils": ^3.10.5 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: df540b7fea2b1f41201ceed927ba4d37fa0f1e758c75f4a1d2e5f2b8eceabaa7e14513523a73101f8013bc8f8792e75f100157d50830928655556affe841e41c + languageName: node + linkType: hard + +"@react-stately/utils@npm:^3.10.5": + version: 3.10.5 + resolution: "@react-stately/utils@npm:3.10.5" + dependencies: + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 4f4292ccf7bb86578a20b354cf9569f88d2d50ecb2e10ac6046fab3b9eb2175f734acf1b9bd87787e439220b912785a54551a724ab285f03e4f33b2942831f57 + languageName: node + linkType: hard + +"@react-stately/virtualizer@npm:^4.2.0": + version: 4.2.0 + resolution: "@react-stately/virtualizer@npm:4.2.0" + dependencies: + "@react-aria/utils": ^3.26.0 + "@react-types/shared": ^3.26.0 + "@swc/helpers": ^0.5.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 862c5edacadd927f248ce1e7b28c7082508e3f699da5ef2aabedf1d94a86c1e8aee764ad43368d94e3de0dee3f6da0415a77da4a9a45d22008952d7618a8d5de + languageName: node + linkType: hard + +"@react-types/breadcrumbs@npm:^3.7.9": + version: 3.7.9 + resolution: "@react-types/breadcrumbs@npm:3.7.9" + dependencies: + "@react-types/link": ^3.5.9 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 707114b57d986daba02808d04d0c38570cfacd2e4e44dcc923c1fd72807797cce4af4c7278f0d6afff68b316ec5f8576959ac50f50b3e6787bd6ad14bbaa3854 + languageName: node + linkType: hard + +"@react-types/button@npm:^3.10.1": + version: 3.10.1 + resolution: "@react-types/button@npm:3.10.1" + dependencies: + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 35d783d8f5eddebec3947791d7e166c41f80cd052956da3899f36e6ce112b13af549c9521c321995a27add57a759934b0a8ad7c6a3038be221454a0e4019d0db + languageName: node + linkType: hard + +"@react-types/calendar@npm:^3.5.0": + version: 3.5.0 + resolution: "@react-types/calendar@npm:3.5.0" + dependencies: + "@internationalized/date": ^3.6.0 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 467ee28ec5dfc3cac8f1899059208394c77cb41d2e0aad92db723a57de9f104c9917e9f83c368b55f8d2a6a47521d259f8c9d939d0920478336cbf2ef157d35d + languageName: node + linkType: hard + +"@react-types/checkbox@npm:^3.9.0": + version: 3.9.0 + resolution: "@react-types/checkbox@npm:3.9.0" + dependencies: + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 5da37fe94e9e6e544b00313c7eaacd1b349a5da69c6a89dad1d822161ba29d4304c0201d12dda141f557caec5b1f297e3c283d49e5f880c8b274ef4f6cc01f09 + languageName: node + linkType: hard + +"@react-types/color@npm:^3.0.1": + version: 3.0.1 + resolution: "@react-types/color@npm:3.0.1" + dependencies: + "@react-types/shared": ^3.26.0 + "@react-types/slider": ^3.7.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 5b4f4e606d44b9c2fec9f72dfac9e124fd7b491604646d47583ed3e5323803204e56c29085aa51d2da1fe152a61d522958fdc5238d5c7def5a5db2ec55de35e7 languageName: node linkType: hard -"@next/swc-win32-ia32-msvc@npm:12.3.4": - version: 12.3.4 - resolution: "@next/swc-win32-ia32-msvc@npm:12.3.4" - conditions: os=win32 & cpu=ia32 +"@react-types/combobox@npm:^3.13.1": + version: 3.13.1 + resolution: "@react-types/combobox@npm:3.13.1" + dependencies: + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 726522ca43131dfcac45f410c47834715b2668dd6fa80cde5e46835cceed5f82fc6157f23a53b3a5cafcf2e6e7d860bd61194dcf393e02f72e7bed358317332b languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:12.3.4": - version: 12.3.4 - resolution: "@next/swc-win32-x64-msvc@npm:12.3.4" - conditions: os=win32 & cpu=x64 +"@react-types/datepicker@npm:^3.9.0": + version: 3.9.0 + resolution: "@react-types/datepicker@npm:3.9.0" + dependencies: + "@internationalized/date": ^3.6.0 + "@react-types/calendar": ^3.5.0 + "@react-types/overlays": ^3.8.11 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 5a7d734babd1e07835b50fb077e798137c235082633cd3aa93ff700c0836eb801df39bf6bc046143db2c50ebb747829eda65bb81b22d70cde2761bfefc87e19f languageName: node linkType: hard -"@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3": - version: 2.1.8-no-fsevents.3 - resolution: "@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3" - checksum: ee55cc9241aeea7eb94b8a8551bfa4246c56c53bc71ecda0a2104018fcc328ba5723b33686bdf9cc65d4df4ae65e8016b89e0bbdeb94e0309fe91bb9ced42344 +"@react-types/dialog@npm:^3.5.14": + version: 3.5.14 + resolution: "@react-types/dialog@npm:3.5.14" + dependencies: + "@react-types/overlays": ^3.8.11 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 99f7c4789feef7a99a7e85ae861a8299e84133848824933fe2534bf045b932af8da6bd6d65f113a5d993910a1f3dc5e71ab9c6a204287e3bc58c781d18fe408b languageName: node linkType: hard -"@node-kit/extra.fs@npm:3.2.0": - version: 3.2.0 - resolution: "@node-kit/extra.fs@npm:3.2.0" - checksum: 48781d37ddd45f544774c17fccf31e1bfe648a16354cf8b20b28f0315798d977336a50c2a4cbb421fd9016792a0860cb2254e7450885324e7ace08903176b58b +"@react-types/form@npm:^3.7.8": + version: 3.7.8 + resolution: "@react-types/form@npm:3.7.8" + dependencies: + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: eb6771ab4628d242c1f5c3a27fe7f29628368c28e646bb67ad950f022246a8ecc35c6d3f49a71b1cc0fba1fb7ce1c5b8aaa0e3331763b51eaea6cbbfcb3a6ce5 languageName: node linkType: hard -"@node-kit/yarn-workspace-root@npm:^3.2.0": - version: 3.2.0 - resolution: "@node-kit/yarn-workspace-root@npm:3.2.0" +"@react-types/grid@npm:^3.2.10": + version: 3.2.10 + resolution: "@react-types/grid@npm:3.2.10" dependencies: - "@node-kit/extra.fs": 3.2.0 - find-up: ^5.0.0 - micromatch: ^4.0.5 - checksum: 18eca9649017f1b419a230909c319d57fe26400d3074685bb89946be30b3eb6670594dc7bb20d1a4d83cb4b991acf9818026b214fb879717f5ca0290ed934c3e + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 49f3a933ce9e62e78a309eb9f0ef80d29583a5e96b4f9b455f3c04fb40839f758d2b7a87b22bf9c846c3d0a71d39a9201951aa3e5ae0107330aa63ee5af29514 languageName: node linkType: hard -"@nodelib/fs.scandir@npm:2.1.5": - version: 2.1.5 - resolution: "@nodelib/fs.scandir@npm:2.1.5" +"@react-types/link@npm:^3.5.9": + version: 3.5.9 + resolution: "@react-types/link@npm:3.5.9" dependencies: - "@nodelib/fs.stat": 2.0.5 - run-parallel: ^1.1.9 - checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 8d04d420fe287c71ae3130b92e9457f35dbfeb06228f57e606e9c8f9d3431e7920e04c81cfcce887ae51d255a524ba442be902c20e88ab1cbbf9703afd6f0fa7 languageName: node linkType: hard -"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": - version: 2.0.5 - resolution: "@nodelib/fs.stat@npm:2.0.5" - checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 +"@react-types/listbox@npm:^3.5.3": + version: 3.5.3 + resolution: "@react-types/listbox@npm:3.5.3" + dependencies: + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 00170013a05a794d3bff1df6ed65f09f1e4b9d0517a2a28b7724eb847cd1d3d5f1a756ddf0831143b8bbef57d9f07013e8c2235f2010176575f6c5cbf5d5f7ce languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3": - version: 1.2.8 - resolution: "@nodelib/fs.walk@npm:1.2.8" +"@react-types/menu@npm:^3.9.13": + version: 3.9.13 + resolution: "@react-types/menu@npm:3.9.13" dependencies: - "@nodelib/fs.scandir": 2.1.5 - fastq: ^1.6.0 - checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53 + "@react-types/overlays": ^3.8.11 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 6e96dc7faa10d731385015185d50cc79f3595eee2ab713261121753ba59477635a504559097fac781af139ea45935e2a81e9dedb1b70ae2878a4f49f63704bea languageName: node linkType: hard -"@npmcli/fs@npm:^2.1.0": - version: 2.1.0 - resolution: "@npmcli/fs@npm:2.1.0" +"@react-types/meter@npm:^3.4.5": + version: 3.4.5 + resolution: "@react-types/meter@npm:3.4.5" dependencies: - "@gar/promisify": ^1.1.3 - semver: ^7.3.5 - checksum: 6ec6d678af6da49f9dac50cd882d7f661934dd278972ffbaacde40d9eaa2871292d634000a0cca9510f6fc29855fbd4af433e1adbff90a524ec3eaf140f1219b + "@react-types/progress": ^3.5.8 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 415a27294b9098b0614ef95893781e9564d4046a490fcb532b12ab521d3f9c2b76be3d4d2fba15773feb88083cb8e4d57942dbf3c05c5e393b4dceccbefc6cd3 languageName: node linkType: hard -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.0 - resolution: "@npmcli/move-file@npm:2.0.0" +"@react-types/numberfield@npm:^3.8.7": + version: 3.8.7 + resolution: "@react-types/numberfield@npm:3.8.7" dependencies: - mkdirp: ^1.0.4 - rimraf: ^3.0.2 - checksum: 1388777b507b0c592d53f41b9d182e1a8de7763bc625fc07999b8edbc22325f074e5b3ec90af79c89d6987fdb2325bc66d59f483258543c14a43661621f841b0 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 72ad06684d9e1c1f4d1c5ec8b108ff4e9fba9c36cae8737efc57021e1e114255ba9094019ac38e20681addf6d06bef25c3f3b9af2470317b963ab10b1d60cd9b languageName: node linkType: hard -"@npmcli/promise-spawn@npm:^7.0.0": - version: 7.0.0 - resolution: "@npmcli/promise-spawn@npm:7.0.0" +"@react-types/overlays@npm:^3.8.11": + version: 3.8.11 + resolution: "@react-types/overlays@npm:3.8.11" dependencies: - which: ^4.0.0 - checksum: 22a8c4fd4ef2729cf75d13b0b294e8c695e08bdb2143e951288056656091fc5281e8baf330c97a6bc803e6fc09489028bf80dcd787972597ef9fda9a9349fc0f + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: adde53027f40991874519edb14edf763ba61310f837850985d934c3493dc646f2c0d5b0eb507e00d1c89631105b742a9a27a73d9e7a0fb9a3eb6d82a5692dbf5 languageName: node linkType: hard -"@pkgjs/parseargs@npm:^0.11.0": - version: 0.11.0 - resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f +"@react-types/progress@npm:^3.5.8": + version: 3.5.8 + resolution: "@react-types/progress@npm:3.5.8" + dependencies: + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: c2333df01c47c89359f545c1723307f744b1eab7c618dad220eae1a8d80645fb2330c63780444fd06bda3e0ec807b094be7c018141391bec6fa6f62986e92bcf languageName: node linkType: hard -"@playground/next-playground@workspace:playgrounds/next-playground": - version: 0.0.0-use.local - resolution: "@playground/next-playground@workspace:playgrounds/next-playground" +"@react-types/radio@npm:^3.8.5": + version: 3.8.5 + resolution: "@react-types/radio@npm:3.8.5" dependencies: - "@builder.io/partytown": ^0.7.4 - "@next/bundle-analyzer": ^12.1.5 - "@segment/analytics-next": "workspace:^" - "@types/faker": ^5.1.2 - "@types/react": ^17.0.37 - eslint-config-next: ^12.1.6 - faker: ^5.1.0 - lodash: ^4.17.21 - next: ^12.1.0 - prismjs: ^1.27.0 - rc-table: ^7.10.0 - react: ^17.0.2 - react-dom: ^17.0.2 - react-json-tree: ^0.13.0 - react-simple-code-editor: ^0.11.0 - source-map-loader: ^3.0.1 - languageName: unknown - linkType: soft + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 9ba139ae4d6814a6bd3849b446324d35970a16937495ec4354e1b9b0fcfbf26d590db6f010613c7af609710b6b828c229c65299bbdf78542effffea8ad127b67 + languageName: node + linkType: hard -"@playground/standalone-playground@workspace:playgrounds/standalone-playground": - version: 0.0.0-use.local - resolution: "@playground/standalone-playground@workspace:playgrounds/standalone-playground" +"@react-types/searchfield@npm:^3.5.10": + version: 3.5.10 + resolution: "@react-types/searchfield@npm:3.5.10" dependencies: - "@segment/analytics-consent-wrapper-onetrust": "workspace:^" - "@segment/analytics-next": "workspace:^" - "@segment/analytics-signals": "workspace:^" - http-server: 14.1.1 - languageName: unknown - linkType: soft + "@react-types/shared": ^3.26.0 + "@react-types/textfield": ^3.10.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 4b6181ed064bfbdc90b336900f684f2216b58ebe14579fee5ad8d4a8116fff58ed555dd9929ed71a08cbdcb80e2e61be68183cad930761a28a44dd5fe195ee90 + languageName: node + linkType: hard -"@playground/with-vite@workspace:playgrounds/with-vite": - version: 0.0.0-use.local - resolution: "@playground/with-vite@workspace:playgrounds/with-vite" +"@react-types/select@npm:^3.9.8": + version: 3.9.8 + resolution: "@react-types/select@npm:3.9.8" dependencies: - "@segment/analytics-next": "workspace:^" - "@types/react": ^18 - "@types/react-dom": ^18 - "@vitejs/plugin-react": ^1.3.0 - react: ^18.0.0 - react-dom: ^18.0.0 - typescript: ^4.7.0 - vite: ^2.9.18 - languageName: unknown - linkType: soft + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 4ae8366a72d84ef979c5be3c283bc417b791dcb2c5c52bfb670c2c352f6be50bbe7e1159349e3150e610f6ef21a4b463a8f33c958e8506120e13d957d4851973 + languageName: node + linkType: hard -"@playwright/test@npm:^1.28.1": - version: 1.28.1 - resolution: "@playwright/test@npm:1.28.1" +"@react-types/shared@npm:^3.26.0": + version: 3.26.0 + resolution: "@react-types/shared@npm:3.26.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: f51381af98a89e1a9823ee18ed16418c5e8badd640dffb0a3523437aa003b144eea878bb49b4f62672484c361f380864d8dcaba742259da32a67b29692a63b06 + languageName: node + linkType: hard + +"@react-types/slider@npm:^3.7.7": + version: 3.7.7 + resolution: "@react-types/slider@npm:3.7.7" dependencies: - "@types/node": "*" - playwright-core: 1.28.1 - bin: - playwright: cli.js - checksum: dc39dfdf848171a6c65fc32a9dbc95162684a4a1e3401dd157d7d6822a065d8dcb96b2484fc3b223baea4da774450fddaeaa6d4d21546d17d45f01884fa8d7c5 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 8ddd93140178d1166d35059b3a3780c4e79aafc1050f957171ddbf0cb9a9b29a0f37fe14706fa36776a1a762619927b777870972e017592ab21844e52e32aa33 languageName: node linkType: hard -"@polka/url@npm:^1.0.0-next.9": - version: 1.0.0-next.11 - resolution: "@polka/url@npm:1.0.0-next.11" - checksum: db1626fb6d7167ce2de6223c95f0a5ff8e1e7c56b2e8709f904f219d8fcc7b075de842ea8bf0ed7af9f5bc350b166b286b241636982f10d0f02964f34215a0e0 +"@react-types/switch@npm:^3.5.7": + version: 3.5.7 + resolution: "@react-types/switch@npm:3.5.7" + dependencies: + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 9bb6dfe7a6e9eae5b7979a0db23dd0daca5724469af24132c54479bcc71b2e21bc2826e22001b2f7f842077e00537d07f884844b6e1b1a570f9c2aeb393d4b76 languageName: node linkType: hard -"@puppeteer/browsers@npm:1.3.0": - version: 1.3.0 - resolution: "@puppeteer/browsers@npm:1.3.0" +"@react-types/table@npm:^3.10.3": + version: 3.10.3 + resolution: "@react-types/table@npm:3.10.3" dependencies: - debug: 4.3.4 - extract-zip: 2.0.1 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - progress: 2.0.3 - proxy-from-env: 1.1.0 - tar-fs: 2.1.1 - unbzip2-stream: 1.4.3 - yargs: 17.7.1 + "@react-types/grid": ^3.2.10 + "@react-types/shared": ^3.26.0 peerDependencies: - typescript: ">= 4.7.4" - peerDependenciesMeta: - typescript: - optional: true - bin: - browsers: lib/cjs/main-cli.js - checksum: b966546abc56d23e1546a8139a5c10137e7b67c4a7403947518bab27a47a0d8f8a0b30c12108f04014a08e345f7e5d899b174dab3605d46774bd0245295c8789 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 34e6721e8357b304f2ad333003abf99bd74c55100a8d17f98c4e217a86adb3174f9fc724707472776cb2e1134a70a5a8305ebeb7a47286e81b95241d391f556c languageName: node linkType: hard -"@puppeteer/browsers@npm:1.4.6": - version: 1.4.6 - resolution: "@puppeteer/browsers@npm:1.4.6" +"@react-types/tabs@npm:^3.3.11": + version: 3.3.11 + resolution: "@react-types/tabs@npm:3.3.11" dependencies: - debug: 4.3.4 - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.3.0 - tar-fs: 3.0.4 - unbzip2-stream: 1.4.3 - yargs: 17.7.1 + "@react-types/shared": ^3.26.0 peerDependencies: - typescript: ">= 4.7.4" - peerDependenciesMeta: - typescript: - optional: true - bin: - browsers: lib/cjs/main-cli.js - checksum: 29569dd8a8a41737bb0dd40cce6279cfc8764afc6242d2f9d8ae610bed7e466fc77eeb27b9b3ac53dd04927a1a0e26389f282f6ba057210979b36ab455009d64 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 6b8bea0de3fcea7061079bc6042fdf3ebb815c39f5b0d53084a460801a47797ebc686c244f89242f78992abde9afa20604cbc8485a89b94cb35a81f64659aa35 languageName: node linkType: hard -"@puppeteer/browsers@npm:^1.6.0": - version: 1.7.0 - resolution: "@puppeteer/browsers@npm:1.7.0" +"@react-types/textfield@npm:^3.10.0": + version: 3.10.0 + resolution: "@react-types/textfield@npm:3.10.0" dependencies: - debug: 4.3.4 - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.3.0 - tar-fs: 3.0.4 - unbzip2-stream: 1.4.3 - yargs: 17.7.1 - bin: - browsers: lib/cjs/main-cli.js - checksum: 0a2aecc72fb94a8d94246188f94cfaad730d1d372b34df94ca51ff8a94596bf475a0fee162c317a768fa4b2a707bfa8afd582d594958f49e1019effadfe744b6 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 34899054212a44f615ca2e5ca13176dbe06a55528f07794a25adb1383d40be0cd53a28b10047b88526df46561eaabeb89ded7719fef1fc5b4aa9381eceb6eef6 languageName: node linkType: hard -"@rc-component/context@npm:^1.4.0": - version: 1.4.0 - resolution: "@rc-component/context@npm:1.4.0" +"@react-types/tooltip@npm:^3.4.13": + version: 3.4.13 + resolution: "@react-types/tooltip@npm:3.4.13" dependencies: - "@babel/runtime": ^7.10.1 - rc-util: ^5.27.0 + "@react-types/overlays": ^3.8.11 + "@react-types/shared": ^3.26.0 peerDependencies: - react: ">=16.9.0" - react-dom: ">=16.9.0" - checksum: 3771237de1e82a453cfff7b5f0ca0dcc370a2838be8ecbfe172c26dec2e94dc2354a8b3061deaff7e633e418fc1b70ce3d10d770603f12dc477fe03f2ada7059 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: fb63e7ecac075a87416a69f53ac0391b26b884a17148f8cca042a4700f9994b49e0a3b3231bd47598e43c2cd38d032b6edcc5a7b8853f04887409a2e1f474ecd languageName: node linkType: hard @@ -5929,6 +7591,15 @@ __metadata: languageName: node linkType: hard +"@swc/helpers@npm:^0.5.0": + version: 0.5.15 + resolution: "@swc/helpers@npm:0.5.15" + dependencies: + tslib: ^2.8.0 + checksum: 1a9e0dbb792b2d1e0c914d69c201dbc96af3a0e6e6e8cf5a7f7d6a5d7b0e8b762915cd4447acb6b040e2ecc1ed49822875a7239f99a2d63c96c3c3407fb6fccf + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^1.1.2": version: 1.1.2 resolution: "@szmarczak/http-timer@npm:1.1.2" @@ -9621,6 +11292,13 @@ __metadata: languageName: node linkType: hard +"client-only@npm:^0.0.1": + version: 0.0.1 + resolution: "client-only@npm:0.0.1" + checksum: 0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8 + languageName: node + linkType: hard + "cliui@npm:^6.0.0": version: 6.0.0 resolution: "cliui@npm:6.0.0" @@ -9681,6 +11359,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^2.0.0": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -10547,6 +12232,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:10": + version: 10.4.3 + resolution: "decimal.js@npm:10.4.3" + checksum: 796404dcfa9d1dbfdc48870229d57f788b48c21c603c3f6554a1c17c10195fc1024de338b0cf9e1efe0c7c167eeb18f04548979bcc5fdfabebb7cc0ae3287bae + languageName: node + linkType: hard + "decimal.js@npm:^10.3.1": version: 10.3.1 resolution: "decimal.js@npm:10.3.1" @@ -14347,6 +16039,18 @@ __metadata: languageName: node linkType: hard +"intl-messageformat@npm:^10.1.0": + version: 10.7.10 + resolution: "intl-messageformat@npm:10.7.10" + dependencies: + "@formatjs/ecma402-abstract": 2.3.1 + "@formatjs/fast-memoize": 2.2.5 + "@formatjs/icu-messageformat-parser": 2.9.7 + tslib: 2 + checksum: ff929fbb1883f6598e0dc0e0ff341a7feb244fccc844ddd6a9045b4a9ced6b3f357de73547d050d20390496a7b283811741ff75c267e81063860258a4914e80e + languageName: node + linkType: hard + "intl-messageformat@npm:^4.4.0": version: 4.4.0 resolution: "intl-messageformat@npm:4.4.0" @@ -19309,6 +21013,99 @@ __metadata: languageName: node linkType: hard +"react-aria-components@npm:^1.5.0": + version: 1.5.0 + resolution: "react-aria-components@npm:1.5.0" + dependencies: + "@internationalized/date": ^3.6.0 + "@internationalized/string": ^3.2.5 + "@react-aria/collections": 3.0.0-alpha.6 + "@react-aria/color": ^3.0.2 + "@react-aria/disclosure": ^3.0.0 + "@react-aria/dnd": ^3.8.0 + "@react-aria/focus": ^3.19.0 + "@react-aria/interactions": ^3.22.5 + "@react-aria/live-announcer": ^3.4.1 + "@react-aria/menu": ^3.16.0 + "@react-aria/toolbar": 3.0.0-beta.11 + "@react-aria/tree": 3.0.0-beta.2 + "@react-aria/utils": ^3.26.0 + "@react-aria/virtualizer": ^4.1.0 + "@react-stately/color": ^3.8.1 + "@react-stately/disclosure": ^3.0.0 + "@react-stately/layout": ^4.1.0 + "@react-stately/menu": ^3.9.0 + "@react-stately/selection": ^3.18.0 + "@react-stately/table": ^3.13.0 + "@react-stately/utils": ^3.10.5 + "@react-stately/virtualizer": ^4.2.0 + "@react-types/color": ^3.0.1 + "@react-types/form": ^3.7.8 + "@react-types/grid": ^3.2.10 + "@react-types/shared": ^3.26.0 + "@react-types/table": ^3.10.3 + "@swc/helpers": ^0.5.0 + client-only: ^0.0.1 + react-aria: ^3.36.0 + react-stately: ^3.34.0 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: d785a35c5506de0521f2a3f9938980a47111fd6ce28244dee3107e82ef4acb855177a657853a759977eafa22ad3ca45d626b8478eaf9182327ae1e3bfdc1364b + languageName: node + linkType: hard + +"react-aria@npm:^3.36.0": + version: 3.36.0 + resolution: "react-aria@npm:3.36.0" + dependencies: + "@internationalized/string": ^3.2.5 + "@react-aria/breadcrumbs": ^3.5.19 + "@react-aria/button": ^3.11.0 + "@react-aria/calendar": ^3.6.0 + "@react-aria/checkbox": ^3.15.0 + "@react-aria/color": ^3.0.2 + "@react-aria/combobox": ^3.11.0 + "@react-aria/datepicker": ^3.12.0 + "@react-aria/dialog": ^3.5.20 + "@react-aria/disclosure": ^3.0.0 + "@react-aria/dnd": ^3.8.0 + "@react-aria/focus": ^3.19.0 + "@react-aria/gridlist": ^3.10.0 + "@react-aria/i18n": ^3.12.4 + "@react-aria/interactions": ^3.22.5 + "@react-aria/label": ^3.7.13 + "@react-aria/link": ^3.7.7 + "@react-aria/listbox": ^3.13.6 + "@react-aria/menu": ^3.16.0 + "@react-aria/meter": ^3.4.18 + "@react-aria/numberfield": ^3.11.9 + "@react-aria/overlays": ^3.24.0 + "@react-aria/progress": ^3.4.18 + "@react-aria/radio": ^3.10.10 + "@react-aria/searchfield": ^3.7.11 + "@react-aria/select": ^3.15.0 + "@react-aria/selection": ^3.21.0 + "@react-aria/separator": ^3.4.4 + "@react-aria/slider": ^3.7.14 + "@react-aria/ssr": ^3.9.7 + "@react-aria/switch": ^3.6.10 + "@react-aria/table": ^3.16.0 + "@react-aria/tabs": ^3.9.8 + "@react-aria/tag": ^3.4.8 + "@react-aria/textfield": ^3.15.0 + "@react-aria/tooltip": ^3.7.10 + "@react-aria/utils": ^3.26.0 + "@react-aria/visually-hidden": ^3.8.18 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 788b459f4bb9977bbd7817fec52cb303640ba42ea7f29dd43a563db644a16f93fee8df25f3d7bef9ae877c4ca4b73aa3c16636013999ad9f8448d42aa860324d + languageName: node + linkType: hard + "react-base16-styling@npm:^0.8.0": version: 0.8.2 resolution: "react-base16-styling@npm:0.8.2" @@ -19424,6 +21221,41 @@ __metadata: languageName: node linkType: hard +"react-stately@npm:^3.34.0": + version: 3.34.0 + resolution: "react-stately@npm:3.34.0" + dependencies: + "@react-stately/calendar": ^3.6.0 + "@react-stately/checkbox": ^3.6.10 + "@react-stately/collections": ^3.12.0 + "@react-stately/color": ^3.8.1 + "@react-stately/combobox": ^3.10.1 + "@react-stately/data": ^3.12.0 + "@react-stately/datepicker": ^3.11.0 + "@react-stately/disclosure": ^3.0.0 + "@react-stately/dnd": ^3.5.0 + "@react-stately/form": ^3.1.0 + "@react-stately/list": ^3.11.1 + "@react-stately/menu": ^3.9.0 + "@react-stately/numberfield": ^3.9.8 + "@react-stately/overlays": ^3.6.12 + "@react-stately/radio": ^3.10.9 + "@react-stately/searchfield": ^3.5.8 + "@react-stately/select": ^3.6.9 + "@react-stately/selection": ^3.18.0 + "@react-stately/slider": ^3.6.0 + "@react-stately/table": ^3.13.0 + "@react-stately/tabs": ^3.7.0 + "@react-stately/toggle": ^3.8.0 + "@react-stately/tooltip": ^3.5.0 + "@react-stately/tree": ^3.8.6 + "@react-types/shared": ^3.26.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 6e888c8d0a6d7b8234e6170fab0eebb9e72b163c17dd5111198c9eea5d3e1d7c31a1080f174b45ca30920ff3ebf647250317b8697e0b3a9507a01c211e323c29 + languageName: node + linkType: hard + "react@npm:^17.0.2": version: 17.0.2 resolution: "react@npm:17.0.2" @@ -22040,6 +23872,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2, tslib@npm:^2.8.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a + languageName: node + linkType: hard + "tslib@npm:^1.8.1, tslib@npm:^1.9.0": version: 1.13.0 resolution: "tslib@npm:1.13.0" @@ -22633,6 +24472,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.4.0 + resolution: "use-sync-external-store@npm:1.4.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: dc3843a1b59ac8bd01417bd79498d4c688d5df8bf4801be50008ef4bfaacb349058c0b1605b5b43c828e0a2d62722d7e861573b3f31cea77a7f23e8b0fc2f7e3 + languageName: node + linkType: hard + "userhome@npm:1.0.0": version: 1.0.0 resolution: "userhome@npm:1.0.0" From 6db5d754e6094bdf175b4f9ec1a9503edfeeb6ee Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:39:28 -0600 Subject: [PATCH 02/21] fix lint errors --- .../signals-runtime/src/__tests__/signals-runtime.test.ts | 2 +- .../src/test-helpers/mocks/mock-signal-types-web.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts b/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts index 16ee82d4f..bf3ce6361 100644 --- a/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts +++ b/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts @@ -18,7 +18,7 @@ describe(WebSignalsRuntime, () => { ...mockInteractionSignal, data: { eventType: 'change', - target: {}, + target: {} as any, change: {}, listener: 'onchange', }, diff --git a/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts b/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts index 70d247c74..08a19988a 100644 --- a/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts +++ b/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts @@ -10,7 +10,11 @@ export const mockInteractionSignal: InteractionSignal = { type: 'interaction', data: { eventType: 'click', - target: { id: 'button1', className: 'btn-primary' }, + target: { + id: 'button1', + className: 'btn-primary', + attributes: { id: 'button1', class: 'btn-primary' }, + }, }, metadata: { timestamp: Date.now() }, } From d44ad5c20896c8e00dfa4bf2377d8c4f8d5fc4b0 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:01:44 -0600 Subject: [PATCH 03/21] tweak pre-push --- .husky/pre-push | 2 +- playgrounds/next-playground/package.json | 4 ++-- playgrounds/with-vite/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.husky/pre-push b/.husky/pre-push index 9f948b6f2..91dae9915 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -CI=true yarn test --colors --silent +yarn concurrently --names 'LINT,TEST' -c 'green,blue' 'yarn lint' 'CI=true yarn test --colors --silent' diff --git a/playgrounds/next-playground/package.json b/playgrounds/next-playground/package.json index 79a3f9583..5c10e6baf 100644 --- a/playgrounds/next-playground/package.json +++ b/playgrounds/next-playground/package.json @@ -6,10 +6,10 @@ ".": "yarn run -T turbo run --filter=@playground/next-playground...", "concurrently": "yarn run -T concurrently", "dev": "yarn partytown && yarn concurrently 'yarn run -T watch --filter=next-playground' 'sleep 10 && next dev'", - "build": "yarn partytown && next build", + "build-playground": "yarn partytown && next build", "partytown": "partytown copylib public/~partytown", "start": "next start", - "lint": "next lint" + "lint-playground": "next lint" }, "dependencies": { "@builder.io/partytown": "^0.7.4", diff --git a/playgrounds/with-vite/package.json b/playgrounds/with-vite/package.json index 833174ad9..3b08ae3e2 100644 --- a/playgrounds/with-vite/package.json +++ b/playgrounds/with-vite/package.json @@ -5,7 +5,7 @@ ".": "yarn run -T turbo run --filter=@playground/with-vite", "concurrently": "yarn run -T concurrently", "dev": "yarn concurrently 'yarn run -T watch --filter=with-vite' 'sleep 10 && vite'", - "build": "tsc && vite build", + "build-playground": "tsc && vite build", "preview": "vite preview" }, "dependencies": { From 2193e204e21786c5eb3e7942fb672831973c5d16 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:02:38 -0600 Subject: [PATCH 04/21] wip --- .husky/pre-push | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-push b/.husky/pre-push index 91dae9915..3b9e433be 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -yarn concurrently --names 'LINT,TEST' -c 'green,blue' 'yarn lint' 'CI=true yarn test --colors --silent' +yarn concurrently --names 'LINT,TEST' -c 'yellow,blue' 'yarn lint' 'CI=true yarn test --colors --silent' From 71414a52816add571023ff54ad760ef1fe6a3971 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:06:57 -0600 Subject: [PATCH 05/21] wip --- .husky/pre-push | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.husky/pre-push b/.husky/pre-push index 3b9e433be..84489edd6 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -yarn concurrently --names 'LINT,TEST' -c 'yellow,blue' 'yarn lint' 'CI=true yarn test --colors --silent' +yarn prepush diff --git a/package.json b/package.json index 246980765..642f258e4 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "lint": "yarn constraints && turbo run lint --continue", "build": "turbo run build --filter='./packages/**'", "watch": "turbo run watch --filter='./packages/**'", - "dev": "yarn workspace @playground/next-playground run dev", + "dev": "yarn workspace @playground/nOxt-playground run dev", + "prepush": "yarn concurrently --names 'LINT,TEST' -c 'yellow,blue' 'yarn lint' 'CI=true yarn test --colors --silent'", "postinstall": "husky install", "changeset": "changeset", "update-versions-and-changelogs": "changeset version && yarn version-run-all && bash scripts/update-lockfile.sh", From 79937cfca5e09cd17a01c4cf4865804df135be0b Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:29:36 -0600 Subject: [PATCH 06/21] make webpack less verbose --- packages/browser/webpack.config.js | 2 +- packages/config-webpack/webpack.config.common.js | 2 +- .../consent/consent-tools-integration-tests/webpack.config.ts | 1 + packages/signals/signals-example/webpack.config.js | 1 + packages/signals/signals-integration-tests/webpack.config.ts | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/browser/webpack.config.js b/packages/browser/webpack.config.js index d8ede4a9e..a51fd7774 100644 --- a/packages/browser/webpack.config.js +++ b/packages/browser/webpack.config.js @@ -38,7 +38,7 @@ if (process.env.ANALYZE) { /** @type { import('webpack').Configuration } */ const config = { - stats: process.env.WATCH === 'true' ? 'errors-warnings' : 'normal', + stats: process.env.WATCH === 'true' ? 'errors-warnings' : 'minimal', node: { global: false, // do not polyfill global object, we can use getGlobal function if needed. }, diff --git a/packages/config-webpack/webpack.config.common.js b/packages/config-webpack/webpack.config.common.js index d69005c4d..b9fb1eedb 100644 --- a/packages/config-webpack/webpack.config.common.js +++ b/packages/config-webpack/webpack.config.common.js @@ -16,7 +16,7 @@ const isWatch = process.env.WATCH === 'true' */ module.exports = { devtool: 'source-map', - stats: isWatch ? 'errors-warnings' : 'normal', + stats: isWatch ? 'errors-warnings' : 'minimal', mode: isProd ? 'production' : 'development', target: ['web', 'es5'], // target es5 for ie11 support (generates module boilerplate in es5) module: { diff --git a/packages/consent/consent-tools-integration-tests/webpack.config.ts b/packages/consent/consent-tools-integration-tests/webpack.config.ts index a00a02946..b1f184671 100644 --- a/packages/consent/consent-tools-integration-tests/webpack.config.ts +++ b/packages/consent/consent-tools-integration-tests/webpack.config.ts @@ -15,6 +15,7 @@ const entries = files.reduce((acc, file) => { }, {}) const config: WebpackConfiguration = { + stats: 'minimal', mode: 'development', devtool: 'source-map', entry: entries, diff --git a/packages/signals/signals-example/webpack.config.js b/packages/signals/signals-example/webpack.config.js index 1a8c12075..4848d5051 100644 --- a/packages/signals/signals-example/webpack.config.js +++ b/packages/signals/signals-example/webpack.config.js @@ -12,6 +12,7 @@ const bodyParser = require('body-parser') */ module.exports = { + stats: 'minimal', entry: './src/index.tsx', devtool: 'source-map', mode: 'development', diff --git a/packages/signals/signals-integration-tests/webpack.config.ts b/packages/signals/signals-integration-tests/webpack.config.ts index a22f8c2f7..6febeaed6 100644 --- a/packages/signals/signals-integration-tests/webpack.config.ts +++ b/packages/signals/signals-integration-tests/webpack.config.ts @@ -18,6 +18,7 @@ const entries = files.reduce((acc, file) => { }, {}) const config: WebpackConfiguration = { + stats: 'minimal', mode: 'production', devtool: 'source-map', entry: entries, From 2bf02ce908e56f51b80c6fe88a21a95d4b6b2cb4 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:32:44 -0600 Subject: [PATCH 07/21] update prepush --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 642f258e4..390b54416 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build": "turbo run build --filter='./packages/**'", "watch": "turbo run watch --filter='./packages/**'", "dev": "yarn workspace @playground/nOxt-playground run dev", - "prepush": "yarn concurrently --names 'LINT,TEST' -c 'yellow,blue' 'yarn lint' 'CI=true yarn test --colors --silent'", + "prepush": "yarn lint && CI=true yarn test --colors --silent", "postinstall": "husky install", "changeset": "changeset", "update-versions-and-changelogs": "changeset version && yarn version-run-all && bash scripts/update-lockfile.sh", From c5e81e97d559bce06b068bc3ae97f8a38f27ea14 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:33:01 -0600 Subject: [PATCH 08/21] wip --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 390b54416..d1fbda7c9 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "lint": "yarn constraints && turbo run lint --continue", "build": "turbo run build --filter='./packages/**'", "watch": "turbo run watch --filter='./packages/**'", - "dev": "yarn workspace @playground/nOxt-playground run dev", + "dev": "yarn workspace @playground/next-playground run dev", "prepush": "yarn lint && CI=true yarn test --colors --silent", "postinstall": "husky install", "changeset": "changeset", From 792292818994ed6795b991427b43d34405c6fc49 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:36:15 -0600 Subject: [PATCH 09/21] wip --- .../consent/consent-tools-integration-tests/webpack.config.ts | 3 +++ packages/signals/signals-example/webpack.config.js | 3 +++ packages/signals/signals-integration-tests/webpack.config.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/packages/consent/consent-tools-integration-tests/webpack.config.ts b/packages/consent/consent-tools-integration-tests/webpack.config.ts index b1f184671..27f3aa90b 100644 --- a/packages/consent/consent-tools-integration-tests/webpack.config.ts +++ b/packages/consent/consent-tools-integration-tests/webpack.config.ts @@ -16,6 +16,9 @@ const entries = files.reduce((acc, file) => { const config: WebpackConfiguration = { stats: 'minimal', + performance: { + hints: false, + }, mode: 'development', devtool: 'source-map', entry: entries, diff --git a/packages/signals/signals-example/webpack.config.js b/packages/signals/signals-example/webpack.config.js index 4848d5051..3ad742b6c 100644 --- a/packages/signals/signals-example/webpack.config.js +++ b/packages/signals/signals-example/webpack.config.js @@ -13,6 +13,9 @@ const bodyParser = require('body-parser') module.exports = { stats: 'minimal', + performance: { + hints: false, + }, entry: './src/index.tsx', devtool: 'source-map', mode: 'development', diff --git a/packages/signals/signals-integration-tests/webpack.config.ts b/packages/signals/signals-integration-tests/webpack.config.ts index 6febeaed6..874319e8e 100644 --- a/packages/signals/signals-integration-tests/webpack.config.ts +++ b/packages/signals/signals-integration-tests/webpack.config.ts @@ -19,6 +19,9 @@ const entries = files.reduce((acc, file) => { const config: WebpackConfiguration = { stats: 'minimal', + performance: { + hints: false, + }, mode: 'production', devtool: 'source-map', entry: entries, From 3be864dc85e756abf6d227f6cc9b4da53e8a7835 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:14:33 -0600 Subject: [PATCH 10/21] add tests --- .../dom-gen/mutation-observer.test.ts | 108 ++++++++++++++++++ .../dom-gen/mutation-observer.ts | 35 ++++-- 2 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.test.ts diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.test.ts new file mode 100644 index 000000000..fb2eea073 --- /dev/null +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.test.ts @@ -0,0 +1,108 @@ +/* eslint-disable jest/no-done-callback */ +import { sleep } from '@segment/analytics-core' +import { + MutationObservable, + MutationObservableSettings, + MutationObservableSubscriber, +} from './mutation-observer' + +describe('MutationObservable', () => { + let mutationObservable: MutationObservable + let testButton: HTMLButtonElement + let testInput: HTMLInputElement + const subscribeFn = jest.fn() as jest.Mock + beforeEach(() => { + document.body.innerHTML = + '
' + + '' + testButton = document.getElementById('test-element') as HTMLButtonElement + testInput = document.getElementById('test-input') as HTMLInputElement + }) + + afterEach(() => { + mutationObservable.cleanup() + }) + + it('should capture attribute changes', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + observedRoles: () => ['button'], + observedAttributes: () => ['aria-pressed'], + debounceMs: 500, + }) + ) + + mutationObservable.subscribe(subscribeFn) + testButton.setAttribute('aria-pressed', 'true') + await sleep(0) + + expect(subscribeFn).toHaveBeenCalledTimes(1) + expect(subscribeFn).toHaveBeenCalledWith({ + element: testButton, + attributes: [{ attributeName: 'aria-pressed', newValue: 'true' }], + }) + }) + + it('should capture multiple attribute changes', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + observedRoles: () => ['button'], + observedAttributes: () => ['aria-pressed'], + debounceMs: 500, + }) + ) + + mutationObservable.subscribe(subscribeFn) + testButton.setAttribute('aria-pressed', 'true') + await sleep(0) + testButton.setAttribute('aria-pressed', 'false') + await sleep(0) + + expect(subscribeFn).toHaveBeenCalledTimes(2) + expect(subscribeFn).toHaveBeenNthCalledWith(1, { + element: testButton, + attributes: [{ attributeName: 'aria-pressed', newValue: 'true' }], + }) + expect(subscribeFn).toHaveBeenNthCalledWith(2, { + element: testButton, + attributes: [{ attributeName: 'aria-pressed', newValue: 'false' }], + }) + }) + + it('should debounce attribute changes if they occur in text inputs', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + debounceMs: 100, + }) + ) + mutationObservable.subscribe(subscribeFn) + testInput.setAttribute('value', 'hello') + await sleep(0) + testInput.setAttribute('value', 'hello wor') + await sleep(0) + testInput.setAttribute('value', 'hello world') + await sleep(200) + + expect(subscribeFn).toHaveBeenCalledTimes(1) + expect(subscribeFn).toHaveBeenCalledWith({ + element: testInput, + attributes: [{ attributeName: 'value', newValue: 'hello world' }], + }) + }) + + it('should not emit event for aria-selected=false', (done) => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + observedRoles: () => ['button'], + observedAttributes: () => ['aria-selected'], + }) + ) + + mutationObservable.subscribe(() => { + done.fail('Should not emit event for aria-selected=false') + }) + + testButton.setAttribute('aria-selected', 'false') + setTimeout(done, 1000) // Wait to ensure no event is emitted + }) +}) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index 02f454f99..54e0215a8 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -1,6 +1,7 @@ import { Emitter } from '@segment/analytics-generic-utils' import { exists } from '../../../lib/exists' import { debounceWithKey } from '../../../lib/debounce' +import { logger } from '../../../lib/logger' const DEFAULT_OBSERVED_ATTRIBUTES = [ 'aria-pressed', @@ -133,6 +134,9 @@ const shouldDebounce = (el: HTMLElement): boolean => { return false } +export type MutationObservableSubscriber = ( + event: AttributeChangedEvent +) => void /** * This class is responsible for observing changes to elements in the DOM * This is preferred over monitoring document 'change' events, as it captures changes to custom elements @@ -143,9 +147,9 @@ export class MutationObservable { // WeakSet is used here to allow garbage collection of elements that are no longer in the DOM private observedElements = new WeakSet() private emitter = new ElementChangedEmitter() - private listeners = new Set<(event: AttributeChangedEvent) => void>() + private listeners = new Set() - subscribe(fn: (event: AttributeChangedEvent) => void) { + subscribe(fn: MutationObservableSubscriber) { this.listeners.add(fn) this.emitter.on('attributeChanged', fn) } @@ -158,13 +162,8 @@ export class MutationObservable { private pollTimeout: ReturnType - constructor( - settings: MutationObservableSettingsConfig | MutationObservableSettings = {} - ) { - this.settings = - settings instanceof MutationObservableSettings - ? settings - : new MutationObservableSettings(settings) + constructor(settings?: MutationObservableSettings) { + this.settings = settings ?? new MutationObservableSettings() this.checkForNewElements(this.emitter) @@ -205,7 +204,9 @@ export class MutationObservable { ? addOnBlurListener : _emitAttributeMutationEvent - const _emitAttributeMutationEventDebounced = shouldDebounce(element) + const shouldDebounceElement = shouldDebounce(element) + + const _emitAttributeMutationEventDebounced = shouldDebounceElement ? debounceWithKey( emit, // debounce based on the attribute names, so that we can debounce all changes to a single attribute. e.g if attribute "value" changes, that gets debounced, but if another attribute changes, that gets debounced separately @@ -214,17 +215,26 @@ export class MutationObservable { ) : _emitAttributeMutationEvent + // any call to setAttribute triggers a mutation event const cb: MutationCallback = (mutationsList) => { const attributeMutations = mutationsList .filter((m) => m.type === 'attributes') .map((m) => { const attributeName = m.attributeName - if (!attributeName) return - const newValue = element.getAttribute(attributeName) + const target = m.target + if (!attributeName || !target || !(target instanceof HTMLElement)) + return + + const newValue = target.getAttribute(attributeName) const v: AttributeMutation = { attributeName, newValue: newValue, } + logger.debug('Attribute mutation', { + newValue, + oldValue: m.oldValue, + target: m.target, + }) return v }) .filter(exists) @@ -258,6 +268,7 @@ export class MutationObservable { if (this.observedElements.has(element)) { return } + logger.debug('Observing element', element) this.observeElementAttributes( element as HTMLElement, this.settings.observedAttributes, From a075de8ab77493339cf9e5708c2779d693eccd31 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:04:56 -0600 Subject: [PATCH 11/21] add dedupe logic --- .../__tests__/mutation-observer.test.ts | 203 ++++++++++++++++++ .../signal-generators/dom-gen/change-gen.ts | 17 +- .../dom-gen/mutation-observer.test.ts | 108 ---------- .../dom-gen/mutation-observer.ts | 73 +++++-- 4 files changed, 256 insertions(+), 145 deletions(-) create mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.test.ts diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts new file mode 100644 index 000000000..ebdc561d4 --- /dev/null +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts @@ -0,0 +1,203 @@ +/* eslint-disable jest/no-done-callback */ +import { sleep } from '@segment/analytics-core' +import { + MutationObservable, + MutationObservableSettings, + MutationObservableSubscriber, +} from '../mutation-observer' + +describe('MutationObservable', () => { + let mutationObservable: MutationObservable + let testButton: HTMLButtonElement + let testInput: HTMLInputElement + const subscribeFn = jest.fn() as jest.Mock + beforeEach(() => { + document.body.innerHTML = + '
' + + '' + testButton = document.getElementById('test-element') as HTMLButtonElement + testInput = document.getElementById('test-input') as HTMLInputElement + }) + + afterEach(() => { + mutationObservable.cleanup() + }) + + it('should capture attribute changes', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + observedRoles: () => ['button'], + observedAttributes: () => ['aria-pressed'], + debounceMs: 500, + }) + ) + + mutationObservable.subscribe(subscribeFn) + testButton.setAttribute('aria-pressed', 'true') + await sleep(0) + + expect(subscribeFn).toHaveBeenCalledTimes(1) + expect(subscribeFn).toHaveBeenCalledWith({ + element: testButton, + attributes: { 'aria-pressed': 'true' }, + }) + }) + + it('should capture multiple attribute changes', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + observedRoles: () => ['button'], + observedAttributes: () => ['aria-pressed'], + debounceMs: 500, + }) + ) + + mutationObservable.subscribe(subscribeFn) + testButton.setAttribute('aria-pressed', 'true') + await sleep(0) + testButton.setAttribute('aria-pressed', 'false') + await sleep(0) + + expect(subscribeFn).toHaveBeenCalledTimes(2) + expect(subscribeFn).toHaveBeenNthCalledWith(1, { + element: testButton, + attributes: { 'aria-pressed': 'true' }, + }) + expect(subscribeFn).toHaveBeenNthCalledWith(2, { + element: testButton, + attributes: { 'aria-pressed': 'false' }, + }) + }) + + it('should debounce attribute changes if they occur in text inputs', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + debounceMs: 100, + }) + ) + mutationObservable.subscribe(subscribeFn) + testInput.setAttribute('value', 'hello') + await sleep(0) + testInput.setAttribute('value', 'hello wor') + await sleep(0) + testInput.setAttribute('value', 'hello world') + + await sleep(200) + expect(subscribeFn).toHaveBeenCalledTimes(1) + expect(subscribeFn).toHaveBeenCalledWith({ + element: testInput, + attributes: { value: 'hello world' }, + }) + }) + + it('should handle multiple attributes changeing', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + debounceMs: 100, + observedAttributes: (roles) => [...roles, 'aria-foo'], + }) + ) + mutationObservable.subscribe(subscribeFn) + testInput.setAttribute('value', 'hello') + testInput.setAttribute('aria-foo', 'bar') + await sleep(200) + + expect(subscribeFn).toHaveBeenCalledTimes(1) + expect(subscribeFn).toHaveBeenCalledWith({ + element: testInput, + attributes: { value: 'hello', 'aria-foo': 'bar' }, + }) + }) + + it('should debounce if happening in the same tick', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + debounceMs: 50, + }) + ) + mutationObservable.subscribe(subscribeFn) + testInput.setAttribute('value', 'hello') + testInput.setAttribute('value', 'hello wor') + testInput.setAttribute('value', 'hello world') + await sleep(100) + + expect(subscribeFn).toHaveBeenCalledTimes(1) + expect(subscribeFn).toHaveBeenCalledWith({ + element: testInput, + attributes: { value: 'hello world' }, + }) + }) + + it('should not emit duplicate events', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + observedRoles: () => ['button'], + observedAttributes: () => ['aria-pressed'], + debounceMs: 0, + }) + ) + + mutationObservable.subscribe(subscribeFn) + testButton.setAttribute('aria-pressed', 'true') + await sleep(0) + testButton.setAttribute('aria-pressed', 'true') + await sleep(0) + + expect(subscribeFn).toHaveBeenCalledTimes(1) + expect(subscribeFn).toHaveBeenCalledWith({ + element: testButton, + attributes: { 'aria-pressed': 'true' }, + }) + }) + + it('should not emit duplicate events if overlapping', async () => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + observedRoles: () => ['button'], + observedAttributes: () => ['aria-pressed', 'aria-foo'], + debounceMs: 0, + }) + ) + + mutationObservable.subscribe(subscribeFn) + testButton.setAttribute('aria-pressed', 'true') + testButton.setAttribute('aria-foo', 'bar') + await sleep(0) + + testButton.setAttribute('aria-pressed', 'false') + await sleep(50) + + testButton.setAttribute('aria-pressed', 'false') + await sleep(50) + + testButton.setAttribute('aria-foo', 'bar') + await sleep(50) + + expect(subscribeFn).toHaveBeenNthCalledWith(1, { + element: testButton, + attributes: { 'aria-pressed': 'true', 'aria-foo': 'bar' }, + }) + + expect(subscribeFn).toHaveBeenNthCalledWith(2, { + element: testButton, + attributes: { 'aria-pressed': 'false' }, + }) + expect(subscribeFn).toHaveBeenCalledTimes(2) + }) + + it('should not emit event for aria-selected=false', (done) => { + mutationObservable = new MutationObservable( + new MutationObservableSettings({ + observedRoles: () => ['button'], + observedAttributes: () => ['aria-selected'], + }) + ) + + mutationObservable.subscribe(() => { + done.fail('Should not emit event for aria-selected=false') + }) + + testButton.setAttribute('aria-selected', 'false') + setTimeout(done, 1000) // Wait to ensure no event is emitted + }) +}) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts index 39c3666cd..7a9c2b7f5 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts @@ -25,21 +25,6 @@ export class MutationChangeGenerator implements SignalGenerator { } register(emitter: SignalEmitter) { - type NormalizedAttributes = { [attributeName: string]: string | null } - const normalizeAttributes = ( - attributeMutation: AttributeChangedEvent - ): NormalizedAttributes => { - const attributes = - attributeMutation.attributes.reduce( - (acc, { attributeName, newValue }) => { - acc[attributeName] = newValue - return acc - }, - {} - ) - return attributes - } - const callback = (ev: AttributeChangedEvent) => { const target = ev.element as HTMLElement | null if (!target || shouldIgnoreElement(target)) { @@ -51,7 +36,7 @@ export class MutationChangeGenerator implements SignalGenerator { eventType: 'change', target: el, listener: 'mutation', - change: normalizeAttributes(ev), + change: ev.attributes, }) ) } diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.test.ts deleted file mode 100644 index fb2eea073..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* eslint-disable jest/no-done-callback */ -import { sleep } from '@segment/analytics-core' -import { - MutationObservable, - MutationObservableSettings, - MutationObservableSubscriber, -} from './mutation-observer' - -describe('MutationObservable', () => { - let mutationObservable: MutationObservable - let testButton: HTMLButtonElement - let testInput: HTMLInputElement - const subscribeFn = jest.fn() as jest.Mock - beforeEach(() => { - document.body.innerHTML = - '
' + - '' - testButton = document.getElementById('test-element') as HTMLButtonElement - testInput = document.getElementById('test-input') as HTMLInputElement - }) - - afterEach(() => { - mutationObservable.cleanup() - }) - - it('should capture attribute changes', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - observedRoles: () => ['button'], - observedAttributes: () => ['aria-pressed'], - debounceMs: 500, - }) - ) - - mutationObservable.subscribe(subscribeFn) - testButton.setAttribute('aria-pressed', 'true') - await sleep(0) - - expect(subscribeFn).toHaveBeenCalledTimes(1) - expect(subscribeFn).toHaveBeenCalledWith({ - element: testButton, - attributes: [{ attributeName: 'aria-pressed', newValue: 'true' }], - }) - }) - - it('should capture multiple attribute changes', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - observedRoles: () => ['button'], - observedAttributes: () => ['aria-pressed'], - debounceMs: 500, - }) - ) - - mutationObservable.subscribe(subscribeFn) - testButton.setAttribute('aria-pressed', 'true') - await sleep(0) - testButton.setAttribute('aria-pressed', 'false') - await sleep(0) - - expect(subscribeFn).toHaveBeenCalledTimes(2) - expect(subscribeFn).toHaveBeenNthCalledWith(1, { - element: testButton, - attributes: [{ attributeName: 'aria-pressed', newValue: 'true' }], - }) - expect(subscribeFn).toHaveBeenNthCalledWith(2, { - element: testButton, - attributes: [{ attributeName: 'aria-pressed', newValue: 'false' }], - }) - }) - - it('should debounce attribute changes if they occur in text inputs', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - debounceMs: 100, - }) - ) - mutationObservable.subscribe(subscribeFn) - testInput.setAttribute('value', 'hello') - await sleep(0) - testInput.setAttribute('value', 'hello wor') - await sleep(0) - testInput.setAttribute('value', 'hello world') - await sleep(200) - - expect(subscribeFn).toHaveBeenCalledTimes(1) - expect(subscribeFn).toHaveBeenCalledWith({ - element: testInput, - attributes: [{ attributeName: 'value', newValue: 'hello world' }], - }) - }) - - it('should not emit event for aria-selected=false', (done) => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - observedRoles: () => ['button'], - observedAttributes: () => ['aria-selected'], - }) - ) - - mutationObservable.subscribe(() => { - done.fail('Should not emit event for aria-selected=false') - }) - - testButton.setAttribute('aria-selected', 'false') - setTimeout(done, 1000) // Wait to ensure no event is emitted - }) -}) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index 54e0215a8..fc0e12486 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -32,13 +32,18 @@ const DEFAULT_OBSERVED_ROLES = [ 'treeitem', ] -type AttributeMutation = { - attributeName: string - newValue: string | null +const partialMatch = >( + a: Partial, + b: Obj +): boolean => { + return Object.keys(a).every( + (key) => a[key as keyof Obj] === b[key as keyof Obj] + ) } +type AttributeMutations = { [attributeName: string]: string | null } export type AttributeChangedEvent = { element: HTMLElement - attributes: AttributeMutation[] + attributes: AttributeMutations } export interface MutationObservableSettingsConfig { @@ -53,7 +58,7 @@ export interface MutationObservableSettingsConfig { export class MutationObservableSettings { pollIntervalMs: number - debounceTextInputMs: number + debounceMs: number emitInputStrategy: 'debounce-only' | 'blur' extraSelectors: string[] observedRoles: string[] @@ -72,12 +77,10 @@ export class MutationObservableSettings { if (pollIntervalMs < 300) { throw new Error('Poll interval must be at least 300ms') } - if (debounceMs < 100) { - throw new Error('Debounce must be at least 100ms') - } + this.emitInputStrategy = emitInputStrategy this.pollIntervalMs = pollIntervalMs - this.debounceTextInputMs = debounceMs + this.debounceMs = debounceMs this.extraSelectors = extraSelectors this.observedRoles = observedRoles @@ -146,6 +149,7 @@ export class MutationObservable { // Track observed elements to avoid duplicate observers // WeakSet is used here to allow garbage collection of elements that are no longer in the DOM private observedElements = new WeakSet() + private prevMutationsCache = new WeakMap() private emitter = new ElementChangedEmitter() private listeners = new Set() @@ -173,9 +177,12 @@ export class MutationObservable { ) } - private shouldEmitEvent(mut: AttributeMutation): boolean { + private shouldEmitEvent( + attributeName: string, + newValue: string | null + ): boolean { // Filter out aria-selected events where the new value is false, since there will always be another selected value -- otherwise, checked would/should be used - if (mut.attributeName === 'aria-selected' && mut.newValue === 'false') { + if (attributeName === 'aria-selected' && newValue === 'false') { return false } return true @@ -188,13 +195,13 @@ export class MutationObservable { attributes: string[], emitter: ElementChangedEmitter ) { - const _emitAttributeMutationEvent = (attributes: AttributeMutation[]) => { + const _emitAttributeMutationEvent = (attributes: AttributeMutations) => { emitter.emit('attributeChanged', { element, attributes, }) } - const addOnBlurListener = (attributeMutations: AttributeMutation[]) => + const addOnBlurListener = (attributeMutations: AttributeMutations) => this.experimentalOnChangeAdapter.onBlur(element, () => _emitAttributeMutationEvent(attributeMutations) ) @@ -210,14 +217,14 @@ export class MutationObservable { ? debounceWithKey( emit, // debounce based on the attribute names, so that we can debounce all changes to a single attribute. e.g if attribute "value" changes, that gets debounced, but if another attribute changes, that gets debounced separately - (m) => Object.keys(m.map((m) => m.attributeName)).sort(), - this.settings.debounceTextInputMs + (m) => Object.keys(m).sort(), + this.settings.debounceMs ) : _emitAttributeMutationEvent // any call to setAttribute triggers a mutation event const cb: MutationCallback = (mutationsList) => { - const attributeMutations = mutationsList + const mutations: AttributeMutations = mutationsList .filter((m) => m.type === 'attributes') .map((m) => { const attributeName = m.attributeName @@ -226,10 +233,10 @@ export class MutationObservable { return const newValue = target.getAttribute(attributeName) - const v: AttributeMutation = { + const v = { attributeName, newValue: newValue, - } + } as const logger.debug('Attribute mutation', { newValue, oldValue: m.oldValue, @@ -238,11 +245,35 @@ export class MutationObservable { return v }) .filter(exists) - .filter((event) => this.shouldEmitEvent(event)) + .filter((event) => + this.shouldEmitEvent(event.attributeName, event.newValue) + ) + .reduce((acc, mut) => { + acc[mut.attributeName] = mut.newValue + return acc + }, {}) - if (attributeMutations.length) { - _emitAttributeMutationEventDebounced(attributeMutations) + const isEmpty = Object.keys(mutations).length > 0 + if (!isEmpty) { + return } + + // only emit if there are actual change to an attribute. + // in mutationObserver, setAttribute('value', ''), setAttribute('value', '') will both trigger a mutation event + // if the value is the same as the last one emitted from a given element, we don't want to emit it again + const prevMutations = this.prevMutationsCache.get(element) + if (prevMutations) { + const hasActuallyChanged = !partialMatch(mutations, prevMutations) + if (!hasActuallyChanged) { + return + } + } + this.prevMutationsCache.set(element, { + ...prevMutations, + ...mutations, + }) + + _emitAttributeMutationEventDebounced(mutations) } const observer = new MutationObserver(cb) From a3e34e5dd6af8e0b3f73449cd4164bc4a24cdec6 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:22:49 -0600 Subject: [PATCH 12/21] wip --- .../__tests__/mutation-observer.test.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts index ebdc561d4..950906967 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts @@ -69,7 +69,7 @@ describe('MutationObservable', () => { }) }) - it('should debounce attribute changes if they occur in text inputs', async () => { + it('should debounce text inputs', async () => { mutationObservable = new MutationObservable( new MutationObservableSettings({ debounceMs: 100, @@ -89,42 +89,41 @@ describe('MutationObservable', () => { attributes: { value: 'hello world' }, }) }) - - it('should handle multiple attributes changeing', async () => { + it('should debounce tect inputs if happening in the same tick', async () => { mutationObservable = new MutationObservable( new MutationObservableSettings({ debounceMs: 100, - observedAttributes: (roles) => [...roles, 'aria-foo'], }) ) mutationObservable.subscribe(subscribeFn) testInput.setAttribute('value', 'hello') - testInput.setAttribute('aria-foo', 'bar') - await sleep(200) + testInput.setAttribute('value', 'hello wor') + testInput.setAttribute('value', 'hello world') + await sleep(100) expect(subscribeFn).toHaveBeenCalledTimes(1) expect(subscribeFn).toHaveBeenCalledWith({ element: testInput, - attributes: { value: 'hello', 'aria-foo': 'bar' }, + attributes: { value: 'hello world' }, }) }) - it('should debounce if happening in the same tick', async () => { + it('should handle multiple attributes changing', async () => { mutationObservable = new MutationObservable( new MutationObservableSettings({ - debounceMs: 50, + debounceMs: 100, + observedAttributes: (roles) => [...roles, 'aria-foo'], }) ) mutationObservable.subscribe(subscribeFn) testInput.setAttribute('value', 'hello') - testInput.setAttribute('value', 'hello wor') - testInput.setAttribute('value', 'hello world') - await sleep(100) + testInput.setAttribute('aria-foo', 'bar') + await sleep(200) expect(subscribeFn).toHaveBeenCalledTimes(1) expect(subscribeFn).toHaveBeenCalledWith({ element: testInput, - attributes: { value: 'hello world' }, + attributes: { value: 'hello', 'aria-foo': 'bar' }, }) }) From e25ddbd0a50a79db4b0719584621695edaec47f1 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:25:35 -0600 Subject: [PATCH 13/21] wip --- .../dom-gen/__tests__/mutation-observer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts index 950906967..6cf8a36a4 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts @@ -92,7 +92,7 @@ describe('MutationObservable', () => { it('should debounce tect inputs if happening in the same tick', async () => { mutationObservable = new MutationObservable( new MutationObservableSettings({ - debounceMs: 100, + debounceMs: 50, }) ) mutationObservable.subscribe(subscribeFn) From ae55158616be04357b64f8cd2f3de262d934648c Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:26:22 -0600 Subject: [PATCH 14/21] wip --- .../src/core/signal-generators/dom-gen/mutation-observer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index fc0e12486..0017614b2 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -148,7 +148,7 @@ export class MutationObservable { private settings: MutationObservableSettings // Track observed elements to avoid duplicate observers // WeakSet is used here to allow garbage collection of elements that are no longer in the DOM - private observedElements = new WeakSet() + private observedElements = new WeakSet() private prevMutationsCache = new WeakMap() private emitter = new ElementChangedEmitter() private listeners = new Set() @@ -296,7 +296,7 @@ export class MutationObservable { allElementSelectors.forEach((selector) => { const elements = document.querySelectorAll(selector) elements.forEach((element) => { - if (this.observedElements.has(element)) { + if (this.observedElements.has(element as HTMLElement)) { return } logger.debug('Observing element', element) From b38d605f2c315fb800d55b9be913c4d83fd47810 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:38:11 -0600 Subject: [PATCH 15/21] wip --- .../signal-generators/dom-gen/mutation-observer.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index 0017614b2..e7fd8e6b0 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -32,12 +32,13 @@ const DEFAULT_OBSERVED_ROLES = [ 'treeitem', ] -const partialMatch = >( - a: Partial, - b: Obj +// Check if a subset object is a partial match of another object +const isObjectMatch = >( + partialObj: Partial, + mainObj: Obj ): boolean => { - return Object.keys(a).every( - (key) => a[key as keyof Obj] === b[key as keyof Obj] + return Object.keys(partialObj).every( + (key) => partialObj[key as keyof Obj] === mainObj[key as keyof Obj] ) } type AttributeMutations = { [attributeName: string]: string | null } @@ -263,7 +264,7 @@ export class MutationObservable { // if the value is the same as the last one emitted from a given element, we don't want to emit it again const prevMutations = this.prevMutationsCache.get(element) if (prevMutations) { - const hasActuallyChanged = !partialMatch(mutations, prevMutations) + const hasActuallyChanged = !isObjectMatch(mutations, prevMutations) if (!hasActuallyChanged) { return } From 6185301129ec2bb1e8b4d31554effd608dacba5f Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:38:48 -0600 Subject: [PATCH 16/21] wip --- .../src/core/signal-generators/dom-gen/helpers.ts | 10 ++++++++++ .../signal-generators/dom-gen/mutation-observer.ts | 10 +--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts index 4383aa3cb..8030d3143 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts @@ -5,3 +5,13 @@ export const cleanText = (str: string): string => { .replace(/\u00A0/g, ' ') // Replace non-breaking spaces with a regular space .trim() // Trim leading and trailing spaces } + +// Check if a subset object is a partial match of another object +export const isObjectMatch = >( + partialObj: Partial, + mainObj: Obj +): boolean => { + return Object.keys(partialObj).every( + (key) => partialObj[key as keyof Obj] === mainObj[key as keyof Obj] + ) +} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index e7fd8e6b0..905914ebd 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -2,6 +2,7 @@ import { Emitter } from '@segment/analytics-generic-utils' import { exists } from '../../../lib/exists' import { debounceWithKey } from '../../../lib/debounce' import { logger } from '../../../lib/logger' +import { isObjectMatch } from './helpers' const DEFAULT_OBSERVED_ATTRIBUTES = [ 'aria-pressed', @@ -32,15 +33,6 @@ const DEFAULT_OBSERVED_ROLES = [ 'treeitem', ] -// Check if a subset object is a partial match of another object -const isObjectMatch = >( - partialObj: Partial, - mainObj: Obj -): boolean => { - return Object.keys(partialObj).every( - (key) => partialObj[key as keyof Obj] === mainObj[key as keyof Obj] - ) -} type AttributeMutations = { [attributeName: string]: string | null } export type AttributeChangedEvent = { element: HTMLElement From 6c8497064bf73067bdfbb346c996e19f73609a94 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:42:59 -0600 Subject: [PATCH 17/21] wip --- .../src/core/signal-generators/dom-gen/mutation-observer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index 905914ebd..ed84ee3be 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -141,7 +141,7 @@ export class MutationObservable { private settings: MutationObservableSettings // Track observed elements to avoid duplicate observers // WeakSet is used here to allow garbage collection of elements that are no longer in the DOM - private observedElements = new WeakSet() + private observedElements = new WeakSet() private prevMutationsCache = new WeakMap() private emitter = new ElementChangedEmitter() private listeners = new Set() @@ -289,7 +289,7 @@ export class MutationObservable { allElementSelectors.forEach((selector) => { const elements = document.querySelectorAll(selector) elements.forEach((element) => { - if (this.observedElements.has(element as HTMLElement)) { + if (this.observedElements.has(element)) { return } logger.debug('Observing element', element) From a878c9c1273d5c5bf37cc65a9458bf30420879b0 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:26:53 -0600 Subject: [PATCH 18/21] wip --- .../src/core/signal-generators/dom-gen/mutation-observer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index ed84ee3be..45e840510 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -246,8 +246,8 @@ export class MutationObservable { return acc }, {}) - const isEmpty = Object.keys(mutations).length > 0 - if (!isEmpty) { + const isEmpty = Object.keys(mutations).length === 0 + if (isEmpty) { return } From 4bee35baff5ef61bed392c39024b0fd0b029aa98 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:28:28 -0600 Subject: [PATCH 19/21] wip --- .../src/core/signal-generators/dom-gen/mutation-observer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index 45e840510..391fa83f9 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -261,6 +261,7 @@ export class MutationObservable { return } } + this.prevMutationsCache.set(element, { ...prevMutations, ...mutations, From 60cdecf82a27952e3d1dc4a35a3333934f8a87be Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:28:56 -0600 Subject: [PATCH 20/21] wip --- .../src/core/signal-generators/dom-gen/mutation-observer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index 391fa83f9..43e7984e3 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -206,7 +206,7 @@ export class MutationObservable { const shouldDebounceElement = shouldDebounce(element) - const _emitAttributeMutationEventDebounced = shouldDebounceElement + const _emitMaybeDebouncedEvent = shouldDebounceElement ? debounceWithKey( emit, // debounce based on the attribute names, so that we can debounce all changes to a single attribute. e.g if attribute "value" changes, that gets debounced, but if another attribute changes, that gets debounced separately @@ -267,7 +267,7 @@ export class MutationObservable { ...mutations, }) - _emitAttributeMutationEventDebounced(mutations) + _emitMaybeDebouncedEvent(mutations) } const observer = new MutationObserver(cb) From ad029116ba8937885446be37b3fca1407a22c444 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:37:49 -0600 Subject: [PATCH 21/21] wip --- .../core/signal-generators/dom-gen/mutation-observer.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts index 43e7984e3..63d8e345d 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts @@ -184,10 +184,13 @@ export class MutationObservable { private experimentalOnChangeAdapter = new ExperimentalOnChangeEventAdapter() private observeElementAttributes( - element: HTMLElement, + element: Element, attributes: string[], emitter: ElementChangedEmitter ) { + if (!(element instanceof HTMLElement)) { + return + } const _emitAttributeMutationEvent = (attributes: AttributeMutations) => { emitter.emit('attributeChanged', { element, @@ -295,7 +298,7 @@ export class MutationObservable { } logger.debug('Observing element', element) this.observeElementAttributes( - element as HTMLElement, + element, this.settings.observedAttributes, emitter )