@allxsmith/bestax-bulma 5.52.0 → 5.54.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/dist/bestax.css +1 -1
- package/dist/bestax.css.map +1 -1
- package/dist/esm/components/Avatars.js +7 -2
- package/dist/esm/components/Avatars.js.map +1 -1
- package/dist/esm/components/Badge.js +7 -2
- package/dist/esm/components/Badge.js.map +1 -1
- package/dist/esm/components/Command.js +19 -9
- package/dist/esm/components/Command.js.map +1 -1
- package/dist/esm/components/Modal.js +2 -1
- package/dist/esm/components/Modal.js.map +1 -1
- package/dist/esm/components/Pagination.js +9 -1
- package/dist/esm/components/Pagination.js.map +1 -1
- package/dist/esm/components/Sidebar.js +3 -1
- package/dist/esm/components/Sidebar.js.map +1 -1
- package/dist/esm/components/Steps.js +5 -1
- package/dist/esm/components/Steps.js.map +1 -1
- package/dist/esm/components/Toast.js +8 -5
- package/dist/esm/components/Toast.js.map +1 -1
- package/dist/esm/elements/Notification.js +4 -2
- package/dist/esm/elements/Notification.js.map +1 -1
- package/dist/esm/elements/RingProgress.js +6 -1
- package/dist/esm/elements/RingProgress.js.map +1 -1
- package/dist/esm/form/Autocomplete.js +7 -2
- package/dist/esm/form/Autocomplete.js.map +1 -1
- package/dist/esm/form/Checkbox.js +3 -2
- package/dist/esm/form/Checkbox.js.map +1 -1
- package/dist/esm/form/CheckboxCard.js +3 -2
- package/dist/esm/form/CheckboxCard.js.map +1 -1
- package/dist/esm/form/Combobox.js +10 -4
- package/dist/esm/form/Combobox.js.map +1 -1
- package/dist/esm/form/DateInputBase.js +15 -6
- package/dist/esm/form/DateInputBase.js.map +1 -1
- package/dist/esm/form/DateRangeInputBase.js +15 -5
- package/dist/esm/form/DateRangeInputBase.js.map +1 -1
- package/dist/esm/form/DateTimeInputBase.js +15 -6
- package/dist/esm/form/DateTimeInputBase.js.map +1 -1
- package/dist/esm/form/Fieldset.js +64 -0
- package/dist/esm/form/Fieldset.js.map +1 -0
- package/dist/esm/form/File.js +4 -1
- package/dist/esm/form/File.js.map +1 -1
- package/dist/esm/form/FormContext.js +17 -1
- package/dist/esm/form/FormContext.js.map +1 -1
- package/dist/esm/form/InputBase.js +3 -2
- package/dist/esm/form/InputBase.js.map +1 -1
- package/dist/esm/form/Numberinput.js +3 -2
- package/dist/esm/form/Numberinput.js.map +1 -1
- package/dist/esm/form/PinInput.js +3 -2
- package/dist/esm/form/PinInput.js.map +1 -1
- package/dist/esm/form/Radio.js +3 -2
- package/dist/esm/form/Radio.js.map +1 -1
- package/dist/esm/form/RadioCard.js +3 -2
- package/dist/esm/form/RadioCard.js.map +1 -1
- package/dist/esm/form/Rate.js +12 -3
- package/dist/esm/form/Rate.js.map +1 -1
- package/dist/esm/form/SelectBase.js +3 -2
- package/dist/esm/form/SelectBase.js.map +1 -1
- package/dist/esm/form/Slider.js +10 -6
- package/dist/esm/form/Slider.js.map +1 -1
- package/dist/esm/form/Switch.js +3 -1
- package/dist/esm/form/Switch.js.map +1 -1
- package/dist/esm/form/Taginput.js +12 -5
- package/dist/esm/form/Taginput.js.map +1 -1
- package/dist/esm/form/TextAreaBase.js +3 -2
- package/dist/esm/form/TextAreaBase.js.map +1 -1
- package/dist/esm/form/TimeInputBase.js +13 -6
- package/dist/esm/form/TimeInputBase.js.map +1 -1
- package/dist/esm/form/_pickerInternals/PickerPopover.js +4 -2
- package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
- package/dist/esm/form/_pickerInternals/useLocaleDefaults.js +80 -0
- package/dist/esm/form/_pickerInternals/useLocaleDefaults.js.map +1 -0
- package/dist/esm/helpers/LocaleProvider.js +156 -0
- package/dist/esm/helpers/LocaleProvider.js.map +1 -0
- package/dist/esm/helpers/localeFormat.js +108 -0
- package/dist/esm/helpers/localeFormat.js.map +1 -0
- package/dist/esm/helpers/localeInfo.js +109 -0
- package/dist/esm/helpers/localeInfo.js.map +1 -0
- package/dist/esm/helpers/portal.js +14 -1
- package/dist/esm/helpers/portal.js.map +1 -1
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +690 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +2 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Avatars.d.ts +1 -1
- package/dist/types/components/Badge.d.ts +1 -1
- package/dist/types/components/Command.d.ts +4 -2
- package/dist/types/components/Pagination.d.ts +3 -0
- package/dist/types/components/Steps.d.ts +4 -1
- package/dist/types/elements/RingProgress.d.ts +3 -2
- package/dist/types/form/Autocomplete.d.ts +6 -1
- package/dist/types/form/Combobox.d.ts +3 -2
- package/dist/types/form/DateInputBase.d.ts +25 -3
- package/dist/types/form/DateRangeInputBase.d.ts +25 -3
- package/dist/types/form/DateTimeInputBase.d.ts +25 -3
- package/dist/types/form/Fieldset.d.ts +86 -0
- package/dist/types/form/File.d.ts +2 -1
- package/dist/types/form/FormContext.d.ts +12 -0
- package/dist/types/form/Rate.d.ts +4 -1
- package/dist/types/form/Slider.d.ts +9 -2
- package/dist/types/form/Taginput.d.ts +8 -2
- package/dist/types/form/TimeInputBase.d.ts +5 -1
- package/dist/types/form/_pickerInternals/PickerPopover.d.ts +6 -0
- package/dist/types/form/_pickerInternals/useLocaleDefaults.d.ts +49 -0
- package/dist/types/helpers/LocaleProvider.d.ts +131 -0
- package/dist/types/helpers/localeFormat.d.ts +26 -0
- package/dist/types/helpers/localeInfo.d.ts +23 -0
- package/dist/types/helpers/portal.d.ts +11 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types-cjs/components/Avatars.d.ts +1 -1
- package/dist/types-cjs/components/Badge.d.ts +1 -1
- package/dist/types-cjs/components/Command.d.ts +4 -2
- package/dist/types-cjs/components/Pagination.d.ts +3 -0
- package/dist/types-cjs/components/Steps.d.ts +4 -1
- package/dist/types-cjs/elements/RingProgress.d.ts +3 -2
- package/dist/types-cjs/form/Autocomplete.d.ts +6 -1
- package/dist/types-cjs/form/Combobox.d.ts +3 -2
- package/dist/types-cjs/form/DateInputBase.d.ts +25 -3
- package/dist/types-cjs/form/DateRangeInputBase.d.ts +25 -3
- package/dist/types-cjs/form/DateTimeInputBase.d.ts +25 -3
- package/dist/types-cjs/form/Fieldset.d.ts +86 -0
- package/dist/types-cjs/form/File.d.ts +2 -1
- package/dist/types-cjs/form/FormContext.d.ts +12 -0
- package/dist/types-cjs/form/Rate.d.ts +4 -1
- package/dist/types-cjs/form/Slider.d.ts +9 -2
- package/dist/types-cjs/form/Taginput.d.ts +8 -2
- package/dist/types-cjs/form/TimeInputBase.d.ts +5 -1
- package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +6 -0
- package/dist/types-cjs/form/_pickerInternals/useLocaleDefaults.d.ts +49 -0
- package/dist/types-cjs/helpers/LocaleProvider.d.ts +131 -0
- package/dist/types-cjs/helpers/localeFormat.d.ts +26 -0
- package/dist/types-cjs/helpers/localeInfo.d.ts +23 -0
- package/dist/types-cjs/helpers/portal.d.ts +11 -0
- package/dist/types-cjs/index.d.ts +3 -0
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/form/_fieldset.scss +108 -0
- package/src/scss/form/_index.scss +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useLocaleScope } from '../../helpers/LocaleProvider.js';
|
|
4
|
+
import { canonicalLocale, localeDirection, localeFirstDay } from '../../helpers/localeInfo.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A picker's locale: its own `locale`, else the one a `LocaleProvider` around
|
|
8
|
+
* it sets, else `undefined`, which leaves `Intl` to use the runtime's.
|
|
9
|
+
*/
|
|
10
|
+
function usePickerLocale(own) {
|
|
11
|
+
const scope = useLocaleScope();
|
|
12
|
+
return own || scope?.locale;
|
|
13
|
+
}
|
|
14
|
+
const NO_ROOT_ATTRS = {};
|
|
15
|
+
/**
|
|
16
|
+
* A picker's language and direction inside a `LocaleProvider`, by the rule a
|
|
17
|
+
* nested provider follows: its `lang` is its own `locale`, else the
|
|
18
|
+
* provider's, and its `dir` is the `dir` passed to it, else its own locale's
|
|
19
|
+
* direction when it has one, else the provider's. Outside a provider it
|
|
20
|
+
* changes nothing. A `locale` `Intl` refuses counts as none here, since the
|
|
21
|
+
* picker's own formatting reports it.
|
|
22
|
+
*/
|
|
23
|
+
function usePickerLocaleScope(ownLocale, ownDir) {
|
|
24
|
+
const scope = useLocaleScope();
|
|
25
|
+
return useMemo(() => {
|
|
26
|
+
if (!scope)
|
|
27
|
+
return { rootAttrs: NO_ROOT_ATTRS };
|
|
28
|
+
let own;
|
|
29
|
+
try {
|
|
30
|
+
own = canonicalLocale(ownLocale);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
own = undefined;
|
|
34
|
+
}
|
|
35
|
+
const dir = ownDir || (own ? localeDirection(own) : scope.dir);
|
|
36
|
+
return {
|
|
37
|
+
rootAttrs: own ? { lang: own, dir } : NO_ROOT_ATTRS,
|
|
38
|
+
panel: { locale: own || scope.locale, dir },
|
|
39
|
+
};
|
|
40
|
+
}, [scope, ownLocale, ownDir]);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a picker's `dayNames` replace the calendar's own, which `Calendar`
|
|
44
|
+
* does only for a full week of seven.
|
|
45
|
+
*/
|
|
46
|
+
function hasOwnDayNames(dayNames) {
|
|
47
|
+
return dayNames !== undefined && dayNames.length === 7;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The day a picker's week starts on: its own `firstDayOfWeek`, else, inside a
|
|
51
|
+
* `LocaleProvider`, the first day of the locale the picker uses, else Sunday.
|
|
52
|
+
* Outside a provider it stays Sunday whatever the locale, as it always has,
|
|
53
|
+
* and so does a picker inside one with no locale from either, since the
|
|
54
|
+
* runtime's own differs between the server and the browser.
|
|
55
|
+
*
|
|
56
|
+
* It also stays Sunday for a picker that brings its own `dayNames`. The
|
|
57
|
+
* calendar shows those exactly as given, in calendar order, and before the
|
|
58
|
+
* provider an unset `firstDayOfWeek` was always Sunday, so names written
|
|
59
|
+
* Sunday first would sit a day or more off their columns if the week moved
|
|
60
|
+
* under them.
|
|
61
|
+
*/
|
|
62
|
+
function usePickerFirstDayOfWeek(own, locale, hasDayNames) {
|
|
63
|
+
const inProvider = useLocaleScope() !== null;
|
|
64
|
+
return useMemo(() => {
|
|
65
|
+
if (own !== undefined)
|
|
66
|
+
return own;
|
|
67
|
+
if (!inProvider || !locale || hasDayNames)
|
|
68
|
+
return 0;
|
|
69
|
+
try {
|
|
70
|
+
return localeFirstDay(canonicalLocale(locale));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// A tag Intl refuses: the picker's own formatting says so.
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
}, [own, inProvider, locale, hasDayNames]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { hasOwnDayNames, usePickerFirstDayOfWeek, usePickerLocale, usePickerLocaleScope };
|
|
80
|
+
//# sourceMappingURL=useLocaleDefaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLocaleDefaults.js","sources":["../../../../src/form/_pickerInternals/useLocaleDefaults.ts"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n useLocaleScope,\n type PortalLocale,\n} from '../../helpers/LocaleProvider';\nimport {\n canonicalLocale,\n localeDirection,\n localeFirstDay,\n} from '../../helpers/localeInfo';\nimport type { DayOfWeek } from './pickerTypes';\n\n/**\n * A picker's locale: its own `locale`, else the one a `LocaleProvider` around\n * it sets, else `undefined`, which leaves `Intl` to use the runtime's.\n */\nexport function usePickerLocale(own: string | undefined): string | undefined {\n const scope = useLocaleScope();\n return own || scope?.locale;\n}\n\n/** Where a picker's own language and direction go, inside a `LocaleProvider`. */\nexport interface PickerLocaleScope {\n /**\n * The `lang` and `dir` for the picker's root, so what renders inside it,\n * an inline calendar or a popover in place, follows them. Empty unless the\n * picker has a `locale` of its own.\n */\n rootAttrs: { lang?: string; dir?: string };\n /** What its portaled panel carries; unset outside a provider. */\n panel?: PortalLocale;\n}\n\nconst NO_ROOT_ATTRS = {};\n\n/**\n * A picker's language and direction inside a `LocaleProvider`, by the rule a\n * nested provider follows: its `lang` is its own `locale`, else the\n * provider's, and its `dir` is the `dir` passed to it, else its own locale's\n * direction when it has one, else the provider's. Outside a provider it\n * changes nothing. A `locale` `Intl` refuses counts as none here, since the\n * picker's own formatting reports it.\n */\nexport function usePickerLocaleScope(\n ownLocale: string | undefined,\n ownDir: string | undefined\n): PickerLocaleScope {\n const scope = useLocaleScope();\n return useMemo(() => {\n if (!scope) return { rootAttrs: NO_ROOT_ATTRS };\n let own: string | undefined;\n try {\n own = canonicalLocale(ownLocale);\n } catch {\n own = undefined;\n }\n const dir = ownDir || (own ? localeDirection(own) : scope.dir);\n return {\n rootAttrs: own ? { lang: own, dir } : NO_ROOT_ATTRS,\n panel: { locale: own || scope.locale, dir },\n };\n }, [scope, ownLocale, ownDir]);\n}\n\n/**\n * Whether a picker's `dayNames` replace the calendar's own, which `Calendar`\n * does only for a full week of seven.\n */\nexport function hasOwnDayNames(dayNames: string[] | undefined): boolean {\n return dayNames !== undefined && dayNames.length === 7;\n}\n\n/**\n * The day a picker's week starts on: its own `firstDayOfWeek`, else, inside a\n * `LocaleProvider`, the first day of the locale the picker uses, else Sunday.\n * Outside a provider it stays Sunday whatever the locale, as it always has,\n * and so does a picker inside one with no locale from either, since the\n * runtime's own differs between the server and the browser.\n *\n * It also stays Sunday for a picker that brings its own `dayNames`. The\n * calendar shows those exactly as given, in calendar order, and before the\n * provider an unset `firstDayOfWeek` was always Sunday, so names written\n * Sunday first would sit a day or more off their columns if the week moved\n * under them.\n */\nexport function usePickerFirstDayOfWeek(\n own: DayOfWeek | undefined,\n locale: string | undefined,\n hasDayNames: boolean\n): DayOfWeek {\n const inProvider = useLocaleScope() !== null;\n return useMemo(() => {\n if (own !== undefined) return own;\n if (!inProvider || !locale || hasDayNames) return 0;\n try {\n return localeFirstDay(canonicalLocale(locale) as string);\n } catch {\n // A tag Intl refuses: the picker's own formatting says so.\n return 0;\n }\n }, [own, inProvider, locale, hasDayNames]);\n}\n"],"names":[],"mappings":";;;;;AAYA;;;AAGG;AACG,SAAU,eAAe,CAAC,GAAuB,EAAA;AACrD,IAAA,MAAM,KAAK,GAAG,cAAc,EAAE;AAC9B,IAAA,OAAO,GAAG,IAAI,KAAK,EAAE,MAAM;AAC7B;AAcA,MAAM,aAAa,GAAG,EAAE;AAExB;;;;;;;AAOG;AACG,SAAU,oBAAoB,CAClC,SAA6B,EAC7B,MAA0B,EAAA;AAE1B,IAAA,MAAM,KAAK,GAAG,cAAc,EAAE;IAC9B,OAAO,OAAO,CAAC,MAAK;AAClB,QAAA,IAAI,CAAC,KAAK;AAAE,YAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE;AAC/C,QAAA,IAAI,GAAuB;AAC3B,QAAA,IAAI;AACF,YAAA,GAAG,GAAG,eAAe,CAAC,SAAS,CAAC;QAClC;AAAE,QAAA,MAAM;YACN,GAAG,GAAG,SAAS;QACjB;QACA,MAAM,GAAG,GAAG,MAAM,KAAK,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC;QAC9D,OAAO;AACL,YAAA,SAAS,EAAE,GAAG,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,aAAa;YACnD,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE;SAC5C;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AAChC;AAEA;;;AAGG;AACG,SAAU,cAAc,CAAC,QAA8B,EAAA;IAC3D,OAAO,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;AACxD;AAEA;;;;;;;;;;;;AAYG;SACa,uBAAuB,CACrC,GAA0B,EAC1B,MAA0B,EAC1B,WAAoB,EAAA;AAEpB,IAAA,MAAM,UAAU,GAAG,cAAc,EAAE,KAAK,IAAI;IAC5C,OAAO,OAAO,CAAC,MAAK;QAClB,IAAI,GAAG,KAAK,SAAS;AAAE,YAAA,OAAO,GAAG;AACjC,QAAA,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM,IAAI,WAAW;AAAE,YAAA,OAAO,CAAC;AACnD,QAAA,IAAI;AACF,YAAA,OAAO,cAAc,CAAC,eAAe,CAAC,MAAM,CAAW,CAAC;QAC1D;AAAE,QAAA,MAAM;;AAEN,YAAA,OAAO,CAAC;QACV;IACF,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;AAC5C;;"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
import { createContext, forwardRef, useContext, useMemo, useRef, useCallback, useState } from 'react';
|
|
4
|
+
import { classNames } from './classNames.js';
|
|
5
|
+
import { useBulmaClasses } from './useBulmaClasses.js';
|
|
6
|
+
import { warnOnce } from './devWarnings.js';
|
|
7
|
+
import { localeDirection, canonicalLocale } from './localeInfo.js';
|
|
8
|
+
|
|
9
|
+
const LocaleContext = createContext(null);
|
|
10
|
+
/** The nearest provider's wrapper element, for `LocalePortalScope` to test targets against. */
|
|
11
|
+
const LocaleElementContext = createContext(null);
|
|
12
|
+
/** A tag in canonical form, or `undefined`, warning about one `Intl` refuses. */
|
|
13
|
+
function validLocale(tag) {
|
|
14
|
+
try {
|
|
15
|
+
return canonicalLocale(tag);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
warnOnce(`locale-provider-invalid:${tag}`, `[bestax-bulma] <LocaleProvider locale="${tag}">: not a language ` +
|
|
19
|
+
'tag Intl accepts, so it is ignored. Use a BCP 47 tag such as ' +
|
|
20
|
+
'"en-US", with a hyphen rather than an underscore.');
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sets the language and writing direction for the part of the app inside it, on a wrapper `<div>` and on what the library portals out of it.
|
|
26
|
+
*
|
|
27
|
+
* The wrapper carries `dir` and `lang`, so CSS and the components that read
|
|
28
|
+
* the computed direction follow it, as they follow `<html dir>`. A `locale`
|
|
29
|
+
* alone sets the direction of its script, and a `dir` sets the direction
|
|
30
|
+
* whatever the locale. A nested provider keeps whatever it leaves unset.
|
|
31
|
+
*
|
|
32
|
+
* Inside it, the date and time pickers take its `locale` unless given their
|
|
33
|
+
* own, and their week starts on the first day of the locale they use unless
|
|
34
|
+
* `firstDayOfWeek` is set, and a picker with a `locale` of its own is read
|
|
35
|
+
* like a nested provider. With a `locale`, the numbers the library writes,
|
|
36
|
+
* such as page numbers and badge counts, follow it, and so does the
|
|
37
|
+
* filtering in `Combobox`, `Autocomplete`, `Taginput` and `Command`, which then ignores
|
|
38
|
+
* accents where the language treats letters as the same. The library's
|
|
39
|
+
* portaled content, such as a `Popover` panel, a `Modal` with `portal`, a
|
|
40
|
+
* `Toast` or a `Portal`'s children, keeps the same `dir` and `lang` though
|
|
41
|
+
* it renders outside the wrapper: in a container outside the wrapper, it
|
|
42
|
+
* sits in a `display: contents` element that carries them. Without a
|
|
43
|
+
* provider none of this changes.
|
|
44
|
+
*
|
|
45
|
+
* @function
|
|
46
|
+
* @param {LocaleProviderProps} props - Props for the LocaleProvider.
|
|
47
|
+
* @returns {JSX.Element} The wrapper, inside the context.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* <LocaleProvider locale="ar-EG">
|
|
51
|
+
* <App />
|
|
52
|
+
* </LocaleProvider>
|
|
53
|
+
*/
|
|
54
|
+
const LocaleProvider = forwardRef(({ locale: localeProp, dir: dirProp, className, children, ...props }, ref) => {
|
|
55
|
+
const outer = useContext(LocaleContext);
|
|
56
|
+
const ownLocale = useMemo(() => validLocale(localeProp), [localeProp]);
|
|
57
|
+
const ownLocaleDir = useMemo(() => (ownLocale ? localeDirection(ownLocale) : undefined), [ownLocale]);
|
|
58
|
+
const locale = ownLocale || outer?.locale;
|
|
59
|
+
const dir = dirProp || ownLocaleDir || outer?.dir;
|
|
60
|
+
const value = useMemo(() => ({ locale, dir }), [locale, dir]);
|
|
61
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
62
|
+
// The wrapper, for portaled content to tell whether its target is inside
|
|
63
|
+
// it, and the caller's ref. A callback ref's cleanup is held and run on
|
|
64
|
+
// detach, as Toast and Modal do, since React 18 discards the return value.
|
|
65
|
+
const elementRef = useRef(null);
|
|
66
|
+
const consumerCleanupRef = useRef(null);
|
|
67
|
+
const setElement = useCallback((node) => {
|
|
68
|
+
elementRef.current = node;
|
|
69
|
+
if (typeof ref === 'function') {
|
|
70
|
+
if (node === null) {
|
|
71
|
+
const consumerCleanup = consumerCleanupRef.current;
|
|
72
|
+
consumerCleanupRef.current = null;
|
|
73
|
+
if (consumerCleanup)
|
|
74
|
+
consumerCleanup();
|
|
75
|
+
else
|
|
76
|
+
ref(null);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const cleanup = ref(node);
|
|
80
|
+
consumerCleanupRef.current =
|
|
81
|
+
typeof cleanup === 'function' ? cleanup : null;
|
|
82
|
+
}
|
|
83
|
+
else if (ref) {
|
|
84
|
+
ref.current = node;
|
|
85
|
+
}
|
|
86
|
+
}, [ref]);
|
|
87
|
+
return (jsx(LocaleContext.Provider, { value: value, children: jsx(LocaleElementContext.Provider, { value: elementRef, children: jsx("div", { ref: setElement, className: classNames(bulmaHelperClasses, className) || undefined, ...rest, dir: dir, lang: locale, children: children }) }) }));
|
|
88
|
+
});
|
|
89
|
+
LocaleProvider.displayName = 'LocaleProvider';
|
|
90
|
+
/** What `useLocale` returns outside a provider, the same object each time. */
|
|
91
|
+
const noLocale = {};
|
|
92
|
+
/**
|
|
93
|
+
* The locale and direction the nearest `LocaleProvider` sets, for an app's own `Intl` calls or its own portaled content; both are `undefined` outside one.
|
|
94
|
+
*
|
|
95
|
+
* It isn't the page's direction: with no provider, or one that sets no
|
|
96
|
+
* direction, the page's `<html dir>` still decides. To act on the direction
|
|
97
|
+
* an element is laid out in, read `getComputedStyle(element).direction`, as
|
|
98
|
+
* the library's components do.
|
|
99
|
+
*
|
|
100
|
+
* @function
|
|
101
|
+
* @returns {LocaleContextValue} The nearest provider's `locale` and `dir`.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* const { locale } = useLocale();
|
|
105
|
+
* const price = new Intl.NumberFormat(locale, {
|
|
106
|
+
* style: 'currency',
|
|
107
|
+
* currency: 'EUR',
|
|
108
|
+
* }).format(12.5);
|
|
109
|
+
*/
|
|
110
|
+
function useLocale() {
|
|
111
|
+
return useContext(LocaleContext) ?? noLocale;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* INTERNAL, deliberately not exported from `src/index.ts`. The nearest
|
|
115
|
+
* provider's settings, or `null` outside any provider, so a caller can tell
|
|
116
|
+
* "no provider" from "a provider that sets nothing".
|
|
117
|
+
*/
|
|
118
|
+
function useLocaleScope() {
|
|
119
|
+
return useContext(LocaleContext);
|
|
120
|
+
}
|
|
121
|
+
const displayContents = { display: 'contents' };
|
|
122
|
+
/**
|
|
123
|
+
* INTERNAL, deliberately not exported from `src/index.ts`. Keeps the `dir`
|
|
124
|
+
* and `lang` of the nearest provider, or the `scope` given in their place,
|
|
125
|
+
* on content the library portals into `target`, which has left the
|
|
126
|
+
* provider's wrapper behind.
|
|
127
|
+
*
|
|
128
|
+
* A target inside the provider's own wrapper gets the content as it is: it
|
|
129
|
+
* inherits from the wrapper, and follows it when the provider changes.
|
|
130
|
+
* Content that must be its container's direct child, a `<tr>` portaled into
|
|
131
|
+
* a `<tbody>` there, stays one. Any other target, `document.body` included,
|
|
132
|
+
* gets the content inside a `display: contents` element carrying the
|
|
133
|
+
* values, which adds no box. So does content with a `scope` of its own, a
|
|
134
|
+
* picker's panel, since the provider's values aren't its own.
|
|
135
|
+
*
|
|
136
|
+
* Which of the two is decided once, when the content mounts, and kept while
|
|
137
|
+
* it stays: switching between them would put a different element in its
|
|
138
|
+
* place and remount everything inside, losing typed text, focus and timers.
|
|
139
|
+
* A change of locale or direction updates the wrapper's attributes instead.
|
|
140
|
+
* A target is never left bare because something else, such as `<html>`,
|
|
141
|
+
* happens to carry the same values, since that can stop being true without
|
|
142
|
+
* the content remounting. Outside a provider it renders the children alone,
|
|
143
|
+
* so nothing changes there.
|
|
144
|
+
*/
|
|
145
|
+
function LocalePortalScope({ children, target, scope, }) {
|
|
146
|
+
const provider = useContext(LocaleContext);
|
|
147
|
+
const element = useContext(LocaleElementContext);
|
|
148
|
+
const [wrap] = useState(() => scope !== undefined || !element?.current?.contains(target));
|
|
149
|
+
if (!provider || !wrap)
|
|
150
|
+
return jsx(Fragment, { children: children });
|
|
151
|
+
const values = scope ?? provider;
|
|
152
|
+
return (jsx("div", { dir: values.dir, lang: values.locale, style: displayContents, children: children }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { LocalePortalScope, LocaleProvider, useLocale, useLocaleScope };
|
|
156
|
+
//# sourceMappingURL=LocaleProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocaleProvider.js","sources":["../../../src/helpers/LocaleProvider.tsx"],"sourcesContent":["import React, {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { classNames } from './classNames';\nimport { useBulmaClasses, BulmaClassesProps } from './useBulmaClasses';\nimport { warnOnce } from './devWarnings';\nimport { canonicalLocale, localeDirection } from './localeInfo';\n\n/** A writing direction. */\nexport type TextDirection = 'ltr' | 'rtl';\n\n/** What the nearest `LocaleProvider` sets, both `undefined` outside one. */\nexport interface LocaleContextValue {\n /**\n * The locale, as a canonical BCP 47 tag such as `\"ar-EG\"`: the provider's\n * own, else the one an outer provider sets.\n */\n locale?: string;\n /**\n * The direction: the provider's own `dir`, else its own locale's, else the\n * one an outer provider sets.\n */\n dir?: TextDirection;\n}\n\nconst LocaleContext = createContext<LocaleContextValue | null>(null);\n\n/** The nearest provider's wrapper element, for `LocalePortalScope` to test targets against. */\nconst LocaleElementContext =\n createContext<React.RefObject<HTMLDivElement | null> | null>(null);\n\n/**\n * Props for the LocaleProvider component. The helper props, the other\n * attributes and the `ref` land on its `<div>`.\n */\nexport interface LocaleProviderProps\n extends\n Omit<React.HTMLAttributes<HTMLDivElement>, 'dir' | 'lang' | 'color'>,\n Omit<BulmaClassesProps, 'color' | 'backgroundColor'> {\n /**\n * A BCP 47 language tag, such as `\"de\"` or `\"ar-EG\"`. It becomes the\n * wrapper's `lang`, the default `locale` of the date and time pickers\n * inside, and, with no `dir`, the direction, from the way its script is\n * written. Unset, a nested provider keeps the outer one's. A tag `Intl`\n * refuses is ignored, with a warning in development.\n */\n locale?: string;\n /**\n * The direction, whatever the locale. Unset, it follows this provider's\n * `locale`, else an outer provider's direction, else the page's.\n */\n dir?: TextDirection;\n /** The part of the app it covers. */\n children?: React.ReactNode;\n}\n\n/** A tag in canonical form, or `undefined`, warning about one `Intl` refuses. */\nfunction validLocale(tag: string | undefined): string | undefined {\n try {\n return canonicalLocale(tag);\n } catch {\n warnOnce(\n `locale-provider-invalid:${tag}`,\n `[bestax-bulma] <LocaleProvider locale=\"${tag}\">: not a language ` +\n 'tag Intl accepts, so it is ignored. Use a BCP 47 tag such as ' +\n '\"en-US\", with a hyphen rather than an underscore.'\n );\n return undefined;\n }\n}\n\n/**\n * Sets the language and writing direction for the part of the app inside it, on a wrapper `<div>` and on what the library portals out of it.\n *\n * The wrapper carries `dir` and `lang`, so CSS and the components that read\n * the computed direction follow it, as they follow `<html dir>`. A `locale`\n * alone sets the direction of its script, and a `dir` sets the direction\n * whatever the locale. A nested provider keeps whatever it leaves unset.\n *\n * Inside it, the date and time pickers take its `locale` unless given their\n * own, and their week starts on the first day of the locale they use unless\n * `firstDayOfWeek` is set, and a picker with a `locale` of its own is read\n * like a nested provider. With a `locale`, the numbers the library writes,\n * such as page numbers and badge counts, follow it, and so does the\n * filtering in `Combobox`, `Autocomplete`, `Taginput` and `Command`, which then ignores\n * accents where the language treats letters as the same. The library's\n * portaled content, such as a `Popover` panel, a `Modal` with `portal`, a\n * `Toast` or a `Portal`'s children, keeps the same `dir` and `lang` though\n * it renders outside the wrapper: in a container outside the wrapper, it\n * sits in a `display: contents` element that carries them. Without a\n * provider none of this changes.\n *\n * @function\n * @param {LocaleProviderProps} props - Props for the LocaleProvider.\n * @returns {JSX.Element} The wrapper, inside the context.\n *\n * @example\n * <LocaleProvider locale=\"ar-EG\">\n * <App />\n * </LocaleProvider>\n */\nexport const LocaleProvider = forwardRef<HTMLDivElement, LocaleProviderProps>(\n (\n { locale: localeProp, dir: dirProp, className, children, ...props },\n ref\n ) => {\n const outer = useContext(LocaleContext);\n const ownLocale = useMemo(() => validLocale(localeProp), [localeProp]);\n const ownLocaleDir = useMemo(\n () => (ownLocale ? localeDirection(ownLocale) : undefined),\n [ownLocale]\n );\n const locale = ownLocale || outer?.locale;\n const dir = dirProp || ownLocaleDir || outer?.dir;\n const value = useMemo(() => ({ locale, dir }), [locale, dir]);\n\n const { bulmaHelperClasses, rest } = useBulmaClasses(props);\n\n // The wrapper, for portaled content to tell whether its target is inside\n // it, and the caller's ref. A callback ref's cleanup is held and run on\n // detach, as Toast and Modal do, since React 18 discards the return value.\n const elementRef = useRef<HTMLDivElement | null>(null);\n const consumerCleanupRef = useRef<(() => void) | null>(null);\n const setElement = useCallback(\n (node: HTMLDivElement | null) => {\n elementRef.current = node;\n if (typeof ref === 'function') {\n if (node === null) {\n const consumerCleanup = consumerCleanupRef.current;\n consumerCleanupRef.current = null;\n if (consumerCleanup) consumerCleanup();\n else ref(null);\n return;\n }\n const cleanup: unknown = ref(node);\n consumerCleanupRef.current =\n typeof cleanup === 'function' ? (cleanup as () => void) : null;\n } else if (ref) {\n ref.current = node;\n }\n },\n [ref]\n );\n\n return (\n <LocaleContext.Provider value={value}>\n <LocaleElementContext.Provider value={elementRef}>\n <div\n ref={setElement}\n className={classNames(bulmaHelperClasses, className) || undefined}\n {...rest}\n dir={dir}\n lang={locale}\n >\n {children}\n </div>\n </LocaleElementContext.Provider>\n </LocaleContext.Provider>\n );\n }\n);\n\nLocaleProvider.displayName = 'LocaleProvider';\n\n/** What `useLocale` returns outside a provider, the same object each time. */\nconst noLocale: LocaleContextValue = {};\n\n/**\n * The locale and direction the nearest `LocaleProvider` sets, for an app's own `Intl` calls or its own portaled content; both are `undefined` outside one.\n *\n * It isn't the page's direction: with no provider, or one that sets no\n * direction, the page's `<html dir>` still decides. To act on the direction\n * an element is laid out in, read `getComputedStyle(element).direction`, as\n * the library's components do.\n *\n * @function\n * @returns {LocaleContextValue} The nearest provider's `locale` and `dir`.\n *\n * @example\n * const { locale } = useLocale();\n * const price = new Intl.NumberFormat(locale, {\n * style: 'currency',\n * currency: 'EUR',\n * }).format(12.5);\n */\nexport function useLocale(): LocaleContextValue {\n return useContext(LocaleContext) ?? noLocale;\n}\n\n/**\n * INTERNAL, deliberately not exported from `src/index.ts`. The nearest\n * provider's settings, or `null` outside any provider, so a caller can tell\n * \"no provider\" from \"a provider that sets nothing\".\n */\nexport function useLocaleScope(): LocaleContextValue | null {\n return useContext(LocaleContext);\n}\n\nconst displayContents: React.CSSProperties = { display: 'contents' };\n\n/** The `dir` and `lang` portaled content carries: a provider's, or a picker's own. */\nexport interface PortalLocale {\n /** The `lang` to carry, unset for none. */\n locale?: string;\n /** The `dir` to carry, unset for none. */\n dir?: string;\n}\n\n/**\n * INTERNAL, deliberately not exported from `src/index.ts`. Keeps the `dir`\n * and `lang` of the nearest provider, or the `scope` given in their place,\n * on content the library portals into `target`, which has left the\n * provider's wrapper behind.\n *\n * A target inside the provider's own wrapper gets the content as it is: it\n * inherits from the wrapper, and follows it when the provider changes.\n * Content that must be its container's direct child, a `<tr>` portaled into\n * a `<tbody>` there, stays one. Any other target, `document.body` included,\n * gets the content inside a `display: contents` element carrying the\n * values, which adds no box. So does content with a `scope` of its own, a\n * picker's panel, since the provider's values aren't its own.\n *\n * Which of the two is decided once, when the content mounts, and kept while\n * it stays: switching between them would put a different element in its\n * place and remount everything inside, losing typed text, focus and timers.\n * A change of locale or direction updates the wrapper's attributes instead.\n * A target is never left bare because something else, such as `<html>`,\n * happens to carry the same values, since that can stop being true without\n * the content remounting. Outside a provider it renders the children alone,\n * so nothing changes there.\n */\nexport function LocalePortalScope({\n children,\n target,\n scope,\n}: {\n children: React.ReactNode;\n /** The element the content is portaled into. */\n target: Element;\n /** Values to carry in place of the provider's, such as a picker's own. */\n scope?: PortalLocale;\n}) {\n const provider = useContext(LocaleContext);\n const element = useContext(LocaleElementContext);\n const [wrap] = useState(\n () => scope !== undefined || !element?.current?.contains(target)\n );\n if (!provider || !wrap) return <>{children}</>;\n const values = scope ?? provider;\n return (\n <div dir={values.dir} lang={values.locale} style={displayContents}>\n {children}\n </div>\n );\n}\n"],"names":["_jsx","_Fragment"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useLocaleScope } from './LocaleProvider.js';
|
|
4
|
+
|
|
5
|
+
// INTERNAL: deliberately not exported from src/index.ts. Number formatting
|
|
6
|
+
// and text matching in a LocaleProvider's locale. Both return `null` outside
|
|
7
|
+
// a provider that gives a locale, so each caller keeps the formatting and the
|
|
8
|
+
// matching it has always had there.
|
|
9
|
+
/** A combining mark: an accent, a vowel sign or a point written on a letter. */
|
|
10
|
+
const MARK = /\p{M}/u;
|
|
11
|
+
/**
|
|
12
|
+
* The locale a component formats and matches in: inside a `LocaleProvider`,
|
|
13
|
+
* the component's own locale when it has one (a picker's `locale`), else the
|
|
14
|
+
* provider's; outside a provider, none.
|
|
15
|
+
*/
|
|
16
|
+
function useScopedLocale(own) {
|
|
17
|
+
const scope = useLocaleScope();
|
|
18
|
+
if (!scope)
|
|
19
|
+
return undefined;
|
|
20
|
+
return scope.locale;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A formatter for numbers shown on screen, in the locale of the
|
|
24
|
+
* `LocaleProvider` around the component, or `null` outside one that gives a
|
|
25
|
+
* locale. Formatters are kept per set of options, since building one is the
|
|
26
|
+
* expensive part.
|
|
27
|
+
*
|
|
28
|
+
* @param own - The component's own locale, such as a picker's `locale`,
|
|
29
|
+
* which wins inside a provider.
|
|
30
|
+
*/
|
|
31
|
+
function useNumberFormat(own) {
|
|
32
|
+
const locale = useScopedLocale();
|
|
33
|
+
return useMemo(() => {
|
|
34
|
+
if (!locale)
|
|
35
|
+
return null;
|
|
36
|
+
const formats = new Map();
|
|
37
|
+
return (value, options) => {
|
|
38
|
+
const key = options ? JSON.stringify(options) : '';
|
|
39
|
+
let format = formats.get(key);
|
|
40
|
+
if (!format) {
|
|
41
|
+
format = new Intl.NumberFormat(locale, options);
|
|
42
|
+
formats.set(key, format);
|
|
43
|
+
}
|
|
44
|
+
return format.format(value);
|
|
45
|
+
};
|
|
46
|
+
}, [locale]);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A matcher for filtering a list by what was typed, in the locale of the
|
|
50
|
+
* `LocaleProvider` around the component, or `null` outside one that gives a
|
|
51
|
+
* locale. It compares with `Intl.Collator` at `usage: 'search'` and
|
|
52
|
+
* `sensitivity: 'base'`, so it ignores case and accents where the language
|
|
53
|
+
* treats the letters as the same: "cafe" finds "Café", while in Swedish "a"
|
|
54
|
+
* doesn't find "å", a letter of its own there. Marks the language ignores
|
|
55
|
+
* count for nothing on either side, so in Arabic محمد finds مُحَمَّد and the
|
|
56
|
+
* other way round. An empty query, or one of nothing but such marks,
|
|
57
|
+
* matches everything.
|
|
58
|
+
*/
|
|
59
|
+
function useLocaleMatcher() {
|
|
60
|
+
const locale = useScopedLocale();
|
|
61
|
+
return useMemo(() => {
|
|
62
|
+
if (!locale)
|
|
63
|
+
return null;
|
|
64
|
+
const collator = new Intl.Collator(locale, {
|
|
65
|
+
usage: 'search',
|
|
66
|
+
sensitivity: 'base',
|
|
67
|
+
});
|
|
68
|
+
return (text, query) => {
|
|
69
|
+
const needle = query.normalize('NFC');
|
|
70
|
+
if (collator.compare(needle, '') === 0)
|
|
71
|
+
return true;
|
|
72
|
+
// Compared by code point, so a slice never splits a surrogate pair.
|
|
73
|
+
const chars = Array.from(text.normalize('NFC'));
|
|
74
|
+
const marks = chars.map(char => MARK.test(char));
|
|
75
|
+
// A slice equal to the needle can be longer or shorter than it: the
|
|
76
|
+
// marks the collator ignores, Arabic harakat or Hebrew niqqud, take
|
|
77
|
+
// code points on one side and not the other, and the expansions the
|
|
78
|
+
// collator makes, "ß" against "ss" or "æ" against "ae", turn one letter
|
|
79
|
+
// into two. So each slice runs from a letter to just before the next
|
|
80
|
+
// letter, never cutting a letter from its marks, and the slices from
|
|
81
|
+
// one start grow until they hold twice the needle's letters, which
|
|
82
|
+
// those expansions can't get past. That keeps the work proportional to
|
|
83
|
+
// the text's length, however long a label is, rather than to its
|
|
84
|
+
// square.
|
|
85
|
+
const most = 2 * Array.from(needle).filter(char => !MARK.test(char)).length;
|
|
86
|
+
for (let start = 0; start < chars.length; start++) {
|
|
87
|
+
if (marks[start])
|
|
88
|
+
continue;
|
|
89
|
+
let letters = 0;
|
|
90
|
+
for (let end = start + 1; end <= chars.length; end++) {
|
|
91
|
+
if (!marks[end - 1])
|
|
92
|
+
letters++;
|
|
93
|
+
if (letters > most)
|
|
94
|
+
break;
|
|
95
|
+
if (marks[end])
|
|
96
|
+
continue;
|
|
97
|
+
if (collator.compare(chars.slice(start, end).join(''), needle) === 0) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
};
|
|
104
|
+
}, [locale]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { useLocaleMatcher, useNumberFormat };
|
|
108
|
+
//# sourceMappingURL=localeFormat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localeFormat.js","sources":["../../../src/helpers/localeFormat.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts. Number formatting\n// and text matching in a LocaleProvider's locale. Both return `null` outside\n// a provider that gives a locale, so each caller keeps the formatting and the\n// matching it has always had there.\n\nimport { useMemo } from 'react';\nimport { useLocaleScope } from './LocaleProvider';\n\n/** A combining mark: an accent, a vowel sign or a point written on a letter. */\nconst MARK = /\\p{M}/u;\n\n/** Formats a number for display, with `Intl.NumberFormat` options. */\nexport type NumberFormatter = (\n value: number,\n options?: Intl.NumberFormatOptions\n) => string;\n\n/** Whether `text` contains `query`, as the locale compares letters. */\nexport type TextMatcher = (text: string, query: string) => boolean;\n\n/**\n * The locale a component formats and matches in: inside a `LocaleProvider`,\n * the component's own locale when it has one (a picker's `locale`), else the\n * provider's; outside a provider, none.\n */\nfunction useScopedLocale(own: string | undefined): string | undefined {\n const scope = useLocaleScope();\n if (!scope) return undefined;\n return own || scope.locale;\n}\n\n/**\n * A formatter for numbers shown on screen, in the locale of the\n * `LocaleProvider` around the component, or `null` outside one that gives a\n * locale. Formatters are kept per set of options, since building one is the\n * expensive part.\n *\n * @param own - The component's own locale, such as a picker's `locale`,\n * which wins inside a provider.\n */\nexport function useNumberFormat(own?: string): NumberFormatter | null {\n const locale = useScopedLocale(own);\n return useMemo(() => {\n if (!locale) return null;\n const formats = new Map<string, Intl.NumberFormat>();\n return (value, options) => {\n const key = options ? JSON.stringify(options) : '';\n let format = formats.get(key);\n if (!format) {\n format = new Intl.NumberFormat(locale, options);\n formats.set(key, format);\n }\n return format.format(value);\n };\n }, [locale]);\n}\n\n/**\n * A matcher for filtering a list by what was typed, in the locale of the\n * `LocaleProvider` around the component, or `null` outside one that gives a\n * locale. It compares with `Intl.Collator` at `usage: 'search'` and\n * `sensitivity: 'base'`, so it ignores case and accents where the language\n * treats the letters as the same: \"cafe\" finds \"Café\", while in Swedish \"a\"\n * doesn't find \"å\", a letter of its own there. Marks the language ignores\n * count for nothing on either side, so in Arabic محمد finds مُحَمَّد and the\n * other way round. An empty query, or one of nothing but such marks,\n * matches everything.\n */\nexport function useLocaleMatcher(): TextMatcher | null {\n const locale = useScopedLocale(undefined);\n return useMemo(() => {\n if (!locale) return null;\n const collator = new Intl.Collator(locale, {\n usage: 'search',\n sensitivity: 'base',\n });\n return (text, query) => {\n const needle = query.normalize('NFC');\n if (collator.compare(needle, '') === 0) return true;\n // Compared by code point, so a slice never splits a surrogate pair.\n const chars = Array.from(text.normalize('NFC'));\n const marks = chars.map(char => MARK.test(char));\n // A slice equal to the needle can be longer or shorter than it: the\n // marks the collator ignores, Arabic harakat or Hebrew niqqud, take\n // code points on one side and not the other, and the expansions the\n // collator makes, \"ß\" against \"ss\" or \"æ\" against \"ae\", turn one letter\n // into two. So each slice runs from a letter to just before the next\n // letter, never cutting a letter from its marks, and the slices from\n // one start grow until they hold twice the needle's letters, which\n // those expansions can't get past. That keeps the work proportional to\n // the text's length, however long a label is, rather than to its\n // square.\n const most =\n 2 * Array.from(needle).filter(char => !MARK.test(char)).length;\n for (let start = 0; start < chars.length; start++) {\n if (marks[start]) continue;\n let letters = 0;\n for (let end = start + 1; end <= chars.length; end++) {\n if (!marks[end - 1]) letters++;\n if (letters > most) break;\n if (marks[end]) continue;\n if (\n collator.compare(chars.slice(start, end).join(''), needle) === 0\n ) {\n return true;\n }\n }\n }\n return false;\n };\n }, [locale]);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
// INTERNAL: deliberately not exported from src/index.ts. What a locale says
|
|
3
|
+
// about its direction, read from `Intl.Locale` where the engine has the data,
|
|
4
|
+
// and about its week, read from a copy of CLDR's week data.
|
|
5
|
+
/**
|
|
6
|
+
* Scripts written right to left: every script for which ICU's text info
|
|
7
|
+
* says `rtl`, as Node reported it, and `Aran`, the Nastaliq form of Arabic
|
|
8
|
+
* that WebKit fills in for Urdu. Read only where the engine has no text info
|
|
9
|
+
* of its own, which includes Firefox before 153.
|
|
10
|
+
*/
|
|
11
|
+
const RTL_SCRIPTS = ' Adlm Arab Aran Armi Avst Chrs Cprt Elym Gara Hatr Hebr Hung Khar Lydi ' +
|
|
12
|
+
'Mand Mani Mend Merc Mero Narb Nbat Nkoo Orkh Ougr Palm Phli Phlp Phnx ' +
|
|
13
|
+
'Prti Rohg Samr Sarb Sidt Sogd Sogo Syrc Thaa Yezi ';
|
|
14
|
+
/**
|
|
15
|
+
* Widely used languages whose usual script reads right to left, for an engine
|
|
16
|
+
* that can't fill in a locale's script with `maximize()`.
|
|
17
|
+
*/
|
|
18
|
+
const RTL_LANGUAGES = ' ar ckb dv fa he iw ji ks mzn nqo pnb ps sd syr ug ur yi ';
|
|
19
|
+
/**
|
|
20
|
+
* The regions whose week doesn't start on Monday, by the day it starts on
|
|
21
|
+
* (0 = Sunday, 5 = Friday, 6 = Saturday), from CLDR 48's week data as Node
|
|
22
|
+
* reported it. The library reads this rather than `Intl.Locale`'s week info
|
|
23
|
+
* because engines disagree: Firefox before 153 has none, and WebKit's differs
|
|
24
|
+
* from Node's, Chromium's and current Firefox's for China, Algeria, Jordan and
|
|
25
|
+
* Iceland among others, so an inline calendar rendered on the server would
|
|
26
|
+
* fail to hydrate there. With one table, the server and every browser agree.
|
|
27
|
+
*/
|
|
28
|
+
const WEEK_STARTS = [
|
|
29
|
+
[
|
|
30
|
+
0,
|
|
31
|
+
' AG AS BD BR BS BT BU BW BZ CA CO DM DO ET GT GU HK HN ID IL IN IS JM JP ' +
|
|
32
|
+
'KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY RH SA SG SV TH ' +
|
|
33
|
+
'TT TW UM US VE VI WS YD YE ZA ZW ',
|
|
34
|
+
],
|
|
35
|
+
[5, ' MV '],
|
|
36
|
+
[6, ' AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY '],
|
|
37
|
+
];
|
|
38
|
+
/** Whether a space-separated list, padded with spaces, holds `code`. */
|
|
39
|
+
const listed = (list, code) => list.includes(` ${code} `);
|
|
40
|
+
/** The `-u-fw-` keyword's values, by the day they name. */
|
|
41
|
+
const FW_DAYS = {
|
|
42
|
+
sun: 0,
|
|
43
|
+
mon: 1,
|
|
44
|
+
tue: 2,
|
|
45
|
+
wed: 3,
|
|
46
|
+
thu: 4,
|
|
47
|
+
fri: 5,
|
|
48
|
+
sat: 6,
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The canonical form of a BCP 47 tag (`"en-us"` becomes `"en-US"`), or
|
|
52
|
+
* `undefined` for an empty or missing one. Throws a `RangeError` for a tag
|
|
53
|
+
* `Intl` refuses, as `Intl.DateTimeFormat` would.
|
|
54
|
+
*/
|
|
55
|
+
function canonicalLocale(tag) {
|
|
56
|
+
if (!tag)
|
|
57
|
+
return undefined;
|
|
58
|
+
return Intl.getCanonicalLocales(tag)[0];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The direction a locale's script is written in: from `Intl.Locale`'s text
|
|
62
|
+
* info, else from the script `maximize()` fills in, else from the language.
|
|
63
|
+
*
|
|
64
|
+
* @param locale - A canonical tag, as `canonicalLocale` returns.
|
|
65
|
+
*/
|
|
66
|
+
function localeDirection(locale) {
|
|
67
|
+
const intlLocale = new Intl.Locale(locale);
|
|
68
|
+
const info = intlLocale;
|
|
69
|
+
const direction = typeof info.getTextInfo === 'function'
|
|
70
|
+
? info.getTextInfo().direction
|
|
71
|
+
: info.textInfo?.direction;
|
|
72
|
+
if (direction === 'rtl' || direction === 'ltr')
|
|
73
|
+
return direction;
|
|
74
|
+
if (typeof intlLocale.maximize === 'function') {
|
|
75
|
+
const script = intlLocale.maximize().script;
|
|
76
|
+
return script && listed(RTL_SCRIPTS, script) ? 'rtl' : 'ltr';
|
|
77
|
+
}
|
|
78
|
+
return listed(RTL_LANGUAGES, intlLocale.language) ? 'rtl' : 'ltr';
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The day a locale's week starts on, 0 for Sunday, as `getWeekInfo()` reads
|
|
82
|
+
* it: the tag's `-u-fw-` keyword, else the region its `-u-rg-` keyword names,
|
|
83
|
+
* else its own region or the one `maximize()` fills in, looked up in CLDR's
|
|
84
|
+
* week data, where a region not listed starts on Monday.
|
|
85
|
+
*
|
|
86
|
+
* @param locale - A canonical tag, as `canonicalLocale` returns.
|
|
87
|
+
*/
|
|
88
|
+
function localeFirstDay(locale) {
|
|
89
|
+
const fw = /-u(?:-[a-z0-9]{2,8})*?-fw-([a-z]{3})(?=-|$)/i.exec(locale);
|
|
90
|
+
if (fw && fw[1].toLowerCase() in FW_DAYS)
|
|
91
|
+
return FW_DAYS[fw[1].toLowerCase()];
|
|
92
|
+
const rg = /-u(?:-[a-z0-9]{2,8})*?-rg-([a-z]{2})[a-z0-9]{1,4}(?=-|$)/i.exec(locale);
|
|
93
|
+
const intlLocale = new Intl.Locale(locale);
|
|
94
|
+
const region = rg
|
|
95
|
+
? rg[1].toUpperCase()
|
|
96
|
+
: typeof intlLocale.maximize === 'function'
|
|
97
|
+
? intlLocale.maximize().region
|
|
98
|
+
: intlLocale.region;
|
|
99
|
+
if (region) {
|
|
100
|
+
for (const [day, regions] of WEEK_STARTS) {
|
|
101
|
+
if (listed(regions, region))
|
|
102
|
+
return day;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { canonicalLocale, localeDirection, localeFirstDay };
|
|
109
|
+
//# sourceMappingURL=localeInfo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localeInfo.js","sources":["../../../src/helpers/localeInfo.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts. What a locale says\n// about its direction, read from `Intl.Locale` where the engine has the data,\n// and about its week, read from a copy of CLDR's week data.\n\nimport type { DayOfWeek } from '../form/_pickerInternals/pickerTypes';\n\n/** The parts of `Intl.Locale` newer than the ES2020 library types. */\ninterface LocaleInfo {\n getTextInfo?: () => { direction?: string };\n textInfo?: { direction?: string };\n}\n\n/**\n * Scripts written right to left: every script for which ICU's text info\n * says `rtl`, as Node reported it, and `Aran`, the Nastaliq form of Arabic\n * that WebKit fills in for Urdu. Read only where the engine has no text info\n * of its own, which includes Firefox before 153.\n */\nconst RTL_SCRIPTS =\n ' Adlm Arab Aran Armi Avst Chrs Cprt Elym Gara Hatr Hebr Hung Khar Lydi ' +\n 'Mand Mani Mend Merc Mero Narb Nbat Nkoo Orkh Ougr Palm Phli Phlp Phnx ' +\n 'Prti Rohg Samr Sarb Sidt Sogd Sogo Syrc Thaa Yezi ';\n\n/**\n * Widely used languages whose usual script reads right to left, for an engine\n * that can't fill in a locale's script with `maximize()`.\n */\nconst RTL_LANGUAGES =\n ' ar ckb dv fa he iw ji ks mzn nqo pnb ps sd syr ug ur yi ';\n\n/**\n * The regions whose week doesn't start on Monday, by the day it starts on\n * (0 = Sunday, 5 = Friday, 6 = Saturday), from CLDR 48's week data as Node\n * reported it. The library reads this rather than `Intl.Locale`'s week info\n * because engines disagree: Firefox before 153 has none, and WebKit's differs\n * from Node's, Chromium's and current Firefox's for China, Algeria, Jordan and\n * Iceland among others, so an inline calendar rendered on the server would\n * fail to hydrate there. With one table, the server and every browser agree.\n */\nconst WEEK_STARTS: ReadonlyArray<readonly [DayOfWeek, string]> = [\n [\n 0,\n ' AG AS BD BR BS BT BU BW BZ CA CO DM DO ET GT GU HK HN ID IL IN IS JM JP ' +\n 'KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY RH SA SG SV TH ' +\n 'TT TW UM US VE VI WS YD YE ZA ZW ',\n ],\n [5, ' MV '],\n [6, ' AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY '],\n];\n\n/** Whether a space-separated list, padded with spaces, holds `code`. */\nconst listed = (list: string, code: string) => list.includes(` ${code} `);\n\n/** The `-u-fw-` keyword's values, by the day they name. */\nconst FW_DAYS: Record<string, DayOfWeek> = {\n sun: 0,\n mon: 1,\n tue: 2,\n wed: 3,\n thu: 4,\n fri: 5,\n sat: 6,\n};\n\n/**\n * The canonical form of a BCP 47 tag (`\"en-us\"` becomes `\"en-US\"`), or\n * `undefined` for an empty or missing one. Throws a `RangeError` for a tag\n * `Intl` refuses, as `Intl.DateTimeFormat` would.\n */\nexport function canonicalLocale(tag: string | undefined): string | undefined {\n if (!tag) return undefined;\n return Intl.getCanonicalLocales(tag)[0];\n}\n\n/**\n * The direction a locale's script is written in: from `Intl.Locale`'s text\n * info, else from the script `maximize()` fills in, else from the language.\n *\n * @param locale - A canonical tag, as `canonicalLocale` returns.\n */\nexport function localeDirection(locale: string): 'ltr' | 'rtl' {\n const intlLocale = new Intl.Locale(locale);\n const info = intlLocale as unknown as LocaleInfo;\n const direction =\n typeof info.getTextInfo === 'function'\n ? info.getTextInfo().direction\n : info.textInfo?.direction;\n if (direction === 'rtl' || direction === 'ltr') return direction;\n if (typeof intlLocale.maximize === 'function') {\n const script = intlLocale.maximize().script;\n return script && listed(RTL_SCRIPTS, script) ? 'rtl' : 'ltr';\n }\n return listed(RTL_LANGUAGES, intlLocale.language) ? 'rtl' : 'ltr';\n}\n\n/**\n * The day a locale's week starts on, 0 for Sunday, as `getWeekInfo()` reads\n * it: the tag's `-u-fw-` keyword, else the region its `-u-rg-` keyword names,\n * else its own region or the one `maximize()` fills in, looked up in CLDR's\n * week data, where a region not listed starts on Monday.\n *\n * @param locale - A canonical tag, as `canonicalLocale` returns.\n */\nexport function localeFirstDay(locale: string): DayOfWeek {\n const fw = /-u(?:-[a-z0-9]{2,8})*?-fw-([a-z]{3})(?=-|$)/i.exec(locale);\n if (fw && fw[1].toLowerCase() in FW_DAYS) return FW_DAYS[fw[1].toLowerCase()];\n const rg = /-u(?:-[a-z0-9]{2,8})*?-rg-([a-z]{2})[a-z0-9]{1,4}(?=-|$)/i.exec(\n locale\n );\n const intlLocale = new Intl.Locale(locale);\n const region = rg\n ? rg[1].toUpperCase()\n : typeof intlLocale.maximize === 'function'\n ? intlLocale.maximize().region\n : intlLocale.region;\n if (region) {\n for (const [day, regions] of WEEK_STARTS) {\n if (listed(regions, region)) return day;\n }\n }\n return 1;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { jsx, Fragment } from 'react/jsx-runtime';
|
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { useIsHydrated } from './useIsHydrated.js';
|
|
5
5
|
import { useConfig } from './Config.js';
|
|
6
|
+
import { LocalePortalScope } from './LocaleProvider.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Resolves a portal target: an `HTMLElement` is used directly, a non-empty
|
|
@@ -65,6 +66,17 @@ function usePortalContainer() {
|
|
|
65
66
|
* also reaches what the library's components portal, such as Popover's
|
|
66
67
|
* `appendToBody` panel or a `Modal` with `portal`.
|
|
67
68
|
*
|
|
69
|
+
* Inside a `LocaleProvider`, the children keep its `dir` and `lang`. A
|
|
70
|
+
* container inside the provider's own wrapper takes the children as they
|
|
71
|
+
* are, and they follow the provider through the page. Any other,
|
|
72
|
+
* `document.body` included, gets them inside one `<div>` carrying the
|
|
73
|
+
* provider's values, with `display: contents` so it adds no box. It is
|
|
74
|
+
* still an element, so content that must be the container's direct child,
|
|
75
|
+
* a `<tr>` in a `<tbody>`, or that a direct-child selector styles, belongs
|
|
76
|
+
* in a container inside the provider. Which of the two is settled when the
|
|
77
|
+
* children mount, so a change of locale updates them in place rather than
|
|
78
|
+
* remounting them.
|
|
79
|
+
*
|
|
68
80
|
* Tab order and focus follow the DOM too. Content portaled from inside a
|
|
69
81
|
* focus trap is no longer inside the trapped element, so the trap neither
|
|
70
82
|
* reaches it nor holds focus there: render nested overlays inside the
|
|
@@ -94,7 +106,8 @@ function Portal({ children, container, disabled = false }) {
|
|
|
94
106
|
return jsx(Fragment, { children: children });
|
|
95
107
|
if (!hydrated)
|
|
96
108
|
return null;
|
|
97
|
-
|
|
109
|
+
const target = resolveContainer(container);
|
|
110
|
+
return createPortal(jsx(LocalePortalScope, { target: target, children: children }), target);
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
export { Portal, resolvePortalContainer, usePortalContainer };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portal.js","sources":["../../../src/helpers/portal.tsx"],"sourcesContent":["import React from 'react';\nimport { createPortal } from 'react-dom';\nimport { useIsHydrated } from './useIsHydrated';\nimport { useConfig } from './Config';\n\n/**\n * Resolves a portal target: an `HTMLElement` is used directly, a non-empty\n * `string` is treated as a `document.querySelector` selector (falling back to\n * `document.body` when it matches nothing), and any falsy value — `undefined`\n * or the empty string — resolves to `document.body`.\n *\n * The empty string is deliberately treated as \"no target\" rather than passed\n * through: `document.querySelector('')` throws a `SyntaxError`, and callers\n * building a selector from state can easily hand us `''`.\n *\n * @function resolvePortalContainer\n * @param container - The requested portal target, if any.\n * @returns The resolved DOM node to portal into.\n */\nexport function resolvePortalContainer(\n container?: string | HTMLElement\n): HTMLElement {\n if (!container) {\n return document.body;\n }\n if (typeof container === 'string') {\n return (\n (document.querySelector(container) as HTMLElement | null) ?? document.body\n );\n }\n return container;\n}\n\n/**\n * Where a component's portal goes, as a function to call when the portal is\n * created: the target the component was given, else the `portalContainer` a\n * `ConfigProvider` sets, else `document.body`, each read as\n * `resolvePortalContainer` reads it. A component that portals resolves its\n * target through this, so the provider's default reaches it.\n *\n * The hook only reads context, so it is safe to call on the server, ahead of\n * any early return. The function it returns reads the DOM, so call it only\n * where the portal is created, which the server never reaches.\n *\n * @function usePortalContainer\n * @returns A function that takes the component's own target, if any, and\n * returns the DOM node to portal into.\n */\nexport function usePortalContainer(): (\n container?: string | HTMLElement\n) => HTMLElement {\n const { portalContainer } = useConfig();\n return container => resolvePortalContainer(container || portalContainer);\n}\n\n/**\n * Props for the Portal component.\n */\nexport interface PortalProps {\n /** Content to render into the container. */\n children?: React.ReactNode;\n /**\n * Where the content goes: an element, or a `document.querySelector`\n * selector. Omitted or empty, it goes to the `portalContainer` a\n * `ConfigProvider` sets, and to `document.body` when none is set. A\n * selector that matches nothing means `document.body` too. A selector is\n * looked up again on each render, so the\n * content follows the target when it changes. Moving to another target\n * remounts the children and resets their state, which includes a selector\n * that only starts matching after the content has shown in `document.body`.\n * Render the target first, or pass the element once you have it.\n */\n container?: string | HTMLElement;\n /**\n * Render the children in place instead, on the server as well as the\n * client. Lets a component that portals only sometimes keep a single code\n * path.\n * @defaultValue false\n */\n disabled?: boolean;\n}\n\n/**\n * Renders its children into another part of the page, `document.body` unless `container` or a `ConfigProvider` says otherwise, so floating content escapes an ancestor's `overflow`, `transform` or stacking context.\n *\n * React's server renderer can't render a portal, so `Portal` renders nothing\n * on the server and during hydration, and mounts its children in the commit\n * that follows. The first client render then matches the server markup. In an app\n * without server rendering it portals on the first render.\n *\n * React context, such as `ConfigProvider`'s class prefix, reaches the\n * portaled children, and their React events bubble through the React tree.\n * CSS inheritance follows the DOM instead: a scoped (non-`isRoot`) `Theme`\n * sets its variables on a wrapper element the portaled content has left, so\n * that content sees the page-level theme, and styles scoped to a shadow root\n * don't reach `document.body` at all. To keep them, portal into an element\n * inside the themed area or the shadow root: pass it as `container`, or as\n * the `portalContainer` of a `ConfigProvider` around that area. The second\n * also reaches what the library's components portal, such as Popover's\n * `appendToBody` panel or a `Modal` with `portal`.\n *\n * Tab order and focus follow the DOM too. Content portaled from inside a\n * focus trap is no longer inside the trapped element, so the trap neither\n * reaches it nor holds focus there: render nested overlays inside the\n * trapped element, or give them a trap of their own. To trap portaled\n * content, put `useFocusTrap`'s ref on the element inside the `Portal`.\n *\n * @function\n * @param {PortalProps} props - Props for the Portal component.\n * @returns The portal, the children in place when `disabled`, or nothing\n * before hydration.\n *\n * @example\n * <Portal>\n * <div className=\"box\">Rendered at the end of document.body</div>\n * </Portal>\n *\n * @example\n * // Into a dedicated overlay root\n * <Portal container=\"#overlays\">\n * <CommandPalette />\n * </Portal>\n */\nexport function Portal({ children, container, disabled = false }: PortalProps) {\n const hydrated = useIsHydrated();\n const resolveContainer = usePortalContainer();\n if (disabled) return <>{children}</>;\n if (!hydrated) return null;\n
|
|
1
|
+
{"version":3,"file":"portal.js","sources":["../../../src/helpers/portal.tsx"],"sourcesContent":["import React from 'react';\nimport { createPortal } from 'react-dom';\nimport { useIsHydrated } from './useIsHydrated';\nimport { useConfig } from './Config';\nimport { LocalePortalScope } from './LocaleProvider';\n\n/**\n * Resolves a portal target: an `HTMLElement` is used directly, a non-empty\n * `string` is treated as a `document.querySelector` selector (falling back to\n * `document.body` when it matches nothing), and any falsy value — `undefined`\n * or the empty string — resolves to `document.body`.\n *\n * The empty string is deliberately treated as \"no target\" rather than passed\n * through: `document.querySelector('')` throws a `SyntaxError`, and callers\n * building a selector from state can easily hand us `''`.\n *\n * @function resolvePortalContainer\n * @param container - The requested portal target, if any.\n * @returns The resolved DOM node to portal into.\n */\nexport function resolvePortalContainer(\n container?: string | HTMLElement\n): HTMLElement {\n if (!container) {\n return document.body;\n }\n if (typeof container === 'string') {\n return (\n (document.querySelector(container) as HTMLElement | null) ?? document.body\n );\n }\n return container;\n}\n\n/**\n * Where a component's portal goes, as a function to call when the portal is\n * created: the target the component was given, else the `portalContainer` a\n * `ConfigProvider` sets, else `document.body`, each read as\n * `resolvePortalContainer` reads it. A component that portals resolves its\n * target through this, so the provider's default reaches it.\n *\n * The hook only reads context, so it is safe to call on the server, ahead of\n * any early return. The function it returns reads the DOM, so call it only\n * where the portal is created, which the server never reaches.\n *\n * @function usePortalContainer\n * @returns A function that takes the component's own target, if any, and\n * returns the DOM node to portal into.\n */\nexport function usePortalContainer(): (\n container?: string | HTMLElement\n) => HTMLElement {\n const { portalContainer } = useConfig();\n return container => resolvePortalContainer(container || portalContainer);\n}\n\n/**\n * Props for the Portal component.\n */\nexport interface PortalProps {\n /** Content to render into the container. */\n children?: React.ReactNode;\n /**\n * Where the content goes: an element, or a `document.querySelector`\n * selector. Omitted or empty, it goes to the `portalContainer` a\n * `ConfigProvider` sets, and to `document.body` when none is set. A\n * selector that matches nothing means `document.body` too. A selector is\n * looked up again on each render, so the\n * content follows the target when it changes. Moving to another target\n * remounts the children and resets their state, which includes a selector\n * that only starts matching after the content has shown in `document.body`.\n * Render the target first, or pass the element once you have it.\n */\n container?: string | HTMLElement;\n /**\n * Render the children in place instead, on the server as well as the\n * client. Lets a component that portals only sometimes keep a single code\n * path.\n * @defaultValue false\n */\n disabled?: boolean;\n}\n\n/**\n * Renders its children into another part of the page, `document.body` unless `container` or a `ConfigProvider` says otherwise, so floating content escapes an ancestor's `overflow`, `transform` or stacking context.\n *\n * React's server renderer can't render a portal, so `Portal` renders nothing\n * on the server and during hydration, and mounts its children in the commit\n * that follows. The first client render then matches the server markup. In an app\n * without server rendering it portals on the first render.\n *\n * React context, such as `ConfigProvider`'s class prefix, reaches the\n * portaled children, and their React events bubble through the React tree.\n * CSS inheritance follows the DOM instead: a scoped (non-`isRoot`) `Theme`\n * sets its variables on a wrapper element the portaled content has left, so\n * that content sees the page-level theme, and styles scoped to a shadow root\n * don't reach `document.body` at all. To keep them, portal into an element\n * inside the themed area or the shadow root: pass it as `container`, or as\n * the `portalContainer` of a `ConfigProvider` around that area. The second\n * also reaches what the library's components portal, such as Popover's\n * `appendToBody` panel or a `Modal` with `portal`.\n *\n * Inside a `LocaleProvider`, the children keep its `dir` and `lang`. A\n * container inside the provider's own wrapper takes the children as they\n * are, and they follow the provider through the page. Any other,\n * `document.body` included, gets them inside one `<div>` carrying the\n * provider's values, with `display: contents` so it adds no box. It is\n * still an element, so content that must be the container's direct child,\n * a `<tr>` in a `<tbody>`, or that a direct-child selector styles, belongs\n * in a container inside the provider. Which of the two is settled when the\n * children mount, so a change of locale updates them in place rather than\n * remounting them.\n *\n * Tab order and focus follow the DOM too. Content portaled from inside a\n * focus trap is no longer inside the trapped element, so the trap neither\n * reaches it nor holds focus there: render nested overlays inside the\n * trapped element, or give them a trap of their own. To trap portaled\n * content, put `useFocusTrap`'s ref on the element inside the `Portal`.\n *\n * @function\n * @param {PortalProps} props - Props for the Portal component.\n * @returns The portal, the children in place when `disabled`, or nothing\n * before hydration.\n *\n * @example\n * <Portal>\n * <div className=\"box\">Rendered at the end of document.body</div>\n * </Portal>\n *\n * @example\n * // Into a dedicated overlay root\n * <Portal container=\"#overlays\">\n * <CommandPalette />\n * </Portal>\n */\nexport function Portal({ children, container, disabled = false }: PortalProps) {\n const hydrated = useIsHydrated();\n const resolveContainer = usePortalContainer();\n if (disabled) return <>{children}</>;\n if (!hydrated) return null;\n const target = resolveContainer(container);\n return createPortal(\n <LocalePortalScope target={target}>{children}</LocalePortalScope>,\n target\n );\n}\n"],"names":["_jsx","_Fragment"],"mappings":";;;;;;;AAMA;;;;;;;;;;;;;AAaG;AACG,SAAU,sBAAsB,CACpC,SAAgC,EAAA;IAEhC,IAAI,CAAC,SAAS,EAAE;QACd,OAAO,QAAQ,CAAC,IAAI;IACtB;AACA,IAAA,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE;AACjC,QAAA,QACG,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAwB,IAAI,QAAQ,CAAC,IAAI;IAE9E;AACA,IAAA,OAAO,SAAS;AAClB;AAEA;;;;;;;;;;;;;;AAcG;SACa,kBAAkB,GAAA;AAGhC,IAAA,MAAM,EAAE,eAAe,EAAE,GAAG,SAAS,EAAE;IACvC,OAAO,SAAS,IAAI,sBAAsB,CAAC,SAAS,IAAI,eAAe,CAAC;AAC1E;AA6BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDG;AACG,SAAU,MAAM,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,GAAG,KAAK,EAAe,EAAA;AAC3E,IAAA,MAAM,QAAQ,GAAG,aAAa,EAAE;AAChC,IAAA,MAAM,gBAAgB,GAAG,kBAAkB,EAAE;AAC7C,IAAA,IAAI,QAAQ;QAAE,OAAOA,GAAA,CAAAC,QAAA,EAAA,EAAA,QAAA,EAAG,QAAQ,EAAA,CAAI;AACpC,IAAA,IAAI,CAAC,QAAQ;AAAE,QAAA,OAAO,IAAI;AAC1B,IAAA,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC;AAC1C,IAAA,OAAO,YAAY,CACjBD,GAAA,CAAC,iBAAiB,IAAC,MAAM,EAAE,MAAM,EAAA,QAAA,EAAG,QAAQ,EAAA,CAAqB,EACjE,MAAM,CACP;AACH;;"}
|