Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 10 additions & 22 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ on:
push:
branches: [main]
tags:
- "v*"
- 'v*'
pull_request:
branches: ["*"]
branches: ['*']

workflow_dispatch:

Expand All @@ -31,30 +31,18 @@ jobs:
- 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
uses: pnpm/action-setup@v4
with:
version: 8
version: 10
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
- name: Setup Node
uses: actions/setup-node@v4
with:
path: ${{ env.STORE_PATH }}
key: "${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}"
restore-keys: |
${{ runner.os }}-pnpm-store-
node-version: 22
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'

- name: Install Packages
run: pnpm install
Expand All @@ -67,7 +55,7 @@ jobs:

- name: SonarCloud Scan
if: "!startsWith(github.ref, 'refs/tags/')"
uses: SonarSource/sonarcloud-github-action@master
uses: SonarSource/sonarqube-scan-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Expand Down
61 changes: 61 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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`): The `ReactFloater` component manages state using `useReducer` and handles:
- Popper.js instance creation/management for positioning
- Event handling (click/hover) with mobile detection
- Portal rendering for the floating element
- Status transitions: IDLE → 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 component uses `useReducer` with status-based state transitions. Changes are tracked using `tree-changes-hook` for efficient callback triggers.

**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 files in `test/` directory
- Coverage requirements: 90% for all metrics
- Mock components in `test/__fixtures__/`
169 changes: 67 additions & 102 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@

[![NPM version](https://badge.fury.io/js/react-floater.svg)](https://www.npmjs.com/package/react-floater) [![CI](https://github.com/gilbarbara/react-floater/actions/workflows/main.yml/badge.svg)](https://github.com/gilbarbara/react-floater/actions/workflows/main.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gilbarbara_react-floater&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=gilbarbara_react-floater) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=gilbarbara_react-floater&metric=coverage)](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

```shell
npm install react-floater
```

Import it in your app:
Import it into your app:

```tsx
import Floater from 'react-floater';
Expand All @@ -28,72 +28,64 @@ import Floater from 'react-floater';
</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
<Floater
content={<div>Custom content <b>with bold!</b></div>}
placement="right"
arrow={<MyCustomArrow />}
styles={{
container: { backgroundColor: "#222", color: "#fff" },
arrow: { color: "#222", size: 16, base: 24 },
}}
>
<button>Hover or click me</button>
</Floater>
```
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. |

<details>
<summary>Type Definition</summary>
<summary><b id="poppermodifiers-type-definition">PopperModifiers Type Definition</b></summary>

```typescript
interface PopperModifiers {
Expand All @@ -111,13 +103,10 @@ interface PopperModifiers {

</details>

> 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.**

<details>
<summary>Type Definition</summary>
<summary><b id="placement-type-definition">Placement Type Definition</b></summary>

```typescript
type Placement =
Expand All @@ -131,24 +120,15 @@ type Placement =

</details>

**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.

<details>
<summary>Type Definition</summary>
<summary><b id="styles-type-definition">Styles Type Definition</b></summary>

```typescript
interface Styles {
arrow: CSSProperties & {
length: number;
spread: number;
size: number;
base: number;
};
close: CSSProperties;
container: CSSProperties;
Expand All @@ -171,40 +151,25 @@ interface Styles {

</details>

**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._

<details>
<summary>Type Definition</summary>
<summary><b id="wrapperoptions-type-definition">WrapperOptions Type Definition</b></summary>

```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
}
```

</details>

## 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
<Floater content="This is the Floater content">
Expand All @@ -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
<div className="App">
Expand All @@ -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
<div className="App">
Expand All @@ -251,7 +216,7 @@ It is the same as the **proxy mode,** but the wrapper will be positioned relativ
```

**Controlled**
Setting a boolean to the open prop will enter the controlled mode and not respond to events.
You manage the Floater’s visibility with the `open` prop - no trigger events are needed.
In this mode, you don't even need to have `children`

```tsx
Expand Down
Loading