@onekeyfe/react-native-native-list 6.7.0-alpha.428 → 6.7.0-alpha.432

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.
@@ -31,8 +31,6 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
31
31
  private val titleLine = PackedTitleLineLayout(context)
32
32
  private val title = NativeListTextView(context)
33
33
  private val titleIcon = OneKeyIconView(context)
34
- private val titleLeadingIcon = OneKeyIconView(context)
35
- private val titleSegments = NativeListTitleSegmentsView(context)
36
34
  private var titleImage: NativeListImageSlot? = null
37
35
  private val subtitle = NativeListTextView(context)
38
36
  private val tertiary = NativeListTextView(context)
@@ -111,13 +109,8 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
111
109
  mainColumn.gravity = Gravity.CENTER_VERTICAL
112
110
  titleLine.orientation = HORIZONTAL
113
111
  titleLine.gravity = Gravity.CENTER_VERTICAL
114
- titleLine.addView(titleLeadingIcon, LayoutParams(dp(16), dp(16)).apply { marginEnd = dp(6) })
115
112
  titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
116
- titleLine.addView(titleSegments, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
117
113
  titleLine.addView(titleIcon, LayoutParams(dp(20), dp(20)).apply { marginStart = dp(4) })
118
- titleLine.titleChild = title
119
- titleLeadingIcon.visibility = GONE
120
- titleSegments.visibility = GONE
121
114
  titleLine.addView(badgeLine)
122
115
  trailingColumn.onAction = { key, view, slot, target ->
123
116
  emitAction(key, view, "trailingAccessory", slot, target, trailingColumn.anchorInset(view))
@@ -230,12 +223,6 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
230
223
  titleIcon.visibility = GONE
231
224
  titleIcon.contentDescription = null
232
225
  titleIcon.setOnClickListener(null)
233
- titleLeadingIcon.visibility = GONE
234
- titleLeadingIcon.contentDescription = null
235
- titleSegments.visibility = GONE
236
- titleSegments.clearRuns()
237
- titleSegments.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
238
- titleLine.titleChild = title
239
226
  for (view in listOf(title, subtitle, tertiary, badgeLine)) {
240
227
  view.text = ""
241
228
  view.visibility = GONE
@@ -356,46 +343,6 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
356
343
  }
357
344
  // Legacy: overhanging selector icons/checkboxes must not be clipped by row padding.
358
345
  if (trailingColumn.parent === this && trailingColumn.requestsUnclippedHost) clipToPadding = false
359
- if (titleSegments.visibility == VISIBLE) bindTitleRuns(item, theme)
360
- }
361
-
362
- // Runs take the title's final text appearance and color unless they set
363
- // their own tone or weight.
364
- private fun bindTitleRuns(item: NativeListItem, theme: JSONObject?) {
365
- titleSegments.typeface = title.typeface
366
- titleSegments.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, title.textSize)
367
- titleSegments.includeFontPadding = title.includeFontPadding
368
- titleSegments.letterSpacing = title.letterSpacing
369
- titleSegments.opticalOffsetY = title.opticalOffsetY
370
- titleSegments.maxLines = 1
371
- titleSegments.ellipsize = TextUtils.TruncateAt.END
372
- TextViewCompat.setLineHeight(titleSegments, title.lineHeight)
373
- val baseColor = title.currentTextColor
374
- titleSegments.setTextColor(baseColor)
375
- val segments = item.json.optJSONArray("titleSegments") ?: return
376
- val runs =
377
- (0 until segments.length()).mapNotNull { index ->
378
- val segment = segments.optJSONObject(index) ?: return@mapNotNull null
379
- NativeListTitleSegmentsView.Run(
380
- text = segment.optString("text"),
381
- typeface =
382
- when (segment.optString("fontWeight")) {
383
- "medium" -> fonts.medium(context)
384
- "regular" -> fonts.regular(context)
385
- else -> null
386
- },
387
- color =
388
- when (segment.optString("tone")) {
389
- "secondary" -> color(theme, "secondaryText", "#0000009B")
390
- "primary" -> color(theme, "primaryText", "#000000DF")
391
- else -> baseColor
392
- },
393
- shrink = segment.optBoolean("shrink", false),
394
- maxWidthRatio =
395
- if (segment.has("maxWidthRatio")) segment.optDouble("maxWidthRatio").toFloat() else null,
396
- )
397
- }
398
- titleSegments.setRuns(runs, item.json.optString("title"))
399
346
  }
400
347
 
401
348
  private fun styleGap(column: LinearLayout, gap: Int) {
@@ -529,8 +476,7 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
529
476
  val imageWidth = titleImage?.view?.takeIf { it.visibility == VISIBLE }?.let {
530
477
  it.layoutParams.width + dp(4)
531
478
  } ?: 0
532
- val icon = (if (titleIcon.visibility == VISIBLE) dp(24) else 0) + imageWidth +
533
- (if (titleLeadingIcon.visibility == VISIBLE) dp(22) else 0)
479
+ val icon = (if (titleIcon.visibility == VISIBLE) dp(24) else 0) + imageWidth
534
480
  (inner * ratio).roundToInt()
535
481
  .minus(leading.width + leading.marginEnd + icon)
536
482
  .coerceAtLeast(0)
@@ -541,10 +487,9 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
541
487
  super.onMeasure(widthMeasureSpec, heightMeasureSpec)
542
488
  if (titleSuffixLabels.isNotEmpty()) {
543
489
  val fixedWidth =
544
- (0 until titleLine.childCount).sumOf { index ->
490
+ (1 until titleLine.childCount).sumOf { index ->
545
491
  val view = titleLine.getChildAt(index)
546
- if (view.visibility == GONE || view === titleLine.titleChild ||
547
- titleSuffixLabels.any { it === view }) 0
492
+ if (view.visibility == GONE || titleSuffixLabels.any { it === view }) 0
548
493
  else {
549
494
  val margins = view.layoutParams as MarginLayoutParams
550
495
  view.measuredWidth + margins.leftMargin + margins.rightMargin
@@ -808,30 +753,6 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
808
753
  titleLine.packsChildrenAtStart = true
809
754
  title.ellipsize = TextUtils.TruncateAt.END
810
755
  showText(title, item.json.optString("title"), item.json.optInt("titleLines", 1))
811
- // Runs replace the drawn title except where `title` itself is highlighted;
812
- // their content is bound once the title style is final.
813
- if ((item.json.optJSONArray("titleSegments")?.length() ?: 0) > 0 &&
814
- (item.json.optJSONArray("titleMatch")?.length() ?: 0) == 0
815
- ) {
816
- title.visibility = GONE
817
- titleSegments.visibility = VISIBLE
818
- titleLine.titleChild = titleSegments
819
- }
820
- item.json.optJSONObject("titleLeadingIcon")?.let { icon ->
821
- titleLeadingIcon.iconName = icon.optString("name")
822
- titleLeadingIcon.glyphSizeDp = 16
823
- titleLeadingIcon.useSourceScale = sourceScale
824
- titleLeadingIcon.tintColor = safeColor(
825
- icon.optString("tintColor").takeIf(String::isNotEmpty),
826
- color(theme, "iconSubdued", "#00000072"),
827
- )
828
- titleLeadingIcon.contentDescription =
829
- icon.optString("accessibilityLabel").takeIf(String::isNotEmpty)
830
- titleLeadingIcon.importantForAccessibility =
831
- if (titleLeadingIcon.contentDescription == null) View.IMPORTANT_FOR_ACCESSIBILITY_NO
832
- else View.IMPORTANT_FOR_ACCESSIBILITY_YES
833
- titleLeadingIcon.visibility = VISIBLE
834
- }
835
756
  item.json.optJSONArray("titleSuffixes")?.let { suffixes ->
836
757
  for (index in 0 until suffixes.length()) {
837
758
  val suffix = suffixes.optJSONObject(index) ?: continue
@@ -109,9 +109,6 @@ internal class PackedTitleLineLayout(context: android.content.Context) : LinearL
109
109
  var packsChildrenAtStart = false
110
110
  // OneKey patch: optional cap for the Market subtitle's localized name.
111
111
  var leadingTextMaxWidth = Int.MAX_VALUE
112
- // The text view that takes what the other children leave; the first child
113
- // when unset, so a glyph may precede the title once this names it.
114
- var titleChild: TextView? = null
115
112
 
116
113
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
117
114
  if (!packsChildrenAtStart || childCount < 2) {
@@ -119,13 +116,13 @@ internal class PackedTitleLineLayout(context: android.content.Context) : LinearL
119
116
  return
120
117
  }
121
118
 
122
- val title = titleChild?.takeIf { it.parent === this } ?: getChildAt(0)
119
+ val title = getChildAt(0)
123
120
  val widthMode = MeasureSpec.getMode(widthMeasureSpec)
124
121
  val widthSize = MeasureSpec.getSize(widthMeasureSpec)
125
122
  var accessoryWidth = 0
126
- for (index in 0 until childCount) {
123
+ for (index in 1 until childCount) {
127
124
  val child = getChildAt(index)
128
- if (child === title || child.visibility == GONE) continue
125
+ if (child.visibility == GONE) continue
129
126
  measureChildWithMargins(
130
127
  child,
131
128
  widthMeasureSpec,
@@ -54,14 +54,7 @@ separately from source implementation in STYLE_SPEC §9.
54
54
 
55
55
  - **Use:** an identifiable asset, account, wallet, network or setting.
56
56
  - **Required data:** `leading`, `title`; optional `subtitle`, `tertiary`, `badges`,
57
- `trailing`, `leadingAction`, `titleIcon`, Web-only `secondaryTitleIcon`, `titleSegments`,
58
- `titleLeadingIcon`, match ranges and structured subtitle segments.
59
- - **Title runs:** optional `titleSegments` draw 1...4 runs with their own tone and
60
- weight in place of `title`, on one line. One run may shrink; the others keep
61
- their width under an optional `maxWidthRatio` cap. `title` stays the spoken
62
- text. See SPEC.md.
63
- - **Leading title icon:** optional `titleLeadingIcon` draws a 16-point glyph 6
64
- points before the title; it is decorative unless labelled.
57
+ `trailing`, `leadingAction`, `titleIcon`, Web-only `secondaryTitleIcon`, match ranges and structured subtitle segments.
65
58
  - **Static title icon:** optional 20-point icon immediately after the title with
66
59
  a 4-point gap. It has no click action; an optional Web `hoverActionKey`
67
60
  emits an icon-anchored tooltip request on hover or touch. A trailing `button` is the fixed small
package/docs/SPEC.md CHANGED
@@ -188,90 +188,6 @@ and Web acceptance must cover narrow addresses, dynamic type/fonts, long press
188
188
  versus tap, keyboard interaction, and the horizontal warning border. Source or
189
189
  build success alone is not runtime evidence.
190
190
 
191
- ### Identity title segments and leading title icon
192
-
193
- Status: implemented in source for iOS, Android and Web; device and Web runtime
194
- acceptance is pending. These fields travel in snapshot/patch JSON and do not
195
- change the Nitro ABI. They serve recipient rows whose title is a wallet and an
196
- account name in two styles, and address book rows marked by an icon before the
197
- name.
198
-
199
- `identity.titleSegments` is an optional list of 1...4 runs drawn on the title
200
- line in place of `title`:
201
-
202
- ```ts
203
- titleSegments?: readonly {
204
- text: string;
205
- tone?: 'primary' | 'secondary';
206
- fontWeight?: 'regular' | 'medium';
207
- shrink?: boolean;
208
- maxWidthRatio?: number; // (0, 1]
209
- }[];
210
- ```
211
-
212
- - `title` stays required. It is the spoken label and the fallback text, so it
213
- carries the same words as the runs; every renderer exposes `title`, not the
214
- runs, to accessibility.
215
- - Each run uses the title's font family, size and line height, including
216
- `style.title` and the configured fonts. `tone` picks the primary or secondary
217
- text color and defaults to the title color; `fontWeight` picks the weight and
218
- defaults to the title weight.
219
- - The runs share one line. `titleLines` and `style.title.lines` do not apply.
220
- - Layout. The runs take the whole width the title line leaves after the
221
- leading title icon and the title accessories (title icon, title image,
222
- suffixes, badges), so those accessories sit at the line's end rather than
223
- right after the text. W is that width:
224
- 1. A run's base width is its natural width, capped at `maxWidthRatio x W`
225
- when given. A run longer than its cap truncates at the cap with a tail
226
- ellipsis.
227
- 2. At most one run sets `shrink: true`. Every other run keeps its base width.
228
- 3. The shrinking run gets `min(base, W - sum of the other runs)` and
229
- truncates with a tail ellipsis; it may shrink to zero width.
230
- 4. When the runs that do not shrink exceed W on their own, the line is cut at
231
- its end. Native renderers end the last visible run with an ellipsis; Web
232
- clips it. This divergence is accepted: callers avoid the case with
233
- `maxWidthRatio`.
234
- - Development validation rejects an empty or oversized list, blank or
235
- oversized text (the 4096-code-unit text bound), unknown keys, tones or
236
- weights, a ratio outside `(0, 1]`, more than one shrinking run, `titleMatch`
237
- on the same row (its ranges index `title`), and
238
- `presentation: 'walletSidebar'`. Production treats the runs as decoration:
239
- an invalid list is dropped and the row shows `title`. Renderers draw `title`
240
- instead of runs on a row that also has `titleMatch`, and ignore runs and the
241
- leading icon with `presentation: 'walletSidebar'`.
242
-
243
- `identity.titleLeadingIcon` is an optional `{ name, tintColor?,
244
- accessibilityLabel? }`: a 16-point glyph before the title text or its runs,
245
- with a 6-point gap, vertically centered on the title line. The name comes from
246
- the same closed icon set as `titleIcon`; an unmapped name leaves an empty
247
- 16-point slot. `tintColor` is a hex `#RRGGBB` or `#RRGGBBAA` color and defaults
248
- to the theme's `iconSubdued`. Without `accessibilityLabel` the glyph is hidden
249
- from accessibility. It has no action of its own. Development validation
250
- rejects unknown keys, a blank name, a malformed color and
251
- `presentation: 'walletSidebar'`; production drops an invalid icon.
252
-
253
- Both fields are identity patch fields. Omission in a full snapshot removes
254
- them; omission in a patch keeps the current value. Rebind and recycle remove the
255
- previous runs and glyph, so a row without them never shows another row's.
256
-
257
- | Platform | Title runs | Leading icon |
258
- | --- | --- | --- |
259
- | iOS | `NativeListTitleSegmentsLabel` draws the runs at the widths that `NativeListTitleSegmentLayout.swift` allocates for its bounds; it fills the title row in place of the row's filler. | `UIImageView`, first in the title row stack. |
260
- | Android | `NativeListTitleSegmentsView`, a `TextView` whose text is rebuilt for each measured width from `NativeListTitleSegmentLayout.kt` and which measures to the width it is offered. `PackedTitleLineLayout.titleChild` names the flexible title view, so a view may precede it. | `OneKeyIconView`, first in the title line. |
261
- | Web | A flex line that fills the title line: the shrinking run is `flex: 0 1 auto; min-width: 0`, the others `flex: 0 0 auto` with a `max-width` percentage for a cap. | An icon element before the title text. |
262
-
263
- Conformance: `src/validation.ts` (`assertTitleSegments`,
264
- `assertTitleLeadingIcon`, production sanitizing) with `validation.test.ts`;
265
- `src/web/templates/IdentityRowRenderer.ts` with `IdentityTitleRunsWeb.test.ts`
266
- (run styles, flex behavior, accessibility, rebind);
267
- `ios/NativeListTitleSegmentLayout.swift` with the SwiftPM
268
- `NativeListTitleSegmentLayoutTests`; `NativeListTitleSegmentLayout.kt` with the
269
- JVM `NativeListTitleSegmentLayoutTest`. iOS, Android and Web runtime acceptance is pending: recipient rows
270
- with long wallet and account names in both truncation directions, narrow rows,
271
- dark mode, dynamic type and configured fonts, and recycling between rows with
272
- and without runs or a leading icon. Source and build success alone are not
273
- runtime evidence.
274
-
275
191
  ### DeFi position row controls
276
192
 
277
193
  Status: implemented in local source, unreleased. Focused validation and Web
@@ -849,6 +765,26 @@ Existing geometry/typography differences are recorded in
849
765
  them. iOS uses UICollectionView, Android RecyclerView, and Web a windowed DOM
850
766
  engine. See [DESIGN.md](DESIGN.md) for their current ownership boundaries.
851
767
 
768
+ ### iOS account-selector trailing width (OK-65638)
769
+
770
+ Status: implemented. Manual consuming-app verification on an iOS Simulator
771
+ reported normal balance rendering after a local native rebuild. Automated
772
+ XCTest execution and broader device acceptance are tracked separately.
773
+
774
+ For an `identity` row with `presentation: 'accountSelector'`, an empty trailing
775
+ collection or a spinner-only accessory MUST leave the remaining horizontal
776
+ space available to the title and subtitle column. A short title MUST NOT cause
777
+ the balance subtitle to collapse while unused row width remains. The existing
778
+ gap before the trailing column and the shortened address's compression priority
779
+ remain unchanged; content may still truncate when the row is actually too narrow.
780
+ Visible controls keep their intrinsic width without introducing required-width
781
+ conflicts. Rebinding between empty, spinner and single-icon states MUST release
782
+ the previous row's width constraint.
783
+
784
+ `NativeListAccountSelectorLayoutTests` covers balance/address geometry and
785
+ control widths on production UIKit cells, including rebinds. Android and Web
786
+ keep their existing layout and public interfaces.
787
+
852
788
  ### Android section-index attachment
853
789
 
854
790
  When `sectionIndex.centeredInWindow` is enabled, the Android index rail moves
@@ -81,8 +81,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
81
81
  let title = NativeListResolvedText(
82
82
  item.data.string("title"), style: style.dictionary("title"), size: 16,
83
83
  weight: .medium, color: .black, lineHeight: 24,
84
- lines: item.data.dictionaries("titleSegments").isEmpty
85
- ? item.data.int("titleLines", default: 1) : 1, fonts: fonts)
84
+ lines: item.data.int("titleLines", default: 1), fonts: fonts)
86
85
  let subtitle = NativeListResolvedText(
87
86
  item.data.string("subtitle"), style: style.dictionary("subtitle"), size: 14,
88
87
  color: .black, lineHeight: 20,
@@ -111,12 +110,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
111
110
  private let titleRowStack = UIStackView()
112
111
  private let titleLabel = NativeListTextLabel()
113
112
  private let titleIconView = NativeListIconImageView()
114
- private let titleLeadingIconView = NativeListIconImageView()
115
- private let titleSegmentsLabel = NativeListTitleSegmentsLabel()
116
- // Runs, when shown, take the title's place in the title row.
117
- private var visibleTitleView: UIView {
118
- titleSegmentsLabel.isHidden ? titleLabel : titleSegmentsLabel
119
- }
120
113
  private var titleImageSlot: NativeListImageSlot?
121
114
  private var titleImageConstraints: [NSLayoutConstraint] = []
122
115
  // Absorbs the title row's slack so the title keeps its intrinsic width and the
@@ -177,9 +170,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
177
170
  mainStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
178
171
  titleRowStack.axis = .horizontal
179
172
  titleRowStack.alignment = .center
180
- titleRowStack.addArrangedSubview(titleLeadingIconView)
181
173
  titleRowStack.addArrangedSubview(titleLabel)
182
- titleRowStack.addArrangedSubview(titleSegmentsLabel)
183
174
  titleRowStack.addArrangedSubview(titleIconView)
184
175
  titleRowStack.addArrangedSubview(badgeLabel)
185
176
  titleRowStack.addArrangedSubview(titleRowFiller)
@@ -194,18 +185,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
194
185
  ])
195
186
  titleIconView.contentMode = .scaleAspectFit
196
187
  titleIconView.setContentHuggingPriority(.required, for: .horizontal)
197
- titleLeadingIconView.translatesAutoresizingMaskIntoConstraints = false
198
- let leadingIconWidth = titleLeadingIconView.widthAnchor.constraint(equalToConstant: 16)
199
- leadingIconWidth.priority = UILayoutPriority(999)
200
- NSLayoutConstraint.activate([
201
- leadingIconWidth,
202
- titleLeadingIconView.heightAnchor.constraint(equalToConstant: 16),
203
- ])
204
- titleLeadingIconView.contentMode = .scaleAspectFit
205
- titleLeadingIconView.setContentHuggingPriority(.required, for: .horizontal)
206
- titleLeadingIconView.setContentCompressionResistancePriority(.required, for: .horizontal)
207
- titleLeadingIconView.isHidden = true
208
- titleSegmentsLabel.isHidden = true
209
188
  [titleRowStack, subtitleLabel, tertiaryLabel].forEach(mainStack.addArrangedSubview)
210
189
  visual.translatesAutoresizingMaskIntoConstraints = false
211
190
  leadingWidth = visual.widthAnchor.constraint(equalToConstant: 40)
@@ -270,15 +249,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
270
249
  titleIconView.isHidden = true
271
250
  titleIconView.accessibilityLabel = nil
272
251
  titleIconView.isAccessibilityElement = false
273
- titleLeadingIconView.image = nil
274
- titleLeadingIconView.isHidden = true
275
- titleLeadingIconView.accessibilityLabel = nil
276
- titleLeadingIconView.isAccessibilityElement = false
277
- titleSegmentsLabel.isHidden = true
278
- titleSegmentsLabel.reset()
279
- titleRowFiller.isHidden = false
280
- titleSegmentsLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
281
- titleSegmentsLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
282
252
  titleImageSlot?.view.isHidden = true
283
253
  titleImageSlot?.view.accessibilityLabel = nil
284
254
  titleImageSlot?.view.isAccessibilityElement = false
@@ -340,7 +310,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
340
310
  let label = titleIcon.string("accessibilityLabel")
341
311
  titleIconView.accessibilityLabel = label.isEmpty ? nil : label
342
312
  titleIconView.isAccessibilityElement = !label.isEmpty
343
- titleRowStack.setCustomSpacing(4, after: visibleTitleView)
313
+ titleRowStack.setCustomSpacing(4, after: titleLabel)
344
314
  // A long title truncates ahead of the 20pt glyph (Android caps the title
345
315
  // at row width minus accessories, Web shrinks the title first); with the
346
316
  // title at `.required` the icon's 999 width constraint collapsed to 0.
@@ -372,7 +342,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
372
342
  ]
373
343
  titleImageConstraints.forEach { $0.priority = UILayoutPriority(999) }
374
344
  NSLayoutConstraint.activate(titleImageConstraints)
375
- titleRowStack.setCustomSpacing(4, after: titleIconView.isHidden ? visibleTitleView : titleIconView)
345
+ titleRowStack.setCustomSpacing(4, after: titleIconView.isHidden ? titleLabel : titleIconView)
376
346
  titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
377
347
  slot.bind(image, key: "\(item.key):titleImage", placeholder: "#00000000")
378
348
  } else {
@@ -415,7 +385,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
415
385
  titleRowStack.setCustomSpacing(
416
386
  CGFloat(style.double("titleBadgeGap")),
417
387
  after: titleImageSlot?.view.isHidden == false ? titleImageSlot!.view
418
- : titleIconView.isHidden ? visibleTitleView : titleIconView)
388
+ : titleIconView.isHidden ? titleLabel : titleIconView)
419
389
  }
420
390
  }
421
391
  if style["trailingGap"] != nil { trailingStack.spacing = CGFloat(style.double("trailingGap")) }
@@ -429,7 +399,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
429
399
  labels.forEach { NativeListTextStyles.applyStyledText($0, text, fonts: fonts) }
430
400
  }
431
401
  }
432
- if !titleSegmentsLabel.isHidden { bindTitleRuns(item, theme: theme, style: style) }
433
402
  contentInsets = UIEdgeInsets(
434
403
  top: topInset, left: leftInset, bottom: -bottomInset, right: -rightInset)
435
404
  if root.axis == .horizontal {
@@ -495,8 +464,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
495
464
  }
496
465
  override func recycleContent() {
497
466
  visual.recycle()
498
- titleSegmentsLabel.reset()
499
- titleLeadingIconView.image = nil
500
467
  titleImageSlot?.recycle()
501
468
  titleImageSlot?.view.isHidden = true
502
469
  titleImageSlot?.view.accessibilityLabel = nil
@@ -545,30 +512,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
545
512
  visual.bind(
546
513
  descriptor, style: image, key: key, theme: currentTheme, isUnread: false, fonts: fonts)
547
514
  }
548
- // Runs take the title's font, size and color unless they set their own tone
549
- // or weight; the title label holds the final style by now.
550
- private func bindTitleRuns(_ item: NativeListItem, theme: [String: Any]?, style: [String: Any]) {
551
- let baseFont = titleLabel.font ?? nativeListFont(ofSize: 16, weight: .medium, fonts: fonts)
552
- let baseColor = titleLabel.textColor ?? nativeListColor(theme, "primaryText", "#202020")
553
- let runs = item.data.dictionaries("titleSegments").map { segment in
554
- let weight = segment.string("fontWeight")
555
- let tone = segment.string("tone")
556
- return NativeListTitleSegmentsLabel.Run(
557
- text: segment.string("text"),
558
- font: weight.isEmpty
559
- ? baseFont
560
- : nativeListFont(
561
- ofSize: baseFont.pointSize, weight: weight == "medium" ? .medium : .regular, fonts: fonts),
562
- color: tone == "secondary"
563
- ? nativeListColor(theme, "secondaryText", "#646464")
564
- : tone == "primary" ? nativeListColor(theme, "primaryText", "#202020") : baseColor,
565
- shrink: segment.bool("shrink"),
566
- maxWidthRatio: (segment["maxWidthRatio"] as? NSNumber).map { CGFloat(truncating: $0) })
567
- }
568
- titleSegmentsLabel.lineHeight = CGFloat(
569
- style.dictionary("title")?.double("lineHeight", default: 24) ?? 24)
570
- titleSegmentsLabel.configure(runs, spokenText: item.data.string("title"))
571
- }
572
515
  private func bindIdentity(
573
516
  _ item: NativeListItem,
574
517
  theme: [String: Any]?,
@@ -700,33 +643,10 @@ final class NativeListIdentityCell: NativeListRendererCell {
700
643
  }
701
644
  root.addArrangedSubview(mainStack)
702
645
  show(titleLabel, item.data.string("title"), lines: item.data.int("titleLines", default: 1))
703
- // Runs replace the drawn title except where `title` itself is highlighted;
704
- // their content is bound once the title style is final.
705
- if !item.data.dictionaries("titleSegments").isEmpty,
706
- item.data.dictionaries("titleMatch").isEmpty
707
- {
708
- titleLabel.isHidden = true
709
- titleSegmentsLabel.isHidden = false
710
- // The runs fill what the title row leaves, so a ratio is a share of it.
711
- titleRowFiller.isHidden = true
712
- titleSegmentsLabel.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
713
- }
714
- if let icon = item.data.dictionary("titleLeadingIcon") {
715
- titleLeadingIconView.bindIcon(icon.string("name"))
716
- titleLeadingIconView.tintColor = UIColor(
717
- nativeListHex: icon.string("tintColor",
718
- default: theme?["iconSubdued"] as? String ?? "#8D8D8D"),
719
- fallback: .gray)
720
- titleLeadingIconView.isHidden = false
721
- let label = icon.string("accessibilityLabel")
722
- titleLeadingIconView.accessibilityLabel = label.isEmpty ? nil : label
723
- titleLeadingIconView.isAccessibilityElement = !label.isEmpty
724
- titleRowStack.setCustomSpacing(6, after: titleLeadingIconView)
725
- }
726
646
  let titleSuffixes = item.data.dictionaries("titleSuffixes")
727
647
  if !titleSuffixes.isEmpty {
728
- visibleTitleView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
729
- let minimumTitleWidth = visibleTitleView.widthAnchor.constraint(greaterThanOrEqualToConstant: 80)
648
+ titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
649
+ let minimumTitleWidth = titleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 80)
730
650
  minimumTitleWidth.priority = UILayoutPriority(751)
731
651
  minimumTitleWidth.isActive = true
732
652
  selectorConstraints.append(minimumTitleWidth)
@@ -889,6 +809,13 @@ final class NativeListIdentityCell: NativeListRendererCell {
889
809
  let width = trailingStack.widthAnchor.constraint(equalTo: accessory.widthAnchor)
890
810
  width.isActive = true
891
811
  selectorConstraints.append(width)
812
+ } else if item.data.string("presentation") == "accountSelector" {
813
+ // Keep flexible space in the text column when there is no accessory button.
814
+ // Stay below visible controls' compression resistance and the hidden checkbox's width.
815
+ let width = trailingStack.widthAnchor.constraint(equalToConstant: 0)
816
+ width.priority = UILayoutPriority(749)
817
+ width.isActive = true
818
+ selectorConstraints.append(width)
892
819
  }
893
820
  }
894
821
  private func dataTextColor(_ tone: String, theme: [String: Any]?) -> UIColor {
@@ -0,0 +1,96 @@
1
+ import UIKit
2
+ import XCTest
3
+ @testable import NativeListModule
4
+
5
+ @MainActor
6
+ final class NativeListAccountSelectorLayoutTests: XCTestCase {
7
+ private func child<T>(_ cell: NativeListIdentityCell, _ label: String, as type: T.Type) throws -> T {
8
+ try XCTUnwrap(Mirror(reflecting: cell).children.first { $0.label == label }?.value as? T)
9
+ }
10
+
11
+ private func bind(
12
+ _ cell: NativeListIdentityCell, title: String = "A", balance: String = "$16.30",
13
+ trailing: [[String: Any]] = []
14
+ ) throws {
15
+ let item = try NativeListItem(data: [
16
+ "key": "account", "type": "identity", "presentation": "accountSelector",
17
+ "title": title, "height": 60,
18
+ "leading": ["kind": "account", "fallbackText": "A"],
19
+ "subtitleSegments": [
20
+ ["text": balance, "tone": "secondary"],
21
+ ["text": "0x1234...5678", "tone": "secondary", "separatorBefore": true],
22
+ ],
23
+ "trailing": trailing,
24
+ ])
25
+ cell.bind(item: item, theme: nil, layout: "linear", selected: false,
26
+ checkboxState: { _, _, fallback in fallback }, fonts: NativeListFontFamilies())
27
+ cell.setNeedsLayout()
28
+ cell.layoutIfNeeded()
29
+ }
30
+
31
+ private func assertSubtitleFits(
32
+ _ cell: NativeListIdentityCell, file: StaticString = #filePath, line: UInt = #line
33
+ ) throws {
34
+ let labels = try child(cell, "semanticSubtitleLabels", as: [UILabel].self)
35
+ XCTAssertEqual(labels.count, 2, file: file, line: line)
36
+ for label in labels {
37
+ XCTAssertGreaterThanOrEqual(
38
+ label.bounds.width + 0.5, label.intrinsicContentSize.width,
39
+ "The balance and address must fit when the row has spare width", file: file, line: line)
40
+ }
41
+ }
42
+
43
+ func testEmptyTrailingDoesNotCollapseBalanceToShortTitleWidth() throws {
44
+ for width: CGFloat in [320, 390] {
45
+ let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: width, height: 60))
46
+ let window = UIWindow(frame: CGRect(x: 0, y: 0, width: width, height: 200))
47
+ window.addSubview(cell)
48
+ for title in ["A", "返佣5", "Pendle account"] {
49
+ for balance in ["$16.30", "$0", "****"] {
50
+ try bind(cell, title: title, balance: balance)
51
+ window.layoutIfNeeded()
52
+ try assertSubtitleFits(cell)
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ func testSpinnerPreservesItsIntrinsicWidthAndSubtitleSpace() throws {
59
+ let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 60))
60
+ let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
61
+ window.addSubview(cell)
62
+ try bind(cell, trailing: [["kind": "spinner"]])
63
+ window.layoutIfNeeded()
64
+
65
+ try assertSubtitleFits(cell)
66
+ let trailing = try child(cell, "trailingStack", as: NativeListAccessoryStack.self)
67
+ let spinner = try XCTUnwrap(trailing.arrangedSubviews.compactMap { $0 as? UIActivityIndicatorView }.first)
68
+ XCTAssertTrue(spinner.isAnimating)
69
+ XCTAssertFalse(spinner.isHidden)
70
+ XCTAssertGreaterThanOrEqual(spinner.bounds.width + 0.5, spinner.intrinsicContentSize.width)
71
+ XCTAssertGreaterThanOrEqual(trailing.bounds.width + 0.5, spinner.bounds.width)
72
+ }
73
+
74
+ func testRebindingBetweenIconEmptyAndSpinnerKeepsControlAndSubtitleWidths() throws {
75
+ let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 60))
76
+ let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
77
+ window.addSubview(cell)
78
+ for icon in ["DotHorOutline", "PlusSmallOutline"] {
79
+ let descriptor: [String: Any] = ["kind": "icon", "name": icon, "actionKey": "action"]
80
+ let states: [[[String: Any]]] = [[descriptor], [], [["kind": "spinner"]], [descriptor], []]
81
+ for accessories in states {
82
+ try bind(cell, trailing: accessories)
83
+ window.layoutIfNeeded()
84
+ try assertSubtitleFits(cell)
85
+ let trailing = try child(cell, "trailingStack", as: NativeListAccessoryStack.self)
86
+ if accessories.first?["kind"] as? String == "icon" {
87
+ let control = try XCTUnwrap(trailing.firstVisibleControl)
88
+ XCTAssertGreaterThan(control.bounds.width, 0)
89
+ XCTAssertEqual(trailing.bounds.width, control.bounds.width, accuracy: 0.5)
90
+ } else {
91
+ XCTAssertNil(trailing.firstVisibleControl)
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
@@ -39,8 +39,6 @@ export const ROW_BOX_STYLE_KEYS_BY_TYPE = {
39
39
 
40
40
  // OneKey patch: preserve compact very-small-balance digit runs.
41
41
 
42
- /** One run of an identity title line; see docs/SPEC.md. */
43
-
44
42
  /** Native Market quote row. All values are already localized/formatted strings. */
45
43
 
46
44
  /** Compact KPI card for dashboards and grids. */