@particle-academy/react-fancy 6.9.0 → 6.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/dist/autocomplete.cjs +3 -3
- package/dist/autocomplete.js +2 -2
- package/dist/{chunk-ZUJVGHFW.js → chunk-24RWB7Q3.js} +3 -3
- package/dist/{chunk-ZUJVGHFW.js.map → chunk-24RWB7Q3.js.map} +1 -1
- package/dist/{chunk-GODJVIIO.js → chunk-3W6BKQVZ.js} +4 -4
- package/dist/{chunk-GODJVIIO.js.map → chunk-3W6BKQVZ.js.map} +1 -1
- package/dist/{chunk-AZ5DWOQY.cjs → chunk-6B43ARGW.cjs} +4 -4
- package/dist/{chunk-AZ5DWOQY.cjs.map → chunk-6B43ARGW.cjs.map} +1 -1
- package/dist/{chunk-Q62RDKRA.cjs → chunk-7N7XVBH7.cjs} +6 -6
- package/dist/{chunk-Q62RDKRA.cjs.map → chunk-7N7XVBH7.cjs.map} +1 -1
- package/dist/{chunk-VGUAIFGE.js → chunk-C2VGO375.js} +3 -3
- package/dist/{chunk-VGUAIFGE.js.map → chunk-C2VGO375.js.map} +1 -1
- package/dist/{chunk-H7OTLGRD.cjs → chunk-DQWAG7YY.cjs} +4 -4
- package/dist/{chunk-H7OTLGRD.cjs.map → chunk-DQWAG7YY.cjs.map} +1 -1
- package/dist/{chunk-3KJN3GTB.cjs → chunk-EDIB6V6Y.cjs} +4 -4
- package/dist/{chunk-3KJN3GTB.cjs.map → chunk-EDIB6V6Y.cjs.map} +1 -1
- package/dist/{chunk-G5MDMQVQ.cjs → chunk-EEFU5BVM.cjs} +6 -6
- package/dist/{chunk-G5MDMQVQ.cjs.map → chunk-EEFU5BVM.cjs.map} +1 -1
- package/dist/{chunk-IYG436ZY.js → chunk-EW2N52FD.js} +3 -3
- package/dist/{chunk-IYG436ZY.js.map → chunk-EW2N52FD.js.map} +1 -1
- package/dist/{chunk-RSB2P3JK.js → chunk-FECD3WKN.js} +3 -3
- package/dist/{chunk-RSB2P3JK.js.map → chunk-FECD3WKN.js.map} +1 -1
- package/dist/{chunk-6RXEWM3V.js → chunk-FNHKZMMI.js} +3 -3
- package/dist/{chunk-6RXEWM3V.js.map → chunk-FNHKZMMI.js.map} +1 -1
- package/dist/{chunk-M5ER3Y5I.cjs → chunk-FR3V5IAJ.cjs} +4 -4
- package/dist/{chunk-M5ER3Y5I.cjs.map → chunk-FR3V5IAJ.cjs.map} +1 -1
- package/dist/{chunk-DXLPBLNC.cjs → chunk-FSLEFL4U.cjs} +4 -4
- package/dist/{chunk-DXLPBLNC.cjs.map → chunk-FSLEFL4U.cjs.map} +1 -1
- package/dist/{chunk-FQCLJZZS.js → chunk-FX37P4KR.js} +3 -3
- package/dist/{chunk-FQCLJZZS.js.map → chunk-FX37P4KR.js.map} +1 -1
- package/dist/{chunk-DW2PHLOA.js → chunk-H5LJFN7D.js} +3 -3
- package/dist/{chunk-DW2PHLOA.js.map → chunk-H5LJFN7D.js.map} +1 -1
- package/dist/{chunk-F3U4AVGF.js → chunk-HCIEXBEB.js} +3 -3
- package/dist/{chunk-F3U4AVGF.js.map → chunk-HCIEXBEB.js.map} +1 -1
- package/dist/{chunk-QAM6N3VG.cjs → chunk-HEGM5LMZ.cjs} +4 -4
- package/dist/{chunk-QAM6N3VG.cjs.map → chunk-HEGM5LMZ.cjs.map} +1 -1
- package/dist/{chunk-KAKKZDCE.js → chunk-J57JSHDC.js} +3 -3
- package/dist/{chunk-KAKKZDCE.js.map → chunk-J57JSHDC.js.map} +1 -1
- package/dist/{chunk-6BCXGP2T.js → chunk-JN5DHZ6I.js} +3 -3
- package/dist/{chunk-6BCXGP2T.js.map → chunk-JN5DHZ6I.js.map} +1 -1
- package/dist/chunk-LJCQI6ZS.js +1212 -0
- package/dist/chunk-LJCQI6ZS.js.map +1 -0
- package/dist/{chunk-JV2HEOCB.cjs → chunk-MAY4NMRI.cjs} +4 -4
- package/dist/{chunk-JV2HEOCB.cjs.map → chunk-MAY4NMRI.cjs.map} +1 -1
- package/dist/{chunk-BQENLOEV.js → chunk-MD7HUHJB.js} +4 -4
- package/dist/{chunk-BQENLOEV.js.map → chunk-MD7HUHJB.js.map} +1 -1
- package/dist/{chunk-QUZOGRZS.js → chunk-OE4AIUAN.js} +3 -3
- package/dist/{chunk-QUZOGRZS.js.map → chunk-OE4AIUAN.js.map} +1 -1
- package/dist/chunk-OP7A4SZZ.cjs +1216 -0
- package/dist/chunk-OP7A4SZZ.cjs.map +1 -0
- package/dist/{chunk-YBBBX5H2.cjs → chunk-OUPHLIXV.cjs} +11 -11
- package/dist/{chunk-YBBBX5H2.cjs.map → chunk-OUPHLIXV.cjs.map} +1 -1
- package/dist/{chunk-XGGAAG74.cjs → chunk-QQKWI7QP.cjs} +7 -3
- package/dist/chunk-QQKWI7QP.cjs.map +1 -0
- package/dist/{chunk-XT4WBBCD.cjs → chunk-R4SANV3G.cjs} +11 -11
- package/dist/{chunk-XT4WBBCD.cjs.map → chunk-R4SANV3G.cjs.map} +1 -1
- package/dist/{chunk-GJWNZMJT.js → chunk-RUEDMYBF.js} +7 -3
- package/dist/chunk-RUEDMYBF.js.map +1 -0
- package/dist/{chunk-UORN3Z7R.cjs → chunk-TBLKVTJC.cjs} +6 -6
- package/dist/{chunk-UORN3Z7R.cjs.map → chunk-TBLKVTJC.cjs.map} +1 -1
- package/dist/{chunk-L5NDZEL6.cjs → chunk-ZFXCZ2QO.cjs} +9 -9
- package/dist/{chunk-L5NDZEL6.cjs.map → chunk-ZFXCZ2QO.cjs.map} +1 -1
- package/dist/composer.cjs +6 -6
- package/dist/composer.js +4 -4
- package/dist/dropdown.cjs +4 -4
- package/dist/dropdown.js +2 -2
- package/dist/editor.cjs +1 -1
- package/dist/editor.js +1 -1
- package/dist/index.cjs +99 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +22 -21
- package/dist/index.js.map +1 -1
- package/dist/input.cjs +5 -5
- package/dist/input.js +4 -4
- package/dist/inputs.cjs +19 -19
- package/dist/inputs.js +8 -8
- package/dist/json-editor.cjs +17 -17
- package/dist/json-editor.js +6 -6
- package/dist/mobile-menu.cjs +1 -1
- package/dist/mobile-menu.js +1 -1
- package/dist/number-field.cjs +6 -6
- package/dist/number-field.js +5 -5
- package/dist/paste-pills.cjs +16 -16
- package/dist/paste-pills.js +3 -3
- package/dist/phone-input.cjs +39 -0
- package/dist/phone-input.cjs.map +1 -0
- package/dist/phone-input.d.cts +139 -0
- package/dist/phone-input.d.ts +139 -0
- package/dist/phone-input.js +22 -0
- package/dist/phone-input.js.map +1 -0
- package/dist/popover.cjs +4 -4
- package/dist/popover.js +2 -2
- package/dist/prompt-input.cjs +8 -8
- package/dist/prompt-input.js +5 -5
- package/dist/reason-tag.cjs +4 -4
- package/dist/reason-tag.js +3 -3
- package/dist/select.cjs +3 -3
- package/dist/select.js +2 -2
- package/dist/sidebar.cjs +5 -5
- package/dist/sidebar.js +3 -3
- package/dist/tooltip.cjs +3 -3
- package/dist/tooltip.js +2 -2
- package/docs/PhoneInput.md +104 -0
- package/package.json +11 -1
- package/dist/chunk-GJWNZMJT.js.map +0 -1
- package/dist/chunk-XGGAAG74.cjs.map +0 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# PhoneInput
|
|
2
|
+
|
|
3
|
+
A country picker and a number field whose value is an E.164 number (`+95945123456`) or `null`. Blank is `null`, half-typed text is `null` with a reason, and pasting a full international number picks its country.
|
|
4
|
+
|
|
5
|
+
It is a **light** phone field: a first-party table of dial codes and national-number lengths, and no phone-number library or data. **The length check is a sanity check, not validation.** It catches a number that is plainly too short or too long for its country; it does not know which ranges are allocated, whether a number is mobile, or whether anyone holds it. Validate on your server (Twilio Lookup, for example) before you rely on a number.
|
|
6
|
+
|
|
7
|
+
**`detail.valid` means plausible, not valid.** Real per-country lengths apply only to the rows whose lengths were checked against a cited plan: Benin, Côte d'Ivoire, Colombia, Gabon, Kazakhstan, Lithuania, Myanmar, Saint Helena and Mayotte (`PHONE_COUNTRIES[i].lengthVerified`). Every other country accepts 4 to 15 digits including its dial code, the widest E.164 allows, so there the check is digits, a known dial code and the E.164 maximum. That is deliberate: a range that is too tight would refuse a real person's number and block their form, while a wrong number accepted here is caught by your server.
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { PhoneInput } from "@particle-academy/react-fancy";
|
|
13
|
+
// or the subpath
|
|
14
|
+
import { PhoneInput } from "@particle-academy/react-fancy/phone-input";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic Usage
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
const [phone, setPhone] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
<PhoneInput label="Mobile number" value={phone} onValueChange={setPhone} defaultCountry="MM" />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Props
|
|
26
|
+
|
|
27
|
+
| Prop | Type | Default | Description |
|
|
28
|
+
|------|------|---------|-------------|
|
|
29
|
+
| `value` | `string \| null` | - | The number in E.164, or `null` |
|
|
30
|
+
| `defaultValue` | `string \| null` | `null` | Starting value when uncontrolled |
|
|
31
|
+
| `onValueChange` | `(value: string \| null, detail: PhoneInputDetail) => void` | - | Called when the value or the reading of it changes |
|
|
32
|
+
| `label` | `string` | required | Visible label for the number |
|
|
33
|
+
| `defaultCountry` | `string` | - | ISO 3166-1 alpha-2 selected at first. Also the tie-break for a shared dial code |
|
|
34
|
+
| `countries` | `string[]` | every country, by name | Restrict the picker to these ISO codes, in this order |
|
|
35
|
+
| `locale` | `string` | the browser's | BCP 47 locale for country names. Pass it when you render on the server, or the names can differ between server and browser |
|
|
36
|
+
| `description` | `string` | - | Help text, tied to the number with `aria-describedby` |
|
|
37
|
+
| `error` | `string` | - | Your own message, shown instead of the built-in one |
|
|
38
|
+
| `messages` | `PhoneInputMessages` | English | Every piece of text the component shows, for a host working in another language |
|
|
39
|
+
| `name` | `string` | - | Posts the E.164 (or `""`) through a hidden input, never the text on screen |
|
|
40
|
+
| `required`, `disabled`, `placeholder`, `autoFocus`, `id`, `form`, `className`, `onBlur` | | | As usual. `id` is the number input's |
|
|
41
|
+
|
|
42
|
+
## What `detail` reports
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
{
|
|
46
|
+
country: "MM", // ISO code the number was read in, or null
|
|
47
|
+
dialCode: "95", // or null
|
|
48
|
+
nationalNumber: "945123456", // digits only, no trunk prefix
|
|
49
|
+
valid: true,
|
|
50
|
+
reason: null, // "too_short" | "too_long" | "invalid_chars" | "unknown_country" | null
|
|
51
|
+
approximate: false, // true when the dial code is shared and country is a guess
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## What it does with what you type
|
|
56
|
+
|
|
57
|
+
| Typed (country MM) | `value` | `reason` |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| (blank) | `null` | `null`: blank is not an error. Use `required` |
|
|
60
|
+
| `09 45` | `null` | `too_short` |
|
|
61
|
+
| `09 4512 3456` | `+95945123456` | `null`. The trunk prefix 0 is dropped |
|
|
62
|
+
| `+959 4512 3456`, `00959 4512 3456`, `+95 (9) 4512-3456` | `+95945123456` | `null`. The country follows the dial code |
|
|
63
|
+
| `+44 (0)20 7946 0958` | `+442079460958` | `null`. A `(0)` inside an international number is dropped |
|
|
64
|
+
| `09 45ab` | `null` | `invalid_chars`. Letters are never read past |
|
|
65
|
+
| `+999 1234` | `null` | `unknown_country` |
|
|
66
|
+
| `4512 3456` with no country chosen | `null` | `unknown_country` |
|
|
67
|
+
|
|
68
|
+
Digits in Arabic-Indic, Persian, Devanagari, Bengali, Thai, Myanmar and fullwidth forms are read as digits, and the invisible direction marks a right-to-left page wraps around copied text are ignored. Only `+` and `00` start an international number: other international prefixes (`011` in North America, `010` in Japan) are not recognised.
|
|
69
|
+
|
|
70
|
+
**Partial input.** Nothing below a country's shortest length is ever emitted, so in a verified fixed-length plan (Colombia, Côte d'Ivoire) a half-typed number is always `null`. In an unverified row the shortest length is 4 digits, so a half-typed number of 4 digits or more is emitted. Where a plan has numbers of different lengths (Myanmar runs 7 to 10 digits), a prefix of a long number can itself be a valid length, and is emitted: a length check cannot tell "still typing" from "done". Use `detail` and your server for that.
|
|
71
|
+
|
|
72
|
+
**When messages show.** `invalid_chars` and `too_long` show at once, because typing more cannot fix them. `too_short` and `unknown_country` show when focus leaves the field, and clear while you type again. The message is the readable sentence for the reason, in `messages` if you pass them; `{country}` is replaced with the country's name. Every reason also reaches `setCustomValidity`, so a native form submit is blocked while the number is not whole.
|
|
73
|
+
|
|
74
|
+
## Shared dial codes
|
|
75
|
+
|
|
76
|
+
`+1` (the North American Numbering Plan), `+7`, `+39`, `+44`, `+47`, `+61`, `+212`, `+262`, `+358`, `+590` and `+599` belong to more than one country. The country is chosen in this order, and **is approximate** (`detail.approximate` is `true`):
|
|
77
|
+
|
|
78
|
+
1. The number's leading digits, where a short first-party table knows them: Canadian and Caribbean area codes under `+1`, Kazakhstan's `6` and `7` under `+7`, the Crown Dependencies' fixed-line codes under `+44`, Svalbard, Cocos and Christmas Islands, Mayotte, Aland, Vatican City and the Caribbean Netherlands.
|
|
79
|
+
2. The selected country (or `defaultCountry`), if it shares the code.
|
|
80
|
+
3. The main country for the code: `+1` is the United States, `+7` Russia, `+44` the United Kingdom.
|
|
81
|
+
|
|
82
|
+
The E.164 is exact whichever country is chosen; only `detail.country` and the flag can be wrong, for instance for a ported number. A new Canadian area code reads as the United States until it is added to the table.
|
|
83
|
+
|
|
84
|
+
## Country picker
|
|
85
|
+
|
|
86
|
+
A button showing the flag and dial code, named for assistive tech as "Country: Myanmar (+95)". It opens a searchable list: type a name in the page's language, an ISO code (`mm`) or a dial code (`+95`). ArrowDown / ArrowUp move through the list, PageDown / PageUp by ten, Enter picks (the first match if nothing is highlighted), Escape closes and returns to the button, and Tab returns to the number. ArrowDown on the closed button opens it. Picking a country while the number starts with `+` re-reads it as a national number in that country.
|
|
87
|
+
|
|
88
|
+
Names come from `Intl.DisplayNames` in `locale`. Flags are Unicode regional-indicator letters, not images: Windows does not draw them as flags and shows the two letters instead, which is still the country code.
|
|
89
|
+
|
|
90
|
+
## Right-to-left
|
|
91
|
+
|
|
92
|
+
The number input has `dir="ltr"`, so the `+` and the digits keep their order inside a right-to-left page, isolated from the text around them, and the digits align to the page's inline start (the right, in RTL). Chromium already lays a `type="tel"` input out left-to-right; the attribute is what isolates it, and is the part the HTML standard specifies rather than a browser default. Only Chromium was checked. The dial codes in the picker and list are isolated left-to-right too. The label, the picker and the list follow the page: under `dir="rtl"` the picker sits on the right. Arrow keys inside the number move the caret the way they point.
|
|
93
|
+
|
|
94
|
+
## Accessibility
|
|
95
|
+
|
|
96
|
+
A visible label is required. The description and the message are tied to the number with `aria-describedby`, `aria-invalid` is set while a message shows, and the message is announced politely. The picker, its search box and the list each have an accessible name, the list uses `aria-activedescendant`, and every target is at least 44 by 44 CSS pixels. The message and description text measured 8.4:1 and 10.4:1 against the default white surface in Chromium; check your own theme, and dark mode, against 7:1.
|
|
97
|
+
|
|
98
|
+
## Agents
|
|
99
|
+
|
|
100
|
+
The component is controlled, so an agent writes `value` as an E.164 string and the text and picker follow: the input shows the national number and the picker its country. `detail` is plain JSON. Handles: `[data-react-fancy-phone-input]` (with `data-country`, `data-valid` and `data-reason`), `-country`, `-number`, `-error`, `-search`, `-listbox` and `-option` (VALUE is the ISO code). `parsePhone(text, { country, countries })` is exported, so an agent or a server-side render can read a number the same way the field does, and `PHONE_COUNTRIES` is the table itself.
|
|
101
|
+
|
|
102
|
+
## Data
|
|
103
|
+
|
|
104
|
+
`src/components/PhoneInput/phone-countries.ts` holds one row per country: ISO code, dial code, shortest and longest national number, trunk prefixes and a `VERIFIED` / `UNVERIFIED` status, each with its source in a comment. Dial codes come from the ITU-T E.164 list of assigned country codes. A `VERIFIED` row's length was checked against the country's numbering plan as cited in its comment. An `UNVERIFIED` row's length is the E.164 maximum, and its comment keeps the range that was transcribed from memory. To verify one: check it against the plan, put the range in, mark it `VERIFIED`, and cite the source. A test holds every unverified row at the E.164 maximum, so a narrowed range without a status change fails the build.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@particle-academy/react-fancy",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.10.0",
|
|
4
4
|
"description": "React UI component library for Human+ UX — controlled, agent-bridgeable primitives",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -693,6 +693,16 @@
|
|
|
693
693
|
"default": "./dist/pdf-viewer.cjs"
|
|
694
694
|
}
|
|
695
695
|
},
|
|
696
|
+
"./phone-input": {
|
|
697
|
+
"import": {
|
|
698
|
+
"types": "./dist/phone-input.d.ts",
|
|
699
|
+
"default": "./dist/phone-input.js"
|
|
700
|
+
},
|
|
701
|
+
"require": {
|
|
702
|
+
"types": "./dist/phone-input.d.cts",
|
|
703
|
+
"default": "./dist/phone-input.cjs"
|
|
704
|
+
}
|
|
705
|
+
},
|
|
696
706
|
"./pillbox": {
|
|
697
707
|
"import": {
|
|
698
708
|
"types": "./dist/pillbox.d.ts",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/use-floating-position.ts"],"names":[],"mappings":"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","file":"chunk-GJWNZMJT.js","sourcesContent":["import { useCallback, useEffect, useState, type RefObject } from \"react\";\nimport type { Placement } from \"../utils/types\";\nimport { directionOf, type Direction } from \"../utils/direction\";\n\ninterface FloatingPosition {\n x: number;\n y: number;\n placement: Placement;\n /**\n * The anchor's writing direction. Put it on the floating element as `dir`:\n * a floating element is portalled to `<body>`, so without it a `dir` set on\n * a wrapper (rather than on `<html>`) would not reach the popup.\n */\n dir: Direction;\n}\n\ninterface UseFloatingPositionOptions {\n placement?: Placement;\n offset?: number;\n enabled?: boolean;\n}\n\nfunction getPosition(\n anchor: DOMRect,\n floating: DOMRect,\n placement: Placement,\n offset: number,\n dir: Direction = \"ltr\",\n): FloatingPosition {\n let x = 0;\n let y = 0;\n const base = placement.split(\"-\")[0] as \"top\" | \"bottom\" | \"left\" | \"right\";\n const align = placement.split(\"-\")[1] as \"start\" | \"end\" | undefined;\n\n switch (base) {\n case \"top\":\n x = anchor.left + anchor.width / 2 - floating.width / 2;\n y = anchor.top - floating.height - offset;\n break;\n case \"bottom\":\n x = anchor.left + anchor.width / 2 - floating.width / 2;\n y = anchor.bottom + offset;\n break;\n case \"left\":\n x = anchor.left - floating.width - offset;\n y = anchor.top + anchor.height / 2 - floating.height / 2;\n break;\n case \"right\":\n x = anchor.right + offset;\n y = anchor.top + anchor.height / 2 - floating.height / 2;\n break;\n }\n\n // `start` / `end` are LOGICAL: `bottom-start` lines the popup up with the\n // anchor's inline-start edge, which is its RIGHT edge in RTL. The base side\n // (`left` / `right`) stays physical, as the names say.\n if (base === \"top\" || base === \"bottom\") {\n const startEdge = dir === \"rtl\" ? anchor.right - floating.width : anchor.left;\n const endEdge = dir === \"rtl\" ? anchor.left : anchor.right - floating.width;\n if (align === \"start\") x = startEdge;\n else if (align === \"end\") x = endEdge;\n }\n\n if (base === \"left\" || base === \"right\") {\n if (align === \"start\") y = anchor.top;\n else if (align === \"end\") y = anchor.bottom - floating.height;\n }\n\n // Viewport clamping\n let finalPlacement = placement;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n if (base === \"bottom\" && y + floating.height > vh) {\n y = anchor.top - floating.height - offset;\n finalPlacement = placement.replace(\"bottom\", \"top\") as Placement;\n } else if (base === \"top\" && y < 0) {\n y = anchor.bottom + offset;\n finalPlacement = placement.replace(\"top\", \"bottom\") as Placement;\n }\n\n x = Math.max(4, Math.min(x, vw - floating.width - 4));\n y = Math.max(4, Math.min(y, vh - floating.height - 4));\n\n return { x, y, placement: finalPlacement, dir };\n}\n\nexport function useFloatingPosition(\n anchorRef: RefObject<HTMLElement | null>,\n floatingRef: RefObject<HTMLElement | null>,\n options: UseFloatingPositionOptions = {},\n): FloatingPosition {\n const { placement = \"bottom\", offset = 8, enabled = true } = options;\n const [position, setPosition] = useState<FloatingPosition>({\n x: -9999,\n y: -9999,\n placement,\n dir: \"ltr\",\n });\n\n const update = useCallback(() => {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const anchorRect = anchor.getBoundingClientRect();\n const floatingRect = floating.getBoundingClientRect();\n const next = getPosition(anchorRect, floatingRect, placement, offset, directionOf(anchor));\n // Bail out when nothing changed so this can run after every commit\n // (below) without causing an infinite render loop.\n setPosition((prev) =>\n prev.x === next.x && prev.y === next.y && prev.placement === next.placement && prev.dir === next.dir\n ? prev\n : next,\n );\n }, [anchorRef, floatingRef, placement, offset]);\n\n // Re-measure after every commit while open.\n //\n // The floating element mounts a frame or two AFTER `enabled` flips — its\n // render is gated behind a state flip (e.g. useAnimation's `mounted`), so at\n // the moment `enabled` turns true the floating ref is still null and a single\n // measurement no-ops, leaving the popover parked at its off-screen sentinel.\n //\n // A passive effect with no deps runs after *every* commit — including the one\n // that finally mounts the floating element — and, crucially, keeps running in\n // hidden/background tabs where `requestAnimationFrame` is paused. The bail-out\n // in `update` makes the steady-state calls free, so this both fixes the\n // late-mount race and survives tab-visibility throttling.\n useEffect(() => {\n if (enabled) update();\n });\n\n // …but the per-commit effect ISN'T enough when the floating element mounts\n // through a <Portal> whose own mount gate (SSR-safe; renders nothing until its\n // useEffect runs) attaches the node WITHOUT re-rendering this hook's component.\n // Then no commit happens here, the measurement never runs, and the popover\n // stays parked at its off-screen sentinel until an unrelated scroll/resize\n // fires `update()` — the \"menu only appears after I scroll\" bug. Poll with rAF\n // until the node is actually in the DOM, measure it, then keep it observed for\n // size changes (async content / lazily-loaded icons).\n useEffect(() => {\n if (!enabled) return;\n let raf = 0;\n let ro: ResizeObserver | null = null;\n const tick = () => {\n const el = floatingRef.current;\n if (el) {\n update();\n if (typeof ResizeObserver !== \"undefined\") {\n ro = new ResizeObserver(() => update());\n ro.observe(el);\n }\n } else {\n raf = requestAnimationFrame(tick);\n }\n };\n raf = requestAnimationFrame(tick);\n return () => {\n cancelAnimationFrame(raf);\n ro?.disconnect();\n };\n }, [enabled, update, floatingRef]);\n\n // Park off-screen when closed so the next open re-measures from scratch.\n useEffect(() => {\n if (!enabled) {\n setPosition((prev) =>\n prev.x === -9999 ? prev : { x: -9999, y: -9999, placement, dir: prev.dir },\n );\n }\n }, [enabled, placement]);\n\n // Reposition on scroll/resize while open.\n useEffect(() => {\n if (!enabled) return;\n\n window.addEventListener(\"scroll\", update, true);\n window.addEventListener(\"resize\", update);\n\n return () => {\n window.removeEventListener(\"scroll\", update, true);\n window.removeEventListener(\"resize\", update);\n };\n }, [update, enabled]);\n\n return position;\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/use-floating-position.ts"],"names":["useState","useCallback","directionOf","useEffect"],"mappings":"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","file":"chunk-XGGAAG74.cjs","sourcesContent":["import { useCallback, useEffect, useState, type RefObject } from \"react\";\nimport type { Placement } from \"../utils/types\";\nimport { directionOf, type Direction } from \"../utils/direction\";\n\ninterface FloatingPosition {\n x: number;\n y: number;\n placement: Placement;\n /**\n * The anchor's writing direction. Put it on the floating element as `dir`:\n * a floating element is portalled to `<body>`, so without it a `dir` set on\n * a wrapper (rather than on `<html>`) would not reach the popup.\n */\n dir: Direction;\n}\n\ninterface UseFloatingPositionOptions {\n placement?: Placement;\n offset?: number;\n enabled?: boolean;\n}\n\nfunction getPosition(\n anchor: DOMRect,\n floating: DOMRect,\n placement: Placement,\n offset: number,\n dir: Direction = \"ltr\",\n): FloatingPosition {\n let x = 0;\n let y = 0;\n const base = placement.split(\"-\")[0] as \"top\" | \"bottom\" | \"left\" | \"right\";\n const align = placement.split(\"-\")[1] as \"start\" | \"end\" | undefined;\n\n switch (base) {\n case \"top\":\n x = anchor.left + anchor.width / 2 - floating.width / 2;\n y = anchor.top - floating.height - offset;\n break;\n case \"bottom\":\n x = anchor.left + anchor.width / 2 - floating.width / 2;\n y = anchor.bottom + offset;\n break;\n case \"left\":\n x = anchor.left - floating.width - offset;\n y = anchor.top + anchor.height / 2 - floating.height / 2;\n break;\n case \"right\":\n x = anchor.right + offset;\n y = anchor.top + anchor.height / 2 - floating.height / 2;\n break;\n }\n\n // `start` / `end` are LOGICAL: `bottom-start` lines the popup up with the\n // anchor's inline-start edge, which is its RIGHT edge in RTL. The base side\n // (`left` / `right`) stays physical, as the names say.\n if (base === \"top\" || base === \"bottom\") {\n const startEdge = dir === \"rtl\" ? anchor.right - floating.width : anchor.left;\n const endEdge = dir === \"rtl\" ? anchor.left : anchor.right - floating.width;\n if (align === \"start\") x = startEdge;\n else if (align === \"end\") x = endEdge;\n }\n\n if (base === \"left\" || base === \"right\") {\n if (align === \"start\") y = anchor.top;\n else if (align === \"end\") y = anchor.bottom - floating.height;\n }\n\n // Viewport clamping\n let finalPlacement = placement;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n if (base === \"bottom\" && y + floating.height > vh) {\n y = anchor.top - floating.height - offset;\n finalPlacement = placement.replace(\"bottom\", \"top\") as Placement;\n } else if (base === \"top\" && y < 0) {\n y = anchor.bottom + offset;\n finalPlacement = placement.replace(\"top\", \"bottom\") as Placement;\n }\n\n x = Math.max(4, Math.min(x, vw - floating.width - 4));\n y = Math.max(4, Math.min(y, vh - floating.height - 4));\n\n return { x, y, placement: finalPlacement, dir };\n}\n\nexport function useFloatingPosition(\n anchorRef: RefObject<HTMLElement | null>,\n floatingRef: RefObject<HTMLElement | null>,\n options: UseFloatingPositionOptions = {},\n): FloatingPosition {\n const { placement = \"bottom\", offset = 8, enabled = true } = options;\n const [position, setPosition] = useState<FloatingPosition>({\n x: -9999,\n y: -9999,\n placement,\n dir: \"ltr\",\n });\n\n const update = useCallback(() => {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const anchorRect = anchor.getBoundingClientRect();\n const floatingRect = floating.getBoundingClientRect();\n const next = getPosition(anchorRect, floatingRect, placement, offset, directionOf(anchor));\n // Bail out when nothing changed so this can run after every commit\n // (below) without causing an infinite render loop.\n setPosition((prev) =>\n prev.x === next.x && prev.y === next.y && prev.placement === next.placement && prev.dir === next.dir\n ? prev\n : next,\n );\n }, [anchorRef, floatingRef, placement, offset]);\n\n // Re-measure after every commit while open.\n //\n // The floating element mounts a frame or two AFTER `enabled` flips — its\n // render is gated behind a state flip (e.g. useAnimation's `mounted`), so at\n // the moment `enabled` turns true the floating ref is still null and a single\n // measurement no-ops, leaving the popover parked at its off-screen sentinel.\n //\n // A passive effect with no deps runs after *every* commit — including the one\n // that finally mounts the floating element — and, crucially, keeps running in\n // hidden/background tabs where `requestAnimationFrame` is paused. The bail-out\n // in `update` makes the steady-state calls free, so this both fixes the\n // late-mount race and survives tab-visibility throttling.\n useEffect(() => {\n if (enabled) update();\n });\n\n // …but the per-commit effect ISN'T enough when the floating element mounts\n // through a <Portal> whose own mount gate (SSR-safe; renders nothing until its\n // useEffect runs) attaches the node WITHOUT re-rendering this hook's component.\n // Then no commit happens here, the measurement never runs, and the popover\n // stays parked at its off-screen sentinel until an unrelated scroll/resize\n // fires `update()` — the \"menu only appears after I scroll\" bug. Poll with rAF\n // until the node is actually in the DOM, measure it, then keep it observed for\n // size changes (async content / lazily-loaded icons).\n useEffect(() => {\n if (!enabled) return;\n let raf = 0;\n let ro: ResizeObserver | null = null;\n const tick = () => {\n const el = floatingRef.current;\n if (el) {\n update();\n if (typeof ResizeObserver !== \"undefined\") {\n ro = new ResizeObserver(() => update());\n ro.observe(el);\n }\n } else {\n raf = requestAnimationFrame(tick);\n }\n };\n raf = requestAnimationFrame(tick);\n return () => {\n cancelAnimationFrame(raf);\n ro?.disconnect();\n };\n }, [enabled, update, floatingRef]);\n\n // Park off-screen when closed so the next open re-measures from scratch.\n useEffect(() => {\n if (!enabled) {\n setPosition((prev) =>\n prev.x === -9999 ? prev : { x: -9999, y: -9999, placement, dir: prev.dir },\n );\n }\n }, [enabled, placement]);\n\n // Reposition on scroll/resize while open.\n useEffect(() => {\n if (!enabled) return;\n\n window.addEventListener(\"scroll\", update, true);\n window.addEventListener(\"resize\", update);\n\n return () => {\n window.removeEventListener(\"scroll\", update, true);\n window.removeEventListener(\"resize\", update);\n };\n }, [update, enabled]);\n\n return position;\n}\n"]}
|