@oxy.so/bloom 4.34.1 → 4.35.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 (73) hide show
  1. package/docs/adoption-matrix.mdx +5 -5
  2. package/docs/locale.mdx +3 -1
  3. package/docs/mail-list.mdx +22 -18
  4. package/docs/settings-modal.mdx +1 -1
  5. package/docs/text-field.mdx +20 -0
  6. package/lib/commonjs/mail-list/MailList.js +0 -2
  7. package/lib/commonjs/mail-list/MailList.js.map +1 -1
  8. package/lib/commonjs/mail-list/MailRow.js +23 -9
  9. package/lib/commonjs/mail-list/MailRow.js.map +1 -1
  10. package/lib/commonjs/mail-list/shared.js +10 -26
  11. package/lib/commonjs/mail-list/shared.js.map +1 -1
  12. package/lib/commonjs/settings-modal/SettingsModal.js +18 -12
  13. package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
  14. package/lib/commonjs/text-field/TextField.js +133 -15
  15. package/lib/commonjs/text-field/TextField.js.map +1 -1
  16. package/lib/commonjs/text-field/messages.js +89 -0
  17. package/lib/commonjs/text-field/messages.js.map +1 -0
  18. package/lib/module/mail-list/MailList.js +0 -2
  19. package/lib/module/mail-list/MailList.js.map +1 -1
  20. package/lib/module/mail-list/MailRow.js +23 -9
  21. package/lib/module/mail-list/MailRow.js.map +1 -1
  22. package/lib/module/mail-list/shared.js +10 -26
  23. package/lib/module/mail-list/shared.js.map +1 -1
  24. package/lib/module/settings-modal/SettingsModal.js +18 -12
  25. package/lib/module/settings-modal/SettingsModal.js.map +1 -1
  26. package/lib/module/text-field/TextField.js +133 -15
  27. package/lib/module/text-field/TextField.js.map +1 -1
  28. package/lib/module/text-field/messages.js +85 -0
  29. package/lib/module/text-field/messages.js.map +1 -0
  30. package/lib/typescript/commonjs/mail-list/MailList.d.ts +1 -1
  31. package/lib/typescript/commonjs/mail-list/MailList.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/mail-list/MailRow.d.ts +20 -5
  33. package/lib/typescript/commonjs/mail-list/MailRow.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/mail-list/shared.d.ts +9 -2
  35. package/lib/typescript/commonjs/mail-list/shared.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/mail-list/types.d.ts +6 -8
  37. package/lib/typescript/commonjs/mail-list/types.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/text-field/TextField.d.ts +1 -1
  40. package/lib/typescript/commonjs/text-field/TextField.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/text-field/index.d.ts +1 -1
  42. package/lib/typescript/commonjs/text-field/index.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/text-field/messages.d.ts +15 -0
  44. package/lib/typescript/commonjs/text-field/messages.d.ts.map +1 -0
  45. package/lib/typescript/commonjs/text-field/types.d.ts +18 -0
  46. package/lib/typescript/commonjs/text-field/types.d.ts.map +1 -1
  47. package/lib/typescript/module/mail-list/MailList.d.ts +1 -1
  48. package/lib/typescript/module/mail-list/MailList.d.ts.map +1 -1
  49. package/lib/typescript/module/mail-list/MailRow.d.ts +20 -5
  50. package/lib/typescript/module/mail-list/MailRow.d.ts.map +1 -1
  51. package/lib/typescript/module/mail-list/shared.d.ts +9 -2
  52. package/lib/typescript/module/mail-list/shared.d.ts.map +1 -1
  53. package/lib/typescript/module/mail-list/types.d.ts +6 -8
  54. package/lib/typescript/module/mail-list/types.d.ts.map +1 -1
  55. package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
  56. package/lib/typescript/module/text-field/TextField.d.ts +1 -1
  57. package/lib/typescript/module/text-field/TextField.d.ts.map +1 -1
  58. package/lib/typescript/module/text-field/index.d.ts +1 -1
  59. package/lib/typescript/module/text-field/index.d.ts.map +1 -1
  60. package/lib/typescript/module/text-field/messages.d.ts +15 -0
  61. package/lib/typescript/module/text-field/messages.d.ts.map +1 -0
  62. package/lib/typescript/module/text-field/types.d.ts +18 -0
  63. package/lib/typescript/module/text-field/types.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/mail-list/MailList.tsx +0 -2
  66. package/src/mail-list/MailRow.tsx +23 -9
  67. package/src/mail-list/shared.ts +10 -27
  68. package/src/mail-list/types.ts +6 -8
  69. package/src/settings-modal/SettingsModal.tsx +15 -12
  70. package/src/text-field/TextField.tsx +133 -15
  71. package/src/text-field/index.ts +1 -0
  72. package/src/text-field/messages.ts +36 -0
  73. package/src/text-field/types.ts +19 -0
@@ -26,6 +26,11 @@ import {
26
26
  type TextStyleProp,
27
27
  } from '../styles';
28
28
  import { RiInformationFill } from '../icons/remix/RiInformationFill';
29
+ import { RiEyeLine } from '../icons/remix/RiEyeLine';
30
+ import { RiEyeOffLine } from '../icons/remix/RiEyeOffLine';
31
+ import { GlyphButton } from '../button/GlyphButton';
32
+ import { useMessages } from '../locale/messages';
33
+ import { TEXT_FIELD_MESSAGES } from './messages';
29
34
  import { Text } from '../typography';
30
35
  import {
31
36
  SANS_FONT_FAMILY,
@@ -54,6 +59,7 @@ import type {
54
59
  TextFieldInputProps,
55
60
  TextFieldLabelProps,
56
61
  TextFieldProps,
62
+ TextFieldRevealLabels,
57
63
  } from './types';
58
64
  import { useInheritedControl } from '../control-surface';
59
65
  import { useFieldMembership } from '../field/membership';
@@ -352,10 +358,19 @@ export function TextFieldInput({
352
358
  inputRef,
353
359
  style,
354
360
  floatingLabel = false,
361
+ secureTextEntry,
362
+ revealable = false,
363
+ revealLabels,
364
+ locale,
355
365
  ...rest
356
366
  }: TextFieldInputProps) {
357
367
  const theme = useTheme();
358
368
  const invalidProp = invalidNew ?? isInvalid;
369
+ // Hidden until asked. Only a secure field can be revealed; for any other the
370
+ // prop is inert, so `secureTextEntry` passes through untouched.
371
+ const [revealed, setRevealed] = useState(false);
372
+ const canReveal = revealable && secureTextEntry === true;
373
+ const secure = secureTextEntry === true && !(canReveal && revealed);
359
374
  const onValueChange = onValueChangeProp ?? onChangeText;
360
375
  // Read directly rather than through `useTextFieldContext`: a missing root is
361
376
  // not an error here, it is the branch below.
@@ -401,6 +416,10 @@ export function TextFieldInput({
401
416
  inputRef={inputRef}
402
417
  onFocus={onFocus}
403
418
  onBlur={onBlur}
419
+ secureTextEntry={secureTextEntry}
420
+ revealable={revealable}
421
+ revealLabels={revealLabels}
422
+ locale={locale}
404
423
  {...rest}
405
424
  />
406
425
  </TextField>
@@ -446,6 +465,15 @@ export function TextFieldInput({
446
465
  // at all and this prop is the only name the control has.
447
466
  const accessibleName = field?.labelText ?? label;
448
467
  const webDisabled = IS_WEB && fieldDisabled ? ({ disabled: true } as Record<string, unknown>) : undefined;
468
+ const reveal = canReveal ? (
469
+ <RevealButton
470
+ revealed={revealed}
471
+ onToggle={() => setRevealed((current) => !current)}
472
+ invalid={invalid}
473
+ labels={revealLabels}
474
+ locale={locale}
475
+ />
476
+ ) : null;
449
477
 
450
478
  if (floatingLabel) {
451
479
  // The ids and the described-by still travel, so the error is announced —
@@ -454,20 +482,24 @@ export function TextFieldInput({
454
482
  // alternatives, not layers: use one or the other. `docs/field.mdx` says so
455
483
  // where a caller will read it.
456
484
  return (
457
- <FloatingLabelInput
458
- label={accessibleName}
459
- value={value}
460
- onValueChange={onValueChange}
461
- onFocus={onFocus}
462
- onBlur={onBlur}
463
- invalid={invalid}
464
- refs={refs}
465
- style={style}
466
- {...rest}
467
- {...fieldProps}
468
- {...disabledProps}
469
- {...webDisabled}
470
- />
485
+ <>
486
+ <FloatingLabelInput
487
+ label={accessibleName}
488
+ value={value}
489
+ onValueChange={onValueChange}
490
+ onFocus={onFocus}
491
+ onBlur={onBlur}
492
+ invalid={invalid}
493
+ refs={refs}
494
+ style={style}
495
+ {...rest}
496
+ {...fieldProps}
497
+ {...disabledProps}
498
+ {...webDisabled}
499
+ secureTextEntry={secure}
500
+ />
501
+ {reveal}
502
+ </>
471
503
  );
472
504
  }
473
505
 
@@ -539,14 +571,99 @@ export function TextFieldInput({
539
571
  placeholder={placeholder === null ? undefined : placeholder || label}
540
572
  placeholderTextColor={resolvePlaceholderColor(ctx.palette, state)}
541
573
  keyboardAppearance={theme.mode === 'light' ? 'light' : 'dark'}
574
+ secureTextEntry={secure}
542
575
  style={flattened}
543
576
  />
544
577
 
578
+ {reveal}
579
+
545
580
  <Chrome invalid={invalid} />
546
581
  </>
547
582
  );
548
583
  }
549
584
 
585
+ /**
586
+ * The reveal button's box sits 4px inside the shell's height (28 in the 36px
587
+ * `md` shell, 24 in the 32px `sm`); its glyph is the 20px icon size, less
588
+ * where the box would crowd it (18 in 24).
589
+ */
590
+ function revealGeometry(size: TextFieldSize): { box: number; glyph: number } {
591
+ const box = TEXT_FIELD_GEOMETRY[size].height - 8;
592
+ return { box, glyph: Math.min(TEXT_FIELD_ICON_SIZE, box - 6) };
593
+ }
594
+
595
+ /**
596
+ * Web: `preventDefault` on mousedown, so a pointer press never takes focus
597
+ * from the input. The click needs nothing here: react-native-web's
598
+ * `PressResponder` stops a pressable's click itself, so the shell's own
599
+ * click-to-focus never pulls a keyboard user off the button
600
+ * (`TextFieldRevealWeb.test.tsx` pins it).
601
+ */
602
+ const KEEP_FOCUS_WHERE_IT_IS = IS_WEB
603
+ ? ({ onMouseDown: (event: { preventDefault: () => void }) => event.preventDefault() } as Record<string, unknown>)
604
+ : undefined;
605
+
606
+ /**
607
+ * The eye / eye-off after a `revealable` secure input. It sits where a trailing
608
+ * `TextFieldIcon` would, its GLYPH on the icon's 8px gap and the shell's
609
+ * padding — the round hover wash reaches past both, into the padding.
610
+ *
611
+ * Focus stays in the input. On web the mousedown is cancelled, so the input
612
+ * never blurs (and a caller's `onBlur` validation never runs mid-typing); on
613
+ * native a tap on a sibling does not blur a `TextInput`, and if something did
614
+ * (a parent `ScrollView` handling the tap), the input is focused again. A
615
+ * keyboard user who tabbed to the button keeps focus on the button.
616
+ *
617
+ * Its state is its NAME ("Show password" ↔ "Hide password"), so it carries no
618
+ * `aria-pressed`: a toggle whose label also flips announces its state twice.
619
+ */
620
+ function RevealButton({
621
+ revealed,
622
+ onToggle,
623
+ invalid,
624
+ labels,
625
+ locale,
626
+ }: {
627
+ revealed: boolean;
628
+ onToggle: () => void;
629
+ /** The input's own invalid state, which may be set without the field's. */
630
+ invalid: boolean;
631
+ labels?: TextFieldRevealLabels;
632
+ locale?: string;
633
+ }) {
634
+ const ctx = useTextFieldContext();
635
+ const { messages } = useMessages(TEXT_FIELD_MESSAGES, locale);
636
+ const inputWasFocused = useRef(false);
637
+ const { box: size, glyph } = revealGeometry(ctx.size);
638
+ const inset = (size - glyph) / 2;
639
+ const label = revealed ? (labels?.hide ?? messages.hidePassword) : (labels?.show ?? messages.showPassword);
640
+
641
+ return (
642
+ <View
643
+ style={[a.z_20, { marginLeft: TEXT_FIELD_TRAILING_GAP - inset, marginRight: -inset }]}
644
+ onTouchStart={() => {
645
+ inputWasFocused.current = ctx.inputRef.current?.isFocused() ?? false;
646
+ }}
647
+ {...KEEP_FOCUS_WHERE_IT_IS}>
648
+ <GlyphButton
649
+ size={size}
650
+ glyphSize={glyph}
651
+ icon={revealed ? RiEyeOffLine : RiEyeLine}
652
+ accessibilityLabel={label}
653
+ color={resolveIconColor(ctx.palette, { invalid, disabled: ctx.disabled })}
654
+ hoverColor={ctx.palette.text}
655
+ disabled={ctx.disabled}
656
+ onPress={() => {
657
+ onToggle();
658
+ const input = ctx.inputRef.current;
659
+ if (!IS_WEB && inputWasFocused.current && input && !input.isFocused()) input.focus();
660
+ inputWasFocused.current = false;
661
+ }}
662
+ />
663
+ </View>
664
+ );
665
+ }
666
+
550
667
  /** Vertical band the floating label travels between rest (centered) and floated (top). */
551
668
  const FLOAT_TRAVEL = 11;
552
669
  /** Resting label size (matches the input text) vs. floated caption size. */
@@ -722,6 +839,7 @@ export function TextFieldLabel({
722
839
  }: TextFieldLabelProps) {
723
840
  const theme = useTheme();
724
841
  const palette = useTextFieldPalette();
842
+ const { messages } = useMessages(TEXT_FIELD_MESSAGES);
725
843
  return (
726
844
  <View
727
845
  style={[
@@ -734,7 +852,7 @@ export function TextFieldLabel({
734
852
  {children}
735
853
  </Text>
736
854
  {required ? (
737
- <Text variant="body-medium" accessibilityLabel="required" style={{ color: palette.error }}>
855
+ <Text variant="body-medium" accessibilityLabel={messages.required} style={{ color: palette.error }}>
738
856
  *
739
857
  </Text>
740
858
  ) : null}
@@ -14,4 +14,5 @@ export type {
14
14
  TextFieldHintProps,
15
15
  TextFieldIconProps,
16
16
  TextFieldSize,
17
+ TextFieldRevealLabels,
17
18
  } from './types';
@@ -0,0 +1,36 @@
1
+ import type { MessageCatalog } from '../locale/messages';
2
+
3
+ /**
4
+ * Every fixed string the text-field family draws or announces, in each Bloom
5
+ * language. A caller's `revealLabels` still wins over the reveal pair.
6
+ */
7
+ export interface TextFieldMessages {
8
+ /** The reveal button's name while the value is hidden. */
9
+ showPassword: string;
10
+ /** The reveal button's name while the value is shown. */
11
+ hidePassword: string;
12
+ /** The name of `TextFieldLabel`'s `required` asterisk. */
13
+ required: string;
14
+ }
15
+
16
+ export const TEXT_FIELD_MESSAGES: MessageCatalog<TextFieldMessages> = {
17
+ en: { showPassword: 'Show password', hidePassword: 'Hide password', required: 'required' },
18
+ es: { showPassword: 'Mostrar contraseña', hidePassword: 'Ocultar contraseña', required: 'obligatorio' },
19
+ ca: { showPassword: 'Mostra la contrasenya', hidePassword: 'Amaga la contrasenya', required: 'obligatori' },
20
+ de: { showPassword: 'Passwort anzeigen', hidePassword: 'Passwort verbergen', required: 'erforderlich' },
21
+ fr: {
22
+ showPassword: 'Afficher le mot de passe',
23
+ hidePassword: 'Masquer le mot de passe',
24
+ required: 'obligatoire',
25
+ },
26
+ it: { showPassword: 'Mostra la password', hidePassword: 'Nascondi la password', required: 'obbligatorio' },
27
+ pt: { showPassword: 'Mostrar senha', hidePassword: 'Ocultar senha', required: 'obrigatório' },
28
+ ru: { showPassword: 'Показать пароль', hidePassword: 'Скрыть пароль', required: 'обязательно' },
29
+ tr: { showPassword: 'Şifreyi göster', hidePassword: 'Şifreyi gizle', required: 'zorunlu' },
30
+ ja: { showPassword: 'パスワードを表示', hidePassword: 'パスワードを非表示', required: '必須' },
31
+ zh: { showPassword: '显示密码', hidePassword: '隐藏密码', required: '必填' },
32
+ ar: { showPassword: 'إظهار كلمة المرور', hidePassword: 'إخفاء كلمة المرور', required: 'مطلوب' },
33
+ hi: { showPassword: 'पासवर्ड दिखाएँ', hidePassword: 'पासवर्ड छिपाएँ', required: 'आवश्यक' },
34
+ bn: { showPassword: 'পাসওয়ার্ড দেখান', hidePassword: 'পাসওয়ার্ড লুকান', required: 'আবশ্যক' },
35
+ id: { showPassword: 'Tampilkan kata sandi', hidePassword: 'Sembunyikan kata sandi', required: 'wajib' },
36
+ };
@@ -60,8 +60,27 @@ export type TextFieldInputProps = Omit<
60
60
  * identically. Respects reduced-motion (snaps instead of animating).
61
61
  */
62
62
  floatingLabel?: boolean;
63
+ /**
64
+ * With `secureTextEntry`, draw an eye / eye-off button after the input that
65
+ * shows and hides what was typed. It is named "Show password" / "Hide
66
+ * password" in the locale (`docs/locale.mdx`); `revealLabels` overrides
67
+ * either. Pressing it leaves focus in the input. Ignored without
68
+ * `secureTextEntry`.
69
+ */
70
+ revealable?: boolean;
71
+ /** Override the reveal button's names, one key at a time. */
72
+ revealLabels?: TextFieldRevealLabels;
73
+ /** Locale for the reveal button's names; defaults to the nearest `LocaleProvider`. */
74
+ locale?: string;
63
75
  };
64
76
 
77
+ export interface TextFieldRevealLabels {
78
+ /** The name while the value is hidden. */
79
+ show?: string;
80
+ /** The name while the value is shown. */
81
+ hide?: string;
82
+ }
83
+
65
84
  export type TextFieldLabelProps = React.PropsWithChildren<
66
85
  {
67
86
  nativeID?: string;