@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.
- package/docs/adoption-matrix.mdx +5 -5
- package/docs/locale.mdx +3 -1
- package/docs/mail-list.mdx +22 -18
- package/docs/settings-modal.mdx +1 -1
- package/docs/text-field.mdx +20 -0
- package/lib/commonjs/mail-list/MailList.js +0 -2
- package/lib/commonjs/mail-list/MailList.js.map +1 -1
- package/lib/commonjs/mail-list/MailRow.js +23 -9
- package/lib/commonjs/mail-list/MailRow.js.map +1 -1
- package/lib/commonjs/mail-list/shared.js +10 -26
- package/lib/commonjs/mail-list/shared.js.map +1 -1
- package/lib/commonjs/settings-modal/SettingsModal.js +18 -12
- package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
- package/lib/commonjs/text-field/TextField.js +133 -15
- package/lib/commonjs/text-field/TextField.js.map +1 -1
- package/lib/commonjs/text-field/messages.js +89 -0
- package/lib/commonjs/text-field/messages.js.map +1 -0
- package/lib/module/mail-list/MailList.js +0 -2
- package/lib/module/mail-list/MailList.js.map +1 -1
- package/lib/module/mail-list/MailRow.js +23 -9
- package/lib/module/mail-list/MailRow.js.map +1 -1
- package/lib/module/mail-list/shared.js +10 -26
- package/lib/module/mail-list/shared.js.map +1 -1
- package/lib/module/settings-modal/SettingsModal.js +18 -12
- package/lib/module/settings-modal/SettingsModal.js.map +1 -1
- package/lib/module/text-field/TextField.js +133 -15
- package/lib/module/text-field/TextField.js.map +1 -1
- package/lib/module/text-field/messages.js +85 -0
- package/lib/module/text-field/messages.js.map +1 -0
- package/lib/typescript/commonjs/mail-list/MailList.d.ts +1 -1
- package/lib/typescript/commonjs/mail-list/MailList.d.ts.map +1 -1
- package/lib/typescript/commonjs/mail-list/MailRow.d.ts +20 -5
- package/lib/typescript/commonjs/mail-list/MailRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/mail-list/shared.d.ts +9 -2
- package/lib/typescript/commonjs/mail-list/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/mail-list/types.d.ts +6 -8
- package/lib/typescript/commonjs/mail-list/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/text-field/TextField.d.ts +1 -1
- package/lib/typescript/commonjs/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/commonjs/text-field/index.d.ts +1 -1
- package/lib/typescript/commonjs/text-field/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/text-field/messages.d.ts +15 -0
- package/lib/typescript/commonjs/text-field/messages.d.ts.map +1 -0
- package/lib/typescript/commonjs/text-field/types.d.ts +18 -0
- package/lib/typescript/commonjs/text-field/types.d.ts.map +1 -1
- package/lib/typescript/module/mail-list/MailList.d.ts +1 -1
- package/lib/typescript/module/mail-list/MailList.d.ts.map +1 -1
- package/lib/typescript/module/mail-list/MailRow.d.ts +20 -5
- package/lib/typescript/module/mail-list/MailRow.d.ts.map +1 -1
- package/lib/typescript/module/mail-list/shared.d.ts +9 -2
- package/lib/typescript/module/mail-list/shared.d.ts.map +1 -1
- package/lib/typescript/module/mail-list/types.d.ts +6 -8
- package/lib/typescript/module/mail-list/types.d.ts.map +1 -1
- package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/module/text-field/TextField.d.ts +1 -1
- package/lib/typescript/module/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/module/text-field/index.d.ts +1 -1
- package/lib/typescript/module/text-field/index.d.ts.map +1 -1
- package/lib/typescript/module/text-field/messages.d.ts +15 -0
- package/lib/typescript/module/text-field/messages.d.ts.map +1 -0
- package/lib/typescript/module/text-field/types.d.ts +18 -0
- package/lib/typescript/module/text-field/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/mail-list/MailList.tsx +0 -2
- package/src/mail-list/MailRow.tsx +23 -9
- package/src/mail-list/shared.ts +10 -27
- package/src/mail-list/types.ts +6 -8
- package/src/settings-modal/SettingsModal.tsx +15 -12
- package/src/text-field/TextField.tsx +133 -15
- package/src/text-field/index.ts +1 -0
- package/src/text-field/messages.ts +36 -0
- 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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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=
|
|
855
|
+
<Text variant="body-medium" accessibilityLabel={messages.required} style={{ color: palette.error }}>
|
|
738
856
|
*
|
|
739
857
|
</Text>
|
|
740
858
|
) : null}
|
package/src/text-field/index.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/text-field/types.ts
CHANGED
|
@@ -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;
|