@object-ui/fields 17.3.0 → 17.6.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/CHANGELOG.md +1655 -0
- package/README.md +1 -12
- package/dist/FieldEditWidget.d.ts +23 -3
- package/dist/currency.d.ts +46 -0
- package/dist/field-type-alias.d.ts +35 -1
- package/dist/index.cjs +4 -4
- package/dist/index.css +651 -0
- package/dist/index.d.ts +474 -85
- package/dist/index.js +5418 -5023
- package/dist/widgets/AddressField.d.ts +4 -12
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +1 -1
- package/dist/widgets/BooleanField.d.ts +1 -1
- package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
- package/dist/widgets/CheckboxesField.d.ts +1 -1
- package/dist/widgets/CodeField.d.ts +1 -1
- package/dist/widgets/ColorField.d.ts +1 -1
- package/dist/widgets/CurrencyField.d.ts +1 -1
- package/dist/widgets/DateField.d.ts +1 -1
- package/dist/widgets/DateTimeField.d.ts +1 -1
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +28 -1
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +68 -6
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +1 -1
- package/dist/widgets/LookupField.d.ts +1 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +1 -1
- package/dist/widgets/ObjectField.d.ts +1 -1
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +21 -1
- package/dist/widgets/PasswordField.d.ts +1 -1
- package/dist/widgets/PercentField.d.ts +1 -1
- package/dist/widgets/PhoneField.d.ts +1 -1
- package/dist/widgets/QRCodeField.d.ts +1 -1
- package/dist/widgets/RadioField.d.ts +1 -1
- package/dist/widgets/RatingField.d.ts +1 -1
- package/dist/widgets/RecipientPickerField.d.ts +1 -1
- package/dist/widgets/RecordPickerDialog.d.ts +34 -10
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +26 -6
- package/dist/widgets/SignatureField.d.ts +32 -2
- package/dist/widgets/SliderField.d.ts +37 -2
- package/dist/widgets/SummaryField.d.ts +1 -1
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TextAreaField.d.ts +6 -2
- package/dist/widgets/TextField.d.ts +1 -1
- package/dist/widgets/TimeField.d.ts +1 -1
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/UserField.d.ts +1 -1
- package/dist/widgets/VectorField.d.ts +1 -1
- package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
- package/dist/widgets/address-format.d.ts +88 -0
- package/dist/widgets/file-size-guard.d.ts +41 -0
- package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
- package/dist/widgets/nativeDateValue.d.ts +37 -0
- package/dist/widgets/richTextDisplay.d.ts +80 -0
- package/dist/widgets/toDomProps.d.ts +30 -10
- package/dist/widgets/toHostGroupProps.d.ts +189 -0
- package/dist/widgets/types.d.ts +8 -3
- package/dist/widgets/useRecordQuery.d.ts +7 -1
- package/package.json +17 -14
- package/dist/FieldEditWidget.test.d.ts +0 -8
- package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/cell-truncation.test.d.ts +0 -0
- package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
- package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
- package/dist/__tests__/registry-collision.test.d.ts +0 -0
- package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
- package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
- package/dist/__tests__/validation-feedback.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
- package/dist/coerce-safe-value.test.d.ts +0 -1
- package/dist/complex-widgets.test.d.ts +0 -1
- package/dist/datetime-widgets.test.d.ts +0 -1
- package/dist/field-type-coverage.test.d.ts +0 -1
- package/dist/standard-widgets.test.d.ts +0 -0
- package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
- package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/GridField.test.d.ts +0 -1
- package/dist/widgets/ImageLightbox.test.d.ts +0 -1
- package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
- package/dist/widgets/LookupField.gateHintLabel.test.d.ts +0 -8
- package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
- package/dist/widgets/LookupField.idField.test.d.ts +0 -1
- package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
- package/dist/widgets/PeoplePicker.test.d.ts +0 -8
- package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
- package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
- package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
- package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +0 -8
- package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
- package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
- package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
- package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
- package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
- package/dist/widgets/file-value.test.d.ts +0 -1
- package/dist/widgets/personDisplay.test.d.ts +0 -8
- package/dist/widgets/recentLookups.test.d.ts +0 -1
- package/dist/widgets/useRecordQuery.test.d.ts +0 -8
- package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -191
- package/src/__tests__/DateCellRenderer.test.tsx +0 -73
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
- package/src/__tests__/validation-feedback.test.tsx +0 -361
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -598
- package/src/__tests__/widget-props-contract.test.tsx +0 -142
- package/src/coerce-safe-value.test.ts +0 -34
- package/src/complex-widgets.test.tsx +0 -908
- package/src/currency.ts +0 -14
- package/src/datetime-widgets.test.tsx +0 -93
- package/src/field-type-alias.ts +0 -100
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.tsx +0 -2503
- package/src/standard-widgets.test.tsx +0 -357
- package/src/widgets/AddressField.tsx +0 -132
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -52
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -136
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -95
- package/src/widgets/DateField.tsx +0 -31
- package/src/widgets/DateTimeField.tsx +0 -37
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -494
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -33
- package/src/widgets/FullscreenFieldEditor.tsx +0 -271
- package/src/widgets/GeolocationField.tsx +0 -167
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -406
- package/src/widgets/GridField.tsx +0 -966
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.tsx +0 -255
- package/src/widgets/ImageLightbox.test.tsx +0 -52
- package/src/widgets/ImageLightbox.tsx +0 -108
- package/src/widgets/LocationField.tsx +0 -57
- package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
- package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
- package/src/widgets/LookupField.hydration.test.tsx +0 -136
- package/src/widgets/LookupField.idField.test.tsx +0 -61
- package/src/widgets/LookupField.tsx +0 -1291
- package/src/widgets/MarkdownContent.tsx +0 -19
- package/src/widgets/MasterDetailField.tsx +0 -131
- package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
- package/src/widgets/MultiSelectField.tsx +0 -142
- package/src/widgets/NumberField.tsx +0 -49
- package/src/widgets/ObjectField.tsx +0 -94
- package/src/widgets/ObjectRefField.tsx +0 -121
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
- package/src/widgets/OptionsEmptyState.test.tsx +0 -169
- package/src/widgets/OptionsEmptyState.tsx +0 -83
- package/src/widgets/PasswordField.tsx +0 -49
- package/src/widgets/PeoplePicker.test.tsx +0 -360
- package/src/widgets/PeoplePicker.tsx +0 -579
- package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
- package/src/widgets/PercentField.tsx +0 -95
- package/src/widgets/PersonRow.tsx +0 -117
- package/src/widgets/PhoneField.tsx +0 -37
- package/src/widgets/QRCodeField.tsx +0 -103
- package/src/widgets/RadioField.cascade.test.tsx +0 -141
- package/src/widgets/RadioField.tsx +0 -111
- package/src/widgets/RatingField.tsx +0 -81
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1316
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -162
- package/src/widgets/SelectionTray.tsx +0 -110
- package/src/widgets/SignatureField.tsx +0 -140
- package/src/widgets/SliderField.tsx +0 -39
- package/src/widgets/SummaryField.tsx +0 -31
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
- package/src/widgets/TagsField.placeholder.test.tsx +0 -95
- package/src/widgets/TagsField.tsx +0 -88
- package/src/widgets/TextAreaField.tsx +0 -213
- package/src/widgets/TextField.tsx +0 -45
- package/src/widgets/TimeField.tsx +0 -31
- package/src/widgets/UrlField.tsx +0 -46
- package/src/widgets/UserField.tsx +0 -58
- package/src/widgets/UserFieldPicker.test.tsx +0 -78
- package/src/widgets/VectorField.tsx +0 -29
- package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -142
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
- package/src/widgets/_cell-renderer-bridge.ts +0 -19
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/personDisplay.test.ts +0 -113
- package/src/widgets/personDisplay.ts +0 -92
- package/src/widgets/recentLookups.test.ts +0 -45
- package/src/widgets/recentLookups.ts +0 -47
- package/src/widgets/toDomProps.ts +0 -175
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -150
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -264
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- package/src/withFieldCarrier.tsx +0 -75
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* `TextAreaField`'s character counter announces itself in the session locale —
|
|
11
|
-
* objectui#3406.
|
|
12
|
-
*
|
|
13
|
-
* The counter block is rendered only when the field declares `maxLength`, and
|
|
14
|
-
* what a sighted user sees (`{n}/{max}`) is digits, so nothing about this was
|
|
15
|
-
* visible on screen. The block carries `aria-live="polite"`, and its accessible
|
|
16
|
-
* name was the English literal `Character count: ${n} of ${max}` — so a zh/ja/ar
|
|
17
|
-
* session had an English sentence read out on every keystroke, and only screen
|
|
18
|
-
* reader users could perceive it.
|
|
19
|
-
*
|
|
20
|
-
* Unlike objectui#3404 (keys existed, this path did not consume them), no
|
|
21
|
-
* `characterCount` key existed in any of the ten packs. `fields.textarea.characterCount`
|
|
22
|
-
* is new here, in all ten, with `{{count}}` / `{{max}}`.
|
|
23
|
-
*
|
|
24
|
-
* ## What the cases assert, and why in this shape
|
|
25
|
-
*
|
|
26
|
-
* - **`en` is a NO-OP pin, not a defect reproducer.** The default in
|
|
27
|
-
* `FIELD_DEFAULTS` is byte-identical to the literal it replaced, so the
|
|
28
|
-
* English case was green before this change too. It is here to prove the
|
|
29
|
-
* swap did not alter English rendering — only a positive assertion sees that
|
|
30
|
-
* direction. Its exact expected string is repeated verbatim in
|
|
31
|
-
* `TextAreaField.characterCount.no-provider.test.tsx`; together the two pin
|
|
32
|
-
* pack-default equality across a boundary the packages cannot import across.
|
|
33
|
-
* - **Non-`en` positive AND English-literal negative, together.** A re-inlined
|
|
34
|
-
* literal sitting next to a translated sibling still satisfies a
|
|
35
|
-
* positive-only assertion.
|
|
36
|
-
* - **`ja` carries the load-bearing ORDER check.** Its translation puts the cap
|
|
37
|
-
* BEFORE the count ("of {{max}} characters, {{count}}"). An implementation
|
|
38
|
-
* that assembled the sentence from parts in code — "label + count + separator +
|
|
39
|
-
* max" — cannot produce that order, so it passes a zh-only check by luck and
|
|
40
|
-
* fails here. This is the case that justifies one interpolated key over a
|
|
41
|
-
* per-part assembly.
|
|
42
|
-
* - **`ru` and `ar` are the plural-resolution check.** `count` is not an
|
|
43
|
-
* ordinary interpolation variable to i18next: passing it activates plural
|
|
44
|
-
* lookup (`key_one` / `key_few` / `key_many` / …) BEFORE the base key. Both
|
|
45
|
-
* packs declare the base key only, and both languages have multi-form plural
|
|
46
|
-
* rules, so these two prove the base-key fallback actually resolves rather
|
|
47
|
-
* than leaving the user with a raw key. `{{count}}` is the spelling this
|
|
48
|
-
* repo already uses for counts (`table.selected`, `lookup.recordCount`), and
|
|
49
|
-
* this pins that it keeps working for a pack with no plural forms declared.
|
|
50
|
-
* - **Recompute-on-input.** The name is derived per render. Pinned so a future
|
|
51
|
-
* memo cannot freeze it at the mount-time length while the visible digits
|
|
52
|
-
* move on.
|
|
53
|
-
*
|
|
54
|
-
* ## objectui#3408 — same key, different place, plus a sibling
|
|
55
|
-
*
|
|
56
|
-
* The sentence is no longer an `aria-live` region's `aria-label`. It is the
|
|
57
|
-
* textarea's accessible DESCRIPTION (`aria-describedby`), read once on focus
|
|
58
|
-
* instead of re-announced on every keystroke, and the cases above resolve it
|
|
59
|
-
* through that association. The expected strings did not change: the ten packs
|
|
60
|
-
* still serve them, and this file still guards the same drift (backfilled
|
|
61
|
-
* English, a code-assembled sentence that cannot reproduce ja's word order, a
|
|
62
|
-
* raw key surfacing where plural lookup fails).
|
|
63
|
-
*
|
|
64
|
-
* `fields.textarea.charactersRemaining` is the sibling #3408 added — the only
|
|
65
|
-
* thing the widget still SPEAKS while typing, and therefore the one the
|
|
66
|
-
* English-backfill risk now applies to. Its cases sit at the bottom, with fake
|
|
67
|
-
* timers, because reaching it requires crossing the near-limit threshold and
|
|
68
|
-
* then pausing.
|
|
69
|
-
*/
|
|
70
|
-
import React from 'react';
|
|
71
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
72
|
-
import { render, screen, fireEvent, act } from '@testing-library/react';
|
|
73
|
-
import '@testing-library/jest-dom';
|
|
74
|
-
import { I18nProvider } from '@object-ui/i18n';
|
|
75
|
-
|
|
76
|
-
import { TextAreaField } from '../TextAreaField';
|
|
77
|
-
import type { FieldMetadata } from '@object-ui/types';
|
|
78
|
-
|
|
79
|
-
const textareaField = (extra: Record<string, unknown> = {}) =>
|
|
80
|
-
({ name: 'notes', type: 'textarea', ...extra }) as unknown as FieldMetadata;
|
|
81
|
-
|
|
82
|
-
function renderIn(language: string, element: React.ReactElement) {
|
|
83
|
-
return render(
|
|
84
|
-
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }}>
|
|
85
|
-
{element}
|
|
86
|
-
</I18nProvider>,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Where the sentence lives moved in objectui#3408: it was the `aria-label` of
|
|
92
|
-
* the visible counter, which was ALSO the `aria-live` region — so the whole
|
|
93
|
-
* sentence was re-announced on every keystroke. It is now the textarea's
|
|
94
|
-
* accessible DESCRIPTION, read once when focus lands.
|
|
95
|
-
*
|
|
96
|
-
* These helpers therefore resolve it the way an assistive technology does —
|
|
97
|
-
* follow `aria-describedby`, concatenate the referenced nodes — instead of
|
|
98
|
-
* reading an attribute off a node found by selector. That keeps the
|
|
99
|
-
* ASSOCIATION under assertion here too, and it is why the cases below did not
|
|
100
|
-
* need their expected strings changed: the same ten-pack key still produces
|
|
101
|
-
* them, in a place a screen reader reaches without typing.
|
|
102
|
-
*
|
|
103
|
-
* The digits stay queryable separately (`visibleCounter`) because several
|
|
104
|
-
* cases assert that the visible half is unchanged. The behaviour half of
|
|
105
|
-
* #3408 — how often the live region speaks — is pinned in
|
|
106
|
-
* `TextAreaField.characterCount.announcements.test.tsx`.
|
|
107
|
-
*/
|
|
108
|
-
const visibleCounter = () => document.querySelector('[data-testid="textarea-character-count"]');
|
|
109
|
-
const describedText = () => {
|
|
110
|
-
const box = screen.getByRole('textbox');
|
|
111
|
-
const ids = (box.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
|
|
112
|
-
return ids.map((id) => document.getElementById(id)?.textContent ?? '').join(' ').trim();
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
describe('TextAreaField character counter is translated (objectui#3406)', () => {
|
|
116
|
-
it('renders the English sentence under an en provider', () => {
|
|
117
|
-
renderIn(
|
|
118
|
-
'en',
|
|
119
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ maxLength: 200 })} />,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
// Byte-identical to the literal this replaced, so `en` is a no-op change.
|
|
123
|
-
expect(describedText()).toBe('Character count: 5 of 200');
|
|
124
|
-
expect(visibleCounter()).toHaveTextContent('5/200');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('renders the sentence in Chinese under a zh provider', () => {
|
|
128
|
-
renderIn(
|
|
129
|
-
'zh',
|
|
130
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ maxLength: 200 })} />,
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
expect(describedText()).toBe('已输入 5 个字符,最多 200 个');
|
|
134
|
-
// The literal, asserted absent — a re-inlined one would still satisfy a
|
|
135
|
-
// positive-only check on a sibling case.
|
|
136
|
-
expect(describedText()).not.toMatch(/character count/i);
|
|
137
|
-
// The digits stay language-independent; only the accessible name moved.
|
|
138
|
-
expect(visibleCounter()).toHaveTextContent('5/200');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('renders the sentence in Japanese, with the cap interpolated BEFORE the count', () => {
|
|
142
|
-
renderIn(
|
|
143
|
-
'ja',
|
|
144
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ maxLength: 200 })} />,
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// `{{max}}` precedes `{{count}}` in this pack. Code-side concatenation
|
|
148
|
-
// cannot produce this order — that is the point of the assertion.
|
|
149
|
-
expect(describedText()).toBe('文字数: 200 文字中 5 文字');
|
|
150
|
-
expect(describedText()!.indexOf('200')).toBeLessThan(describedText()!.indexOf('5'));
|
|
151
|
-
expect(describedText()).not.toMatch(/character count/i);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('resolves the base key under ru, whose plural rules i18next consults first', () => {
|
|
155
|
-
renderIn(
|
|
156
|
-
'ru',
|
|
157
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ maxLength: 200 })} />,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
expect(describedText()).toBe('Количество символов: 5 из 200');
|
|
161
|
-
// The failure this guards: plural lookup missing and no base-key fallback
|
|
162
|
-
// would surface the raw key instead of a sentence.
|
|
163
|
-
expect(describedText()).not.toContain('fields.textarea');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('resolves the base key under ar as well', () => {
|
|
167
|
-
renderIn(
|
|
168
|
-
'ar',
|
|
169
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ maxLength: 200 })} />,
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
expect(describedText()).toBe('عدد الأحرف: 5 من 200');
|
|
173
|
-
expect(describedText()).not.toContain('fields.textarea');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('recomputes the spoken count as the user types', () => {
|
|
177
|
-
// Derived per render, not memoized at mount. A frozen name would keep
|
|
178
|
-
// announcing "5" while the visible digits read 11.
|
|
179
|
-
const Host = () => {
|
|
180
|
-
const [v, setV] = React.useState('hello');
|
|
181
|
-
return <TextAreaField value={v} onChange={setV} field={textareaField({ maxLength: 200 })} />;
|
|
182
|
-
};
|
|
183
|
-
renderIn('zh', <Host />);
|
|
184
|
-
expect(describedText()).toBe('已输入 5 个字符,最多 200 个');
|
|
185
|
-
|
|
186
|
-
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'hello world' } });
|
|
187
|
-
|
|
188
|
-
expect(describedText()).toBe('已输入 11 个字符,最多 200 个');
|
|
189
|
-
expect(visibleCounter()).toHaveTextContent('11/200');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('keeps reading the legacy snake_case max_length spelling', () => {
|
|
193
|
-
// The dual-read predates this change (framework#1878 §3). Keying the
|
|
194
|
-
// sentence must not narrow which spelling reaches it.
|
|
195
|
-
renderIn(
|
|
196
|
-
'zh',
|
|
197
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ max_length: 80 })} />,
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
expect(describedText()).toBe('已输入 5 个字符,最多 80 个');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('renders no counter at all when the field declares no maxLength', () => {
|
|
204
|
-
// The block is `{maxLength && …}`. Translating it must not make it
|
|
205
|
-
// unconditional — an always-present live region would announce on every
|
|
206
|
-
// keystroke in every long-text field in the app.
|
|
207
|
-
renderIn('zh', <TextAreaField value="hello" onChange={vi.fn()} field={textareaField()} />);
|
|
208
|
-
|
|
209
|
-
expect(visibleCounter()).toBeNull();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('renders no counter in the readonly branch', () => {
|
|
213
|
-
// The translation hook had to move ABOVE the readonly early return
|
|
214
|
-
// (rules-of-hooks). This pins that the hoist changed nothing a user sees
|
|
215
|
-
// on that branch.
|
|
216
|
-
renderIn(
|
|
217
|
-
'zh',
|
|
218
|
-
<TextAreaField
|
|
219
|
-
value="hello"
|
|
220
|
-
onChange={vi.fn()}
|
|
221
|
-
readonly
|
|
222
|
-
field={textareaField({ maxLength: 200 })}
|
|
223
|
-
/>,
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
expect(visibleCounter()).toBeNull();
|
|
227
|
-
expect(screen.getByText('hello')).toBeInTheDocument();
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* The near-limit warning — the sentence that IS spoken mid-typing
|
|
233
|
-
* (objectui#3408). Reaching it needs the value inside the last 10% / 20
|
|
234
|
-
* characters of the cap AND a pause, so these render already-near-full values
|
|
235
|
-
* and advance fake timers rather than typing 490 characters.
|
|
236
|
-
*/
|
|
237
|
-
describe('TextAreaField near-limit warning is translated (objectui#3408)', () => {
|
|
238
|
-
afterEach(() => vi.useRealTimers());
|
|
239
|
-
|
|
240
|
-
// Scoped to its own container and unmounted before returning: some cases
|
|
241
|
-
// call this twice, and a document-wide query would read the FIRST mount's
|
|
242
|
-
// region — green for the previous language, not the one under test.
|
|
243
|
-
const nearLimit = (language: string) => {
|
|
244
|
-
vi.useFakeTimers();
|
|
245
|
-
const { container, unmount } = renderIn(
|
|
246
|
-
language,
|
|
247
|
-
<TextAreaField value={'x'.repeat(190)} onChange={vi.fn()} field={textareaField({ maxLength: 200 })} />,
|
|
248
|
-
);
|
|
249
|
-
act(() => { vi.advanceTimersByTime(1000); });
|
|
250
|
-
const spoken = container.querySelector('[aria-live="polite"]')?.textContent ?? '';
|
|
251
|
-
unmount();
|
|
252
|
-
vi.useRealTimers();
|
|
253
|
-
return spoken;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
it('renders the English sentence under an en provider', () => {
|
|
257
|
-
expect(nearLimit('en')).toBe('Characters remaining: 10');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('renders the sentence in Chinese under a zh provider', () => {
|
|
261
|
-
expect(nearLimit('zh')).toBe('还可输入 10 个字符');
|
|
262
|
-
// A backfilled English string is invisible at runtime — the whole defect
|
|
263
|
-
// class is English reaching a non-English screen reader.
|
|
264
|
-
expect(nearLimit('zh')).not.toMatch(/characters remaining/i);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('renders the sentence in Japanese', () => {
|
|
268
|
-
expect(nearLimit('ja')).toBe('残り 10 文字');
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('resolves the base key under ru and ar, whose plural rules i18next consults first', () => {
|
|
272
|
-
// Same mechanism as `characterCount` above: `count` sends i18next to
|
|
273
|
-
// `key_one` / `key_few` / … before the base key, and these packs declare
|
|
274
|
-
// the base key only. A raw key reaching a screen-reader-only string is
|
|
275
|
-
// invisible to everyone who could report it.
|
|
276
|
-
expect(nearLimit('ru')).toBe('Осталось символов: 10');
|
|
277
|
-
expect(nearLimit('ar')).toBe('الأحرف المتبقية: 10');
|
|
278
|
-
expect(nearLimit('ru')).not.toContain('fields.textarea');
|
|
279
|
-
expect(nearLimit('ar')).not.toContain('fields.textarea');
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// The provider-LESS leg lives in `TextAreaField.characterCount.no-provider.test.tsx`.
|
|
284
|
-
// It cannot share this file: mounting `I18nProvider` installs its instance as
|
|
285
|
-
// react-i18next's GLOBAL default, so after any case above there is no "no
|
|
286
|
-
// provider" state left in this module graph to observe (measured in
|
|
287
|
-
// objectui#3404 — see that file's header).
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* `TextAreaField`'s character counter with NO `I18nProvider` mounted
|
|
11
|
-
* (objectui#3406) — standalone/embedded hosts, and most of this package's own
|
|
12
|
-
* bare-render tests.
|
|
13
|
-
*
|
|
14
|
-
* ## Why this leg constrains the implementation
|
|
15
|
-
*
|
|
16
|
-
* Routing an accessible name through i18n must not turn it into a raw key
|
|
17
|
-
* where nothing is configured — for a screen-reader-only string that failure
|
|
18
|
-
* is invisible to everyone who could report it. Bare `useObjectTranslation()`
|
|
19
|
-
* with no provider returns the key verbatim AND drops interpolation, which is
|
|
20
|
-
* strictly worse than the English literal being replaced (measured on the
|
|
21
|
-
* pre-change tree in objectui#3404, same hook, same package). So the widget
|
|
22
|
-
* consumes `useFieldTranslation()` — `createSafeTranslation` plus English
|
|
23
|
-
* defaults, which does its own `{{k}}` substitution.
|
|
24
|
-
*
|
|
25
|
-
* The expected sentence below is repeated verbatim in the `en`-provider case
|
|
26
|
-
* of `TextAreaField.characterCount.i18n.test.tsx`. `packages/i18n` cannot
|
|
27
|
-
* import `packages/fields` (the dependency runs the other way), so this pair of
|
|
28
|
-
* literals is what pins pack and code default to the same bytes — and both to
|
|
29
|
-
* the literal that existed before this change.
|
|
30
|
-
*
|
|
31
|
-
* ## Why a separate FILE and not another `describe`
|
|
32
|
-
*
|
|
33
|
-
* Mounting `I18nProvider` calls `initReactI18next`, which installs that
|
|
34
|
-
* instance as react-i18next's GLOBAL default. After any sibling case has done
|
|
35
|
-
* so there is no "no provider" state left in the module graph, and these
|
|
36
|
-
* assertions would read that leaked instance instead of the fallback — green
|
|
37
|
-
* for the wrong reason. Measured in objectui#3404: cases written inside the
|
|
38
|
-
* provider file stayed GREEN under a reverse check that correctly reddened a
|
|
39
|
-
* separate file. vitest's per-file isolation is what makes the observation
|
|
40
|
-
* honest. Same split, same reason as
|
|
41
|
-
* `FullscreenFieldEditor.no-provider.test.tsx` and
|
|
42
|
-
* `TagsField.placeholder.no-provider.test.tsx`.
|
|
43
|
-
*/
|
|
44
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
45
|
-
import { render, screen, act } from '@testing-library/react';
|
|
46
|
-
import '@testing-library/jest-dom';
|
|
47
|
-
|
|
48
|
-
import { TextAreaField } from '../TextAreaField';
|
|
49
|
-
import type { FieldMetadata } from '@object-ui/types';
|
|
50
|
-
|
|
51
|
-
const textareaField = (extra: Record<string, unknown> = {}) =>
|
|
52
|
-
({ name: 'notes', type: 'textarea', ...extra }) as unknown as FieldMetadata;
|
|
53
|
-
|
|
54
|
-
const visibleCounter = () => document.querySelector('[data-testid="textarea-character-count"]');
|
|
55
|
-
const statusRegion = () => document.querySelector('[aria-live="polite"]');
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* objectui#3408 moved the sentence off the live region's `aria-label` and onto
|
|
59
|
-
* the textarea's accessible DESCRIPTION. Resolved here the way an assistive
|
|
60
|
-
* technology resolves it — follow `aria-describedby` — so the association is
|
|
61
|
-
* under assertion alongside the copy.
|
|
62
|
-
*/
|
|
63
|
-
const counterLabel = () => {
|
|
64
|
-
const box = screen.getByRole('textbox');
|
|
65
|
-
const ids = (box.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
|
|
66
|
-
return ids.map((id) => document.getElementById(id)?.textContent ?? '').join(' ').trim();
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
describe('character counter — English fallback survives with no i18n configured (objectui#3406)', () => {
|
|
70
|
-
afterEach(() => vi.useRealTimers());
|
|
71
|
-
|
|
72
|
-
it('renders the English default, not a raw key', () => {
|
|
73
|
-
render(
|
|
74
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ maxLength: 200 })} />,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Byte-identical to the literal this change replaced.
|
|
78
|
-
expect(counterLabel()).toBe('Character count: 5 of 200');
|
|
79
|
-
// The measured failure mode, stated directly — a key reaching a user.
|
|
80
|
-
expect(counterLabel()).not.toMatch(/fields\.textarea/);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('substitutes BOTH placeholders in the fallback path', () => {
|
|
84
|
-
// `createSafeTranslation`'s fallback runs its own `{{k}}` replacement.
|
|
85
|
-
// Bare `useObjectTranslation` would leave the sentence unsubstituted
|
|
86
|
-
// (in fact would not return a sentence at all).
|
|
87
|
-
render(
|
|
88
|
-
<TextAreaField value="hi" onChange={vi.fn()} field={textareaField({ maxLength: 40 })} />,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
expect(counterLabel()).toBe('Character count: 2 of 40');
|
|
92
|
-
expect(counterLabel()).not.toContain('{{');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('announces zero for an empty value rather than a hole in the sentence', () => {
|
|
96
|
-
// `0` is falsy, so an interpolation that guarded on truthiness would drop
|
|
97
|
-
// it and speak "Character count: of 40" — the one input length where the
|
|
98
|
-
// substitution can fail on its own.
|
|
99
|
-
//
|
|
100
|
-
// Deliberately `''` and not `undefined`: `FieldWidgetComponentProps`
|
|
101
|
-
// declares `value: T`, non-optional. The widget's `value || ''` guard
|
|
102
|
-
// predates this change and covers a JS host that ignores the type; pinning
|
|
103
|
-
// an out-of-contract shape here would be the lenient-consumer move
|
|
104
|
-
// AGENTS.md #0.1 rules out, and is not what this issue is about.
|
|
105
|
-
render(<TextAreaField value="" onChange={vi.fn()} field={textareaField({ maxLength: 40 })} />);
|
|
106
|
-
|
|
107
|
-
expect(counterLabel()).toBe('Character count: 0 of 40');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('keeps a live region, now SEPARATE from the visible digits', () => {
|
|
111
|
-
// objectui#3406 pinned `aria-live="polite"` on the counter itself and said
|
|
112
|
-
// so explicitly: the per-keystroke re-announce was a behaviour question,
|
|
113
|
-
// filed separately, deliberately untouched. objectui#3408 is that filing,
|
|
114
|
-
// and this pin is updated in the direction it decided — the live region
|
|
115
|
-
// still exists (that attribute is why anything is ever spoken) but it is a
|
|
116
|
-
// visually-hidden node of its own, and the digits it used to share an
|
|
117
|
-
// element with are now `aria-hidden`.
|
|
118
|
-
render(
|
|
119
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ maxLength: 200 })} />,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
expect(statusRegion()).toHaveAttribute('aria-live', 'polite');
|
|
123
|
-
expect(statusRegion()).not.toBe(visibleCounter());
|
|
124
|
-
expect(visibleCounter()).toHaveAttribute('aria-hidden', 'true');
|
|
125
|
-
expect(visibleCounter()).not.toHaveAttribute('aria-live');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('renders the English near-limit warning too, not a raw key', () => {
|
|
129
|
-
// The second half of the fallback contract: `charactersRemaining` is the
|
|
130
|
-
// only sentence the widget still speaks while typing, so a key leaking
|
|
131
|
-
// here is the same invisible failure — heard by exactly the users who
|
|
132
|
-
// cannot see that it is a key. 190 of 200 is inside the warning band; the
|
|
133
|
-
// 1s pause is what releases it.
|
|
134
|
-
vi.useFakeTimers();
|
|
135
|
-
render(
|
|
136
|
-
<TextAreaField
|
|
137
|
-
value={'x'.repeat(190)}
|
|
138
|
-
onChange={vi.fn()}
|
|
139
|
-
field={textareaField({ maxLength: 200 })}
|
|
140
|
-
/>,
|
|
141
|
-
);
|
|
142
|
-
act(() => { vi.advanceTimersByTime(1000); });
|
|
143
|
-
|
|
144
|
-
expect(statusRegion()).toHaveTextContent('Characters remaining: 10');
|
|
145
|
-
expect(statusRegion()!.textContent).not.toMatch(/fields\.textarea/);
|
|
146
|
-
expect(statusRegion()!.textContent).not.toContain('{{');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('the code-shipped default carries no CJK (Commandment #-1)', () => {
|
|
150
|
-
// With no provider mounted, whatever renders here came from CODE
|
|
151
|
-
// (FIELD_DEFAULTS). Escaped ranges on purpose: a literal CJK class would
|
|
152
|
-
// itself violate the commandment this asserts (same shape as
|
|
153
|
-
// `FullscreenFieldEditor.no-provider.test.tsx`).
|
|
154
|
-
render(
|
|
155
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ maxLength: 200 })} />,
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
expect(counterLabel()).not.toMatch(/[\u3000-\u30ff\u4e00-\u9fff]/);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The fullscreen dialog's character-count description follows the session
|
|
11
|
-
* language — objectui#3417.
|
|
12
|
-
*
|
|
13
|
-
* The sentence is the SAME `fields.textarea.characterCount` key the inline
|
|
14
|
-
* surface has used since objectui#3406, and reusing it rather than minting a
|
|
15
|
-
* `fields.textarea.fullscreenCharacterCount` twin is the point: one form-level
|
|
16
|
-
* setting (`ObjectFormSchema.mobile.fullscreenLongText`) projects onto both
|
|
17
|
-
* surfaces of the same field, so two copies of this copy could only drift, and
|
|
18
|
-
* a twin would mean editing ten locale packs to say what they already say.
|
|
19
|
-
* (Same reasoning, same shape, as objectui#3404's decision to have
|
|
20
|
-
* `FullscreenFieldEditor` consume the existing `form.fullscreen.*` keys.)
|
|
21
|
-
*
|
|
22
|
-
* ## Why this needs its own cases at all
|
|
23
|
-
*
|
|
24
|
-
* `TextAreaField.characterCount.i18n.test.tsx` already pins the key and its ten
|
|
25
|
-
* packs — including the load-bearing `ja` word-order case, which no code-side
|
|
26
|
-
* concatenation can satisfy. What it cannot see is whether the FULLSCREEN
|
|
27
|
-
* surface routes through `t()` or re-inlines an English literal on the way to a
|
|
28
|
-
* node only a screen reader reads. A footer that renders
|
|
29
|
-
* `Character count: 5 of 500` in a zh session looks correct to everyone who
|
|
30
|
-
* could file the bug. So: a positive translated assertion AND the English
|
|
31
|
-
* literal asserted absent, on the fullscreen surface specifically.
|
|
32
|
-
*
|
|
33
|
-
* `ja` is carried over for the same reason it exists on the inline side — its
|
|
34
|
-
* pack puts the cap BEFORE the count, so an implementation that assembled the
|
|
35
|
-
* sentence from parts passes a zh-only check by luck and fails here.
|
|
36
|
-
*
|
|
37
|
-
* The provider-LESS leg lives in `TextAreaField.fullscreenCharacterCount.test.tsx`
|
|
38
|
-
* and cannot share this file: mounting `I18nProvider` installs its instance as
|
|
39
|
-
* react-i18next's GLOBAL default, so after any case below there is no "no
|
|
40
|
-
* provider" state left in this module graph to observe (measured in
|
|
41
|
-
* objectui#3404 — cases written inside the provider file stayed GREEN under a
|
|
42
|
-
* reverse check that correctly reddened a separate file).
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
import React from 'react';
|
|
46
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
47
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
48
|
-
import '@testing-library/jest-dom';
|
|
49
|
-
import { I18nProvider } from '@object-ui/i18n';
|
|
50
|
-
|
|
51
|
-
import { TextAreaField } from '../TextAreaField';
|
|
52
|
-
import type { FieldMetadata } from '@object-ui/types';
|
|
53
|
-
|
|
54
|
-
const fieldMeta = (extra: Record<string, unknown> = {}) =>
|
|
55
|
-
({
|
|
56
|
-
name: 'description',
|
|
57
|
-
label: 'Description',
|
|
58
|
-
type: 'textarea',
|
|
59
|
-
mobile_fullscreen: true,
|
|
60
|
-
...extra,
|
|
61
|
-
}) as unknown as FieldMetadata;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Render, open the dialog, and return the dialog textarea's accessible
|
|
65
|
-
* DESCRIPTION resolved the way an assistive technology resolves it — follow
|
|
66
|
-
* `aria-describedby`, concatenate the referenced nodes. Scoped to its own mount
|
|
67
|
-
* and unmounted before returning: every case here renders a different language,
|
|
68
|
-
* and a document-wide read after a second mount would report the FIRST one.
|
|
69
|
-
*/
|
|
70
|
-
const fullscreenDescription = (language: string, maxLength = 200, value = 'hello') => {
|
|
71
|
-
const { unmount } = render(
|
|
72
|
-
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }}>
|
|
73
|
-
<TextAreaField value={value} onChange={vi.fn()} field={fieldMeta({ maxLength })} />
|
|
74
|
-
</I18nProvider>,
|
|
75
|
-
);
|
|
76
|
-
fireEvent.click(screen.getByTestId('textarea-fullscreen-toggle'));
|
|
77
|
-
|
|
78
|
-
const input = screen.getByTestId('textarea-fullscreen-input');
|
|
79
|
-
const ids = (input.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
|
|
80
|
-
const text = ids
|
|
81
|
-
.map((id) => document.getElementById(id)?.textContent ?? '')
|
|
82
|
-
.join(' ')
|
|
83
|
-
.trim();
|
|
84
|
-
const digits =
|
|
85
|
-
document.querySelector('[data-testid="textarea-fullscreen-character-count"]')?.textContent ??
|
|
86
|
-
null;
|
|
87
|
-
|
|
88
|
-
unmount();
|
|
89
|
-
return { text, digits };
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
describe('the fullscreen counter description is translated (objectui#3417)', () => {
|
|
93
|
-
it('renders the English sentence under an en provider', () => {
|
|
94
|
-
// A no-op pin: `FIELD_DEFAULTS` and the `en` pack are byte-identical, so
|
|
95
|
-
// this is here to prove routing through `t()` did not change English
|
|
96
|
-
// rendering. Only a positive assertion sees that direction.
|
|
97
|
-
const { text, digits } = fullscreenDescription('en');
|
|
98
|
-
|
|
99
|
-
expect(text).toBe('Character count: 5 of 200');
|
|
100
|
-
expect(digits).toBe('5/200');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('renders the sentence in Chinese under a zh provider', () => {
|
|
104
|
-
const { text, digits } = fullscreenDescription('zh');
|
|
105
|
-
|
|
106
|
-
expect(text).toBe('已输入 5 个字符,最多 200 个');
|
|
107
|
-
// The literal, asserted absent — a re-inlined one would still satisfy the
|
|
108
|
-
// positive assertion on a sibling case, and in a screen-reader-only string
|
|
109
|
-
// nobody who can see it is affected.
|
|
110
|
-
expect(text).not.toMatch(/character count/i);
|
|
111
|
-
// The digits stay language-independent; only the description is keyed.
|
|
112
|
-
expect(digits).toBe('5/200');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('renders the sentence in Japanese, with the cap interpolated BEFORE the count', () => {
|
|
116
|
-
// `{{max}}` precedes `{{count}}` in this pack. Code-side concatenation
|
|
117
|
-
// cannot produce this order — that is the point of the assertion.
|
|
118
|
-
const { text } = fullscreenDescription('ja');
|
|
119
|
-
|
|
120
|
-
expect(text).toBe('文字数: 200 文字中 5 文字');
|
|
121
|
-
expect(text.indexOf('200')).toBeLessThan(text.indexOf('5'));
|
|
122
|
-
expect(text).not.toMatch(/character count/i);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('resolves the base key under ru, whose plural rules i18next consults first', () => {
|
|
126
|
-
// `count` sends i18next to `key_one` / `key_few` / … BEFORE the base key,
|
|
127
|
-
// and these packs declare the base key only. A raw key reaching a
|
|
128
|
-
// screen-reader-only string is invisible to everyone who could report it.
|
|
129
|
-
const { text } = fullscreenDescription('ru');
|
|
130
|
-
|
|
131
|
-
expect(text).toBe('Количество символов: 5 из 200');
|
|
132
|
-
expect(text).not.toContain('fields.textarea');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('recomputes the sentence as the DRAFT changes, not the committed value', () => {
|
|
136
|
-
// Derived per render from the draft. A memo frozen at open time would keep
|
|
137
|
-
// describing the value the user opened the dialog with while the digits
|
|
138
|
-
// beside it moved on.
|
|
139
|
-
render(
|
|
140
|
-
<I18nProvider config={{ defaultLanguage: 'zh', detectBrowserLanguage: false }}>
|
|
141
|
-
<TextAreaField value="hello" onChange={vi.fn()} field={fieldMeta({ maxLength: 200 })} />
|
|
142
|
-
</I18nProvider>,
|
|
143
|
-
);
|
|
144
|
-
fireEvent.click(screen.getByTestId('textarea-fullscreen-toggle'));
|
|
145
|
-
|
|
146
|
-
const input = screen.getByTestId('textarea-fullscreen-input');
|
|
147
|
-
const describedText = () => {
|
|
148
|
-
const ids = (input.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
|
|
149
|
-
return ids
|
|
150
|
-
.map((id) => document.getElementById(id)?.textContent ?? '')
|
|
151
|
-
.join(' ')
|
|
152
|
-
.trim();
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
expect(describedText()).toBe('已输入 5 个字符,最多 200 个');
|
|
156
|
-
|
|
157
|
-
fireEvent.change(input, { target: { value: 'hello world' } });
|
|
158
|
-
|
|
159
|
-
expect(describedText()).toBe('已输入 11 个字符,最多 200 个');
|
|
160
|
-
});
|
|
161
|
-
});
|