@object-ui/fields 17.5.0 → 17.7.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 +6150 -0
- package/README.md +108 -8
- package/dist/FieldEditWidget.d.ts +70 -8
- package/dist/coerceToSafeValue.d.ts +54 -0
- package/dist/currency.d.ts +1 -1
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +5 -5
- package/dist/index.css +53 -11
- package/dist/index.d.ts +653 -128
- package/dist/index.js +4275 -3171
- package/dist/widgets/AddressField.d.ts +3 -3
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +20 -4
- package/dist/widgets/BooleanField.d.ts +2 -2
- 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 +2 -2
- package/dist/widgets/DateTimeField.d.ts +2 -2
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +43 -2
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
- package/dist/widgets/GeolocationField.d.ts +10 -4
- package/dist/widgets/GridField.d.ts +102 -86
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +31 -4
- package/dist/widgets/LookupField.d.ts +9 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +2 -2
- package/dist/widgets/ObjectField.d.ts +10 -3
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +38 -8
- package/dist/widgets/PasswordField.d.ts +15 -1
- package/dist/widgets/PeoplePicker.d.ts +17 -2
- package/dist/widgets/PercentField.d.ts +3 -2
- package/dist/widgets/PersonRow.d.ts +13 -2
- 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 +44 -1
- package/dist/widgets/RecordPickerDialog.d.ts +33 -16
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- package/dist/widgets/SelectionTray.d.ts +13 -2
- 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 +2 -2
- package/dist/widgets/TimeField.d.ts +2 -2
- 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 +27 -7
- package/dist/widgets/file-affordance.d.ts +33 -0
- package/dist/widgets/file-size-guard.d.ts +5 -23
- package/dist/widgets/file-value.d.ts +38 -9
- package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
- package/dist/widgets/nativeDateValue.d.ts +9 -27
- package/dist/widgets/number-format.d.ts +137 -0
- package/dist/widgets/numberBadInput.d.ts +135 -0
- package/dist/widgets/personDisplay.d.ts +22 -4
- package/dist/widgets/richTextDisplay.d.ts +116 -0
- package/dist/widgets/toDomProps.d.ts +10 -1
- package/dist/widgets/toHostGroupProps.d.ts +172 -21
- package/dist/widgets/toHostProps.d.ts +105 -0
- package/dist/widgets/types.d.ts +19 -6
- package/dist/widgets/upload-error-message.d.ts +6 -0
- package/dist/widgets/uploadingScope.d.ts +142 -0
- package/dist/widgets/useCascadingOptions.d.ts +3 -2
- package/dist/widgets/useRecordQuery.d.ts +4 -2
- package/dist/widgets/useUploadingSignal.d.ts +8 -0
- package/package.json +11 -13
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -207
- package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
- package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
- package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
- package/src/__tests__/DateCellRenderer.test.tsx +0 -82
- package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
- package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
- package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
- package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
- package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
- package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
- package/src/__tests__/date-locale-channel.test.tsx +0 -305
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
- package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
- package/src/__tests__/group-labelling-declaration.test.ts +0 -148
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
- package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
- package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
- 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 -357
- 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 -924
- package/src/currency.ts +0 -144
- package/src/datetime-widgets.test.tsx +0 -185
- package/src/field-type-alias.multiple.test.ts +0 -81
- package/src/field-type-alias.ts +0 -156
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.css +0 -44
- package/src/index.tsx +0 -2745
- package/src/standard-widgets.test.tsx +0 -364
- package/src/widgets/AddressField.postalCode.test.tsx +0 -257
- package/src/widgets/AddressField.tsx +0 -182
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -86
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -169
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -135
- package/src/widgets/DateField.tsx +0 -40
- package/src/widgets/DateTimeField.tsx +0 -46
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -512
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -38
- package/src/widgets/FullscreenFieldEditor.tsx +0 -60
- package/src/widgets/GeolocationField.tsx +0 -193
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -632
- package/src/widgets/GridField.tsx +0 -1070
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.maxSize.test.tsx +0 -154
- package/src/widgets/ImageField.tsx +0 -305
- 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 -1295
- 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 -196
- 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 -99
- 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 -143
- package/src/widgets/RatingField.tsx +0 -94
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
- package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1365
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -193
- 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 -59
- 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 -166
- 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/address-format.ts +0 -120
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-size-guard.ts +0 -55
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/nativeDateValue.test.ts +0 -115
- package/src/widgets/nativeDateValue.ts +0 -112
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
- package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
- 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 -204
- package/src/widgets/toHostGroupProps.ts +0 -102
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -132
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -291
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- package/src/withFieldCarrier.tsx +0 -75
|
@@ -1,346 +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
|
-
* What a screen reader can find out about the character cap INSIDE the
|
|
11
|
-
* fullscreen long-text dialog — objectui#3417.
|
|
12
|
-
*
|
|
13
|
-
* ## The state this replaces
|
|
14
|
-
*
|
|
15
|
-
* objectui#3408 gave the inline counter three nodes. The fullscreen dialog's
|
|
16
|
-
* counter, handed to `FullscreenFieldEditor` as a `footer`, stayed what it had
|
|
17
|
-
* been since the feature shipped:
|
|
18
|
-
*
|
|
19
|
-
* ```jsx
|
|
20
|
-
* footer={(draft) => maxLength
|
|
21
|
-
* ? <span className="…">{draft.length}/{maxLength}</span>
|
|
22
|
-
* : null}
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* Digits, and nothing else. No accessible name, no `aria-describedby` tying it
|
|
26
|
-
* to the dialog's `textarea`, nothing `aria-live`. Browse mode read "5 slash
|
|
27
|
-
* 500" if it happened to sweep the footer; focusing the input said nothing at
|
|
28
|
-
* all. Same field, same cap, same user as the inline surface — and the
|
|
29
|
-
* fullscreen branch was at zero.
|
|
30
|
-
*
|
|
31
|
-
* Reachability is not exotic: `ObjectForm` stamps `mobile_fullscreen` onto
|
|
32
|
-
* EVERY long-text field when `ObjectFormSchema.mobile.fullscreenLongText` is
|
|
33
|
-
* set, so on a phone with that setting on, every capped long-text field in the
|
|
34
|
-
* form is this case.
|
|
35
|
-
*
|
|
36
|
-
* ## What is pinned here, and what is deliberately NOT
|
|
37
|
-
*
|
|
38
|
-
* The dialog gets two of the three nodes — `aria-hidden` digits and a
|
|
39
|
-
* visually-hidden DESCRIPTION reached through the textarea's
|
|
40
|
-
* `aria-describedby`. It gets NO live region, and that absence is asserted
|
|
41
|
-
* rather than left to chance (`no aria-live node inside the dialog`): a modal
|
|
42
|
-
* the user opened deliberately to write at length should not also interrupt
|
|
43
|
-
* them, the description already delivers the cap on focus, and the inline
|
|
44
|
-
* surface's live region stays mounted behind the overlay — a second one would
|
|
45
|
-
* put two in one document.
|
|
46
|
-
*
|
|
47
|
-
* That assertion is therefore GREEN on `origin/main` too, and is expected to
|
|
48
|
-
* be. It is not evidence of the fix; it is the pin that the fix did not
|
|
49
|
-
* overreach. The cases that change direction are the describedby/description
|
|
50
|
-
* ones, which are red on `origin/main` for the plainest possible reason —
|
|
51
|
-
* `aria-describedby` is absent and there is no description node to point at.
|
|
52
|
-
*
|
|
53
|
-
* ## Why this file mounts no `I18nProvider`
|
|
54
|
-
*
|
|
55
|
-
* Two reasons, and they are the same reason. Mounting `I18nProvider` calls
|
|
56
|
-
* `initReactI18next`, which installs that instance as react-i18next's GLOBAL
|
|
57
|
-
* default — after any sibling case has done so there is no "no provider" state
|
|
58
|
-
* left in this module graph to observe, which is why
|
|
59
|
-
* `TextAreaField.characterCount.no-provider.test.tsx` and
|
|
60
|
-
* `FullscreenFieldEditor.no-provider.test.tsx` are separate FILES rather than
|
|
61
|
-
* separate `describe`s. Keeping this file provider-less makes its English
|
|
62
|
-
* expectations a genuine assertion about the code-shipped `FIELD_DEFAULTS`
|
|
63
|
-
* fallback on the fullscreen path — a raw key surfacing in a
|
|
64
|
-
* screen-reader-only string is invisible to everyone who could report it. The
|
|
65
|
-
* locale leg lives in `TextAreaField.fullscreenCharacterCount.i18n.test.tsx`.
|
|
66
|
-
*/
|
|
67
|
-
|
|
68
|
-
import React from 'react';
|
|
69
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
70
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
71
|
-
import '@testing-library/jest-dom';
|
|
72
|
-
|
|
73
|
-
import { TextAreaField } from '../TextAreaField';
|
|
74
|
-
import type { FieldMetadata } from '@object-ui/types';
|
|
75
|
-
|
|
76
|
-
const fieldMeta = (extra: Record<string, unknown> = {}) =>
|
|
77
|
-
({
|
|
78
|
-
name: 'description',
|
|
79
|
-
label: 'Description',
|
|
80
|
-
type: 'textarea',
|
|
81
|
-
mobile_fullscreen: true,
|
|
82
|
-
...extra,
|
|
83
|
-
}) as unknown as FieldMetadata;
|
|
84
|
-
|
|
85
|
-
const dialog = () => screen.getByTestId('textarea-fullscreen-dialog');
|
|
86
|
-
const fullscreenInput = () =>
|
|
87
|
-
screen.getByTestId('textarea-fullscreen-input') as HTMLTextAreaElement;
|
|
88
|
-
const fullscreenCounter = () =>
|
|
89
|
-
document.querySelector('[data-testid="textarea-fullscreen-character-count"]');
|
|
90
|
-
const inlineCounter = () => document.querySelector('[data-testid="textarea-character-count"]');
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Resolve an element's accessible DESCRIPTION the way an assistive technology
|
|
94
|
-
* does — follow `aria-describedby`, in order, and concatenate the referenced
|
|
95
|
-
* nodes' text. Asserting on this rather than on a node found by test id is
|
|
96
|
-
* what puts "the association actually works" under assertion: a description
|
|
97
|
-
* node nothing references is a node no screen reader will ever reach, and that
|
|
98
|
-
* is precisely the failure a footer-shaped counter invites.
|
|
99
|
-
*/
|
|
100
|
-
const describedText = (el: HTMLElement) => {
|
|
101
|
-
const ids = (el.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
|
|
102
|
-
return ids
|
|
103
|
-
.map((id) => document.getElementById(id)?.textContent ?? '')
|
|
104
|
-
.join(' ')
|
|
105
|
-
.trim();
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/** A host that owns the value, so a committed edit really changes it. */
|
|
109
|
-
function Host({
|
|
110
|
-
initial = '',
|
|
111
|
-
maxLength,
|
|
112
|
-
metaExtra = {},
|
|
113
|
-
}: {
|
|
114
|
-
initial?: string;
|
|
115
|
-
maxLength?: number;
|
|
116
|
-
metaExtra?: Record<string, unknown>;
|
|
117
|
-
}) {
|
|
118
|
-
const [v, setV] = React.useState(initial);
|
|
119
|
-
return (
|
|
120
|
-
<TextAreaField value={v} onChange={setV} field={fieldMeta({ maxLength, ...metaExtra })} />
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const openDialog = () => fireEvent.click(screen.getByTestId('textarea-fullscreen-toggle'));
|
|
125
|
-
|
|
126
|
-
describe('the fullscreen dialog counter is reachable from the input (objectui#3417)', () => {
|
|
127
|
-
it('describes the dialog textarea, so focus announces the cap once', () => {
|
|
128
|
-
// Measured on `origin/main`: the dialog's textarea carried no
|
|
129
|
-
// `aria-describedby` at all, so this is the assertion that flips.
|
|
130
|
-
render(<Host maxLength={500} initial="hello" />);
|
|
131
|
-
openDialog();
|
|
132
|
-
|
|
133
|
-
expect(fullscreenInput()).toHaveAttribute('aria-describedby');
|
|
134
|
-
expect(describedText(fullscreenInput())).toBe('Character count: 5 of 500');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('keeps the visible digits out of the accessibility tree', () => {
|
|
138
|
-
// The digits are a glance affordance. Unhidden they make a screen reader
|
|
139
|
-
// read "5 slash 500" on top of the description that says it in words —
|
|
140
|
-
// which is exactly what browse mode did on the old bare-span footer.
|
|
141
|
-
render(<Host maxLength={500} initial="hello" />);
|
|
142
|
-
openDialog();
|
|
143
|
-
|
|
144
|
-
expect(fullscreenCounter()).toHaveAttribute('aria-hidden', 'true');
|
|
145
|
-
expect(fullscreenCounter()).toHaveTextContent('5/500');
|
|
146
|
-
expect(fullscreenCounter()).not.toHaveAttribute('aria-live');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('counts the DRAFT, in the digits and in the description together', () => {
|
|
150
|
-
// The dialog edits a local draft that is not written back until "Done"
|
|
151
|
-
// (`FullscreenFieldEditor`). A description sourced from the committed
|
|
152
|
-
// value instead would describe the field the user is no longer looking at,
|
|
153
|
-
// and would silently disagree with the digits beside it — so both are
|
|
154
|
-
// asserted from the same edit.
|
|
155
|
-
render(<Host maxLength={500} initial="hello" />);
|
|
156
|
-
openDialog();
|
|
157
|
-
|
|
158
|
-
fireEvent.change(fullscreenInput(), { target: { value: 'hello from fullscreen' } });
|
|
159
|
-
|
|
160
|
-
expect(fullscreenCounter()).toHaveTextContent('21/500');
|
|
161
|
-
expect(describedText(fullscreenInput())).toBe('Character count: 21 of 500');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('substitutes both placeholders from the code-shipped fallback', () => {
|
|
165
|
-
// No provider is mounted, so whatever renders here came from
|
|
166
|
-
// `FIELD_DEFAULTS` via `createSafeTranslation`. The failure this guards is
|
|
167
|
-
// a raw key or an unsubstituted `{{max}}` reaching a screen-reader-only
|
|
168
|
-
// string, where nobody who can see it is affected and nobody affected can
|
|
169
|
-
// see it.
|
|
170
|
-
render(<Host maxLength={40} initial="hi" />);
|
|
171
|
-
openDialog();
|
|
172
|
-
|
|
173
|
-
expect(describedText(fullscreenInput())).toBe('Character count: 2 of 40');
|
|
174
|
-
expect(describedText(fullscreenInput())).not.toMatch(/fields\.textarea/);
|
|
175
|
-
expect(describedText(fullscreenInput())).not.toContain('{{');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('announces zero for an empty draft rather than a hole in the sentence', () => {
|
|
179
|
-
// `0` is falsy — the one input length at which a truthiness-guarded
|
|
180
|
-
// interpolation drops the number and speaks "Character count: of 40".
|
|
181
|
-
render(<Host maxLength={40} />);
|
|
182
|
-
openDialog();
|
|
183
|
-
|
|
184
|
-
expect(describedText(fullscreenInput())).toBe('Character count: 0 of 40');
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('the code-shipped default carries no CJK (Commandment #-1)', () => {
|
|
188
|
-
// Escaped ranges on purpose: a literal CJK character class would itself
|
|
189
|
-
// violate the commandment this asserts (same shape as
|
|
190
|
-
// `FullscreenFieldEditor.no-provider.test.tsx`).
|
|
191
|
-
render(<Host maxLength={200} initial="hello" />);
|
|
192
|
-
openDialog();
|
|
193
|
-
|
|
194
|
-
// The positive assertion FIRST, and not decoration: a negative match on a
|
|
195
|
-
// sentence that does not exist is satisfied by the empty string, so
|
|
196
|
-
// without this line the case is green on `origin/main` for the one reason
|
|
197
|
-
// that proves nothing.
|
|
198
|
-
expect(describedText(fullscreenInput())).toBe('Character count: 5 of 200');
|
|
199
|
-
expect(describedText(fullscreenInput())).not.toMatch(/[\u3000-\u30ff\u4e00-\u9fff]/);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('reads the legacy snake_case max_length spelling here too', () => {
|
|
203
|
-
// The dual-read predates all of this (framework#1878 §3). Sharing the
|
|
204
|
-
// counting UI across surfaces must not narrow which spelling reaches the
|
|
205
|
-
// fullscreen one.
|
|
206
|
-
render(<Host metaExtra={{ max_length: 80 }} initial="hello" />);
|
|
207
|
-
openDialog();
|
|
208
|
-
|
|
209
|
-
expect(fullscreenCounter()).toHaveTextContent('5/80');
|
|
210
|
-
expect(describedText(fullscreenInput())).toBe('Character count: 5 of 80');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe('the fullscreen dialog stays SILENT while typing (objectui#3417)', () => {
|
|
215
|
-
it('renders no aria-live node inside the dialog', () => {
|
|
216
|
-
// The ruling on #3417, pinned as an absence. GREEN on `origin/main` too —
|
|
217
|
-
// there was nothing accessible in that footer at all — so this case is not
|
|
218
|
-
// evidence of the fix, it is the guard against the fix overreaching into a
|
|
219
|
-
// second live region. `[aria-live]` unqualified, because "assertive"
|
|
220
|
-
// instead of "polite" would be worse, not compliant.
|
|
221
|
-
render(<Host maxLength={100} initial={'x'.repeat(95)} />);
|
|
222
|
-
openDialog();
|
|
223
|
-
|
|
224
|
-
// Deep inside the warning band (5 of 100 left): if a status region existed
|
|
225
|
-
// on this surface, this is the state that would fill it.
|
|
226
|
-
expect(dialog().querySelector('[aria-live]')).toBeNull();
|
|
227
|
-
expect(dialog().querySelector('[aria-atomic]')).toBeNull();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('grows no live region as the draft crosses into the near-limit band', () => {
|
|
231
|
-
render(<Host maxLength={100} initial="short" />);
|
|
232
|
-
openDialog();
|
|
233
|
-
|
|
234
|
-
fireEvent.change(fullscreenInput(), { target: { value: 'x'.repeat(99) } });
|
|
235
|
-
|
|
236
|
-
expect(fullscreenCounter()).toHaveTextContent('99/100');
|
|
237
|
-
expect(dialog().querySelector('[aria-live]')).toBeNull();
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe('the two surfaces are wired independently (objectui#3417)', () => {
|
|
242
|
-
it('gives the inline and fullscreen descriptions distinct ids', () => {
|
|
243
|
-
// One shared id would point both textareas at whichever sentence rendered
|
|
244
|
-
// last — and the two diverge the moment the user types in the dialog,
|
|
245
|
-
// which is the whole reason the dialog holds a draft.
|
|
246
|
-
render(<Host maxLength={500} initial="hello" />);
|
|
247
|
-
const inline = screen.getAllByRole('textbox')[0] as HTMLTextAreaElement;
|
|
248
|
-
const inlineDescribedBy = inline.getAttribute('aria-describedby');
|
|
249
|
-
openDialog();
|
|
250
|
-
const fullscreenDescribedBy = fullscreenInput().getAttribute('aria-describedby');
|
|
251
|
-
|
|
252
|
-
// BOTH asserted present before they are compared. `not.toBe` alone is
|
|
253
|
-
// satisfied by `null !== 'some-id'`, which is exactly the `origin/main`
|
|
254
|
-
// state this case is supposed to reject — the trap of an assertion that
|
|
255
|
-
// passes because nothing was produced.
|
|
256
|
-
expect(inlineDescribedBy).not.toBeNull();
|
|
257
|
-
expect(fullscreenDescribedBy).not.toBeNull();
|
|
258
|
-
expect(fullscreenDescribedBy).not.toBe(inlineDescribedBy);
|
|
259
|
-
// And each resolves to its own sentence, not to the other's node.
|
|
260
|
-
expect(describedText(fullscreenInput())).toBe('Character count: 5 of 500');
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('leaves the inline description on the committed value while the draft moves', () => {
|
|
264
|
-
// The draft is local until "Done". Both descriptions are live at once (the
|
|
265
|
-
// inline one behind the overlay), so a shared source would make one of
|
|
266
|
-
// them a lie.
|
|
267
|
-
render(<Host maxLength={500} initial="hello" />);
|
|
268
|
-
const inline = screen.getAllByRole('textbox')[0] as HTMLTextAreaElement;
|
|
269
|
-
openDialog();
|
|
270
|
-
|
|
271
|
-
fireEvent.change(fullscreenInput(), { target: { value: 'hello from fullscreen' } });
|
|
272
|
-
|
|
273
|
-
expect(describedText(fullscreenInput())).toBe('Character count: 21 of 500');
|
|
274
|
-
expect(describedText(inline)).toBe('Character count: 5 of 500');
|
|
275
|
-
expect(inlineCounter()).toHaveTextContent('5/500');
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('lets the committed edit flow back to the inline counter on Done', () => {
|
|
279
|
-
// End to end, because a counter that describes a draft nobody commits
|
|
280
|
-
// would satisfy every assertion above.
|
|
281
|
-
render(<Host maxLength={500} initial="hello" />);
|
|
282
|
-
openDialog();
|
|
283
|
-
|
|
284
|
-
fireEvent.change(fullscreenInput(), { target: { value: 'hello from fullscreen' } });
|
|
285
|
-
fireEvent.click(screen.getByTestId('textarea-fullscreen-save'));
|
|
286
|
-
|
|
287
|
-
expect(inlineCounter()).toHaveTextContent('21/500');
|
|
288
|
-
const inline = screen.getAllByRole('textbox')[0] as HTMLTextAreaElement;
|
|
289
|
-
expect(describedText(inline)).toBe('Character count: 21 of 500');
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('keeps the inline surface at three nodes, live region included', () => {
|
|
293
|
-
// The extraction must not have cost the inline surface anything. Its own
|
|
294
|
-
// pins live in the objectui#3408 files and are untouched; this is the
|
|
295
|
-
// cross-check from the fullscreen side, taken with the dialog OPEN so that
|
|
296
|
-
// "there is one live region in the document, and it is the inline one" is
|
|
297
|
-
// asserted as a whole.
|
|
298
|
-
render(<Host maxLength={100} initial={'x'.repeat(95)} />);
|
|
299
|
-
openDialog();
|
|
300
|
-
|
|
301
|
-
const liveRegions = document.querySelectorAll('[aria-live="polite"]');
|
|
302
|
-
expect(liveRegions).toHaveLength(1);
|
|
303
|
-
expect(dialog().contains(liveRegions[0])).toBe(false);
|
|
304
|
-
expect(inlineCounter()).toHaveAttribute('aria-hidden', 'true');
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
describe('a field with no cap gets no fullscreen counter (objectui#3417)', () => {
|
|
309
|
-
it('renders neither digits nor a description node', () => {
|
|
310
|
-
// `{maxLength && …}` on both surfaces. Sharing the component must not make
|
|
311
|
-
// either one unconditional.
|
|
312
|
-
render(<Host maxLength={undefined} />);
|
|
313
|
-
openDialog();
|
|
314
|
-
|
|
315
|
-
expect(fullscreenCounter()).toBeNull();
|
|
316
|
-
expect(inlineCounter()).toBeNull();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('leaves the dialog textarea`s aria-describedby unset', () => {
|
|
320
|
-
// An `aria-describedby` naming an id that does not exist is worse than
|
|
321
|
-
// none: some assistive technologies fall back to reading the id string.
|
|
322
|
-
render(<Host maxLength={undefined} />);
|
|
323
|
-
openDialog();
|
|
324
|
-
|
|
325
|
-
expect(fullscreenInput()).not.toHaveAttribute('aria-describedby');
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('renders no counter on either surface when the field is readonly', () => {
|
|
329
|
-
// The readonly branch early-returns a display before any of this is
|
|
330
|
-
// computed, so there is no dialog to open at all. Pinned as a
|
|
331
|
-
// non-regression of the hook hoist.
|
|
332
|
-
render(
|
|
333
|
-
<TextAreaField
|
|
334
|
-
value="hello"
|
|
335
|
-
onChange={vi.fn()}
|
|
336
|
-
readonly
|
|
337
|
-
field={fieldMeta({ maxLength: 500 })}
|
|
338
|
-
/>,
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
expect(inlineCounter()).toBeNull();
|
|
342
|
-
expect(fullscreenCounter()).toBeNull();
|
|
343
|
-
expect(screen.queryByTestId('textarea-fullscreen-toggle')).not.toBeInTheDocument();
|
|
344
|
-
expect(document.querySelector('[aria-live="polite"]')).toBeNull();
|
|
345
|
-
});
|
|
346
|
-
});
|
|
@@ -1,246 +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
|
-
* A DISABLED `TextAreaField` cannot be edited through its fullscreen dialog —
|
|
11
|
-
* objectui#3402.
|
|
12
|
-
*
|
|
13
|
-
* `disabled` used to land on the inline `<Textarea>` and nowhere else:
|
|
14
|
-
* `showFullscreenButton` did not consult it, the call site did not forward it,
|
|
15
|
-
* and `FullscreenFieldEditor` did not declare it. Measured on `origin/main`
|
|
16
|
-
* with a probe equivalent to the cases below (the issue itself carried only
|
|
17
|
-
* static evidence, so the premise was reproduced dynamically before any fix):
|
|
18
|
-
*
|
|
19
|
-
* PROBE textarea: inline disabled = true
|
|
20
|
-
* PROBE textarea: toggle present = true, toggle disabled = false
|
|
21
|
-
* PROBE textarea: dialog opened = true
|
|
22
|
-
* PROBE textarea: dialog input disabled = false
|
|
23
|
-
* PROBE textarea: onChange calls = [["EDITED WHILE DISABLED"]]
|
|
24
|
-
*
|
|
25
|
-
* That last line is the defect: a disabled field's value changed. The state is
|
|
26
|
-
* nastier than the read-only one precisely because it LOOKS right — the inline
|
|
27
|
-
* control is greyed out, and the only way around it is the button sitting on
|
|
28
|
-
* top of it.
|
|
29
|
-
*
|
|
30
|
-
* This is the registered-path half of the same family #3400/PR #3401 fixed on
|
|
31
|
-
* the built-in `form.tsx` path, and the two are deliberately pinned to the same
|
|
32
|
-
* behaviours so one form-level setting keeps producing one behaviour:
|
|
33
|
-
*
|
|
34
|
-
* - `readonly` → no affordance at all. Untouched here: this widget's
|
|
35
|
-
* early-return renders a read-only display before the button is computed.
|
|
36
|
-
* Pinned below as a NON-REGRESSION, because the fix must not have moved it.
|
|
37
|
-
* - `disabled` → the button stays but is inert, and the dialog locks on its
|
|
38
|
-
* own. `disabled` is not only a static flag: the form renderer computes it
|
|
39
|
-
* as `disabled || fieldDisabled || isSubmitting || …`, so it can flip to
|
|
40
|
-
* true while the dialog is already open, at which point the button is no
|
|
41
|
-
* longer the gate.
|
|
42
|
-
*
|
|
43
|
-
* Reachability is not exotic. `ObjectForm` stamps `mobile_fullscreen: true`
|
|
44
|
-
* onto EVERY long-text field when `ObjectFormSchema.mobile.fullscreenLongText`
|
|
45
|
-
* is set, without consulting readonly/disabled, and `stripRegisteredFieldProps`
|
|
46
|
-
* does not strip `disabled` from what registered widgets receive.
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
50
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
51
|
-
import '@testing-library/jest-dom';
|
|
52
|
-
|
|
53
|
-
import { TextAreaField } from '../TextAreaField';
|
|
54
|
-
import type { FieldMetadata } from '@object-ui/types';
|
|
55
|
-
|
|
56
|
-
const fieldMeta = (extra: Record<string, unknown> = {}) =>
|
|
57
|
-
({
|
|
58
|
-
name: 'description',
|
|
59
|
-
label: 'Description',
|
|
60
|
-
type: 'textarea',
|
|
61
|
-
mobile_fullscreen: true,
|
|
62
|
-
...extra,
|
|
63
|
-
}) as unknown as FieldMetadata;
|
|
64
|
-
|
|
65
|
-
const toggle = () =>
|
|
66
|
-
screen.queryByTestId('textarea-fullscreen-toggle') as HTMLButtonElement | null;
|
|
67
|
-
const dialogInput = () =>
|
|
68
|
-
screen.queryByTestId('textarea-fullscreen-input') as HTMLTextAreaElement | null;
|
|
69
|
-
const saveButton = () =>
|
|
70
|
-
screen.queryByTestId('textarea-fullscreen-save') as HTMLButtonElement | null;
|
|
71
|
-
|
|
72
|
-
describe('TextAreaField fullscreen — disabled keeps the affordance but makes it inert (objectui#3402)', () => {
|
|
73
|
-
it('keeps the expand button and disables it', () => {
|
|
74
|
-
render(
|
|
75
|
-
<TextAreaField value="hello" onChange={() => {}} field={fieldMeta()} disabled />,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// The affordance stays: `disabled` means "not interactive, muted", which is
|
|
79
|
-
// a different state from `readonly`'s "shown plainly" (no button at all).
|
|
80
|
-
expect(toggle()).not.toBeNull();
|
|
81
|
-
expect(toggle()!.disabled).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('does not open the dialog when that button is clicked anyway', () => {
|
|
85
|
-
// The attribute stops a real user. The handler's own `if (disabled) return`
|
|
86
|
-
// is pinned separately here because the attribute is not the only thing
|
|
87
|
-
// that can be lost — a `pointer-events` rule and the attribute both live in
|
|
88
|
-
// a className/props soup that refactors touch.
|
|
89
|
-
render(
|
|
90
|
-
<TextAreaField value="hello" onChange={() => {}} field={fieldMeta()} disabled />,
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
fireEvent.click(toggle()!);
|
|
94
|
-
|
|
95
|
-
expect(screen.queryByTestId('textarea-fullscreen-dialog')).not.toBeInTheDocument();
|
|
96
|
-
expect(dialogInput()).toBeNull();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('still disables the inline control', () => {
|
|
100
|
-
// The one exit that always held. Kept so a fix that moved `disabled` onto
|
|
101
|
-
// the dialog and off the inline textarea would be caught.
|
|
102
|
-
render(
|
|
103
|
-
<TextAreaField value="hello" onChange={() => {}} field={fieldMeta()} disabled />,
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const inline = screen.getAllByRole('textbox')[0] as HTMLTextAreaElement;
|
|
107
|
-
expect(inline.disabled).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('never commits a value while disabled', () => {
|
|
111
|
-
const onChange = vi.fn();
|
|
112
|
-
render(
|
|
113
|
-
<TextAreaField value="hello" onChange={onChange} field={fieldMeta()} disabled />,
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
fireEvent.click(toggle()!);
|
|
117
|
-
|
|
118
|
-
// The measured defect, in one assertion: the probe above got
|
|
119
|
-
// `[["EDITED WHILE DISABLED"]]` here.
|
|
120
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('TextAreaField fullscreen — the dialog holds on its own when disabled arrives mid-edit (objectui#3402)', () => {
|
|
125
|
-
/**
|
|
126
|
-
* Open the dialog while editable, then flip `disabled` under it — the
|
|
127
|
-
* `isSubmitting` case. This widget never sees `isSubmitting`; it sees
|
|
128
|
-
* `disabled`, which the form renderer has already OR-ed it into, so the honest
|
|
129
|
-
* widget-level reproduction is the prop flipping true while the dialog is
|
|
130
|
-
* open, which is exactly what a submit does to it. (The form-level
|
|
131
|
-
* composition itself is pinned on the built-in path by #3400/PR #3401.)
|
|
132
|
-
*/
|
|
133
|
-
function openThenDisable(onChange: () => void) {
|
|
134
|
-
const { rerender } = render(
|
|
135
|
-
<TextAreaField value="hello" onChange={onChange} field={fieldMeta()} />,
|
|
136
|
-
);
|
|
137
|
-
// Opened while the field is still editable — the legitimate state.
|
|
138
|
-
fireEvent.click(toggle()!);
|
|
139
|
-
expect(dialogInput()).not.toBeNull();
|
|
140
|
-
|
|
141
|
-
// Submit starts. The dialog is still on screen; the button is no longer
|
|
142
|
-
// the gate.
|
|
143
|
-
rerender(
|
|
144
|
-
<TextAreaField value="hello" onChange={onChange} field={fieldMeta()} disabled />,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
it('locks the dialog controls', () => {
|
|
149
|
-
openThenDisable(vi.fn());
|
|
150
|
-
|
|
151
|
-
expect(dialogInput()).not.toBeNull();
|
|
152
|
-
expect(dialogInput()!.disabled).toBe(true);
|
|
153
|
-
expect(saveButton()!.disabled).toBe(true);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Deliberately a SEPARATE test from the attribute assertions above, not a tail
|
|
158
|
-
* on them. Sharing one test made this assertion unreachable whenever an
|
|
159
|
-
* attribute assertion failed — i.e. it could only ever run in the world where
|
|
160
|
-
* the native attribute was already doing the work, which is no pin at all.
|
|
161
|
-
* Split, it is `commitFullscreen`'s own `if (!disabled)` guard that answers
|
|
162
|
-
* when the attribute is gone. Verified by removing each in turn: drop the
|
|
163
|
-
* `disabled` attribute from "Done" and this test stays green (the guard
|
|
164
|
-
* refuses); drop the guard as well and it turns red with
|
|
165
|
-
* `onChange` called with "EDITED WHILE SUBMITTING".
|
|
166
|
-
*/
|
|
167
|
-
it('refuses the write-back even when "Done" is driven directly', () => {
|
|
168
|
-
const onChange = vi.fn();
|
|
169
|
-
openThenDisable(onChange);
|
|
170
|
-
|
|
171
|
-
fireEvent.change(dialogInput()!, { target: { value: 'EDITED WHILE SUBMITTING' } });
|
|
172
|
-
fireEvent.click(saveButton()!);
|
|
173
|
-
|
|
174
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('still lets the user out of the dialog', () => {
|
|
178
|
-
// Cancel and Esc must stay live in every state, or a submit in flight traps
|
|
179
|
-
// the user inside a modal they can no longer use.
|
|
180
|
-
const onChange = vi.fn();
|
|
181
|
-
const { rerender } = render(
|
|
182
|
-
<TextAreaField value="hello" onChange={onChange} field={fieldMeta()} />,
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
fireEvent.click(toggle()!);
|
|
186
|
-
rerender(
|
|
187
|
-
<TextAreaField value="hello" onChange={onChange} field={fieldMeta()} disabled />,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
|
191
|
-
|
|
192
|
-
expect(screen.queryByTestId('textarea-fullscreen-dialog')).not.toBeInTheDocument();
|
|
193
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('TextAreaField fullscreen — the states the fix must not have moved (objectui#3402)', () => {
|
|
198
|
-
it('still edits fullscreen and commits when the field is editable', () => {
|
|
199
|
-
// The control case for every "does not" above: without it they would all
|
|
200
|
-
// pass just as well on a widget that renders no affordance at all.
|
|
201
|
-
const onChange = vi.fn();
|
|
202
|
-
render(<TextAreaField value="hello" onChange={onChange} field={fieldMeta()} />);
|
|
203
|
-
|
|
204
|
-
expect(toggle()!.disabled).toBe(false);
|
|
205
|
-
fireEvent.click(toggle()!);
|
|
206
|
-
|
|
207
|
-
const input = dialogInput()!;
|
|
208
|
-
expect(input.disabled).toBe(false);
|
|
209
|
-
expect(saveButton()!.disabled).toBe(false);
|
|
210
|
-
|
|
211
|
-
fireEvent.change(input, { target: { value: 'hello from fullscreen' } });
|
|
212
|
-
fireEvent.click(saveButton()!);
|
|
213
|
-
|
|
214
|
-
expect(onChange).toHaveBeenCalledWith('hello from fullscreen');
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('still renders no affordance at all in readonly mode', () => {
|
|
218
|
-
// The early return this change deliberately did NOT touch: `readonly`
|
|
219
|
-
// returns a read-only display before `showFullscreenButton` is computed,
|
|
220
|
-
// which is why readonly was never part of this defect.
|
|
221
|
-
render(
|
|
222
|
-
<TextAreaField value="hello" onChange={() => {}} readonly field={fieldMeta()} />,
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
expect(toggle()).toBeNull();
|
|
226
|
-
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
|
227
|
-
expect(screen.getByText('hello')).toBeInTheDocument();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('still renders no affordance when the metadata does not opt in, disabled or not', () => {
|
|
231
|
-
const { rerender } = render(
|
|
232
|
-
<TextAreaField value="hello" onChange={() => {}} field={fieldMeta({ mobile_fullscreen: false })} />,
|
|
233
|
-
);
|
|
234
|
-
expect(toggle()).toBeNull();
|
|
235
|
-
|
|
236
|
-
rerender(
|
|
237
|
-
<TextAreaField
|
|
238
|
-
value="hello"
|
|
239
|
-
onChange={() => {}}
|
|
240
|
-
field={fieldMeta({ mobile_fullscreen: false })}
|
|
241
|
-
disabled
|
|
242
|
-
/>,
|
|
243
|
-
);
|
|
244
|
-
expect(toggle()).toBeNull();
|
|
245
|
-
});
|
|
246
|
-
});
|