@allxsmith/bestax-bulma 5.57.4 → 5.58.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 +2 -2
- package/dist/esm/components/Avatars.js.map +1 -1
- package/dist/esm/components/Badge.js +2 -2
- package/dist/esm/components/Badge.js.map +1 -1
- package/dist/esm/components/Pagination.js +2 -2
- package/dist/esm/components/Pagination.js.map +1 -1
- package/dist/esm/components/Steps.js +2 -2
- package/dist/esm/components/Steps.js.map +1 -1
- package/dist/esm/components/TreeView.js +7 -5
- package/dist/esm/components/TreeView.js.map +1 -1
- package/dist/esm/components/useDataTable.js +2 -2
- package/dist/esm/components/useDataTable.js.map +1 -1
- package/dist/esm/elements/NumberFormatter.js +42 -0
- package/dist/esm/elements/NumberFormatter.js.map +1 -0
- package/dist/esm/elements/RingProgress.js +2 -2
- package/dist/esm/elements/RingProgress.js.map +1 -1
- package/dist/esm/form/Dropzone.js +2 -2
- package/dist/esm/form/Dropzone.js.map +1 -1
- package/dist/esm/form/File.js +2 -2
- package/dist/esm/form/File.js.map +1 -1
- package/dist/esm/form/Rate.js +2 -2
- package/dist/esm/form/Rate.js.map +1 -1
- package/dist/esm/form/Slider.js +2 -2
- package/dist/esm/form/Slider.js.map +1 -1
- package/dist/esm/form/Taginput.js +2 -2
- package/dist/esm/form/Taginput.js.map +1 -1
- package/dist/esm/form/_colorInternals/pickerState.js +2 -2
- package/dist/esm/form/_colorInternals/pickerState.js.map +1 -1
- package/dist/esm/helpers/LocaleProvider.js +13 -6
- package/dist/esm/helpers/LocaleProvider.js.map +1 -1
- package/dist/esm/helpers/localeFormat.js +196 -36
- package/dist/esm/helpers/localeFormat.js.map +1 -1
- package/dist/esm/helpers/useTreeView.js +54 -20
- package/dist/esm/helpers/useTreeView.js.map +1 -1
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +312 -76
- 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/TreeView.d.ts +20 -11
- package/dist/types/elements/NumberFormatter.d.ts +62 -0
- package/dist/types/helpers/LocaleProvider.d.ts +7 -0
- package/dist/types/helpers/localeFormat.d.ts +30 -8
- package/dist/types/helpers/useTreeView.d.ts +12 -6
- package/dist/types/index.d.ts +3 -0
- package/dist/types-cjs/components/TreeView.d.ts +20 -11
- package/dist/types-cjs/elements/NumberFormatter.d.ts +62 -0
- package/dist/types-cjs/helpers/LocaleProvider.d.ts +7 -0
- package/dist/types-cjs/helpers/localeFormat.d.ts +30 -8
- package/dist/types-cjs/helpers/useTreeView.d.ts +12 -6
- 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/_field.scss +40 -0
- package/src/scss/form/_index.scss +1 -0
|
@@ -1,50 +1,210 @@
|
|
|
1
1
|
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
2
|
'use client';
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
import { useLocaleScope } from './LocaleProvider.js';
|
|
4
|
+
import { useLocaleScope, validLocale } from './LocaleProvider.js';
|
|
5
|
+
import { warnOnce } from './devWarnings.js';
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// Number formatting and text matching in a locale.
|
|
8
|
+
//
|
|
9
|
+
// `useNumberFormat` and its `NumberFormatFunction` type are public, exported
|
|
10
|
+
// from src/index.ts. The rest is INTERNAL, deliberately not exported.
|
|
11
|
+
//
|
|
12
|
+
// Which a component calls depends on its job. A component whose job is to
|
|
13
|
+
// write a number, such as `NumberFormatter`, uses `useNumberFormat`, which
|
|
14
|
+
// writes in the runtime's locale outside a `LocaleProvider`, as the pickers
|
|
15
|
+
// do. Any other component that shows a number in passing, such as a badge
|
|
16
|
+
// count, a page number or a step's number, uses `useScopedNumberFormat`,
|
|
17
|
+
// and filters a list through `useLocaleMatcher`. Those two return `null`
|
|
18
|
+
// outside a provider, and inside one that gives no locale, as a provider
|
|
19
|
+
// with only a `dir` doesn't, so the component keeps the formatting and the
|
|
20
|
+
// matching it has always had there, as `LocaleProvider` promises.
|
|
10
21
|
/** A combining mark: an accent, a vowel sign or a point written on a letter. */
|
|
11
22
|
const MARK = /\p{M}/u;
|
|
12
23
|
/**
|
|
13
|
-
* The
|
|
14
|
-
*
|
|
15
|
-
* provider's; outside a provider, none.
|
|
24
|
+
* The warning about a locale `Intl` refuses, given to `useNumberFormat`,
|
|
25
|
+
* and so to `NumberFormatter`, the only callers that pass one.
|
|
16
26
|
*/
|
|
17
|
-
|
|
27
|
+
const warnOwnLocale = (tag) => warnOnce(`number-format-invalid-locale:${tag}`, `[bestax-bulma] NumberFormatter or useNumberFormat was given the locale ` +
|
|
28
|
+
`"${tag}", which is not a language tag Intl accepts, so it is ` +
|
|
29
|
+
"ignored and numbers are written in the LocaleProvider's locale, " +
|
|
30
|
+
'else the runtime\'s. Use a BCP 47 tag such as "en-US", with a ' +
|
|
31
|
+
'hyphen rather than an underscore.');
|
|
32
|
+
/**
|
|
33
|
+
* The locale a component formats and matches in: the locale given, else the
|
|
34
|
+
* `LocaleProvider`'s, else none. A tag `Intl` refuses counts as none, with a
|
|
35
|
+
* development warning, as `LocaleProvider` treats one.
|
|
36
|
+
*/
|
|
37
|
+
function useResolvedLocale(own) {
|
|
18
38
|
const scope = useLocaleScope();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return scope.locale;
|
|
39
|
+
const valid = useMemo(() => validLocale(own, warnOwnLocale), [own]);
|
|
40
|
+
return valid || scope?.locale;
|
|
22
41
|
}
|
|
23
42
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
* The options `Intl.NumberFormat` reads, as ECMA-402 lists them. A key reads
|
|
44
|
+
* each by name, so an inherited option counts as `Intl` counts it.
|
|
45
|
+
*/
|
|
46
|
+
const NUMBER_FORMAT_OPTIONS = [
|
|
47
|
+
'localeMatcher',
|
|
48
|
+
'numberingSystem',
|
|
49
|
+
'style',
|
|
50
|
+
'currency',
|
|
51
|
+
'currencyDisplay',
|
|
52
|
+
'currencySign',
|
|
53
|
+
'unit',
|
|
54
|
+
'unitDisplay',
|
|
55
|
+
'notation',
|
|
56
|
+
'minimumIntegerDigits',
|
|
57
|
+
'minimumFractionDigits',
|
|
58
|
+
'maximumFractionDigits',
|
|
59
|
+
'minimumSignificantDigits',
|
|
60
|
+
'maximumSignificantDigits',
|
|
61
|
+
'roundingIncrement',
|
|
62
|
+
'roundingMode',
|
|
63
|
+
'roundingPriority',
|
|
64
|
+
'trailingZeroDisplay',
|
|
65
|
+
'compactDisplay',
|
|
66
|
+
'useGrouping',
|
|
67
|
+
'signDisplay',
|
|
68
|
+
];
|
|
69
|
+
const KNOWN_OPTIONS = new Set(NUMBER_FORMAT_OPTIONS);
|
|
70
|
+
/** One option's value with its type, or `null` for one a key can't hold. */
|
|
71
|
+
function writeOption(value) {
|
|
72
|
+
switch (typeof value) {
|
|
73
|
+
case 'string':
|
|
74
|
+
return JSON.stringify(value);
|
|
75
|
+
case 'number':
|
|
76
|
+
// `NaN` and `Infinity` as themselves, which `JSON.stringify` makes
|
|
77
|
+
// `null`, and `-0` with its sign.
|
|
78
|
+
return Object.is(value, -0) ? '-0' : String(value);
|
|
79
|
+
case 'boolean':
|
|
80
|
+
return String(value);
|
|
81
|
+
case 'bigint':
|
|
82
|
+
return `${value}n`;
|
|
83
|
+
default:
|
|
84
|
+
return value === null ? 'null' : null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The options as `Intl` will read them, and a key that tells apart any two
|
|
89
|
+
* sets it would read differently; `null` when an option `Intl` reads holds
|
|
90
|
+
* an object, a function or a symbol, which `Intl` converts in ways a key
|
|
91
|
+
* can't follow. It reads each option `Intl` reads by name, inherited ones
|
|
92
|
+
* included, then every other enumerable key, own or inherited, so an option
|
|
93
|
+
* `Intl` gains later still keeps two sets apart. Since `Intl`'s options are
|
|
94
|
+
* all primitives, one of those other keys holding anything else is left
|
|
95
|
+
* out. An option set to `undefined` is unset, as it is for `Intl`. The
|
|
96
|
+
* formatter is built from the copy, so `Intl` sees what the key says, even
|
|
97
|
+
* of a getter.
|
|
98
|
+
*/
|
|
99
|
+
function readOptions(options) {
|
|
100
|
+
const others = [];
|
|
101
|
+
for (const name in options) {
|
|
102
|
+
if (!KNOWN_OPTIONS.has(name))
|
|
103
|
+
others.push(name);
|
|
104
|
+
}
|
|
105
|
+
const names = NUMBER_FORMAT_OPTIONS.concat(others.sort());
|
|
106
|
+
const read = {};
|
|
107
|
+
const parts = [];
|
|
108
|
+
for (const name of names) {
|
|
109
|
+
const value = options[name];
|
|
110
|
+
if (value === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
const written = writeOption(value);
|
|
113
|
+
if (written === null) {
|
|
114
|
+
// An option `Intl` reads converts an object; a key it doesn't read,
|
|
115
|
+
// such as a config's own `meta`, means nothing to it.
|
|
116
|
+
if (KNOWN_OPTIONS.has(name))
|
|
117
|
+
return null;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
read[name] = value;
|
|
121
|
+
parts.push(`${JSON.stringify(name)}:${written}`);
|
|
122
|
+
}
|
|
123
|
+
return { key: parts.join(','), read };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Every formatter's `Intl.NumberFormat`s, by locale and options, so a page of
|
|
127
|
+
* numbers in one locale and format builds one. Building one is the expensive
|
|
128
|
+
* part of formatting. Past `MOST_NUMBER_FORMATS`, the one used longest ago
|
|
129
|
+
* goes, so a server that writes numbers in many locales doesn't keep them
|
|
130
|
+
* all, and the ones in use stay. A `Map` keeps its keys in the order they
|
|
131
|
+
* were set, so a formatter is set again each time it's used.
|
|
132
|
+
*/
|
|
133
|
+
const numberFormats = new Map();
|
|
134
|
+
const MOST_NUMBER_FORMATS = 200;
|
|
135
|
+
/** The `Intl.NumberFormat` for `locale` and `options`, built once. */
|
|
136
|
+
function numberFormatFor(locale, options) {
|
|
137
|
+
let read;
|
|
138
|
+
let key = '';
|
|
139
|
+
if (options !== undefined) {
|
|
140
|
+
const copy = typeof options === 'object' && options !== null
|
|
141
|
+
? readOptions(options)
|
|
142
|
+
: null;
|
|
143
|
+
// Options a key can't hold get a formatter of their own, which also
|
|
144
|
+
// throws what `Intl` throws for them.
|
|
145
|
+
if (!copy)
|
|
146
|
+
return new Intl.NumberFormat(locale, options);
|
|
147
|
+
read = copy.read;
|
|
148
|
+
key = copy.key;
|
|
149
|
+
}
|
|
150
|
+
// A locale tag has no spaces, so the key can't be read two ways.
|
|
151
|
+
const cacheKey = `${locale ?? ''} ${key}`;
|
|
152
|
+
let format = numberFormats.get(cacheKey);
|
|
153
|
+
if (format) {
|
|
154
|
+
numberFormats.delete(cacheKey);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Built before anything is stored, so options `Intl` refuses throw its
|
|
158
|
+
// error and leave nothing behind.
|
|
159
|
+
format = new Intl.NumberFormat(locale, read);
|
|
160
|
+
if (numberFormats.size >= MOST_NUMBER_FORMATS) {
|
|
161
|
+
numberFormats.delete(numberFormats.keys().next().value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
numberFormats.set(cacheKey, format);
|
|
165
|
+
return format;
|
|
166
|
+
}
|
|
167
|
+
/** A function that writes numbers in `locale`, the same one for each locale. */
|
|
168
|
+
function useFormatIn(locale) {
|
|
169
|
+
return useMemo(() => (value, options) => numberFormatFor(locale, options).format(value), [locale]);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A function that writes numbers for display in a locale: `locale` when given, else the `LocaleProvider`'s around the component, else the runtime's.
|
|
173
|
+
*
|
|
174
|
+
* Formatters are kept per locale and set of options, since building one is
|
|
175
|
+
* the expensive part, and shared, so many numbers written the same way build
|
|
176
|
+
* one. The function is the same from one render to the next for the same
|
|
177
|
+
* locale, so it's safe in a dependency array. Use it for text outside JSX,
|
|
178
|
+
* such as an `aria-label`, a `placeholder` or a chart's axis; in JSX,
|
|
179
|
+
* `NumberFormatter` also marks the number up. On a server render the
|
|
180
|
+
* runtime's locale is the server's, so give it a locale, here or through a
|
|
181
|
+
* `LocaleProvider`, where the server's and the browser's can differ. A tag
|
|
182
|
+
* `Intl` refuses is ignored, with a warning in development, as
|
|
183
|
+
* `LocaleProvider` ignores one. Options `Intl` refuses throw its error when
|
|
184
|
+
* the function is called.
|
|
185
|
+
*
|
|
186
|
+
* @function
|
|
187
|
+
* @param locale - A BCP 47 locale tag, which wins over the provider's.
|
|
188
|
+
* @returns {NumberFormatFunction} Writes `value` with `Intl.NumberFormat` options.
|
|
28
189
|
*
|
|
29
|
-
* @
|
|
30
|
-
*
|
|
190
|
+
* @example
|
|
191
|
+
* const format = useNumberFormat();
|
|
192
|
+
* const placeholder = format(1500, { style: 'currency', currency: 'EUR' });
|
|
31
193
|
*/
|
|
32
|
-
function useNumberFormat(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
47
|
-
}, [locale]);
|
|
194
|
+
function useNumberFormat(locale) {
|
|
195
|
+
return useFormatIn(useResolvedLocale(locale));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* INTERNAL, deliberately not exported from `src/index.ts`. Number formatting
|
|
199
|
+
* for a component that shows a number in passing, as `useNumberFormat`
|
|
200
|
+
* writes it in the `LocaleProvider`'s locale, or `null` outside a provider
|
|
201
|
+
* and inside one that gives no locale, as a provider with only a `dir`
|
|
202
|
+
* doesn't, where the component keeps the formatting it has always had.
|
|
203
|
+
*/
|
|
204
|
+
function useScopedNumberFormat() {
|
|
205
|
+
const scoped = useResolvedLocale(undefined);
|
|
206
|
+
const format = useFormatIn(scoped);
|
|
207
|
+
return scoped ? format : null;
|
|
48
208
|
}
|
|
49
209
|
/**
|
|
50
210
|
* A matcher for filtering a list by what was typed, in the locale of the
|
|
@@ -58,7 +218,7 @@ function useNumberFormat(own) {
|
|
|
58
218
|
* matches everything.
|
|
59
219
|
*/
|
|
60
220
|
function useLocaleMatcher() {
|
|
61
|
-
const locale =
|
|
221
|
+
const locale = useResolvedLocale(undefined);
|
|
62
222
|
return useMemo(() => {
|
|
63
223
|
if (!locale)
|
|
64
224
|
return null;
|
|
@@ -105,5 +265,5 @@ function useLocaleMatcher() {
|
|
|
105
265
|
}, [locale]);
|
|
106
266
|
}
|
|
107
267
|
|
|
108
|
-
export { useLocaleMatcher, useNumberFormat };
|
|
268
|
+
export { useLocaleMatcher, useNumberFormat, useScopedNumberFormat };
|
|
109
269
|
//# sourceMappingURL=localeFormat.js.map
|
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"localeFormat.js","sources":["../../../src/helpers/localeFormat.ts"],"sourcesContent":["// Number formatting and text matching in a locale.\n//\n// `useNumberFormat` and its `NumberFormatFunction` type are public, exported\n// from src/index.ts. The rest is INTERNAL, deliberately not exported.\n//\n// Which a component calls depends on its job. A component whose job is to\n// write a number, such as `NumberFormatter`, uses `useNumberFormat`, which\n// writes in the runtime's locale outside a `LocaleProvider`, as the pickers\n// do. Any other component that shows a number in passing, such as a badge\n// count, a page number or a step's number, uses `useScopedNumberFormat`,\n// and filters a list through `useLocaleMatcher`. Those two return `null`\n// outside a provider, and inside one that gives no locale, as a provider\n// with only a `dir` doesn't, so the component keeps the formatting and the\n// matching it has always had there, as `LocaleProvider` promises.\n\nimport { useMemo } from 'react';\nimport { useLocaleScope, validLocale } from './LocaleProvider';\nimport { warnOnce } from './devWarnings';\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 NumberFormatFunction = (\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 warning about a locale `Intl` refuses, given to `useNumberFormat`,\n * and so to `NumberFormatter`, the only callers that pass one.\n */\nconst warnOwnLocale = (tag: string) =>\n warnOnce(\n `number-format-invalid-locale:${tag}`,\n `[bestax-bulma] NumberFormatter or useNumberFormat was given the locale ` +\n `\"${tag}\", which is not a language tag Intl accepts, so it is ` +\n \"ignored and numbers are written in the LocaleProvider's locale, \" +\n 'else the runtime\\'s. Use a BCP 47 tag such as \"en-US\", with a ' +\n 'hyphen rather than an underscore.'\n );\n\n/**\n * The locale a component formats and matches in: the locale given, else the\n * `LocaleProvider`'s, else none. A tag `Intl` refuses counts as none, with a\n * development warning, as `LocaleProvider` treats one.\n */\nfunction useResolvedLocale(own: string | undefined): string | undefined {\n const scope = useLocaleScope();\n const valid = useMemo(() => validLocale(own, warnOwnLocale), [own]);\n return valid || scope?.locale;\n}\n\n/**\n * The options `Intl.NumberFormat` reads, as ECMA-402 lists them. A key reads\n * each by name, so an inherited option counts as `Intl` counts it.\n */\nconst NUMBER_FORMAT_OPTIONS = [\n 'localeMatcher',\n 'numberingSystem',\n 'style',\n 'currency',\n 'currencyDisplay',\n 'currencySign',\n 'unit',\n 'unitDisplay',\n 'notation',\n 'minimumIntegerDigits',\n 'minimumFractionDigits',\n 'maximumFractionDigits',\n 'minimumSignificantDigits',\n 'maximumSignificantDigits',\n 'roundingIncrement',\n 'roundingMode',\n 'roundingPriority',\n 'trailingZeroDisplay',\n 'compactDisplay',\n 'useGrouping',\n 'signDisplay',\n];\nconst KNOWN_OPTIONS = new Set(NUMBER_FORMAT_OPTIONS);\n\n/** One option's value with its type, or `null` for one a key can't hold. */\nfunction writeOption(value: unknown): string | null {\n switch (typeof value) {\n case 'string':\n return JSON.stringify(value);\n case 'number':\n // `NaN` and `Infinity` as themselves, which `JSON.stringify` makes\n // `null`, and `-0` with its sign.\n return Object.is(value, -0) ? '-0' : String(value);\n case 'boolean':\n return String(value);\n case 'bigint':\n return `${value}n`;\n default:\n return value === null ? 'null' : null;\n }\n}\n\n/**\n * The options as `Intl` will read them, and a key that tells apart any two\n * sets it would read differently; `null` when an option `Intl` reads holds\n * an object, a function or a symbol, which `Intl` converts in ways a key\n * can't follow. It reads each option `Intl` reads by name, inherited ones\n * included, then every other enumerable key, own or inherited, so an option\n * `Intl` gains later still keeps two sets apart. Since `Intl`'s options are\n * all primitives, one of those other keys holding anything else is left\n * out. An option set to `undefined` is unset, as it is for `Intl`. The\n * formatter is built from the copy, so `Intl` sees what the key says, even\n * of a getter.\n */\nfunction readOptions(\n options: object\n): { key: string; read: Record<string, unknown> } | null {\n const others: string[] = [];\n for (const name in options) {\n if (!KNOWN_OPTIONS.has(name)) others.push(name);\n }\n const names = NUMBER_FORMAT_OPTIONS.concat(others.sort());\n const read: Record<string, unknown> = {};\n const parts: string[] = [];\n for (const name of names) {\n const value = (options as Record<string, unknown>)[name];\n if (value === undefined) continue;\n const written = writeOption(value);\n if (written === null) {\n // An option `Intl` reads converts an object; a key it doesn't read,\n // such as a config's own `meta`, means nothing to it.\n if (KNOWN_OPTIONS.has(name)) return null;\n continue;\n }\n read[name] = value;\n parts.push(`${JSON.stringify(name)}:${written}`);\n }\n return { key: parts.join(','), read };\n}\n\n/**\n * Every formatter's `Intl.NumberFormat`s, by locale and options, so a page of\n * numbers in one locale and format builds one. Building one is the expensive\n * part of formatting. Past `MOST_NUMBER_FORMATS`, the one used longest ago\n * goes, so a server that writes numbers in many locales doesn't keep them\n * all, and the ones in use stay. A `Map` keeps its keys in the order they\n * were set, so a formatter is set again each time it's used.\n */\nconst numberFormats = new Map<string, Intl.NumberFormat>();\nconst MOST_NUMBER_FORMATS = 200;\n\n/** The `Intl.NumberFormat` for `locale` and `options`, built once. */\nfunction numberFormatFor(\n locale: string | undefined,\n options: Intl.NumberFormatOptions | undefined\n): Intl.NumberFormat {\n let read: Intl.NumberFormatOptions | undefined;\n let key = '';\n if (options !== undefined) {\n const copy =\n typeof options === 'object' && options !== null\n ? readOptions(options)\n : null;\n // Options a key can't hold get a formatter of their own, which also\n // throws what `Intl` throws for them.\n if (!copy) return new Intl.NumberFormat(locale, options);\n read = copy.read;\n key = copy.key;\n }\n // A locale tag has no spaces, so the key can't be read two ways.\n const cacheKey = `${locale ?? ''} ${key}`;\n let format = numberFormats.get(cacheKey);\n if (format) {\n numberFormats.delete(cacheKey);\n } else {\n // Built before anything is stored, so options `Intl` refuses throw its\n // error and leave nothing behind.\n format = new Intl.NumberFormat(locale, read);\n if (numberFormats.size >= MOST_NUMBER_FORMATS) {\n numberFormats.delete(numberFormats.keys().next().value as string);\n }\n }\n numberFormats.set(cacheKey, format);\n return format;\n}\n\n/** A function that writes numbers in `locale`, the same one for each locale. */\nfunction useFormatIn(locale: string | undefined): NumberFormatFunction {\n return useMemo<NumberFormatFunction>(\n () => (value, options) => numberFormatFor(locale, options).format(value),\n [locale]\n );\n}\n\n/**\n * A function that writes numbers for display in a locale: `locale` when given, else the `LocaleProvider`'s around the component, else the runtime's.\n *\n * Formatters are kept per locale and set of options, since building one is\n * the expensive part, and shared, so many numbers written the same way build\n * one. The function is the same from one render to the next for the same\n * locale, so it's safe in a dependency array. Use it for text outside JSX,\n * such as an `aria-label`, a `placeholder` or a chart's axis; in JSX,\n * `NumberFormatter` also marks the number up. On a server render the\n * runtime's locale is the server's, so give it a locale, here or through a\n * `LocaleProvider`, where the server's and the browser's can differ. A tag\n * `Intl` refuses is ignored, with a warning in development, as\n * `LocaleProvider` ignores one. Options `Intl` refuses throw its error when\n * the function is called.\n *\n * @function\n * @param locale - A BCP 47 locale tag, which wins over the provider's.\n * @returns {NumberFormatFunction} Writes `value` with `Intl.NumberFormat` options.\n *\n * @example\n * const format = useNumberFormat();\n * const placeholder = format(1500, { style: 'currency', currency: 'EUR' });\n */\nexport function useNumberFormat(locale?: string): NumberFormatFunction {\n return useFormatIn(useResolvedLocale(locale));\n}\n\n/**\n * INTERNAL, deliberately not exported from `src/index.ts`. Number formatting\n * for a component that shows a number in passing, as `useNumberFormat`\n * writes it in the `LocaleProvider`'s locale, or `null` outside a provider\n * and inside one that gives no locale, as a provider with only a `dir`\n * doesn't, where the component keeps the formatting it has always had.\n */\nexport function useScopedNumberFormat(): NumberFormatFunction | null {\n const scoped = useResolvedLocale(undefined);\n const format = useFormatIn(scoped);\n return scoped ? format : null;\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 = useResolvedLocale(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":";;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAMA;AACA,MAAM,IAAI,GAAG,QAAQ;AAWrB;;;AAGG;AACH,MAAM,aAAa,GAAG,CAAC,GAAW,KAChC,QAAQ,CACN,CAAA,6BAAA,EAAgC,GAAG,CAAA,CAAE,EACrC,CAAA,uEAAA,CAAyE;AACvE,IAAA,CAAA,CAAA,EAAI,GAAG,CAAA,sDAAA,CAAwD;IAC/D,kEAAkE;IAClE,gEAAgE;AAChE,IAAA,mCAAmC,CACtC;AAEH;;;;AAIG;AACH,SAAS,iBAAiB,CAAC,GAAuB,EAAA;AAChD,IAAA,MAAM,KAAK,GAAG,cAAc,EAAE;AAC9B,IAAA,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,WAAW,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;AACnE,IAAA,OAAO,KAAK,IAAI,KAAK,EAAE,MAAM;AAC/B;AAEA;;;AAGG;AACH,MAAM,qBAAqB,GAAG;IAC5B,eAAe;IACf,iBAAiB;IACjB,OAAO;IACP,UAAU;IACV,iBAAiB;IACjB,cAAc;IACd,MAAM;IACN,aAAa;IACb,UAAU;IACV,sBAAsB;IACtB,uBAAuB;IACvB,uBAAuB;IACvB,0BAA0B;IAC1B,0BAA0B;IAC1B,mBAAmB;IACnB,cAAc;IACd,kBAAkB;IAClB,qBAAqB;IACrB,gBAAgB;IAChB,aAAa;IACb,aAAa;CACd;AACD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,qBAAqB,CAAC;AAEpD;AACA,SAAS,WAAW,CAAC,KAAc,EAAA;IACjC,QAAQ,OAAO,KAAK;AAClB,QAAA,KAAK,QAAQ;AACX,YAAA,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;AAC9B,QAAA,KAAK,QAAQ;;;YAGX,OAAO,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC;AACpD,QAAA,KAAK,SAAS;AACZ,YAAA,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,QAAA,KAAK,QAAQ;YACX,OAAO,CAAA,EAAG,KAAK,CAAA,CAAA,CAAG;AACpB,QAAA;YACE,OAAO,KAAK,KAAK,IAAI,GAAG,MAAM,GAAG,IAAI;;AAE3C;AAEA;;;;;;;;;;;AAWG;AACH,SAAS,WAAW,CAClB,OAAe,EAAA;IAEf,MAAM,MAAM,GAAa,EAAE;AAC3B,IAAA,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE;AAC1B,QAAA,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;AAAE,YAAA,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;IACjD;IACA,MAAM,KAAK,GAAG,qBAAqB,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACzD,MAAM,IAAI,GAA4B,EAAE;IACxC,MAAM,KAAK,GAAa,EAAE;AAC1B,IAAA,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;AACxB,QAAA,MAAM,KAAK,GAAI,OAAmC,CAAC,IAAI,CAAC;QACxD,IAAI,KAAK,KAAK,SAAS;YAAE;AACzB,QAAA,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC;AAClC,QAAA,IAAI,OAAO,KAAK,IAAI,EAAE;;;AAGpB,YAAA,IAAI,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;AAAE,gBAAA,OAAO,IAAI;YACxC;QACF;AACA,QAAA,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK;AAClB,QAAA,KAAK,CAAC,IAAI,CAAC,CAAA,EAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA,CAAA,EAAI,OAAO,CAAA,CAAE,CAAC;IAClD;AACA,IAAA,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;AACvC;AAEA;;;;;;;AAOG;AACH,MAAM,aAAa,GAAG,IAAI,GAAG,EAA6B;AAC1D,MAAM,mBAAmB,GAAG,GAAG;AAE/B;AACA,SAAS,eAAe,CACtB,MAA0B,EAC1B,OAA6C,EAAA;AAE7C,IAAA,IAAI,IAA0C;IAC9C,IAAI,GAAG,GAAG,EAAE;AACZ,IAAA,IAAI,OAAO,KAAK,SAAS,EAAE;QACzB,MAAM,IAAI,GACR,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK;AACzC,cAAE,WAAW,CAAC,OAAO;cACnB,IAAI;;;AAGV,QAAA,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC;AACxD,QAAA,IAAI,GAAG,IAAI,CAAC,IAAI;AAChB,QAAA,GAAG,GAAG,IAAI,CAAC,GAAG;IAChB;;IAEA,MAAM,QAAQ,GAAG,CAAA,EAAG,MAAM,IAAI,EAAE,CAAA,CAAA,EAAI,GAAG,CAAA,CAAE;IACzC,IAAI,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC;IACxC,IAAI,MAAM,EAAE;AACV,QAAA,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC;IAChC;SAAO;;;QAGL,MAAM,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC;AAC5C,QAAA,IAAI,aAAa,CAAC,IAAI,IAAI,mBAAmB,EAAE;AAC7C,YAAA,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAe,CAAC;QACnE;IACF;AACA,IAAA,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC;AACnC,IAAA,OAAO,MAAM;AACf;AAEA;AACA,SAAS,WAAW,CAAC,MAA0B,EAAA;AAC7C,IAAA,OAAO,OAAO,CACZ,MAAM,CAAC,KAAK,EAAE,OAAO,KAAK,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACxE,CAAC,MAAM,CAAC,CACT;AACH;AAEA;;;;;;;;;;;;;;;;;;;;;;AAsBG;AACG,SAAU,eAAe,CAAC,MAAe,EAAA;AAC7C,IAAA,OAAO,WAAW,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;AAC/C;AAEA;;;;;;AAMG;SACa,qBAAqB,GAAA;AACnC,IAAA,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC;AAC3C,IAAA,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;IAClC,OAAO,MAAM,GAAG,MAAM,GAAG,IAAI;AAC/B;AAEA;;;;;;;;;;AAUG;SACa,gBAAgB,GAAA;AAC9B,IAAA,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC;IAC3C,OAAO,OAAO,CAAC,MAAK;AAClB,QAAA,IAAI,CAAC,MAAM;AAAE,YAAA,OAAO,IAAI;QACxB,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;AACzC,YAAA,KAAK,EAAE,QAAQ;AACf,YAAA,WAAW,EAAE,MAAM;AACpB,SAAA,CAAC;AACF,QAAA,OAAO,CAAC,IAAI,EAAE,KAAK,KAAI;YACrB,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC;YACrC,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC;AAAE,gBAAA,OAAO,IAAI;;AAEnD,YAAA,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC/C,YAAA,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;;;;;;;;;;YAWhD,MAAM,IAAI,GACR,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;AAChE,YAAA,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;gBACjD,IAAI,KAAK,CAAC,KAAK,CAAC;oBAAE;gBAClB,IAAI,OAAO,GAAG,CAAC;AACf,gBAAA,KAAK,IAAI,GAAG,GAAG,KAAK,GAAG,CAAC,EAAE,GAAG,IAAI,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;AACpD,oBAAA,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;AAAE,wBAAA,OAAO,EAAE;oBAC9B,IAAI,OAAO,GAAG,IAAI;wBAAE;oBACpB,IAAI,KAAK,CAAC,GAAG,CAAC;wBAAE;oBAChB,IACE,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,EAChE;AACA,wBAAA,OAAO,IAAI;oBACb;gBACF;YACF;AACA,YAAA,OAAO,KAAK;AACd,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;AACd;;"}
|
|
@@ -6,15 +6,23 @@ import { getActiveElementInTree } from './shadowDom.js';
|
|
|
6
6
|
import { useTypeahead, typeaheadMatch, typeaheadText } from './typeahead.js';
|
|
7
7
|
|
|
8
8
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
9
|
+
/** The keys and clicks of both checkbox modes, which differ only in what a check reaches. */
|
|
10
|
+
const CHECKBOX_ACTIONS = {
|
|
11
|
+
enter: ['open'],
|
|
12
|
+
space: ['check'],
|
|
13
|
+
click: ['check'],
|
|
14
|
+
};
|
|
9
15
|
/**
|
|
10
16
|
* What Enter, Space and a click on the row do in each mode, from the tables
|
|
11
|
-
* in #1096. `open` opens or closes a
|
|
12
|
-
* click on the chevron only ever opens
|
|
17
|
+
* in #1096, with `checkbox-independent` from #1430. `open` opens or closes a
|
|
18
|
+
* parent and does nothing on a leaf. A click on the chevron only ever opens
|
|
19
|
+
* or closes.
|
|
13
20
|
*/
|
|
14
21
|
const MODES = {
|
|
15
22
|
none: { enter: ['open'], space: ['open'], click: ['open'] },
|
|
16
23
|
single: { enter: ['select'], space: ['select'], click: ['select', 'open'] },
|
|
17
|
-
checkbox:
|
|
24
|
+
checkbox: CHECKBOX_ACTIONS,
|
|
25
|
+
'checkbox-independent': CHECKBOX_ACTIONS,
|
|
18
26
|
};
|
|
19
27
|
/** Left and Right, swapped for a right-to-left page. */
|
|
20
28
|
const MIRRORED = {
|
|
@@ -155,6 +163,13 @@ function useControlled(controlled, initial) {
|
|
|
155
163
|
};
|
|
156
164
|
return [isControlled ? controlled : own, set];
|
|
157
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* The list with `value` added at the end, or, when it's `present`, with
|
|
168
|
+
* every copy of it taken out. Everything else stays as it was, in its order.
|
|
169
|
+
*/
|
|
170
|
+
function addedOrRemoved(list, value, present) {
|
|
171
|
+
return present ? list.filter(v => v !== value) : [...list, value];
|
|
172
|
+
}
|
|
158
173
|
/**
|
|
159
174
|
* The open state, the selection, the keys, the tab stop, the positions and
|
|
160
175
|
* the registry of a TreeView (#1096). Focus moves for real, with a roving
|
|
@@ -163,10 +178,12 @@ function useControlled(controlled, initial) {
|
|
|
163
178
|
* browser (#1171), read off the page once the tree has mounted.
|
|
164
179
|
*
|
|
165
180
|
* Every item registers its value, its parent and whether it's disabled from
|
|
166
|
-
* a layout effect. In a checkbox tree a closed item's children still
|
|
167
|
-
* and register while drawing nothing, so the registry knows every item
|
|
168
|
-
* check reaches all of them.
|
|
169
|
-
*
|
|
181
|
+
* a layout effect. In a `checkbox` tree a closed item's children still
|
|
182
|
+
* render and register while drawing nothing, so the registry knows every item
|
|
183
|
+
* and a check reaches all of them. A `checkbox-independent` tree reads each
|
|
184
|
+
* box off the checked list alone, so a check adds or takes out its own value
|
|
185
|
+
* and nothing else. The keys read the items on the page, in document order,
|
|
186
|
+
* each time they need them.
|
|
170
187
|
*
|
|
171
188
|
* @function useTreeView
|
|
172
189
|
* @param options - The TreeView's props.
|
|
@@ -182,9 +199,15 @@ function useTreeView(options) {
|
|
|
182
199
|
const [expanded, setExpanded] = useControlled(options.expanded, options.defaultExpanded);
|
|
183
200
|
const [selection, setSelection] = useControlled(options.selected, options.defaultSelected);
|
|
184
201
|
const [checkedList, setChecked] = useControlled(options.checked, options.defaultChecked);
|
|
202
|
+
// In a `checkbox` tree a check reaches the items under it, by the rules. In
|
|
203
|
+
// a `checkbox-independent` one each box is its own item's, read off the list.
|
|
204
|
+
const cascade = mode === 'checkbox';
|
|
205
|
+
const independent = mode === 'checkbox-independent';
|
|
206
|
+
// A box on every item, in either checkbox tree.
|
|
207
|
+
const boxed = cascade || independent;
|
|
185
208
|
// Each is read in its own mode only.
|
|
186
209
|
const selected = mode === 'single' ? selection : null;
|
|
187
|
-
const listed = new Set(
|
|
210
|
+
const listed = new Set(boxed ? checkedList : []);
|
|
188
211
|
const treeRef = useRef(null);
|
|
189
212
|
const registryRef = useRef({
|
|
190
213
|
records: new Map(),
|
|
@@ -205,21 +228,28 @@ function useTreeView(options) {
|
|
|
205
228
|
// Each item's place among the items beside it, by its key.
|
|
206
229
|
const [positions, setPositions] = useState(NO_POSITIONS);
|
|
207
230
|
const { records } = registryRef.current;
|
|
208
|
-
const checks =
|
|
231
|
+
const checks = cascade
|
|
209
232
|
? // eslint-disable-next-line react-hooks/refs -- read in render on purpose: items write the registry from their layout effects and then ask for this render with `refresh`
|
|
210
233
|
readChecks(records, listed).states
|
|
211
234
|
: undefined;
|
|
212
235
|
const tabStop = focused ?? (resting === undefined ? selected : resting);
|
|
236
|
+
// An item's box, decided here alone, so the box drawn, the tab stop and a
|
|
237
|
+
// toggle agree: off the list in a `checkbox-independent` tree, and by the
|
|
238
|
+
// rules in a `checkbox` one, where it's `undefined` until the item
|
|
239
|
+
// registers. `undefined` in a tree with no boxes.
|
|
240
|
+
const checkOf = (value) => independent ? listed.has(value) : checks?.get(value);
|
|
213
241
|
const isOpen = (value) => expanded.includes(value);
|
|
214
242
|
const stateOf = (value, path, disabled) => {
|
|
215
243
|
if (mode === 'single') {
|
|
216
244
|
return disabled ? {} : { selected: value === selected };
|
|
217
245
|
}
|
|
218
|
-
if (
|
|
219
|
-
// Until the items register, a value stands for
|
|
220
|
-
// parent can't know it's mixed.
|
|
221
|
-
|
|
222
|
-
|
|
246
|
+
if (boxed) {
|
|
247
|
+
// Until the items register in a `checkbox` tree, a value stands for
|
|
248
|
+
// its branch, and a parent can't know it's mixed.
|
|
249
|
+
return {
|
|
250
|
+
checked: checkOf(value) ??
|
|
251
|
+
(listed.has(value) || (!disabled && path.some(v => listed.has(v)))),
|
|
252
|
+
};
|
|
223
253
|
}
|
|
224
254
|
return {};
|
|
225
255
|
};
|
|
@@ -229,9 +259,8 @@ function useTreeView(options) {
|
|
|
229
259
|
};
|
|
230
260
|
const run = {
|
|
231
261
|
open: ({ value, isParent, open }) => {
|
|
232
|
-
if (isParent)
|
|
233
|
-
changeOpen(
|
|
234
|
-
}
|
|
262
|
+
if (isParent)
|
|
263
|
+
changeOpen(addedOrRemoved(expanded, value, open));
|
|
235
264
|
},
|
|
236
265
|
select: ({ value }) => {
|
|
237
266
|
if (value === selected)
|
|
@@ -240,7 +269,11 @@ function useTreeView(options) {
|
|
|
240
269
|
options.onSelectedChange?.(value);
|
|
241
270
|
},
|
|
242
271
|
check: ({ value }) => {
|
|
243
|
-
|
|
272
|
+
// A `checkbox-independent` box is its own item's alone, so its value
|
|
273
|
+
// comes or goes and the rest of the list stays as it was.
|
|
274
|
+
const next = independent
|
|
275
|
+
? addedOrRemoved(checkedList, value, checkOf(value) === true)
|
|
276
|
+
: toggledList(records, checkedList, value);
|
|
244
277
|
setChecked(next);
|
|
245
278
|
options.onCheckedChange?.(next);
|
|
246
279
|
},
|
|
@@ -289,7 +322,7 @@ function useTreeView(options) {
|
|
|
289
322
|
}
|
|
290
323
|
if (focused !== null && !isShown(focused))
|
|
291
324
|
setFocused(null);
|
|
292
|
-
const preferred = onPage.find(value => value === selected ||
|
|
325
|
+
const preferred = onPage.find(value => value === selected || checkOf(value) === true);
|
|
293
326
|
const next = preferred ?? (isShown(resting) ? resting : heir(restingPathRef.current));
|
|
294
327
|
if (next !== resting) {
|
|
295
328
|
setResting(next);
|
|
@@ -428,7 +461,8 @@ function useTreeView(options) {
|
|
|
428
461
|
};
|
|
429
462
|
return {
|
|
430
463
|
treeRef,
|
|
431
|
-
rendersClosed:
|
|
464
|
+
rendersClosed: cascade,
|
|
465
|
+
multiselectable: boxed,
|
|
432
466
|
tabStop,
|
|
433
467
|
isOpen,
|
|
434
468
|
stateOf,
|