@kanso-labs/react-native-kanso-ui 0.6.0 → 0.8.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.
Files changed (97) hide show
  1. package/README.md +37 -0
  2. package/dist/badge/index.js +25 -23
  3. package/dist/badge/index.js.map +1 -1
  4. package/dist/button/index.d.ts +2 -1
  5. package/dist/button/index.js.map +1 -1
  6. package/dist/button/pending.js +17 -21
  7. package/dist/button/pending.js.map +1 -1
  8. package/dist/calendar/index.js +1182 -0
  9. package/dist/calendar/index.js.map +1 -0
  10. package/dist/calendar/styles.js +120 -0
  11. package/dist/calendar/styles.js.map +1 -0
  12. package/dist/components/autocomplete/index.js +28 -26
  13. package/dist/components/autocomplete/index.js.map +1 -1
  14. package/dist/components/avatar/index.d.ts +2 -2
  15. package/dist/components/avatar/index.js +2 -2
  16. package/dist/components/avatar/index.js.map +1 -1
  17. package/dist/components/calendar/index.d.ts +51 -0
  18. package/dist/components/calendar/index.js +121 -0
  19. package/dist/components/calendar/index.js.map +1 -0
  20. package/dist/components/chip-group/index.d.ts +4 -3
  21. package/dist/components/chip-group/index.js +47 -45
  22. package/dist/components/chip-group/index.js.map +1 -1
  23. package/dist/components/copy-field/index.d.ts +5 -5
  24. package/dist/components/copy-field/index.js +30 -28
  25. package/dist/components/copy-field/index.js.map +1 -1
  26. package/dist/components/currency/index.d.ts +2 -2
  27. package/dist/components/currency/index.js +3 -1
  28. package/dist/components/currency/index.js.map +1 -1
  29. package/dist/components/list/index.d.ts +6 -5
  30. package/dist/components/list/index.js +97 -106
  31. package/dist/components/list/index.js.map +1 -1
  32. package/dist/components/menu/index.d.ts +4 -3
  33. package/dist/components/menu/index.js +33 -40
  34. package/dist/components/menu/index.js.map +1 -1
  35. package/dist/components/meter/index.d.ts +2 -1
  36. package/dist/components/meter/index.js +9 -7
  37. package/dist/components/meter/index.js.map +1 -1
  38. package/dist/components/navigation-bar/index.d.ts +3 -9
  39. package/dist/components/navigation-bar/index.js +10 -34
  40. package/dist/components/navigation-bar/index.js.map +1 -1
  41. package/dist/components/navigation-rail/index.d.ts +68 -0
  42. package/dist/components/navigation-rail/index.js +294 -0
  43. package/dist/components/navigation-rail/index.js.map +1 -0
  44. package/dist/components/number-field/index.d.ts +7 -3
  45. package/dist/components/number-field/index.js +214 -192
  46. package/dist/components/number-field/index.js.map +1 -1
  47. package/dist/components/popover/index.js +1 -1
  48. package/dist/components/product-icon/index.d.ts +2 -2
  49. package/dist/components/product-icon/index.js +2 -2
  50. package/dist/components/product-icon/index.js.map +1 -1
  51. package/dist/components/progress-indicator/index.d.ts +2 -1
  52. package/dist/components/progress-indicator/index.js +15 -13
  53. package/dist/components/progress-indicator/index.js.map +1 -1
  54. package/dist/components/search-field/index.d.ts +3 -3
  55. package/dist/components/search-field/index.js +105 -104
  56. package/dist/components/search-field/index.js.map +1 -1
  57. package/dist/components/sheet/index.d.ts +2 -2
  58. package/dist/components/sheet/index.js +22 -26
  59. package/dist/components/sheet/index.js.map +1 -1
  60. package/dist/components/snackbar/index.d.ts +3 -3
  61. package/dist/components/snackbar/index.js +36 -34
  62. package/dist/components/snackbar/index.js.map +1 -1
  63. package/dist/components/tag/index.d.ts +58 -0
  64. package/dist/components/tag/index.js +205 -0
  65. package/dist/components/tag/index.js.map +1 -0
  66. package/dist/components/text-field/index.d.ts +6 -3
  67. package/dist/components/text-field/index.js +3 -0
  68. package/dist/components/text-field/index.js.map +1 -1
  69. package/dist/field/index.js +39 -36
  70. package/dist/field/index.js.map +1 -1
  71. package/dist/glyphs/index.js +19 -1
  72. package/dist/glyphs/index.js.map +1 -1
  73. package/dist/i18n/index.js +12 -1
  74. package/dist/i18n/index.js.map +1 -1
  75. package/dist/i18n/messages.js +741 -0
  76. package/dist/i18n/messages.js.map +1 -1
  77. package/dist/i18n/provider.d.ts +53 -0
  78. package/dist/i18n/provider.js +96 -0
  79. package/dist/i18n/provider.js.map +1 -0
  80. package/dist/index.d.ts +5 -1
  81. package/dist/index.js +6 -2
  82. package/dist/indicator/index.js +1 -2
  83. package/dist/indicator/index.js.map +1 -1
  84. package/dist/mark/index.js +77 -117
  85. package/dist/mark/index.js.map +1 -1
  86. package/dist/mark/initials.js.map +1 -1
  87. package/dist/navigation/index.d.ts +8 -1
  88. package/dist/navigation/index.js +113 -64
  89. package/dist/navigation/index.js.map +1 -1
  90. package/dist/navigation/styles.js +22 -0
  91. package/dist/navigation/styles.js.map +1 -1
  92. package/dist/number/count.js +15 -0
  93. package/dist/number/count.js.map +1 -0
  94. package/dist/number/locale.js.map +1 -1
  95. package/dist/overlay/host.js +30 -21
  96. package/dist/overlay/host.js.map +1 -1
  97. package/package.json +3 -2
package/README.md CHANGED
@@ -155,6 +155,43 @@ export default function App() {
155
155
  }
156
156
  ```
157
157
 
158
+ ## Localisation
159
+
160
+ Wrap the app in `I18nProvider`, as kanso-ui is wrapped in React Aria's, and the
161
+ components write in its locale: the library's own words, such as a pending
162
+ ring's name, a bottom sheet's handle, a list's or a menu's loading and empty
163
+ rows and a calendar's chevrons, and its numbers and dates, such as a
164
+ `Currency`'s amount, a field's count, a `Meter`'s value and a `Calendar`'s
165
+ month. A component given a `locale` of its own writes in that one instead.
166
+
167
+ Outside any provider, and for a locale the runtime cannot parse, the components
168
+ write in the device's. So an app that follows the device needs no provider, and
169
+ one with a language setting of its own passes the setting, falling back to the
170
+ device's first language from Expo Localization:
171
+
172
+ ```tsx
173
+ // app/_layout.tsx
174
+ import { I18nProvider } from '@kanso-labs/react-native-kanso-ui'
175
+ import { useLocales } from 'expo-localization'
176
+ import { Stack } from 'expo-router'
177
+
178
+ export default function RootLayout() {
179
+ const [device] = useLocales()
180
+ const language = useLanguageSetting() // the app's own, or undefined
181
+
182
+ return (
183
+ <I18nProvider locale={language ?? device?.languageTag}>
184
+ <Stack />
185
+ </I18nProvider>
186
+ )
187
+ }
188
+ ```
189
+
190
+ `useLocale()` returns `{ direction, locale }`, as React Aria's does on web, for
191
+ an app's own formatting to match. The direction is `I18nManager`'s, which is
192
+ what React Native lays out by: the provider sets none, and an app turns its
193
+ layout over with `I18nManager.forceRTL` and a reload.
194
+
158
195
  ## Development
159
196
 
160
197
  Install with the Node version in `.tool-versions`:
@@ -1,30 +1,33 @@
1
1
  import { useTheme } from "../theme/scope.js";
2
2
  import { composeStyles } from "../theme/styles.js";
3
3
  import { useTypeRole } from "../typography/roles.js";
4
+ import { formatCount } from "../number/count.js";
5
+ import { useLocaleOf } from "../i18n/provider.js";
4
6
  import { styles, stylesOf } from "./styles.js";
5
7
  import { c } from "react/compiler-runtime";
6
8
  import { Text, View } from "react-native";
7
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
10
  //#region src/badge/index.tsx
9
- let countFormat;
10
11
  /**
11
12
  * A badge on the top trailing corner of the icon it wraps: the small dot, or
12
13
  * with `count` the large badge holding the number.
13
14
  */
14
15
  function Badge(t0) {
15
- const $ = c(12);
16
+ const $ = c(13);
16
17
  const { children, count, inFlow: t1, max: t2 } = t0;
17
18
  const inFlow = t1 === void 0 ? false : t1;
18
19
  const max = t2 === void 0 ? 999 : t2;
19
20
  const own = stylesOf(useTheme());
20
21
  const role = useTypeRole("labelSmall");
22
+ const locale = useLocaleOf(void 0);
21
23
  let t3;
22
- if ($[0] !== count || $[1] !== max) {
23
- t3 = count === void 0 ? void 0 : countOf(count, max);
24
+ if ($[0] !== count || $[1] !== locale || $[2] !== max) {
25
+ t3 = count === void 0 ? void 0 : countOf(count, max, locale);
24
26
  $[0] = count;
25
- $[1] = max;
26
- $[2] = t3;
27
- } else t3 = $[2];
27
+ $[1] = locale;
28
+ $[2] = max;
29
+ $[3] = t3;
30
+ } else t3 = $[3];
28
31
  const label = t3;
29
32
  const T0 = View;
30
33
  const t4 = styles;
@@ -39,34 +42,33 @@ function Badge(t0) {
39
42
  children: label
40
43
  })] });
41
44
  let t8;
42
- if ($[3] !== T1 || $[4] !== t6 || $[5] !== t7) {
45
+ if ($[4] !== T1 || $[5] !== t6 || $[6] !== t7) {
43
46
  t8 = /* @__PURE__ */ jsx(T1, {
44
47
  "aria-hidden": t5,
45
48
  style: t6,
46
49
  children: t7
47
50
  });
48
- $[3] = T1;
49
- $[4] = t6;
50
- $[5] = t7;
51
- $[6] = t8;
52
- } else t8 = $[6];
51
+ $[4] = T1;
52
+ $[5] = t6;
53
+ $[6] = t7;
54
+ $[7] = t8;
55
+ } else t8 = $[7];
53
56
  let t9;
54
- if ($[7] !== T0 || $[8] !== children || $[9] !== t4.anchor || $[10] !== t8) {
57
+ if ($[8] !== T0 || $[9] !== children || $[10] !== t4.anchor || $[11] !== t8) {
55
58
  t9 = /* @__PURE__ */ jsxs(T0, {
56
59
  style: t4.anchor,
57
60
  children: [children, t8]
58
61
  });
59
- $[7] = T0;
60
- $[8] = children;
61
- $[9] = t4.anchor;
62
- $[10] = t8;
63
- $[11] = t9;
64
- } else t9 = $[11];
62
+ $[8] = T0;
63
+ $[9] = children;
64
+ $[10] = t4.anchor;
65
+ $[11] = t8;
66
+ $[12] = t9;
67
+ } else t9 = $[12];
65
68
  return t9;
66
69
  }
67
- function countOf(count, max) {
68
- countFormat ??= new Intl.NumberFormat();
69
- return count > max ? `${countFormat.format(max)}+` : countFormat.format(count);
70
+ function countOf(count, max, locale) {
71
+ return count > max ? `${formatCount(max, locale)}+` : formatCount(count, locale);
70
72
  }
71
73
  //#endregion
72
74
  export { Badge };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["ReactNode","TextStyle","ViewStyle","Text","NativeText","View","useTheme","composeStyles","useTypeRole","styles","stylesOf","BadgeProps","children","count","inFlow","max","countFormat","Intl","NumberFormat","Badge","t0","$","_c","t1","t2","undefined","own","role","t3","countOf","label","T0","T1","t5","t6","mark","small","large","over","t7","space","t8","t9","t4","anchor","format","_c","NativeText"],"sources":["../../src/badge/index.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport type { TextStyle, ViewStyle } from 'react-native'\n\nimport { Text as NativeText, View } from 'react-native'\n\nimport { useTheme } from '../theme/scope'\nimport { composeStyles } from '../theme/styles'\nimport { useTypeRole } from '../typography/roles'\nimport { styles, stylesOf } from './styles'\n\n// The badges page's badge, as kanso-ui's Badge draws it on a navigation\n// destination: a mark on the corner of an icon that flags something new\n// there, either as the 6dp small badge or as the 16dp large badge holding a\n// count. Both are the error pair, the error role with the count in on error,\n// on a full corner, and the count is label small, capped with a plus, as\n// \"999+\".\n//\n// The badge wraps the icon and places the mark against the icon's box,\n// which is what the page measures from. The small badge fills the icon's top\n// trailing 6dp square. The large badge's bottom leading corner sits 14dp\n// down and 12dp in from the icon's top trailing corner, so it stands 2dp\n// above the icon and grows toward the end as its count lengthens. Both are\n// placed by `start` and `end`, so the mark stays on the trailing corner under\n// right-to-left.\n//\n// Where a label stands beside the icon rather than under it, the large badge\n// joins the flow after the icon instead, so the label starts after it rather\n// than under it: `inFlow`. The small badge stays inside the icon's box, so\n// nothing has to make room for it.\n//\n// The mark is hidden from a screen reader. A count read on its own is a\n// number with no subject; it belongs in the name of the control it sits on,\n// where a reader meets it with what it counts, \"First item, 3 new\".\n//\n// The count is set apart from the text before it by a space drawn out of the\n// flow. Under React Native Web the count is still text inside the control,\n// and with nothing between them a label and its count ran together as\n// \"Second item3\", a word in no name: what axe's label-in-name check fails,\n// and not what a speech-input user would say. Web draws the count as\n// generated content instead, which a style here has no counterpart for.\n\ntype BadgeProps = {\n /** What the badge is drawn on: an icon, at 24dp. */\n readonly children?: ReactNode\n /**\n * The number to show, which draws the large badge. Left out, the badge is\n * the small dot, which flags without counting.\n */\n readonly count?: number | undefined\n /**\n * Draws the large badge in the flow after the icon rather than over its\n * corner, for a label beside the icon.\n * @default false\n */\n readonly inFlow?: boolean\n /**\n * The largest count drawn as itself. Above it the badge reads `max` and a\n * plus, which is the page's 999+ at the default.\n * @default 999\n */\n readonly max?: number\n}\n\nlet countFormat: Intl.NumberFormat | undefined\n\n/**\n * A badge on the top trailing corner of the icon it wraps: the small dot, or\n * with `count` the large badge holding the number.\n */\nfunction Badge({ children, count, inFlow = false, max = 999 }: BadgeProps) {\n const own = stylesOf(useTheme())\n const role = useTypeRole('labelSmall')\n const label = count === undefined ? undefined : countOf(count, max)\n\n return (\n <View style={styles.anchor}>\n {children}\n <View\n aria-hidden\n style={composeStyles<ViewStyle>(\n own.mark,\n label === undefined\n ? styles.small\n : [own.large, inFlow ? styles.inFlow : styles.over],\n )}\n >\n {label === undefined ? null : (\n <>\n <NativeText style={styles.space}> </NativeText>\n <NativeText style={composeStyles<TextStyle>(role, own.count)}>\n {label}\n </NativeText>\n </>\n )}\n </View>\n </View>\n )\n}\n\n// The count in the device's digits and grouping, as web writes it in the\n// reader's locale. One formatter, since a badge's count changes often.\nfunction countOf(count: number, max: number) {\n countFormat ??= new Intl.NumberFormat()\n\n return count > max ? `${countFormat.format(max)}+` : countFormat.format(count)\n}\n\nexport type { BadgeProps }\n\nexport { Badge }\n"],"mappings":";;;;;;;;AA+DA,IAAIgB;;;;;AAMJ,SAAAG,MAAAC,IAAA;CAAA,MAAAC,IAAAC,EAAA,EAAA;CAAe,MAAA,EAAAV,UAAAC,OAAAC,QAAAS,IAAAR,KAAAS,OAAAJ;CAAmB,MAAAN,SAAAS,OAAAE,KAAAA,IAAA,QAAAF;CAAgB,MAAAR,MAAAS,OAAAC,KAAAA,IAAA,MAAAD;CAChD,MAAAE,MAAYhB,SAASJ,SAAS,CAAC;CAC/B,MAAAqB,OAAanB,YAAY,YAAY;CAAC,IAAAoB;CAAA,IAAAP,EAAA,OAAAR,SAAAQ,EAAA,OAAAN,KAAA;EACxBa,KAAAf,UAAUY,KAAAA,IAAVA,KAAAA,IAAkCI,QAAQhB,OAAOE,GAAG;EAACM,EAAA,KAAAR;EAAAQ,EAAA,KAAAN;EAAAM,EAAA,KAAAO;CAAA,OAAAA,KAAAP,EAAA;CAAnE,MAAAS,QAAcF;CAGX,MAAAG,KAAA1B;CAAYI,MAAAA,KAAAA;CAEV,MAAAuB,KAAA3B;CACC,MAAA4B,KAAA;CACO,MAAAC,KAAA3B,cACLmB,IAAGS,MACHL,UAAUL,KAAAA,IACNhB,OAAM2B,QADV,CAEKV,IAAGW,OAAQvB,SAASL,OAAMK,SAAUL,OAAM6B,IAAK,CACtD;CAEC,MAAAC,KAAAT,UAAUL,KAAAA,IAAV,OAAA,qBAAA,UAAA,EAAA,UAAA,CAEG,oBAACsB,MAAD;EAAmB,OAAAtC,OAAM+B;EAAQ,UAAA;CAAtB,CAAA,GACX,oBAACO,MAAD;EAAmB,OAAAxC,cAAyBoB,MAAMD,IAAGb,KAAM;EACxDiB,UAAAA;CADQ,CAAA,CAEE,EAAA,CAAA;CAEhB,IAAAW;CAAA,IAAApB,EAAA,OAAAW,MAAAX,EAAA,OAAAa,MAAAb,EAAA,OAAAkB,IAAA;EAhBHE,KAAA,oBAAC,IAAD;GACE,eAAAR;GACO,OAAAC;GAONK,UAAAA;EATE,CAAA;EAiBElB,EAAA,KAAAW;EAAAX,EAAA,KAAAa;EAAAb,EAAA,KAAAkB;EAAAlB,EAAA,KAAAoB;CAAA,OAAAA,KAAApB,EAAA;CAAA,IAAAqB;CAAA,IAAArB,EAAA,OAAAU,MAAAV,EAAA,OAAAT,YAAAS,EAAA,OAAAsB,GAAAC,UAAAvB,EAAA,QAAAoB,IAAA;EAnBTC,KAAA,qBAAC,IAAD;GAAa,OAAAjC,GAAMmC;GAAnB,UAAA,CACGhC,UACD6B,EAFG;;EAoBEpB,EAAA,KAAAU;EAAAV,EAAA,KAAAT;EAAAS,EAAA,KAAAsB,GAAAC;EAAAvB,EAAA,MAAAoB;EAAApB,EAAA,MAAAqB;CAAA,OAAAA,KAAArB,EAAA;CAAA,OApBPqB;AAoBO;AAMX,SAASb,QAAQhB,OAAeE,KAAa;CAC3CC,gBAAgB,IAAIC,KAAKC,aAAa;CAEtC,OAAOL,QAAQE,MAAM,GAAGC,YAAY6B,OAAO9B,GAAG,EAAC,KAAMC,YAAY6B,OAAOhC,KAAK;AAC/E"}
1
+ {"version":3,"file":"index.js","names":["ReactNode","TextStyle","ViewStyle","Text","NativeText","View","useLocaleOf","formatCount","useTheme","composeStyles","useTypeRole","styles","stylesOf","BadgeProps","children","count","inFlow","max","Badge","t0","$","_c","t1","t2","undefined","own","role","locale","t3","countOf","label","T0","T1","t5","t6","mark","small","large","over","t7","space","t8","t9","t4","anchor","_c","NativeText"],"sources":["../../src/badge/index.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport type { TextStyle, ViewStyle } from 'react-native'\n\nimport { Text as NativeText, View } from 'react-native'\n\nimport { useLocaleOf } from '../i18n/provider'\nimport { formatCount } from '../number/count'\nimport { useTheme } from '../theme/scope'\nimport { composeStyles } from '../theme/styles'\nimport { useTypeRole } from '../typography/roles'\nimport { styles, stylesOf } from './styles'\n\n// The badges page's badge, as kanso-ui's Badge draws it on a navigation\n// destination: a mark on the corner of an icon that flags something new\n// there, either as the 6dp small badge or as the 16dp large badge holding a\n// count. Both are the error pair, the error role with the count in on error,\n// on a full corner, and the count is label small, capped with a plus, as\n// \"999+\".\n//\n// The badge wraps the icon and places the mark against the icon's box,\n// which is what the page measures from. The small badge fills the icon's top\n// trailing 6dp square. The large badge's bottom leading corner sits 14dp\n// down and 12dp in from the icon's top trailing corner, so it stands 2dp\n// above the icon and grows toward the end as its count lengthens. Both are\n// placed by `start` and `end`, so the mark stays on the trailing corner under\n// right-to-left.\n//\n// Where a label stands beside the icon rather than under it, the large badge\n// joins the flow after the icon instead, so the label starts after it rather\n// than under it: `inFlow`. The small badge stays inside the icon's box, so\n// nothing has to make room for it.\n//\n// The mark is hidden from a screen reader. A count read on its own is a\n// number with no subject; it belongs in the name of the control it sits on,\n// where a reader meets it with what it counts, \"First item, 3 new\".\n//\n// The count is set apart from the text before it by a space drawn out of the\n// flow. Under React Native Web the count is still text inside the control,\n// and with nothing between them a label and its count ran together as\n// \"Second item3\", a word in no name: what axe's label-in-name check fails,\n// and not what a speech-input user would say. Web draws the count as\n// generated content instead, which a style here has no counterpart for.\n\ntype BadgeProps = {\n /** What the badge is drawn on: an icon, at 24dp. */\n readonly children?: ReactNode\n /**\n * The number to show, which draws the large badge. Left out, the badge is\n * the small dot, which flags without counting.\n */\n readonly count?: number | undefined\n /**\n * Draws the large badge in the flow after the icon rather than over its\n * corner, for a label beside the icon.\n * @default false\n */\n readonly inFlow?: boolean\n /**\n * The largest count drawn as itself. Above it the badge reads `max` and a\n * plus, which is the page's 999+ at the default.\n * @default 999\n */\n readonly max?: number\n}\n\n/**\n * A badge on the top trailing corner of the icon it wraps: the small dot, or\n * with `count` the large badge holding the number.\n */\nfunction Badge({ children, count, inFlow = false, max = 999 }: BadgeProps) {\n const own = stylesOf(useTheme())\n const role = useTypeRole('labelSmall')\n const locale = useLocaleOf(undefined)\n const label = count === undefined ? undefined : countOf(count, max, locale)\n\n return (\n <View style={styles.anchor}>\n {children}\n <View\n aria-hidden\n style={composeStyles<ViewStyle>(\n own.mark,\n label === undefined\n ? styles.small\n : [own.large, inFlow ? styles.inFlow : styles.over],\n )}\n >\n {label === undefined ? null : (\n <>\n <NativeText style={styles.space}> </NativeText>\n <NativeText style={composeStyles<TextStyle>(role, own.count)}>\n {label}\n </NativeText>\n </>\n )}\n </View>\n </View>\n )\n}\n\n// The count in the locale's digits and grouping, as web writes it in the\n// provider's.\nfunction countOf(count: number, max: number, locale: string) {\n return count > max\n ? `${formatCount(max, locale)}+`\n : formatCount(count, locale)\n}\n\nexport type { BadgeProps }\n\nexport { Badge }\n"],"mappings":";;;;;;;;;;;;;;AAqEA,SAAAkB,MAAAC,IAAA;CAAA,MAAAC,IAAAC,EAAA,EAAA;CAAe,MAAA,EAAAP,UAAAC,OAAAC,QAAAM,IAAAL,KAAAM,OAAAJ;CAAmB,MAAAH,SAAAM,OAAAE,KAAAA,IAAA,QAAAF;CAAgB,MAAAL,MAAAM,OAAAC,KAAAA,IAAA,MAAAD;CAChD,MAAAE,MAAYb,SAASJ,SAAS,CAAC;CAC/B,MAAAkB,OAAahB,YAAY,YAAY;CACrC,MAAAiB,SAAerB,YAAYkB,KAAAA,CAAS;CAAC,IAAAI;CAAA,IAAAR,EAAA,OAAAL,SAAAK,EAAA,OAAAO,UAAAP,EAAA,OAAAH,KAAA;EACvBW,KAAAb,UAAUS,KAAAA,IAAVA,KAAAA,IAAkCK,QAAQd,OAAOE,KAAKU,MAAM;EAACP,EAAA,KAAAL;EAAAK,EAAA,KAAAO;EAAAP,EAAA,KAAAH;EAAAG,EAAA,KAAAQ;CAAA,OAAAA,KAAAR,EAAA;CAA3E,MAAAU,QAAcF;CAGX,MAAAG,KAAA1B;CAAYM,MAAAA,KAAAA;CAEV,MAAAqB,KAAA3B;CACC,MAAA4B,KAAA;CACO,MAAAC,KAAAzB,cACLgB,IAAGU,MACHL,UAAUN,KAAAA,IACNb,OAAMyB,QADV,CAEKX,IAAGY,OAAQrB,SAASL,OAAMK,SAAUL,OAAM2B,IAAK,CACtD;CAEC,MAAAC,KAAAT,UAAUN,KAAAA,IAAV,OAAA,qBAAA,UAAA,EAAA,UAAA,CAEG,oBAACsB,MAAD;EAAmB,OAAAnC,OAAM6B;EAAQ,UAAA;CAAtB,CAAA,GACX,oBAACM,MAAD;EAAmB,OAAArC,cAAyBiB,MAAMD,IAAGV,KAAM;EACxDe,UAAAA;CADQ,CAAA,CAEE,EAAA,CAAA;CAEhB,IAAAW;CAAA,IAAArB,EAAA,OAAAY,MAAAZ,EAAA,OAAAc,MAAAd,EAAA,OAAAmB,IAAA;EAhBHE,KAAA,oBAAC,IAAD;GACE,eAAAR;GACO,OAAAC;GAONK,UAAAA;EATE,CAAA;EAiBEnB,EAAA,KAAAY;EAAAZ,EAAA,KAAAc;EAAAd,EAAA,KAAAmB;EAAAnB,EAAA,KAAAqB;CAAA,OAAAA,KAAArB,EAAA;CAAA,IAAAsB;CAAA,IAAAtB,EAAA,OAAAW,MAAAX,EAAA,OAAAN,YAAAM,EAAA,QAAAuB,GAAAC,UAAAxB,EAAA,QAAAqB,IAAA;EAnBTC,KAAA,qBAAC,IAAD;GAAa,OAAA/B,GAAMiC;GAAnB,UAAA,CACG9B,UACD2B,EAFG;;EAoBErB,EAAA,KAAAW;EAAAX,EAAA,KAAAN;EAAAM,EAAA,MAAAuB,GAAAC;EAAAxB,EAAA,MAAAqB;EAAArB,EAAA,MAAAsB;CAAA,OAAAA,KAAAtB,EAAA;CAAA,OApBPsB;AAoBO;AAMX,SAASb,QAAQd,OAAeE,KAAaU,QAAgB;CAC3D,OAAOZ,QAAQE,MACX,GAAGV,YAAYU,KAAKU,MAAM,EAAC,KAC3BpB,YAAYQ,OAAOY,MAAM;AAC/B"}
@@ -38,7 +38,8 @@ type ButtonPending = {
38
38
  /**
39
39
  * The name of the ring shown while the button is pending, for a screen
40
40
  * reader. The content it replaces is hidden while it shows. Left out, it is
41
- * the word for it in the device's locale, "Loading" in English.
41
+ * the word for it in the `I18nProvider`'s locale, or the device's outside
42
+ * one, "Loading" in English.
42
43
  */
43
44
  readonly pendingLabel?: string;
44
45
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["Ref","GestureResponderEvent","HostInstance","LayoutChangeEvent","PressableProps","SharedValue","use","useState","Platform","Pressable","StyleSheet","Animated","Easing","useAnimatedStyle","useReducedMotion","useSharedValue","withTiming","ResolvedTheme","FormContext","refsOf","useOpenLink","useTheme","ButtonSlotContext","ButtonLinkForm","defaultSelected","href","isPending","isSelected","onChange","pendingLabel","slot","type","ButtonPending","ButtonPressForm","ButtonToggleForm","ControlSize","SlotTriggerProps","delayLongPress","onHoverIn","onHoverOut","onLongPress","ref","ToggleState","HEIGHTS","lg","md","xl","xs","xxl","const","Record","OUTLINE_WIDTHS","Readonly","PRESSED_CORNERS","AnimatedPressable","createAnimatedComponent","toggleState","select","selected","default","web","buttonStyles","create","clip","bottom","left","overflow","pointerEvents","position","right","top","andThen","own","event","E","undefined","measureThen","height","onLayout","set","nativeEvent","layout","pressThen","onPress","then","useButtonAction","t0","$","_c","isToggle","openLink","form","slots","ownSelected","setOwnSelected","filled","t1","follow","requestSubmit","t2","t3","useCorner","radius","motion","reduceMotion","durationShort2","easingEmphasized","bezier","duration","easing","timing","corner","measured","get","target","Math","min","borderRadius","t4","useSlotTrigger","trigger","expanded","_c"],"sources":["../../src/button/index.ts"],"sourcesContent":["import type { Ref } from 'react'\nimport type {\n GestureResponderEvent,\n HostInstance,\n LayoutChangeEvent,\n PressableProps,\n} from 'react-native'\nimport type { SharedValue } from 'react-native-reanimated'\n\nimport { use, useState } from 'react'\nimport { Platform, Pressable, StyleSheet } from 'react-native'\nimport Animated, {\n Easing,\n useAnimatedStyle,\n useReducedMotion,\n useSharedValue,\n withTiming,\n} from 'react-native-reanimated'\n\nimport type { ResolvedTheme } from '../theme/scope'\n\nimport { FormContext } from '../form'\nimport { refsOf } from '../ref'\nimport { useOpenLink } from '../router'\nimport { useTheme } from '../theme/scope'\nimport { ButtonSlotContext } from './slots'\n\n// What every button does past its own styles, shared by Button and\n// IconButton as web's src/button is: the three forms a button takes, the\n// toggle's state and how it is announced, what a press does after the call\n// site's own `onPress`, a submit button's included, and what the slot it\n// fills in an overlay adds to it, the corner that eases between shapes, and\n// the metrics\n// the two components' pages give alike, since the two share their five\n// heights so that they line up in a row. What a pending button draws and\n// says is ./pending.tsx.\n\n/**\n * A plain button, a link, or a toggle, of which a call site writes exactly\n * one. Each form names the props of the other two as `never`, so a toggle\n * given `href` is a compile error rather than a prop that silently does\n * nothing.\n */\ntype ButtonLinkForm = {\n readonly defaultSelected?: never\n /**\n * Where the button leads. Given one, a press opens it through the\n * `RouterProvider` around the button, or through the platform for a URL\n * with a scheme of its own, and the button is announced as a link.\n */\n readonly href: string\n readonly isPending?: never\n readonly isSelected?: never\n readonly onChange?: never\n readonly pendingLabel?: never\n readonly slot?: never\n readonly type?: never\n}\n\n/**\n * Whether the button's action is under way, which the plain button and the\n * toggle both take and the link does not.\n */\ntype ButtonPending = {\n /**\n * Shows that the button's action is under way: a ring takes the place of\n * the label and the icon, which keep their space, and the button keeps its\n * focus and takes no press until it clears. A toggle may be pending, for a\n * state saved somewhere slow.\n * @default false\n */\n readonly isPending?: boolean\n /**\n * The name of the ring shown while the button is pending, for a screen\n * reader. The content it replaces is hidden while it shows. Left out, it is\n * the word for it in the device's locale, \"Loading\" in English.\n */\n readonly pendingLabel?: string\n}\n\ntype ButtonPressForm = ButtonPending & {\n readonly defaultSelected?: never\n readonly href?: never\n readonly isSelected?: never\n readonly onChange?: never\n /**\n * `close` closes the overlay the button is in, as web's React Aria slot of\n * that name does: a `Dialog` given one inside its content closes when it is\n * pressed, after the button's own `onPress`.\n */\n readonly slot?: 'close'\n /**\n * What a press does past `onPress`. `submit` submits the `Form` the button\n * is in, as a submit button does on web; `button` does nothing more.\n * @default 'button'\n */\n readonly type?: 'button' | 'submit'\n}\n\ntype ButtonToggleForm = ButtonPending & {\n /**\n * Whether a toggle starts selected, when it keeps its own state. Passing\n * this, `isSelected` or `onChange` is what makes the button a toggle.\n */\n readonly defaultSelected?: boolean\n readonly href?: never\n /**\n * Whether a toggle is selected, when the call site holds the state. Pass\n * it with `onChange`; passing this, `defaultSelected` or `onChange` is what\n * makes the button a toggle.\n */\n readonly isSelected?: boolean\n /**\n * Called with the new state when a toggle is pressed, after `onPress`.\n * Passing this, `isSelected` or `defaultSelected` is what makes the button\n * a toggle.\n */\n readonly onChange?: (isSelected: boolean) => void\n readonly slot?: never\n readonly type?: never\n}\n\n/** The five control heights both pages give, by the names both props take. */\ntype ControlSize = 'lg' | 'md' | 'xl' | 'xs' | 'xxl'\n\n/** How a toggle reports its state to a screen reader. */\n// What `useSlotTrigger` hands the Pressable, spelled out because the\n// composed ref's inferred type names a private React Native module, which a\n// declaration file cannot reach.\ntype SlotTriggerProps = {\n readonly 'aria-expanded'?: boolean\n readonly delayLongPress: PressableProps['delayLongPress']\n readonly onHoverIn: PressableProps['onHoverIn']\n readonly onHoverOut: PressableProps['onHoverOut']\n readonly onLongPress: PressableProps['onLongPress']\n readonly ref: Ref<HostInstance> | undefined\n}\n\ntype ToggleState = {\n readonly 'aria-pressed'?: boolean\n readonly 'aria-selected'?: boolean\n}\n\n// The control height each size reads: the page's XS, S, M, L and XL, which\n// the theme's sizing scale names one step apart from this prop.\nconst HEIGHTS = {\n lg: 'controlLg',\n md: 'controlSm',\n xl: 'controlXl',\n xs: 'controlXs',\n xxl: 'controlXxl',\n} as const satisfies Record<ControlSize, keyof ResolvedTheme['sizing']>\n\n// The outlined rule thickens with the size, as the buttons page's size tokens\n// have it: 1dp up to M, 2dp at L, 3dp at XL. The icon buttons page keeps its\n// widths in a token set its widget will not open, so the icon button takes\n// these, which a 96dp icon button beside a 96dp outlined button needs anyway.\nconst OUTLINE_WIDTHS: Readonly<Record<ControlSize, number>> = {\n lg: 1,\n md: 1,\n xl: 2,\n xs: 1,\n xxl: 3,\n}\n\n// The corner each size presses to, which both pages give alike.\nconst PRESSED_CORNERS = {\n lg: 'md',\n md: 'sm',\n xl: 'lg',\n xs: 'sm',\n xxl: 'lg',\n} as const satisfies Record<ControlSize, keyof ResolvedTheme['radii']>\n\nconst AnimatedPressable = Animated.createAnimatedComponent(Pressable)\n\n// Web's toggle reports `aria-pressed`, which React Native Web writes onto the\n// element. React Native has no pressed state to report, and reports the\n// selected one instead, which VoiceOver and TalkBack both read out.\nconst toggleState = Platform.select<(selected: boolean) => ToggleState>({\n default: (selected) => ({ 'aria-selected': selected }),\n web: (selected) => ({ 'aria-pressed': selected }),\n})\n\nconst buttonStyles = StyleSheet.create({\n // Clips the state layer and the ripple to the corner as it morphs, and\n // takes no touches.\n clip: {\n bottom: 0,\n left: 0,\n overflow: 'hidden',\n pointerEvents: 'none',\n position: 'absolute',\n right: 0,\n top: 0,\n },\n})\n\n// A handler that runs the call site's own and then the slot's, which takes no\n// event, or the call site's alone where the slot adds nothing.\nfunction andThen<E>(\n own: ((event: E) => void) | null | undefined,\n slot: (() => void) | undefined,\n) {\n if (slot === undefined) {\n return own ?? undefined\n }\n\n return (event: E) => {\n own?.(event)\n slot()\n }\n}\n\n// Records the button's height for the corner, then calls the call site's own\n// `onLayout`. Built by a call rather than declared in a component, which is\n// what react-perf's jsx-no-new-function-as-prop is after; the React Compiler\n// memoises the result on its inputs.\nfunction measureThen(\n height: SharedValue<number>,\n onLayout: PressableProps['onLayout'],\n) {\n return (event: LayoutChangeEvent) => {\n height.set(event.nativeEvent.layout.height)\n onLayout?.(event)\n }\n}\n\n// The call site's own `onPress`, then what the button does with the press.\n// Built by a call for the reason `measureThen` is.\nfunction pressThen(\n onPress: PressableProps['onPress'],\n then: (event: GestureResponderEvent) => void,\n) {\n return (event: GestureResponderEvent) => {\n onPress?.(event)\n then(event)\n }\n}\n\n/**\n * What a press does, and whether a toggle is selected. A press calls the call\n * site's own `onPress` first, then opens the link, changes the toggle's state\n * or submits the form the button is in, which is the order web reports them\n * in. A toggle keeps its own state from `defaultSelected` unless the call\n * site holds it in `isSelected`. A submit button outside a form submits\n * nothing, as on web.\n *\n * Given a `slot`, a button that is not a link then does what that slot of\n * the overlay around it does, as src/button's slots.ts sets out: opens it\n * from the trigger, or closes it from its content.\n */\nfunction useButtonAction({\n defaultSelected,\n href,\n isSelected,\n isToggle,\n onChange,\n onPress,\n slot,\n type,\n}: {\n readonly defaultSelected: boolean | undefined\n readonly href: string | undefined\n readonly isSelected: boolean | undefined\n readonly isToggle: boolean\n readonly onChange: ((isSelected: boolean) => void) | undefined\n readonly onPress: PressableProps['onPress']\n readonly slot?: 'close' | 'default'\n readonly type: 'button' | 'submit' | undefined\n}) {\n const openLink = useOpenLink()\n const form = use(FormContext)\n const slots = use(ButtonSlotContext)\n const [ownSelected, setOwnSelected] = useState(defaultSelected ?? false)\n const selected = isToggle && (isSelected ?? ownSelected)\n const filled = slot === undefined ? undefined : slots?.[slot]\n\n function follow(event: GestureResponderEvent) {\n if (href !== undefined) {\n openLink(href)\n return\n }\n if (isToggle) {\n setOwnSelected(!selected)\n onChange?.(!selected)\n } else if (type === 'submit') {\n form?.requestSubmit()\n }\n filled?.(event)\n }\n\n return { onPress: pressThen(onPress, follow), selected }\n}\n\n/**\n * The corner a button eases to, over the motion tokens' short duration on the\n * emphasised curve, and at once for a reader who asks the device for reduced\n * motion. The pill is the radius scale's `pill`, far larger than any button,\n * so the ease runs from half the button's measured height rather than from\n * that: otherwise the corner would hold the pill for most of the ease and\n * jump at its end. The `onLayout` it hands back measures the button, then\n * calls the call site's own.\n */\nfunction useCorner(radius: number, onLayout: PressableProps['onLayout']) {\n const { motion } = useTheme()\n const reduceMotion = useReducedMotion()\n const height = useSharedValue(0)\n const timing = {\n duration: motion.durationShort2,\n easing: Easing.bezier(...motion.easingEmphasized),\n }\n const corner = useAnimatedStyle(() => {\n 'worklet'\n const measured = height.get()\n const target = measured > 0 ? Math.min(radius, measured / 2) : radius\n\n return {\n borderRadius: reduceMotion ? target : withTiming(target, timing),\n }\n })\n\n return { corner, onLayout: measureThen(height, onLayout) }\n}\n\n/**\n * What a button filling the default slot of an overlay drawn beside it takes\n * on, as src/button's slots.ts sets out: its node is kept for the overlay to\n * be placed against, alongside the call site's own `ref`, `aria-expanded`\n * says whether the overlay is open, and the overlay's hover and long press\n * handlers run after the call site's. A button filling no such slot, or a\n * link, keeps its own props untouched.\n */\nfunction useSlotTrigger({\n delayLongPress,\n href,\n onHoverIn,\n onHoverOut,\n onLongPress,\n ref,\n slot,\n}: {\n readonly delayLongPress: PressableProps['delayLongPress']\n readonly href: string | undefined\n readonly onHoverIn: PressableProps['onHoverIn']\n readonly onHoverOut: PressableProps['onHoverOut']\n readonly onLongPress: PressableProps['onLongPress']\n readonly ref: Ref<HostInstance> | undefined\n readonly slot: 'close' | undefined\n}): SlotTriggerProps {\n const slots = use(ButtonSlotContext)\n const trigger =\n href === undefined && slot === undefined ? slots?.trigger : undefined\n\n return {\n ...(trigger === undefined\n ? { delayLongPress, ref }\n : {\n 'aria-expanded': trigger.expanded,\n delayLongPress: trigger.delayLongPress ?? delayLongPress,\n ref: refsOf(trigger.ref, ref),\n }),\n onHoverIn: andThen(onHoverIn, trigger?.onHoverIn),\n onHoverOut: andThen(onHoverOut, trigger?.onHoverOut),\n onLongPress: andThen(onLongPress, trigger?.onLongPress),\n }\n}\n\nexport {\n AnimatedPressable,\n buttonStyles,\n HEIGHTS,\n OUTLINE_WIDTHS,\n PRESSED_CORNERS,\n toggleState,\n useButtonAction,\n useCorner,\n useSlotTrigger,\n}\nexport type { ButtonLinkForm, ButtonPressForm, ButtonToggleForm, ControlSize }\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["Ref","GestureResponderEvent","HostInstance","LayoutChangeEvent","PressableProps","SharedValue","use","useState","Platform","Pressable","StyleSheet","Animated","Easing","useAnimatedStyle","useReducedMotion","useSharedValue","withTiming","ResolvedTheme","FormContext","refsOf","useOpenLink","useTheme","ButtonSlotContext","ButtonLinkForm","defaultSelected","href","isPending","isSelected","onChange","pendingLabel","slot","type","ButtonPending","ButtonPressForm","ButtonToggleForm","ControlSize","SlotTriggerProps","delayLongPress","onHoverIn","onHoverOut","onLongPress","ref","ToggleState","HEIGHTS","lg","md","xl","xs","xxl","const","Record","OUTLINE_WIDTHS","Readonly","PRESSED_CORNERS","AnimatedPressable","createAnimatedComponent","toggleState","select","selected","default","web","buttonStyles","create","clip","bottom","left","overflow","pointerEvents","position","right","top","andThen","own","event","E","undefined","measureThen","height","onLayout","set","nativeEvent","layout","pressThen","onPress","then","useButtonAction","t0","$","_c","isToggle","openLink","form","slots","ownSelected","setOwnSelected","filled","t1","follow","requestSubmit","t2","t3","useCorner","radius","motion","reduceMotion","durationShort2","easingEmphasized","bezier","duration","easing","timing","corner","measured","get","target","Math","min","borderRadius","t4","useSlotTrigger","trigger","expanded","_c"],"sources":["../../src/button/index.ts"],"sourcesContent":["import type { Ref } from 'react'\nimport type {\n GestureResponderEvent,\n HostInstance,\n LayoutChangeEvent,\n PressableProps,\n} from 'react-native'\nimport type { SharedValue } from 'react-native-reanimated'\n\nimport { use, useState } from 'react'\nimport { Platform, Pressable, StyleSheet } from 'react-native'\nimport Animated, {\n Easing,\n useAnimatedStyle,\n useReducedMotion,\n useSharedValue,\n withTiming,\n} from 'react-native-reanimated'\n\nimport type { ResolvedTheme } from '../theme/scope'\n\nimport { FormContext } from '../form'\nimport { refsOf } from '../ref'\nimport { useOpenLink } from '../router'\nimport { useTheme } from '../theme/scope'\nimport { ButtonSlotContext } from './slots'\n\n// What every button does past its own styles, shared by Button and\n// IconButton as web's src/button is: the three forms a button takes, the\n// toggle's state and how it is announced, what a press does after the call\n// site's own `onPress`, a submit button's included, and what the slot it\n// fills in an overlay adds to it, the corner that eases between shapes, and\n// the metrics\n// the two components' pages give alike, since the two share their five\n// heights so that they line up in a row. What a pending button draws and\n// says is ./pending.tsx.\n\n/**\n * A plain button, a link, or a toggle, of which a call site writes exactly\n * one. Each form names the props of the other two as `never`, so a toggle\n * given `href` is a compile error rather than a prop that silently does\n * nothing.\n */\ntype ButtonLinkForm = {\n readonly defaultSelected?: never\n /**\n * Where the button leads. Given one, a press opens it through the\n * `RouterProvider` around the button, or through the platform for a URL\n * with a scheme of its own, and the button is announced as a link.\n */\n readonly href: string\n readonly isPending?: never\n readonly isSelected?: never\n readonly onChange?: never\n readonly pendingLabel?: never\n readonly slot?: never\n readonly type?: never\n}\n\n/**\n * Whether the button's action is under way, which the plain button and the\n * toggle both take and the link does not.\n */\ntype ButtonPending = {\n /**\n * Shows that the button's action is under way: a ring takes the place of\n * the label and the icon, which keep their space, and the button keeps its\n * focus and takes no press until it clears. A toggle may be pending, for a\n * state saved somewhere slow.\n * @default false\n */\n readonly isPending?: boolean\n /**\n * The name of the ring shown while the button is pending, for a screen\n * reader. The content it replaces is hidden while it shows. Left out, it is\n * the word for it in the `I18nProvider`'s locale, or the device's outside\n * one, \"Loading\" in English.\n */\n readonly pendingLabel?: string\n}\n\ntype ButtonPressForm = ButtonPending & {\n readonly defaultSelected?: never\n readonly href?: never\n readonly isSelected?: never\n readonly onChange?: never\n /**\n * `close` closes the overlay the button is in, as web's React Aria slot of\n * that name does: a `Dialog` given one inside its content closes when it is\n * pressed, after the button's own `onPress`.\n */\n readonly slot?: 'close'\n /**\n * What a press does past `onPress`. `submit` submits the `Form` the button\n * is in, as a submit button does on web; `button` does nothing more.\n * @default 'button'\n */\n readonly type?: 'button' | 'submit'\n}\n\ntype ButtonToggleForm = ButtonPending & {\n /**\n * Whether a toggle starts selected, when it keeps its own state. Passing\n * this, `isSelected` or `onChange` is what makes the button a toggle.\n */\n readonly defaultSelected?: boolean\n readonly href?: never\n /**\n * Whether a toggle is selected, when the call site holds the state. Pass\n * it with `onChange`; passing this, `defaultSelected` or `onChange` is what\n * makes the button a toggle.\n */\n readonly isSelected?: boolean\n /**\n * Called with the new state when a toggle is pressed, after `onPress`.\n * Passing this, `isSelected` or `defaultSelected` is what makes the button\n * a toggle.\n */\n readonly onChange?: (isSelected: boolean) => void\n readonly slot?: never\n readonly type?: never\n}\n\n/** The five control heights both pages give, by the names both props take. */\ntype ControlSize = 'lg' | 'md' | 'xl' | 'xs' | 'xxl'\n\n/** How a toggle reports its state to a screen reader. */\n// What `useSlotTrigger` hands the Pressable, spelled out because the\n// composed ref's inferred type names a private React Native module, which a\n// declaration file cannot reach.\ntype SlotTriggerProps = {\n readonly 'aria-expanded'?: boolean\n readonly delayLongPress: PressableProps['delayLongPress']\n readonly onHoverIn: PressableProps['onHoverIn']\n readonly onHoverOut: PressableProps['onHoverOut']\n readonly onLongPress: PressableProps['onLongPress']\n readonly ref: Ref<HostInstance> | undefined\n}\n\ntype ToggleState = {\n readonly 'aria-pressed'?: boolean\n readonly 'aria-selected'?: boolean\n}\n\n// The control height each size reads: the page's XS, S, M, L and XL, which\n// the theme's sizing scale names one step apart from this prop.\nconst HEIGHTS = {\n lg: 'controlLg',\n md: 'controlSm',\n xl: 'controlXl',\n xs: 'controlXs',\n xxl: 'controlXxl',\n} as const satisfies Record<ControlSize, keyof ResolvedTheme['sizing']>\n\n// The outlined rule thickens with the size, as the buttons page's size tokens\n// have it: 1dp up to M, 2dp at L, 3dp at XL. The icon buttons page keeps its\n// widths in a token set its widget will not open, so the icon button takes\n// these, which a 96dp icon button beside a 96dp outlined button needs anyway.\nconst OUTLINE_WIDTHS: Readonly<Record<ControlSize, number>> = {\n lg: 1,\n md: 1,\n xl: 2,\n xs: 1,\n xxl: 3,\n}\n\n// The corner each size presses to, which both pages give alike.\nconst PRESSED_CORNERS = {\n lg: 'md',\n md: 'sm',\n xl: 'lg',\n xs: 'sm',\n xxl: 'lg',\n} as const satisfies Record<ControlSize, keyof ResolvedTheme['radii']>\n\nconst AnimatedPressable = Animated.createAnimatedComponent(Pressable)\n\n// Web's toggle reports `aria-pressed`, which React Native Web writes onto the\n// element. React Native has no pressed state to report, and reports the\n// selected one instead, which VoiceOver and TalkBack both read out.\nconst toggleState = Platform.select<(selected: boolean) => ToggleState>({\n default: (selected) => ({ 'aria-selected': selected }),\n web: (selected) => ({ 'aria-pressed': selected }),\n})\n\nconst buttonStyles = StyleSheet.create({\n // Clips the state layer and the ripple to the corner as it morphs, and\n // takes no touches.\n clip: {\n bottom: 0,\n left: 0,\n overflow: 'hidden',\n pointerEvents: 'none',\n position: 'absolute',\n right: 0,\n top: 0,\n },\n})\n\n// A handler that runs the call site's own and then the slot's, which takes no\n// event, or the call site's alone where the slot adds nothing.\nfunction andThen<E>(\n own: ((event: E) => void) | null | undefined,\n slot: (() => void) | undefined,\n) {\n if (slot === undefined) {\n return own ?? undefined\n }\n\n return (event: E) => {\n own?.(event)\n slot()\n }\n}\n\n// Records the button's height for the corner, then calls the call site's own\n// `onLayout`. Built by a call rather than declared in a component, which is\n// what react-perf's jsx-no-new-function-as-prop is after; the React Compiler\n// memoises the result on its inputs.\nfunction measureThen(\n height: SharedValue<number>,\n onLayout: PressableProps['onLayout'],\n) {\n return (event: LayoutChangeEvent) => {\n height.set(event.nativeEvent.layout.height)\n onLayout?.(event)\n }\n}\n\n// The call site's own `onPress`, then what the button does with the press.\n// Built by a call for the reason `measureThen` is.\nfunction pressThen(\n onPress: PressableProps['onPress'],\n then: (event: GestureResponderEvent) => void,\n) {\n return (event: GestureResponderEvent) => {\n onPress?.(event)\n then(event)\n }\n}\n\n/**\n * What a press does, and whether a toggle is selected. A press calls the call\n * site's own `onPress` first, then opens the link, changes the toggle's state\n * or submits the form the button is in, which is the order web reports them\n * in. A toggle keeps its own state from `defaultSelected` unless the call\n * site holds it in `isSelected`. A submit button outside a form submits\n * nothing, as on web.\n *\n * Given a `slot`, a button that is not a link then does what that slot of\n * the overlay around it does, as src/button's slots.ts sets out: opens it\n * from the trigger, or closes it from its content.\n */\nfunction useButtonAction({\n defaultSelected,\n href,\n isSelected,\n isToggle,\n onChange,\n onPress,\n slot,\n type,\n}: {\n readonly defaultSelected: boolean | undefined\n readonly href: string | undefined\n readonly isSelected: boolean | undefined\n readonly isToggle: boolean\n readonly onChange: ((isSelected: boolean) => void) | undefined\n readonly onPress: PressableProps['onPress']\n readonly slot?: 'close' | 'default'\n readonly type: 'button' | 'submit' | undefined\n}) {\n const openLink = useOpenLink()\n const form = use(FormContext)\n const slots = use(ButtonSlotContext)\n const [ownSelected, setOwnSelected] = useState(defaultSelected ?? false)\n const selected = isToggle && (isSelected ?? ownSelected)\n const filled = slot === undefined ? undefined : slots?.[slot]\n\n function follow(event: GestureResponderEvent) {\n if (href !== undefined) {\n openLink(href)\n return\n }\n if (isToggle) {\n setOwnSelected(!selected)\n onChange?.(!selected)\n } else if (type === 'submit') {\n form?.requestSubmit()\n }\n filled?.(event)\n }\n\n return { onPress: pressThen(onPress, follow), selected }\n}\n\n/**\n * The corner a button eases to, over the motion tokens' short duration on the\n * emphasised curve, and at once for a reader who asks the device for reduced\n * motion. The pill is the radius scale's `pill`, far larger than any button,\n * so the ease runs from half the button's measured height rather than from\n * that: otherwise the corner would hold the pill for most of the ease and\n * jump at its end. The `onLayout` it hands back measures the button, then\n * calls the call site's own.\n */\nfunction useCorner(radius: number, onLayout: PressableProps['onLayout']) {\n const { motion } = useTheme()\n const reduceMotion = useReducedMotion()\n const height = useSharedValue(0)\n const timing = {\n duration: motion.durationShort2,\n easing: Easing.bezier(...motion.easingEmphasized),\n }\n const corner = useAnimatedStyle(() => {\n 'worklet'\n const measured = height.get()\n const target = measured > 0 ? Math.min(radius, measured / 2) : radius\n\n return {\n borderRadius: reduceMotion ? target : withTiming(target, timing),\n }\n })\n\n return { corner, onLayout: measureThen(height, onLayout) }\n}\n\n/**\n * What a button filling the default slot of an overlay drawn beside it takes\n * on, as src/button's slots.ts sets out: its node is kept for the overlay to\n * be placed against, alongside the call site's own `ref`, `aria-expanded`\n * says whether the overlay is open, and the overlay's hover and long press\n * handlers run after the call site's. A button filling no such slot, or a\n * link, keeps its own props untouched.\n */\nfunction useSlotTrigger({\n delayLongPress,\n href,\n onHoverIn,\n onHoverOut,\n onLongPress,\n ref,\n slot,\n}: {\n readonly delayLongPress: PressableProps['delayLongPress']\n readonly href: string | undefined\n readonly onHoverIn: PressableProps['onHoverIn']\n readonly onHoverOut: PressableProps['onHoverOut']\n readonly onLongPress: PressableProps['onLongPress']\n readonly ref: Ref<HostInstance> | undefined\n readonly slot: 'close' | undefined\n}): SlotTriggerProps {\n const slots = use(ButtonSlotContext)\n const trigger =\n href === undefined && slot === undefined ? slots?.trigger : undefined\n\n return {\n ...(trigger === undefined\n ? { delayLongPress, ref }\n : {\n 'aria-expanded': trigger.expanded,\n delayLongPress: trigger.delayLongPress ?? delayLongPress,\n ref: refsOf(trigger.ref, ref),\n }),\n onHoverIn: andThen(onHoverIn, trigger?.onHoverIn),\n onHoverOut: andThen(onHoverOut, trigger?.onHoverOut),\n onLongPress: andThen(onLongPress, trigger?.onLongPress),\n }\n}\n\nexport {\n AnimatedPressable,\n buttonStyles,\n HEIGHTS,\n OUTLINE_WIDTHS,\n PRESSED_CORNERS,\n toggleState,\n useButtonAction,\n useCorner,\n useSlotTrigger,\n}\nexport type { ButtonLinkForm, ButtonPressForm, ButtonToggleForm, ControlSize }\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { IconStyleContext } from "../glyphs/icon-style.js";
2
+ import { useMessages } from "../i18n/provider.js";
2
3
  import { ProgressIndicator } from "../components/progress-indicator/index.js";
3
- import { messagesFor } from "../i18n/index.js";
4
4
  import { c } from "react/compiler-runtime";
5
5
  import { AccessibilityInfo, Platform, StyleSheet, View } from "react-native";
6
6
  import { useEffect, useRef } from "react";
@@ -99,7 +99,8 @@ function slotOf(color, size) {
99
99
  }
100
100
  /**
101
101
  * The ring's name, and the announcement a button makes as it goes pending.
102
- * The name is `pendingLabel`, or the word for loading in the device's locale.
102
+ * The name is `pendingLabel`, or the word for loading in the locale
103
+ * `useLocaleOf` resolves.
103
104
  *
104
105
  * On iOS and Android the change is said through VoiceOver and TalkBack, as
105
106
  * web's is said through React Aria's live announcer, since a ring appearing
@@ -110,32 +111,27 @@ function slotOf(color, size) {
110
111
  * has no announcer, so there the ring is read when the button next is.
111
112
  */
112
113
  function usePending(isPending, pendingLabel) {
113
- const $ = c(6);
114
- let t0;
115
- if ($[0] !== pendingLabel) {
116
- t0 = pendingLabel ?? messagesFor().loading;
117
- $[0] = pendingLabel;
118
- $[1] = t0;
119
- } else t0 = $[1];
120
- const label = t0;
114
+ const $ = c(4);
115
+ const messages = useMessages();
116
+ const label = pendingLabel ?? messages.loading;
121
117
  const was = useRef(isPending);
118
+ let t0;
122
119
  let t1;
123
- let t2;
124
- if ($[2] !== isPending || $[3] !== label) {
125
- t1 = () => {
120
+ if ($[0] !== isPending || $[1] !== label) {
121
+ t0 = () => {
126
122
  if (isPending && !was.current && (Platform.OS === "ios" || Platform.OS === "android")) AccessibilityInfo.announceForAccessibility(label);
127
123
  was.current = isPending;
128
124
  };
129
- t2 = [isPending, label];
130
- $[2] = isPending;
131
- $[3] = label;
132
- $[4] = t1;
133
- $[5] = t2;
125
+ t1 = [isPending, label];
126
+ $[0] = isPending;
127
+ $[1] = label;
128
+ $[2] = t0;
129
+ $[3] = t1;
134
130
  } else {
135
- t1 = $[4];
136
- t2 = $[5];
131
+ t0 = $[2];
132
+ t1 = $[3];
137
133
  }
138
- useEffect(t1, t2);
134
+ useEffect(t0, t1);
139
135
  return label;
140
136
  }
141
137
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"pending.js","names":["ReactNode","ColorValue","useEffect","useRef","AccessibilityInfo","Platform","StyleSheet","View","IconStyle","ProgressIndicator","IconStyleContext","messagesFor","pendingStyles","create","hidden","opacity","ring","alignItems","bottom","justifyContent","left","pointerEvents","position","right","top","BUSY","const","CONCEALED","PendingContent","t0","$","_c","children","isPending","t1","undefined","t2","t3","PendingRing","color","diameter","label","slotOf","t4","size","usePending","pendingLabel","loading","was","current","OS","announceForAccessibility","_c"],"sources":["../../src/button/pending.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport type { ColorValue } from 'react-native'\n\nimport { useEffect, useRef } from 'react'\nimport { AccessibilityInfo, Platform, StyleSheet, View } from 'react-native'\n\nimport type { IconStyle } from '../glyphs/icon-style'\n\nimport { ProgressIndicator } from '../components/progress-indicator'\nimport { IconStyleContext } from '../glyphs/icon-style'\nimport { messagesFor } from '../i18n'\n\n// What a pending button draws and says, shared by Button and IconButton as\n// web's ButtonBase draws it for both: the content hidden in place, the ring\n// over it, and the announcement that the button has gone pending.\n\nconst pendingStyles = StyleSheet.create({\n // The label and the icon while the ring shows: still laid out, so the\n // button keeps its width, which is what stops a form jumping the moment it\n // is submitted, and not drawn.\n hidden: {\n opacity: 0,\n },\n // The ring sits over the hidden content, centred in the button, and takes\n // no touches.\n ring: {\n alignItems: 'center',\n bottom: 0,\n justifyContent: 'center',\n left: 0,\n pointerEvents: 'none',\n position: 'absolute',\n right: 0,\n top: 0,\n },\n})\n\n// What a pending button reports to a screen reader, spread before the call\n// site's own props, so an `aria-busy` of its own still wins, as on web.\nconst BUSY = { 'aria-busy': true } as const\n\n// What takes the hidden content out of what a screen reader reads, so the\n// button is named by the ring while it shows.\nconst CONCEALED = { 'aria-hidden': true } as const\n\n/**\n * The icon a button draws, in a view that hides it in place while the button\n * is pending. The view is there whether or not the button is, so an icon\n * keeps its state when the button goes pending and comes back.\n */\nfunction PendingContent({\n children,\n isPending,\n}: {\n readonly children: ReactNode\n readonly isPending: boolean\n}) {\n return (\n <View\n {...(isPending ? CONCEALED : undefined)}\n style={isPending ? pendingStyles.hidden : undefined}\n >\n {children}\n </View>\n )\n}\n\n/**\n * The indeterminate ring a pending button draws over its content, in the\n * button's content colour rather than the progress page's primary, which on a\n * filled button is the fill itself. `diameter` is what the content it\n * replaces measures, the label's type size on Button and the icon on\n * IconButton, which is what web's `1em` reads in each.\n */\nfunction PendingRing({\n color,\n diameter,\n label,\n}: {\n readonly color: ColorValue\n readonly diameter: number\n readonly label: string\n}) {\n return (\n <View style={pendingStyles.ring}>\n <IconStyleContext value={slotOf(color, diameter)}>\n <ProgressIndicator\n aria-label={label}\n diameter={`${diameter}px`}\n isIndeterminate\n tone=\"inherit\"\n variant=\"circular\"\n />\n </IconStyleContext>\n </View>\n )\n}\n\nfunction slotOf(color: ColorValue, size: number): IconStyle {\n return { color, size }\n}\n\n/**\n * The ring's name, and the announcement a button makes as it goes pending.\n * The name is `pendingLabel`, or the word for loading in the device's locale.\n *\n * On iOS and Android the change is said through VoiceOver and TalkBack, as\n * web's is said through React Aria's live announcer, since a ring appearing\n * under them is not reliably read. Web says it only to a reader whose focus\n * is on the button; a screen reader's focus is not something a component\n * here can read, so it is said whatever has focus. A button pending as it\n * mounts says nothing, as on web, which announces a change. React Native Web\n * has no announcer, so there the ring is read when the button next is.\n */\nfunction usePending(isPending: boolean, pendingLabel: string | undefined) {\n const label = pendingLabel ?? messagesFor().loading\n const was = useRef(isPending)\n\n useEffect(() => {\n if (\n isPending &&\n !was.current &&\n (Platform.OS === 'ios' || Platform.OS === 'android')\n ) {\n AccessibilityInfo.announceForAccessibility(label)\n }\n was.current = isPending\n }, [isPending, label])\n\n return label\n}\n\nexport {\n BUSY,\n CONCEALED,\n PendingContent,\n PendingRing,\n pendingStyles,\n usePending,\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pending.js","names":["ReactNode","ColorValue","useEffect","useRef","AccessibilityInfo","Platform","StyleSheet","View","IconStyle","ProgressIndicator","IconStyleContext","useMessages","pendingStyles","create","hidden","opacity","ring","alignItems","bottom","justifyContent","left","pointerEvents","position","right","top","BUSY","const","CONCEALED","PendingContent","t0","$","_c","children","isPending","t1","undefined","t2","t3","PendingRing","color","diameter","label","slotOf","t4","size","usePending","pendingLabel","messages","loading","was","current","OS","announceForAccessibility","_c"],"sources":["../../src/button/pending.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport type { ColorValue } from 'react-native'\n\nimport { useEffect, useRef } from 'react'\nimport { AccessibilityInfo, Platform, StyleSheet, View } from 'react-native'\n\nimport type { IconStyle } from '../glyphs/icon-style'\n\nimport { ProgressIndicator } from '../components/progress-indicator'\nimport { IconStyleContext } from '../glyphs/icon-style'\nimport { useMessages } from '../i18n/provider'\n\n// What a pending button draws and says, shared by Button and IconButton as\n// web's ButtonBase draws it for both: the content hidden in place, the ring\n// over it, and the announcement that the button has gone pending.\n\nconst pendingStyles = StyleSheet.create({\n // The label and the icon while the ring shows: still laid out, so the\n // button keeps its width, which is what stops a form jumping the moment it\n // is submitted, and not drawn.\n hidden: {\n opacity: 0,\n },\n // The ring sits over the hidden content, centred in the button, and takes\n // no touches.\n ring: {\n alignItems: 'center',\n bottom: 0,\n justifyContent: 'center',\n left: 0,\n pointerEvents: 'none',\n position: 'absolute',\n right: 0,\n top: 0,\n },\n})\n\n// What a pending button reports to a screen reader, spread before the call\n// site's own props, so an `aria-busy` of its own still wins, as on web.\nconst BUSY = { 'aria-busy': true } as const\n\n// What takes the hidden content out of what a screen reader reads, so the\n// button is named by the ring while it shows.\nconst CONCEALED = { 'aria-hidden': true } as const\n\n/**\n * The icon a button draws, in a view that hides it in place while the button\n * is pending. The view is there whether or not the button is, so an icon\n * keeps its state when the button goes pending and comes back.\n */\nfunction PendingContent({\n children,\n isPending,\n}: {\n readonly children: ReactNode\n readonly isPending: boolean\n}) {\n return (\n <View\n {...(isPending ? CONCEALED : undefined)}\n style={isPending ? pendingStyles.hidden : undefined}\n >\n {children}\n </View>\n )\n}\n\n/**\n * The indeterminate ring a pending button draws over its content, in the\n * button's content colour rather than the progress page's primary, which on a\n * filled button is the fill itself. `diameter` is what the content it\n * replaces measures, the label's type size on Button and the icon on\n * IconButton, which is what web's `1em` reads in each.\n */\nfunction PendingRing({\n color,\n diameter,\n label,\n}: {\n readonly color: ColorValue\n readonly diameter: number\n readonly label: string\n}) {\n return (\n <View style={pendingStyles.ring}>\n <IconStyleContext value={slotOf(color, diameter)}>\n <ProgressIndicator\n aria-label={label}\n diameter={`${diameter}px`}\n isIndeterminate\n tone=\"inherit\"\n variant=\"circular\"\n />\n </IconStyleContext>\n </View>\n )\n}\n\nfunction slotOf(color: ColorValue, size: number): IconStyle {\n return { color, size }\n}\n\n/**\n * The ring's name, and the announcement a button makes as it goes pending.\n * The name is `pendingLabel`, or the word for loading in the locale\n * `useLocaleOf` resolves.\n *\n * On iOS and Android the change is said through VoiceOver and TalkBack, as\n * web's is said through React Aria's live announcer, since a ring appearing\n * under them is not reliably read. Web says it only to a reader whose focus\n * is on the button; a screen reader's focus is not something a component\n * here can read, so it is said whatever has focus. A button pending as it\n * mounts says nothing, as on web, which announces a change. React Native Web\n * has no announcer, so there the ring is read when the button next is.\n */\nfunction usePending(isPending: boolean, pendingLabel: string | undefined) {\n const messages = useMessages()\n const label = pendingLabel ?? messages.loading\n const was = useRef(isPending)\n\n useEffect(() => {\n if (\n isPending &&\n !was.current &&\n (Platform.OS === 'ios' || Platform.OS === 'android')\n ) {\n AccessibilityInfo.announceForAccessibility(label)\n }\n was.current = isPending\n }, [isPending, label])\n\n return label\n}\n\nexport {\n BUSY,\n CONCEALED,\n PendingContent,\n PendingRing,\n pendingStyles,\n usePending,\n}\n"],"mappings":"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"}