diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json
deleted file mode 100644
index 9671dd2..0000000
--- a/.codesandbox/ci.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "node": "18",
- "sandboxes": ["/demo"]
-}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..e85f5bf
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,69 @@
+name: CI
+
+on:
+ push:
+ branches: ['main']
+ tags: ['v*']
+ pull_request:
+ branches: ['*']
+
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 10
+ run_install: false
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+
+ - run: pnpm install
+
+ - run: pnpm validate
+
+ - uses: SonarSource/sonarqube-scan-action@v7.0.0
+ if: "!startsWith(github.ref, 'refs/tags/')"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ publish:
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/v')
+ needs: validate
+ permissions:
+ id-token: write
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 10
+ run_install: false
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+ registry-url: https://registry.npmjs.org
+
+ - run: npm install -g npm@11
+
+ - run: pnpm install
+
+ - run: pnpm build
+
+ - run: npm publish --provenance
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
deleted file mode 100644
index 1492b12..0000000
--- a/.github/workflows/main.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: CI
-
-on:
- push:
- branches: [main]
- tags:
- - "v*"
- pull_request:
- branches: ["*"]
-
- workflow_dispatch:
-
-concurrency:
- group: ${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- main:
- name: Validate and Deploy
- runs-on: ubuntu-latest
-
- env:
- CI: true
-
- steps:
- - name: Setup timezone
- uses: zcong1993/setup-timezone@master
- with:
- timezone: America/Sao_Paulo
-
- - name: Setup repo
- uses: actions/checkout@v4
-
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: 20
- registry-url: "https://registry.npmjs.org"
-
- - name: Install pnpm
- uses: pnpm/action-setup@v3
- with:
- version: 8
- run_install: false
-
- - name: Get pnpm store directory
- shell: bash
- run: |
- echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
-
- - name: Setup pnpm cache
- uses: actions/cache@v4
- with:
- path: ${{ env.STORE_PATH }}
- key: "${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}"
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
- - name: Install Packages
- run: pnpm install
- timeout-minutes: 3
-
- - name: Validate
- if: "!startsWith(github.ref, 'refs/tags/')"
- run: pnpm run validate
- timeout-minutes: 3
-
- - name: SonarCloud Scan
- if: "!startsWith(github.ref, 'refs/tags/')"
- uses: SonarSource/sonarcloud-github-action@master
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- - name: Publish Package
- if: startsWith(github.ref, 'refs/tags/')
- run: npm publish
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 7a33864..8940cba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@ coverage
dist
node_modules
reports
+
+demo/pnpm-lock.yaml
+**/pnpm-workspace.yaml
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..1222d58
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,60 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Commands
+
+### Development
+- `npm run watch` - Build and watch for changes (uses tsup)
+- `npm run build` - Clean and build the library for production
+
+### Testing
+- `npm test` - Run tests in watch mode (development)
+- `npm run test:coverage` - Run tests with coverage report
+- Run a single test: `vitest run test/index.spec.tsx`
+
+### Quality Checks
+- `npm run lint` - Lint and fix code issues
+- `npm run typecheck` - Run TypeScript type checking
+- `npm run validate` - Run full validation suite (lint, typecheck, test, build, size check)
+- `npm run size` - Check bundle size limits
+
+## Architecture
+
+### Core Component Flow
+The library uses a state machine pattern for managing the floater lifecycle:
+
+1. **Main Entry** (`src/index.tsx`): Thin JSX shell that delegates to `useFloater` hook
+ - `useFloater` (`src/modules/useFloater.ts`): All state management, Popper.js integration, event handlers
+ - Status transitions: INIT → IDLE → RENDER → OPENING → OPEN → CLOSING → IDLE
+
+2. **Component Structure**:
+ - `Portal` (`src/components/Portal.tsx`): Manages DOM portal rendering
+ - `Floater` (`src/components/Floater/index.tsx`): The floating UI container
+ - `Container` (`src/components/Floater/Container.tsx`): Content wrapper with title/footer
+ - `Arrow` (`src/components/Floater/Arrow.tsx`): Customizable arrow element
+ - `Wrapper` (`src/components/Wrapper.tsx`): Target element wrapper for beacon mode
+
+3. **Positioning System**: Uses Popper.js v2 with:
+ - Custom modifiers configuration via `getModifiers()` helper
+ - Fallback placements for auto-positioning
+ - Fixed positioning detection for proper scrolling behavior
+
+### Key Patterns
+
+**State Management**: The `useFloater` hook uses `useReducer` with status-based state transitions. A `previousStatus` ref tracks the prior status, and 3 focused `useEffect` hooks handle controlled mode, wrapper positioning, and status transitions.
+
+**Style Merging**: Custom styles are deeply merged with defaults using `deepmerge-ts`. The styles object structure is defined in `src/modules/styles.ts`.
+
+**Event Handling**: Special handling for mobile devices (converts hover to click) and delayed hiding for hover events using timeouts.
+
+**Type Safety**: Uses TypeScript with strict typing. Key type definitions in:
+- `src/types/common.ts`: Component props, states, and common types
+- `src/types/popper.ts`: Popper.js related types
+
+### Testing Approach
+- Uses Vitest with React Testing Library
+- `test/index.spec.tsx`: Rendering, UI, and integration tests
+- `test/modules/useFloater.spec.tsx`: State machine, lifecycle, and event handling tests
+- Coverage requirements: 90% for all metrics
+- Mock components in `test/__fixtures__/`
\ No newline at end of file
diff --git a/README.md b/README.md
index 8bb6ff0..fb4fd50 100644
--- a/README.md
+++ b/README.md
@@ -2,15 +2,15 @@
[](https://www.npmjs.com/package/react-floater) [](https://github.com/gilbarbara/react-floater/actions/workflows/main.yml) [](https://sonarcloud.io/summary/new_code?id=gilbarbara_react-floater) [](https://sonarcloud.io/summary/new_code?id=gilbarbara_react-floater)
-Advanced tooltips for React!
+**Flexible, customizable, and accessible tooltips, popovers, and guided hints for React.**
-View the [demo](https://codesandbox.io/s/github/gilbarbara/react-floater/tree/main/demo)
+[**View the live demo →**](https://codesandbox.io/s/github/gilbarbara/react-floater/tree/main/demo)
## Highlights
- 🏖 **Easy to use:** Just set the `content`
- 🛠 **Flexible:** Personalize the options to fit your needs
-- 🟦 **Typescript:** Nicely typed
+- 🟦 **Type-safe:** Full TypeScript support
## Usage
@@ -18,7 +18,7 @@ View the [demo](https://codesandbox.io/s/github/gilbarbara/react-floater/tree/ma
npm install react-floater
```
-Import it in your app:
+Import it into your app:
```tsx
import Floater from 'react-floater';
@@ -28,72 +28,64 @@ import Floater from 'react-floater';
;
```
-And voíla!
+Voilà! A tooltip will appear on click!
-## Customization
+## Customization & Styling
-You can use a custom component to render the Floater with the `component` prop.
-Check `WithStyledComponents.ts` in the [demo](https://codesandbox.io/s/github/gilbarbara/react-floater/tree/main/demo) for an example.
+React Floater is highly customizable. You can:
-## Props
-
-**autoOpen** `boolean` ▶︎ false
-Open the Floater automatically.
-
-**callback** `(action: 'open' | 'close', props: Props) => void`
-It will be called when the Floater changes state.
-
-**children** `ReactNode`
-An element to trigger the Floater.
-
-**component** `ComponentType | ReactElement`
-A React element or function to use as a custom UI for the Floater.
-The prop `closeFn` will be available in your component.
-
-**content** `ReactNode`
-The Floater content. It can be anything that can be rendered.
-_This is required unless you pass a_ `component`.
-
-**debug** `boolean` ▶︎ false
-Log some basic actions.
-_You can also set a global variable_ `ReactFloaterDebug = true;`
-
-**disableFlip** `boolean` ▶︎ false
-Disable changes in the Floater position on scroll/resize.
-
-**disableHoverToClick** `boolean` ▶︎ false
-Don't convert the _hover_ event to _click_ on mobile.
-
-**event** `'hover' | 'click'` ▶︎ click
-The event that will trigger the Floater.
-
-> This won't work in a controlled mode.
-
-**eventDelay** `number` ▶︎ 0.4
-The amount of time (in seconds) the floater should wait after a `mouseLeave` event before hiding.
-> Only valid for event type `hover`.
-
-**footer** `ReactNode`
-It can be anything that can be rendered.
-
-**getPopper** `(popper: PopperInstance, origin: 'floater' | 'wrapper') => void`
-Get the popper.js instance.
-
-**hideArrow** `boolean` ▶︎ false
-Don't show the arrow. Useful for centered or modal layout.
+- Use a custom component for the content via the `component` prop
+ (see `WithStyledComponents.ts` in the [demo](https://codesandbox.io/s/github/gilbarbara/react-floater/tree/main/demo)).
+- Pass a custom arrow using the `arrow` prop.
+- Customize the UI appearance using the `styles` prop.
+ You only need to provide the keys you want to override—defaults will be merged automatically.
-**offset** `number` ▶︎ 15
-The distance between the Floater and its target in pixels.
+```tsx
+Custom content with bold!}
+ placement="right"
+ arrow={}
+ styles={{
+ container: { backgroundColor: "#222", color: "#fff" },
+ arrow: { color: "#222", size: 16, base: 24 },
+ }}
+>
+
+
+```
+For all available style keys and their default values, see the [styles.ts](src/modules/styles.ts) source.
-**open** `boolean`
-The switch between normal and controlled modes.
-> Setting this prop will disable normal behavior.
+## Props
-**modifiers** `PopperModifiers`
-Customize popper.js modifiers.
+| **Prop** | **Type** | **Default** | **Description** |
+|---------------------|------------------------------------------------------------------|-------------|---------------------------------------------------------------------------|
+| arrow ✨ | ReactNode | – | Custom arrow for the floater. [See styles.arrow](#styles-type-definition) |
+| autoOpen | boolean | false | Open the Floater automatically. |
+| callback | (action: ‘open’ \| ‘close’, props: Props) => void | – | Called when the Floater opens or closes. |
+| children | ReactNode | – | Element to trigger the Floater. |
+| component | ComponentType \| ReactElement | – | Custom component UI for the Floater. Has access to closeFn. |
+| content | ReactNode | – | The content of the Floater. (Required unless you pass a component.) |
+| debug | boolean | false | Log basic actions. |
+| disableFlip | boolean | false | Disable changes in position on scroll/resize. |
+| disableHoverToClick | boolean | false | Don’t convert hover to click on mobile. |
+| event | 'hover' \| 'click' | 'click' | Event that triggers the Floater.*Not used in controlled mode.* |
+| eventDelay | number | 0.4 | Time in seconds before hiding on mouseLeave (only for hover). |
+| footer | ReactNode | – | Footer area content. |
+| getPopper | (popper: PopperInstance, origin: ‘floater’ \| ‘wrapper’) => void | – | Get the popper.js instance. |
+| hideArrow | boolean | false | Hide the arrow (good for centered/modal). |
+| offset | number | 15 | Distance (px) between Floater and target. |
+| open | boolean | – | Switch to controlled mode. Disables normal event triggers. |
+| modifiers | [PopperModifiers](#poppermodifiers-type-definition) | – | Customize popper.js modifiers. |
+| placement | [Placement](#placement-type-definition) | 'bottom' | Floater’s position. |
+| portalElement | string \| HTMLElement | – | Selector or element for rendering. |
+| showCloseButton | boolean | false | Shows a close (×) button. |
+| styles | [Styles](#styles-type-definition) | – | Customize UI styles. |
+| target | string \| HTMLElement | – | Target element for position. Defaults to children. |
+| title | ReactNode | – | Floater title. |
+| wrapperOptions | [WrapperOptions](#wrapperoptions-type-definition) | – | Options for positioning the wrapper. Requires a target. |
- Type Definition
+ PopperModifiers Type Definition
```typescript
interface PopperModifiers {
@@ -111,13 +103,10 @@ interface PopperModifiers {
-> Don't use it unless you know what you're doing
-
-**placement** `Placement` ▶︎ `bottom`
-The placement of the Floater. It will update the position if there's no space available.
+> **Intended for advanced customization—use with caution.**
- Type Definition
+Placement Type Definition
```typescript
type Placement =
@@ -131,24 +120,15 @@ type Placement =
-**portalElement** `string|HTMLElement`
-A css selector or element to render the tooltips
-
-**showCloseButton** `boolean` ▶︎ false
-It will show a ⨉ button to close the Floater.
-This will be `true` when you change the `wrapperOptions` position.
-
-**styles** `Styles`
-Customize the UI.
- Type Definition
+Styles Type Definition
```typescript
interface Styles {
arrow: CSSProperties & {
- length: number;
- spread: number;
+ size: number;
+ base: number;
};
close: CSSProperties;
container: CSSProperties;
@@ -171,40 +151,25 @@ interface Styles {
-**target** `string | HTMLElement`
-The target element to calculate the Floater position. It will use the children as the target if it's not set.
-
-**title** `ReactNode`
-It can be anything that can be rendered.
-
-**wrapperOptions** `WrapperOptions`
-Position the wrapper relative to the target.
-_You need to set a `target` for this to work._
-
- Type Definition
+ WrapperOptions Type Definition
```typescript
interface WrapperOptions {
offset: number; // The distance between the wrapper and the target. It can be a negative value.
placement: string; // the same options as above, except center
- position: bool; // Set to true to position the wrapper
+ position: boolean; // Set to true to position the wrapper
}
```
-## Styling
-
-You can customize everything with the `styles` prop.
-Only set the properties you want to change, and the default styles will be merged.
-
-Check the [styles.ts](src/modules/styles.ts) for the syntax.
-
## Modes
+React Floater supports several modes for flexible positioning and control:
+
**Default**
-The wrapper will trigger the events and use itself as the Floater's target.
+The Floater is anchored to its child and triggers on event.
```tsx
@@ -213,7 +178,7 @@ The wrapper will trigger the events and use itself as the Floater's target.
```
**Proxy**
-The wrapper will trigger the events, but the Floater will use the **target** prop to position itself.
+The Floater is triggered by the child, but positioned relative to the `target`.
```tsx
@@ -226,7 +191,7 @@ The wrapper will trigger the events, but the Floater will use the **target** pro
```
**Beacon**
-It is the same as the **proxy mode,** but the wrapper will be positioned relative to the `target`.
+The Floater wrapper is positioned relative to the target (useful for guided tours or beacons).
```tsx
);
});
+
+export default Target;
diff --git a/demo/src/examples/WithAutoOpen.tsx b/demo/src/examples/WithAutoOpen.tsx
index 4e5bb81..cea2ef5 100755
--- a/demo/src/examples/WithAutoOpen.tsx
+++ b/demo/src/examples/WithAutoOpen.tsx
@@ -1,5 +1,5 @@
import Floater from 'react-floater';
-import { Box, Button, Paragraph } from '@gilbarbara/components';
+import { Button } from '@heroui/react';
import Column from '../components/Column';
@@ -10,22 +10,24 @@ export default function AutoOpen({ cb }: any) {
autoOpen
callback={cb}
content={
-
- React Floater is super easy to use and customize.
-
+
+
React Floater is super easy to use and customize.
+
It's powered by{' '}
popper.js
{' '}
to position the elements
-
-
+
+
}
placement="top"
>
- TOP
+
+ TOP
+
- autoOpen
+
autoOpen
);
}
diff --git a/demo/src/examples/WithCustomStyles.tsx b/demo/src/examples/WithCustomStyles.tsx
index cff4d35..c8e544d 100755
--- a/demo/src/examples/WithCustomStyles.tsx
+++ b/demo/src/examples/WithCustomStyles.tsx
@@ -1,14 +1,12 @@
import Floater from 'react-floater';
-import { Button, H3, Paragraph } from '@gilbarbara/components';
+import { Button } from '@heroui/react';
export default function WithCustomStyles({ cb }: any) {
return (
- You can change the UI completely. Also control placement, offset, flip and much more.
-
+
You can change the UI completely. Also control placement, offset, flip and much more.
}
>
- RIGHT
+
+ RIGHT
+
);
}
diff --git a/demo/src/examples/WithHoverAndNoDelay.tsx b/demo/src/examples/WithHoverAndNoDelay.tsx
index ad9d10d..1082b45 100755
--- a/demo/src/examples/WithHoverAndNoDelay.tsx
+++ b/demo/src/examples/WithHoverAndNoDelay.tsx
@@ -1,5 +1,5 @@
import Floater from 'react-floater';
-import { Button, Paragraph } from '@gilbarbara/components';
+import { Button } from '@heroui/react';
import Column from '../components/Column';
@@ -8,18 +8,18 @@ export default function WithHoverAndNoDelay({ cb }: any) {
I can be triggered by click or hover (on devices with a mouse)
- }
+ content={
I can be triggered by click or hover (on devices with a mouse)
);
}
diff --git a/demo/src/examples/WithHoverDefault.tsx b/demo/src/examples/WithHoverDefault.tsx
index c2486d6..0213d6c 100755
--- a/demo/src/examples/WithHoverDefault.tsx
+++ b/demo/src/examples/WithHoverDefault.tsx
@@ -1,5 +1,5 @@
import Floater from 'react-floater';
-import { Button, Paragraph } from '@gilbarbara/components';
+import { Button } from '@heroui/react';
import Column from '../components/Column';
@@ -8,16 +8,16 @@ export default function WithHoverDefault({ cb }: any) {
I can be triggered by click or hover (on devices with a mouse)
- }
+ content={
I can be triggered by click or hover (on devices with a mouse)
Semantics (from Ancient Greek: σημαντικός sēmantikos, "significant")[1][2] is the
linguistic and philosophical study of meaning, in language, programming languages, formal
logics, and semiotics.
-
+
}
text="Semantics"
/>
);
return (
-
+
Far far {away}, behind the word mountains, far from the countries Vokalia and Consonantia,
there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the{' '}
{semantics}, a large language ocean. A small river named Duden flows by their place and
supplies it with the necessary regelialia. It is a paradisematic country, in which roasted
parts of sentences fly into your mouth.
-
+
);
}
diff --git a/demo/src/examples/WithTitleAndFooter.tsx b/demo/src/examples/WithTitleAndFooter.tsx
index 4229ba8..8eb8813 100755
--- a/demo/src/examples/WithTitleAndFooter.tsx
+++ b/demo/src/examples/WithTitleAndFooter.tsx
@@ -1,5 +1,5 @@
import Floater from 'react-floater';
-import { Button, Paragraph } from '@gilbarbara/components';
+import { Button } from '@heroui/react';
export default function WithTitleAndFooter({ cb }: any) {
return (
@@ -7,23 +7,23 @@ export default function WithTitleAndFooter({ cb }: any) {
callback={cb}
content={
<>
-
- My content can be anything that can be rendered: numbers, strings, elements.
-
- Also I have a custom long arrow.
+
My content can be anything that can be rendered: numbers, strings, elements.