diff --git a/CHANGELOG.md b/CHANGELOG.md index a82ce94..0832539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,59 @@ For more context information, please read through the To see all merged commits on the default branch that will be part of the next plotly.js release, go to: - + where X.Y.Z is the semver of most recent react-plotly.js release. ## [Unreleased] +## [4.1.0] - 2026-07-29 + +### Added + +- Added missing `onTreemapClick` and `onIcicleClick` event props for the `plotly_treemapclick` and `plotly_icicleclick` events [[#376](https://github.com/plotly/react-plotly.js/pull/376)] +- Added additional missing events: `onLegendTitleClick`, `onLegendTitleDoubleClick`, `onBeforePlot`, `onAnimating`, and `onTransitioned` [[#376](https://github.com/plotly/react-plotly.js/pull/376)] + +### Fixed + +- Returning `false` from `onSunburstClick`, `onTreemapClick`, or `onIcicleClick` now prevents the drill-down [[#376](https://github.com/plotly/react-plotly.js/pull/376)] + - `onUpdate` now fires for these via `plotly_animated`, so it reports the figure after the new `level` is applied and no longer fires when the drill-down is cancelled + +### Removed + +- Removed the errantly added `onClickAnywhere` and `onHoverAnywhere` props [[#376](https://github.com/plotly/react-plotly.js/pull/376)] + - plotly.js has no `plotly_clickanywhere` or `plotly_hoveranywhere` events, so these never fired + - `clickanywhere` and `hoveranywhere` are **layout attributes** that widen the ordinary `plotly_click` / `plotly_hover`, so the feature is reached through `onClick` / `onHover` with the layout flag set — see the README + +## [4.0.0] - 2026-06-18 + +### Added + +- Added native ESM output alongside CJS [[#367](https://github.com/plotly/react-plotly.js/pull/367)] + - Published artifacts now live under `dist/` instead of at the repo root; the `main` entry moved from `react-plotly.js` to `./dist/index.cjs` + - Dropped the unminified UMD bundle + - `import Plot from 'react-plotly.js'` now resolves correctly in Rolldown and any other strict ESM environment +- Added TypeScript declaration files (`index.d.ts` / `factory.d.ts`) [[#368](https://github.com/plotly/react-plotly.js/pull/368)] + +### Changed + +- Update linting dependencies [[#366](https://github.com/plotly/react-plotly.js/pull/366)] +- Refactored the wrapper from a class component to a functional component using hooks [[#369](https://github.com/plotly/react-plotly.js/pull/369)] + - Refs forwarded to `` now resolve to the rendered `
` element (the plotly graph div) directly via `forwardRef`, instead of to the class instance + - **Migration:** replace `ref.current.el` with `ref.current` + +### Fixed + +- `onPurge` now fires and `Plotly.purge` runs on unmount [[#372](https://github.com/plotly/react-plotly.js/pull/372)] +- Plot is re-initialized correctly after React StrictMode's dev-only simulated unmount/remount [[#372](https://github.com/plotly/react-plotly.js/pull/372)] +- TypeScript declarations now expose ref forwarding so consumers can attach a typed `ref` prop without a TS error [[#373](https://github.com/plotly/react-plotly.js/pull/373)] + ## [3.0.0] - 2026-06-09 ### Added -- `onClickAnywhere` and `onHoverAnywhere` event props for the corresponding `plotly_clickanywhere` and `plotly_hoveranywhere` events introduced in plotly.js v3 [[#360](https://github.com/plotly/react-plotly.js/pull/360)] +- `onClickAnywhere` and `onHoverAnywhere` event props [[#360](https://github.com/plotly/react-plotly.js/pull/360)] + - **Correction:** plotly.js v3 introduced `clickanywhere` / `hoveranywhere` as _layout attributes_, not as events, so these props never fired. They are removed in the next release. ### Changed diff --git a/README.md b/README.md index 5552775..2a5a170 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,9 @@ # react-plotly.js -![plotly-react-logo](https://images.plot.ly/plotly-documentation/thumbnail/react.png) +![plotly-react-logo](plotly-react-logo.png) > A [plotly.js](https://github.com/plotly/plotly.js) React component from -> [Plotly](https://plot.ly/). The basis of Plotly's -> [React component suite](https://plot.ly/products/react/). - -👉 [DEMO](http://react-plotly.js-demo.getforge.io/) - -👉 [Demo source code](https://github.com/plotly/react-plotly.js-demo-app) +> [Plotly](https://plotly.com/).
@@ -24,9 +19,14 @@ - [Quick start](#quick-start) - [State management](#state-management) - [Refreshing the Plot](#refreshing-the-plot) -- [API](#api) +- [API](#api-reference) - [Basic props](#basic-props) - [Event handler props](#event-handler-props) +- [Examples](#examples) + - [Responsive plot](#responsive-plot) + - [Event handlers](#event-handlers) + - [TypeScript](#typescript) + - [Grabbing the graph div via `ref`](#grabbing-the-graph-div-via-ref) - [Customizing the `plotly.js` bundle](#customizing-the-plotlyjs-bundle) - [Loading from a ` + + + ``` -And instantiate the component with +React is pinned to 18 here because React 19 stopped shipping UMD builds; consumers wanting React 19 should load it via [importmap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) or use a bundler. + +The factory is exposed as the global `createPlotlyComponent`. Build the component and mount it: ```javascript const Plot = createPlotlyComponent(Plotly); - -ReactDOM.render( +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( React.createElement(Plot, { data: [{x: [1, 2, 3], y: [2, 1, 3]}], - }), - document.getElementById('root') + }) ); ``` -You can see an example of this method in action -[here](https://codepen.io/rsreusser/pen/qPgwwJ?editors=1010). - ## Development To get started: @@ -240,18 +334,26 @@ To get started: $ npm install ``` -To transpile from ES2015 + JSX into the ES5 npm-distributed version: +To build the published artifacts (`dist/index.{mjs,cjs}`, `dist/factory.{mjs,cjs}`, the UMD bundle, and the declaration files) via [tsup](https://tsup.egoist.dev/): ```bash -$ npm run prepublishOnly +$ npm run build ``` -To run the tests: +To run lint + typecheck + jest: ```bash $ npm run test ``` +To watch source files and rebuild on change: + +```bash +$ npm run watch +``` + +Releases are cut from `main` per the steps in [`RELEASE.md`](./RELEASE.md). + ## License -© 2017-2020 Plotly, Inc. MIT License. +© 2017-2026 Plotly, Inc. MIT License. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..28d4737 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,105 @@ +# Releasing react-plotly.js + +This document covers how to cut a new release of `react-plotly.js` to npm. +It's evergreen — substitute `X.Y.Z` with the target version (e.g. `3.0.0`) +and `vX.Y.Z` with the git tag (e.g. `v3.0.0`). + +## Prerequisites + +- npm publish access to the [`react-plotly.js`](https://www.npmjs.com/package/react-plotly.js) package. Granted by an existing maintainer via the npm `plotly` org. +- npm two-factor authentication configured (`npm profile get`). Publishes require an OTP. +- Push access to `plotly/react-plotly.js` on GitHub, including tag-creation permission. +- A clean local clone of the repo, ideally with `node_modules` reinstalled fresh against `main`. + +## Release sequence + +### 1. Pre-flight checks + +From the branch that will become the release and in a clean state (typically `main`, or an integration branch like `vN` that's about to merge into `main`): + +```bash +npm install # fresh install against the current lockfile +npm test # lint + typecheck + jest +npm run build # verifies the tsup build pipeline (ESM + CJS + UMD bundles) +npm run clean # removes build artifacts (also runs as part of prepublishOnly) +``` + +`npm test` and `npm run build` must succeed. CI on the source branch should also be green. + +### 2. Update the changelog + +`CHANGELOG.md` follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). For each release: + +1. Rename the `## [Unreleased]` heading to `## [X.Y.Z] - YYYY-MM-DD` using the actual release date. +2. Add a fresh empty `## [Unreleased]` heading above it. + +Section order within a release: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security` (omit any that have no entries). Use past-tense verbs. Link each entry to the PR that introduced it, and credit external contributors with `, with thanks to @user for the contribution!` at the end of the line. + +### 3. Bump the version + +```bash +npm version X.Y.Z --no-git-tag-version +``` + +`--no-git-tag-version` prevents npm from tagging immediately; the tag is created later, after the release PR has merged. + +### 4. Open the release PR + +Branch the release off the appropriate parent — the integration branch (e.g. `vN`) for a major cumulative release, or `main` for a minor/patch. Branch name convention: `release-X.Y.Z`. + +```bash +git checkout vN # or main, depending on the release type +git pull +git checkout -b release-X.Y.Z +git add * +git commit -m "chore: release X.Y.Z" +git push -u origin release-X.Y.Z +gh pr create --base main --title "chore: release X.Y.Z" \ + --body "See CHANGELOG.md entry for [$X.Y.Z]." +``` + +When branching from an integration branch, the PR carries all of that branch's commits plus the release-prep commits — reviewers see the full changeset alongside the version bump. + +### 5. Merge and tag + +Once the PR is reviewed, CI is green, and merged into `main`: + +```bash +git checkout main +git pull +git tag vX.Y.Z +git push origin vX.Y.Z +``` + +The tag must point at the merge commit on `main`. + +### 6. Publish to npm + +```bash +npm publish +``` + +This triggers `prepublishOnly`, which runs `npm run build` (tsup, which cleans `dist/` and emits the ESM + CJS + UMD bundles + declaration files) against the current working tree before publishing. npm will prompt for the OTP from your authenticator. + +After publish, confirm: + +```bash +npm view react-plotly.js version # should report X.Y.Z +npm view react-plotly.js dist-tags # `latest` should be X.Y.Z +``` + +### 7. Create the GitHub release + +Via the GitHub UI: *Releases → Draft a new release*, target tag `vX.Y.Z`, paste the relevant `CHANGELOG.md` section into the body. + +## Pre-release versions + +For release candidates or beta builds, append a pre-release identifier and publish under a non-`latest` dist-tag so existing consumers aren't auto-upgraded: + +```bash +npm version X.Y.Z-rc.0 --no-git-tag-version +# ...PR + merge + tag as above, with tag vX.Y.Z-rc.0... +npm publish --tag next +``` + +Consumers can opt in with `npm install react-plotly.js@next`. diff --git a/package-lock.json b/package-lock.json index 1c5defe..3828e83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-plotly.js", - "version": "3.0.0", + "version": "4.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-plotly.js", - "version": "3.0.0", + "version": "4.1.0", "license": "MIT", "devDependencies": { "@babel/core": "^7.19.0", @@ -6980,17 +6980,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" diff --git a/package.json b/package.json index ce0382d..0be2c0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-plotly.js", - "version": "3.0.0", + "version": "4.1.0", "license": "MIT", "description": "A plotly.js react component from Plotly", "author": "Plotly, Inc.", diff --git a/plotly-react-logo.png b/plotly-react-logo.png new file mode 100644 index 0000000..29a5c5c Binary files /dev/null and b/plotly-react-logo.png differ diff --git a/src/__mocks__/plotly.js b/src/__mocks__/plotly.js index fe1ad31..5a77948 100644 --- a/src/__mocks__/plotly.js +++ b/src/__mocks__/plotly.js @@ -3,6 +3,17 @@ const state = {}; const ASYNC_DELAY = 1; +// Real plotly.js binds node's EventEmitter onto the graph div, so a gd exposes +// `removeListener` (see plotly.js src/lib/events.js). The `event-emitter` +// package only gives us `off`, and the wrapper treats a missing +// `removeListener` as "not a plotly graph div" — which silently disabled the +// whole update-event path under test. Alias it so the mock matches reality. +function attachEmitter(gd) { + EventEmitter(gd); // eslint-disable-line new-cap + gd.removeListener = gd.off; + return gd; +} + export default { plot: jest.fn((gd) => { state.gd = gd; @@ -11,16 +22,14 @@ export default { }, ASYNC_DELAY); }), newPlot: jest.fn((gd) => { - state.gd = gd; - EventEmitter(state.gd); // eslint-disable-line new-cap + state.gd = attachEmitter(gd); setTimeout(() => { state.gd.emit('plotly_afterplot'); }, ASYNC_DELAY); }), react: jest.fn((gd) => { - state.gd = gd; - EventEmitter(state.gd); // eslint-disable-line new-cap + state.gd = attachEmitter(gd); setTimeout(() => { state.gd.emit('plotly_afterplot'); diff --git a/src/__tests__/events.test.js b/src/__tests__/events.test.js new file mode 100644 index 0000000..794ce5a --- /dev/null +++ b/src/__tests__/events.test.js @@ -0,0 +1,45 @@ +import {readFileSync} from 'node:fs'; +import {join} from 'node:path'; +import {events, eventNames, getPlotlyEventName, getPropName, updateEvents} from '../events'; + +// `events` drives the runtime wiring. The two hand-maintained copies — the +// `on*` props in `factory.d.ts` and the README table — can't be derived from +// it, so they're checked here instead. The declarations are shipped to +// consumers but unused by this library, which is why a text comparison is +// enough; nothing here needs the type system. +describe('events', () => { + test('has no duplicate event names', () => { + expect(new Set(eventNames).size).toBe(eventNames.length); + }); + + test('every update event is one of the forwarded events', () => { + const forwarded = eventNames.map(getPlotlyEventName); + expect(updateEvents.length).toBeGreaterThan(0); + expect(forwarded).toEqual(expect.arrayContaining(updateEvents)); + }); + + test('PlotParams declares a prop for every event', () => { + const dts = readFileSync(join(__dirname, '..', 'factory.d.ts'), 'utf8'); + + // Lines look like: onAfterPlot?: EventCallback; + const propPattern = /^\s*(on\w+)\?: EventCallback;$/gm; + const declared = [...dts.matchAll(propPattern)].map(([, prop]) => prop); + + expect(declared).toEqual(events.map((event) => getPropName(event.name))); + }); + + test('README table matches the event list', () => { + const readme = readFileSync(join(__dirname, '..', '..', 'README.md'), 'utf8'); + + // Rows look like: | `onAfterPlot` | `Function` | `plotly_afterplot` | + const rowPattern = /^\|\s*`(on\w+)`\s*\|\s*`Function`\s*\|\s*`(plotly_\w+)`\s*\|$/gm; + const documented = [...readme.matchAll(rowPattern)].map(([, prop, event]) => [prop, event]); + + const expected = events.map((event) => [ + getPropName(event.name), + getPlotlyEventName(event.name), + ]); + + expect(documented).toEqual(expected); + }); +}); diff --git a/src/__tests__/react-plotly.test.js b/src/__tests__/react-plotly.test.js index bace500..db23fae 100644 --- a/src/__tests__/react-plotly.test.js +++ b/src/__tests__/react-plotly.test.js @@ -2,6 +2,7 @@ import React, {StrictMode, useState} from 'react'; import {act, render} from '@testing-library/react'; import createComponent from '../factory'; +import {eventNames, getPlotlyEventName, getPropName} from '../events'; import once from 'onetime'; describe('', () => { @@ -185,7 +186,7 @@ describe('', () => { }); }); - describe('manging event handlers', () => { + describe('managing event handlers', () => { test('should add an event handler when one does not already exist', (done) => { let received; const onRelayout = (evt) => { @@ -203,6 +204,45 @@ describe('', () => { }) .catch((err) => done(err)); }); + + // Every forwarded event should reach its `on*` prop. Driving this from + // the event list means a new entry is covered without touching the test. + test.each(eventNames)('forwards plotly_%s to its prop', (eventName) => { + let received; + const handler = (evt) => { + received = evt; + }; + + return createPlot({[getPropName(eventName)]: handler}).then((plot) => { + plot.gd.emit(getPlotlyEventName(eventName), {eventName}); + expect(received).toEqual({eventName}); + }); + }); + + // Cancelable — the consumer's handler must be the only listener or + // plotly could drop its `return false`. See the note in `events.js`. + test.each(['SunburstClick', 'TreemapClick', 'IcicleClick'])( + 'attaches only the consumer handler to %s', + (eventName) => { + const handler = () => false; + + return createPlot({[getPropName(eventName)]: handler, onUpdate: () => {}}).then( + (plot) => { + const listeners = [].concat(plot.gd.__ee__[getPlotlyEventName(eventName)] || []); + expect(listeners).toEqual([handler]); + } + ); + } + ); + + test('fires onUpdate when a drill-down animation completes', () => { + const onUpdate = jest.fn(); + + return createPlot({onUpdate}).then((plot) => { + plot.gd.emit('plotly_animated'); + expect(onUpdate).toHaveBeenCalled(); + }); + }); }); describe('StrictMode', () => { diff --git a/src/events.js b/src/events.js new file mode 100644 index 0000000..8f9410e --- /dev/null +++ b/src/events.js @@ -0,0 +1,70 @@ +// The single source of truth for the plotly.js events this wrapper forwards. +// +// The naming convention is: +// - events are attached as `'plotly_' + name.toLowerCase()` +// - react props are `'on' + name` +// +// `triggersUpdate` marks events plotly.js emits *after* changing the figure; +// the wrapper listens to those and fires `onUpdate`. Never set it on a +// cancelable event — that adds a second listener alongside the consumer's, +// and plotly keeps only the last listener's return value, so a consumer's +// `return false` could be discarded. The drill-down clicks rely on +// `plotly_animated` instead. +// +// The `on*` prop types in `factory.d.ts` are maintained by hand against this list +export const events = [ + {name: 'AfterExport'}, + {name: 'AfterPlot'}, + {name: 'Animated', triggersUpdate: true}, + {name: 'Animating'}, + {name: 'AnimatingFrame'}, + {name: 'AnimationInterrupted'}, + {name: 'AutoSize'}, + {name: 'BeforeExport'}, + {name: 'BeforeHover'}, + {name: 'BeforePlot'}, + {name: 'ButtonClicked'}, + {name: 'Click'}, + {name: 'ClickAnnotation'}, + {name: 'Deselect'}, + {name: 'DoubleClick', triggersUpdate: true}, + {name: 'Framework'}, + {name: 'Hover'}, + {name: 'IcicleClick'}, + {name: 'LegendClick'}, + {name: 'LegendDoubleClick'}, + {name: 'LegendTitleClick'}, + {name: 'LegendTitleDoubleClick'}, + {name: 'Relayout', triggersUpdate: true}, + {name: 'Relayouting', triggersUpdate: true}, + {name: 'Restyle', triggersUpdate: true}, + {name: 'Redraw', triggersUpdate: true}, + {name: 'Selected'}, + {name: 'Selecting'}, + {name: 'SliderChange'}, + {name: 'SliderEnd'}, + {name: 'SliderStart'}, + {name: 'SunburstClick'}, + {name: 'Transitioned'}, + {name: 'Transitioning'}, + {name: 'TransitionInterrupted'}, + {name: 'TreemapClick'}, + {name: 'Unhover'}, + {name: 'WebGlContextLost'}, +]; + +/** The plotly.js event name a given entry is attached as. */ +export function getPlotlyEventName(eventName) { + return 'plotly_' + eventName.toLowerCase(); +} + +/** The React prop name a given entry is read from. */ +export function getPropName(eventName) { + return 'on' + eventName; +} + +export const eventNames = events.map((event) => event.name); + +export const updateEvents = events + .filter((event) => event.triggersUpdate) + .map((event) => getPlotlyEventName(event.name)); diff --git a/src/factory.d.ts b/src/factory.d.ts index 34e75ea..91631a1 100644 --- a/src/factory.d.ts +++ b/src/factory.d.ts @@ -35,22 +35,25 @@ export interface PlotParams { onAfterExport?: EventCallback; onAfterPlot?: EventCallback; onAnimated?: EventCallback; + onAnimating?: EventCallback; onAnimatingFrame?: EventCallback; onAnimationInterrupted?: EventCallback; onAutoSize?: EventCallback; onBeforeExport?: EventCallback; onBeforeHover?: EventCallback; + onBeforePlot?: EventCallback; onButtonClicked?: EventCallback; onClick?: EventCallback; onClickAnnotation?: EventCallback; - onClickAnywhere?: EventCallback; onDeselect?: EventCallback; onDoubleClick?: EventCallback; onFramework?: EventCallback; onHover?: EventCallback; - onHoverAnywhere?: EventCallback; + onIcicleClick?: EventCallback; onLegendClick?: EventCallback; onLegendDoubleClick?: EventCallback; + onLegendTitleClick?: EventCallback; + onLegendTitleDoubleClick?: EventCallback; onRelayout?: EventCallback; onRelayouting?: EventCallback; onRestyle?: EventCallback; @@ -61,8 +64,10 @@ export interface PlotParams { onSliderEnd?: EventCallback; onSliderStart?: EventCallback; onSunburstClick?: EventCallback; + onTransitioned?: EventCallback; onTransitioning?: EventCallback; onTransitionInterrupted?: EventCallback; + onTreemapClick?: EventCallback; onUnhover?: EventCallback; onWebGlContextLost?: EventCallback; } @@ -78,8 +83,6 @@ export interface PlotParams { */ declare function createPlotlyComponent( Plotly: unknown -): React.ForwardRefExoticComponent< - PlotParams & React.RefAttributes ->; +): React.ForwardRefExoticComponent>; export default createPlotlyComponent; diff --git a/src/factory.js b/src/factory.js index 99c4860..823a4b9 100644 --- a/src/factory.js +++ b/src/factory.js @@ -1,53 +1,6 @@ import React, {forwardRef, useCallback, useEffect, useRef} from 'react'; -// The naming convention is: -// - events are attached as `'plotly_' + eventName.toLowerCase()` -// - react props are `'on' + eventName` -const eventNames = [ - 'AfterExport', - 'AfterPlot', - 'Animated', - 'AnimatingFrame', - 'AnimationInterrupted', - 'AutoSize', - 'BeforeExport', - 'BeforeHover', - 'ButtonClicked', - 'Click', - 'ClickAnnotation', - 'ClickAnywhere', - 'Deselect', - 'DoubleClick', - 'Framework', - 'Hover', - 'HoverAnywhere', - 'LegendClick', - 'LegendDoubleClick', - 'Relayout', - 'Relayouting', - 'Restyle', - 'Redraw', - 'Selected', - 'Selecting', - 'SliderChange', - 'SliderEnd', - 'SliderStart', - 'SunburstClick', - 'Transitioning', - 'TransitionInterrupted', - 'Unhover', - 'WebGlContextLost', -]; - -const updateEvents = [ - 'plotly_restyle', - 'plotly_redraw', - 'plotly_relayout', - 'plotly_relayouting', - 'plotly_doubleclick', - 'plotly_animated', - 'plotly_sunburstclick', -]; +import {eventNames, getPlotlyEventName, getPropName, updateEvents} from './events.js'; // Check if a window is available since SSR (server-side rendering) // breaks unnecessarily if you try to use it server-side. @@ -59,10 +12,6 @@ const isBrowser = typeof window !== 'undefined'; const DEFAULT_DATA = Object.freeze([]); const DEFAULT_STYLE = Object.freeze({position: 'relative', display: 'inline-block'}); -function getPlotlyEventName(eventName) { - return 'plotly_' + eventName.toLowerCase(); -} - export default function plotComponentFactory(Plotly) { return forwardRef(function PlotlyComponent( { @@ -248,7 +197,7 @@ export default function plotComponentFactory(Plotly) { function syncEventHandlers() { eventNames.forEach((eventName) => { - const prop = eventProps['on' + eventName]; + const prop = eventProps[getPropName(eventName)]; const handler = handlersRef.current[eventName]; const hasHandler = Boolean(handler); if (prop && !hasHandler) {