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

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/src/validation.ts CHANGED
@@ -1344,6 +1344,108 @@ function assertButtonGroup(
1344
1344
  });
1345
1345
  }
1346
1346
 
1347
+ const MAX_TITLE_SEGMENTS = 4;
1348
+ const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
1349
+
1350
+ function assertTitleSegments(value: unknown, path: string): void {
1351
+ if (
1352
+ !Array.isArray(value) ||
1353
+ value.length === 0 ||
1354
+ value.length > MAX_TITLE_SEGMENTS
1355
+ )
1356
+ fail(path, `must hold 1...${MAX_TITLE_SEGMENTS} segments`);
1357
+ let shrinking = 0;
1358
+ value.forEach((item: unknown, index) => {
1359
+ const segmentPath = `${path}[${index}]`;
1360
+ const segment = protocolObject(item, segmentPath);
1361
+ assertUnknownKeys(
1362
+ segment,
1363
+ ['text', 'tone', 'fontWeight', 'shrink', 'maxWidthRatio'],
1364
+ segmentPath,
1365
+ 'title segment key'
1366
+ );
1367
+ const { text, tone, fontWeight, shrink, maxWidthRatio } = segment;
1368
+ if (typeof text !== 'string' || !text.trim())
1369
+ fail(`${segmentPath}.text`, 'must be non-blank');
1370
+ assertText(text, `${segmentPath}.text`);
1371
+ if (tone !== undefined && tone !== 'primary' && tone !== 'secondary')
1372
+ fail(`${segmentPath}.tone`, 'must be primary or secondary');
1373
+ if (
1374
+ fontWeight !== undefined &&
1375
+ fontWeight !== 'regular' &&
1376
+ fontWeight !== 'medium'
1377
+ )
1378
+ fail(`${segmentPath}.fontWeight`, 'must be regular or medium');
1379
+ if (shrink !== undefined && typeof shrink !== 'boolean')
1380
+ fail(`${segmentPath}.shrink`, 'must be a boolean');
1381
+ if (shrink) shrinking += 1;
1382
+ if (
1383
+ maxWidthRatio !== undefined &&
1384
+ (typeof maxWidthRatio !== 'number' ||
1385
+ !Number.isFinite(maxWidthRatio) ||
1386
+ maxWidthRatio <= 0 ||
1387
+ maxWidthRatio > 1)
1388
+ )
1389
+ fail(`${segmentPath}.maxWidthRatio`, 'must be finite within (0, 1]');
1390
+ });
1391
+ if (shrinking > 1) fail(path, 'allows at most one shrinking segment');
1392
+ }
1393
+
1394
+ function assertTitleLeadingIcon(value: unknown, path: string): void {
1395
+ const icon = protocolObject(value, path);
1396
+ assertUnknownKeys(
1397
+ icon,
1398
+ ['name', 'tintColor', 'accessibilityLabel'],
1399
+ path,
1400
+ 'title leading icon key'
1401
+ );
1402
+ const { name, tintColor, accessibilityLabel } = icon;
1403
+ if (typeof name !== 'string' || !name.trim())
1404
+ fail(`${path}.name`, 'must be non-blank');
1405
+ if (
1406
+ tintColor !== undefined &&
1407
+ (typeof tintColor !== 'string' || !HEX_COLOR_PATTERN.test(tintColor))
1408
+ )
1409
+ fail(`${path}.tintColor`, 'must be #RRGGBB or #RRGGBBAA');
1410
+ if (
1411
+ accessibilityLabel !== undefined &&
1412
+ typeof accessibilityLabel !== 'string'
1413
+ )
1414
+ fail(`${path}.accessibilityLabel`, 'must be a string');
1415
+ }
1416
+
1417
+ function assertIdentityTitleRuns(
1418
+ row: Pick<
1419
+ IdentityRow,
1420
+ 'titleSegments' | 'titleLeadingIcon' | 'titleMatch' | 'presentation'
1421
+ >,
1422
+ path: string
1423
+ ): void {
1424
+ if (row.titleSegments !== undefined) {
1425
+ assertTitleSegments(row.titleSegments, `${path}.titleSegments`);
1426
+ // titleMatch ranges index `title`, which the runs replace on screen.
1427
+ if (row.titleMatch?.length)
1428
+ fail(`${path}.titleMatch`, 'cannot be combined with titleSegments');
1429
+ if (row.presentation === 'walletSidebar')
1430
+ fail(`${path}.titleSegments`, 'is not supported with walletSidebar');
1431
+ }
1432
+ if (row.titleLeadingIcon !== undefined) {
1433
+ assertTitleLeadingIcon(row.titleLeadingIcon, `${path}.titleLeadingIcon`);
1434
+ if (row.presentation === 'walletSidebar')
1435
+ fail(`${path}.titleLeadingIcon`, 'is not supported with walletSidebar');
1436
+ }
1437
+ }
1438
+
1439
+ /** Production keeps decorative title runs only when they pass the rules. */
1440
+ function passesTitleRunRules(check: () => void): boolean {
1441
+ try {
1442
+ check();
1443
+ return true;
1444
+ } catch {
1445
+ return false;
1446
+ }
1447
+ }
1448
+
1347
1449
  function assertIdentityExtras(
1348
1450
  row: Pick<
1349
1451
  IdentityRow,
@@ -1834,6 +1936,7 @@ function assertRow(
1834
1936
  );
1835
1937
  });
1836
1938
  assertIdentityExtras(row, path);
1939
+ assertIdentityTitleRuns(row, path);
1837
1940
  assertText(row.subtitle, `${path}.subtitle`);
1838
1941
  // OneKey patch: selector text segments truncate independently.
1839
1942
  row.subtitleSegments?.forEach((segment, segmentIndex) => {
@@ -2416,6 +2519,8 @@ const ROW_FIELDS: Readonly<Record<string, readonly string[]>> = {
2416
2519
  'titleIcon',
2417
2520
  'secondaryTitleIcon',
2418
2521
  'titleImage',
2522
+ 'titleSegments',
2523
+ 'titleLeadingIcon',
2419
2524
  'subtitle',
2420
2525
  'tertiary',
2421
2526
  'tertiaryTone',
@@ -2593,6 +2698,8 @@ const PATCH_FIELDS: Readonly<Record<string, readonly string[]>> = {
2593
2698
  'titleIcon',
2594
2699
  'secondaryTitleIcon',
2595
2700
  'titleImage',
2701
+ 'titleSegments',
2702
+ 'titleLeadingIcon',
2596
2703
  'subtitle',
2597
2704
  'tertiary',
2598
2705
  'tertiaryTone',
@@ -2724,6 +2831,7 @@ function protocolKey(value: unknown, path: string): void {
2724
2831
  const PROTOCOL_ARRAY_FIELDS = new Set([
2725
2832
  'columns',
2726
2833
  'titleSuffixes',
2834
+ 'titleSegments',
2727
2835
  'cells',
2728
2836
  'points',
2729
2837
  'images',
@@ -2965,7 +3073,10 @@ function assertRowProtocol(row: RowModel, path: string): void {
2965
3073
  if (row.height !== undefined && row.height < 0)
2966
3074
  fail(`${path}.height`, 'must be non-negative');
2967
3075
  if (row.type === 'identity') {
2968
- if (isDevelopment()) assertIdentityExtras(row, path);
3076
+ if (isDevelopment()) {
3077
+ assertIdentityExtras(row, path);
3078
+ assertIdentityTitleRuns(row, path);
3079
+ }
2969
3080
  if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes))
2970
3081
  fail(`${path}.titleSuffixes`, 'must be an array');
2971
3082
  row.titleSuffixes?.forEach((suffix, index) => {
@@ -3560,6 +3671,14 @@ const NESTED_DECORATION_FIELDS: Readonly<Record<string, readonly string[]>> = {
3560
3671
 
3561
3672
  function sanitizeDecoration(value: unknown, field: string): unknown {
3562
3673
  if (value === undefined || value === null) return undefined;
3674
+ if (field === 'titleSegments')
3675
+ return passesTitleRunRules(() => assertTitleSegments(value, field))
3676
+ ? value
3677
+ : undefined;
3678
+ if (field === 'titleLeadingIcon')
3679
+ return passesTitleRunRules(() => assertTitleLeadingIcon(value, field))
3680
+ ? value
3681
+ : undefined;
3563
3682
  if (
3564
3683
  !Array.isArray(value) &&
3565
3684
  [
@@ -845,7 +845,7 @@ export const WEB_LIST_CSS = `
845
845
  .ok-native-list-trailing-button{appearance:none;box-sizing:border-box;flex:0 0 auto;padding:4px 10px;border:1px solid transparent;border-radius:9999px;background:var(--nl-button-bg,var(--nl-strong));color:var(--nl-button-text,var(--nl-primary));font:500 14px/20px var(--nl-font-medium);white-space:nowrap;cursor:pointer}.ok-native-list-trailing-button:hover:not(:disabled){background:var(--nl-button-hover,var(--nl-strong))}.ok-native-list-trailing-button:active:not(:disabled){background:var(--nl-button-pressed,var(--nl-pressed))}.ok-native-list-trailing-button:focus-visible{outline:2px solid var(--nl-button-focus,var(--nl-accent))}.ok-native-list-trailing-button:disabled{opacity:.4;cursor:default}
846
846
  .ok-native-list-icon-button.ok-native-list-tertiary-icon{box-sizing:border-box;flex:0 0 38px;width:38px;height:38px;margin:-7px;padding:6px;border:1px solid transparent;border-radius:9999px;color:var(--nl-icon-subdued);background:transparent}.ok-native-list-icon-button.ok-native-list-tertiary-icon:hover:not(:disabled){background:var(--nl-hover)}.ok-native-list-icon-button.ok-native-list-tertiary-icon:active:not(:disabled){background:var(--nl-pressed)}.ok-native-list-icon-button.ok-native-list-tertiary-icon:disabled{opacity:.4;cursor:default}
847
847
  .ok-native-list-icon-button.ok-native-list-tertiary-icon:focus-visible:not(:disabled){outline:2px solid var(--nl-accent)}
848
- .ok-native-list-identity-title-icon{flex:0 0 20px;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--nl-icon);font-size:20px}.ok-native-list-identity-title-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
848
+ .ok-native-list-identity-title-icon{flex:0 0 20px;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--nl-icon);font-size:20px}.ok-native-list-identity-title-leading-icon{flex:0 0 16px;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;color:var(--nl-icon-subdued);font-size:16px}.ok-native-list-identity-title-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
849
849
  .ok-native-list-account-row .ok-native-list-visual,.ok-native-list-account-action-row .ok-native-list-visual{width:32px;height:32px;flex-basis:32px}.ok-native-list-account-row .ok-native-list-visual>img,.ok-native-list-account-row .ok-native-list-visual-main,.ok-native-list-account-action-row .ok-native-list-visual>img,.ok-native-list-account-action-row .ok-native-list-visual-main{width:32px;height:32px}.ok-native-list-account-row .ok-native-list-visual>.ok-native-list-visual-corner{width:20px;height:20px;padding:2px}.ok-native-list-account-action-row .ok-native-list-visual{border-radius:8px!important}.ok-native-list-wallet-row .ok-native-list-title{color:var(--nl-secondary);font-size:12px;line-height:16px;font-weight:400;font-family:var(--nl-font-regular)}.ok-native-list-item[data-native-list-selected="true"]>.ok-native-list-wallet-row .ok-native-list-title{color:var(--nl-primary)}.ok-native-list-account-row .ok-native-list-title{font-size:16px;line-height:20px;font-weight:400;font-family:var(--nl-font-regular)}.ok-native-list-account-row .ok-native-list-secondary{font-size:14px;line-height:20px;font-weight:400;font-family:var(--nl-font-regular)}
850
850
  .ok-native-list-flex{display:flex;flex:1;min-width:0;flex-direction:column;justify-content:center}.ok-native-list-title{font-size:15px;line-height:20px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:var(--nl-font-semibold)}.ok-native-list-secondary{font-size:13px;line-height:18px;color:var(--nl-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ok-native-list-tertiary{color:var(--nl-secondary)}.ok-native-list-info{color:var(--nl-info)}
851
851
  .ok-native-list-identity-row .ok-native-list-title,.ok-native-list-identity-row .ok-native-list-amounts>span:first-child{font-size:16px;line-height:20px}.ok-native-list-identity-row .ok-native-list-secondary{font-size:14px;line-height:20px}
@@ -30,6 +30,79 @@ function toneColor(
30
30
  if (tone === 'secondary') return 'var(--nl-secondary)';
31
31
  return fallback === 'secondary' ? 'var(--nl-secondary)' : 'var(--nl-primary)';
32
32
  }
33
+ // Hidden from sight, still read: the runs themselves are hidden from
34
+ // assistive technology in favor of `title`.
35
+ const VISUALLY_HIDDEN: Partial<CSSStyleDeclaration> = {
36
+ position: 'absolute',
37
+ width: '1px',
38
+ height: '1px',
39
+ overflow: 'hidden',
40
+ clip: 'rect(0 0 0 0)',
41
+ whiteSpace: 'nowrap',
42
+ };
43
+ function appendTitleRuns(
44
+ document: Document,
45
+ target: HTMLElement,
46
+ row: IdentityRow
47
+ ): void {
48
+ const runs = createElement(
49
+ document,
50
+ 'span',
51
+ 'ok-native-list-identity-title-runs'
52
+ );
53
+ runs.setAttribute('aria-hidden', 'true');
54
+ Object.assign(runs.style, {
55
+ display: 'flex',
56
+ flex: '1 1 auto',
57
+ minWidth: '0',
58
+ maxWidth: '100%',
59
+ overflow: 'hidden',
60
+ });
61
+ row.titleSegments?.forEach((segment) => {
62
+ const run = createElement(
63
+ document,
64
+ 'span',
65
+ 'ok-native-list-identity-title-run',
66
+ segment.text
67
+ );
68
+ // Only the shrinking run gives up width; a cap is a share of the line.
69
+ Object.assign(run.style, {
70
+ flex: segment.shrink ? '0 1 auto' : '0 0 auto',
71
+ minWidth: segment.shrink ? '0' : '',
72
+ maxWidth:
73
+ segment.maxWidthRatio === undefined
74
+ ? ''
75
+ : `${segment.maxWidthRatio * 100}%`,
76
+ overflow: 'hidden',
77
+ textOverflow: 'ellipsis',
78
+ whiteSpace: 'pre',
79
+ });
80
+ if (segment.tone)
81
+ run.style.color =
82
+ segment.tone === 'secondary'
83
+ ? 'var(--nl-secondary)'
84
+ : 'var(--nl-primary)';
85
+ if (segment.fontWeight === 'medium') {
86
+ run.style.fontWeight = '500';
87
+ run.style.fontFamily = 'var(--nl-font-medium)';
88
+ } else if (segment.fontWeight === 'regular') {
89
+ run.style.fontWeight = '400';
90
+ run.style.fontFamily = 'var(--nl-font-regular)';
91
+ }
92
+ setData(run, 'tone', segment.tone);
93
+ runs.appendChild(run);
94
+ });
95
+ const spoken = createElement(
96
+ document,
97
+ 'span',
98
+ 'ok-native-list-identity-title-spoken',
99
+ row.title
100
+ );
101
+ Object.assign(spoken.style, VISUALLY_HIDDEN);
102
+ target.replaceChildren(runs, spoken);
103
+ target.style.display = 'flex';
104
+ target.style.minWidth = '0';
105
+ }
33
106
  function appendAccessories(
34
107
  parent: HTMLElement,
35
108
  document: Document,
@@ -195,6 +268,18 @@ function createIdentityRow(
195
268
  fragment.appendChild(document.createTextNode(row.title.slice(offset)));
196
269
  textTarget.prepend(fragment);
197
270
  }
271
+ // The runs replace the plain title, except where `title` itself is
272
+ // highlighted or shown in the wallet sidebar.
273
+ const hasTitleRuns =
274
+ !!row.titleSegments?.length &&
275
+ presentation !== 'walletSidebar' &&
276
+ !row.titleMatch?.length;
277
+ if (hasTitleRuns) {
278
+ const textTarget =
279
+ titleElement.querySelector<HTMLElement>('[data-nl-slot="title"]') ??
280
+ titleElement;
281
+ appendTitleRuns(document, textTarget, row);
282
+ }
198
283
  if (row.subtitleSegments?.length) {
199
284
  column.querySelector('.ok-native-list-secondary')?.remove();
200
285
  const segments = createElement(
@@ -238,6 +323,14 @@ function createIdentityRow(
238
323
  while (titleElement.firstChild && titleElement.firstChild !== badgeLine)
239
324
  titleText.appendChild(titleElement.firstChild);
240
325
  titleElement.prepend(titleText);
326
+ // The title slot tag moves with the text. `applyTextLayout` writes the
327
+ // single-line clamp (`display: block`, nowrap, ellipsis) onto the slot
328
+ // element; left on this container it would undo the flex row below and
329
+ // leave the inline-flex glyph baseline-aligned above the title (OK-66402).
330
+ if (titleElement.hasAttribute('data-nl-slot')) {
331
+ titleElement.removeAttribute('data-nl-slot');
332
+ tagSlot(titleText, 'title');
333
+ }
241
334
  const icons = [row.titleIcon, row.secondaryTitleIcon]
242
335
  .filter((icon): icon is NonNullable<typeof icon> => icon !== undefined)
243
336
  .map((descriptor) => {
@@ -383,6 +476,58 @@ function createIdentityRow(
383
476
  badges.style.minWidth = '0';
384
477
  }
385
478
  }
479
+ if (hasTitleRuns) {
480
+ // The runs fill what the title line leaves, so a ratio is a share of
481
+ // that width and title accessories sit at the line's end.
482
+ const runs = titleElement.querySelector<HTMLElement>(
483
+ '.ok-native-list-identity-title-runs'
484
+ );
485
+ for (
486
+ let node = runs?.parentElement;
487
+ node && node !== titleElement;
488
+ node = node.parentElement
489
+ ) {
490
+ node.style.display = 'flex';
491
+ node.style.flex = '1 1 auto';
492
+ node.style.minWidth = '0';
493
+ }
494
+ titleElement.style.display = 'flex';
495
+ titleElement.style.alignItems = 'center';
496
+ }
497
+ if (row.titleLeadingIcon && presentation !== 'walletSidebar') {
498
+ const descriptor = row.titleLeadingIcon;
499
+ if (titleElement.hasAttribute('data-nl-slot')) {
500
+ // Keep the glyph out of the title slot that text styles apply to.
501
+ const titleText = tagSlot(
502
+ createElement(document, 'span', 'ok-native-list-identity-title-text'),
503
+ 'title'
504
+ );
505
+ while (titleElement.firstChild)
506
+ titleText.appendChild(titleElement.firstChild);
507
+ if (hasTitleRuns) {
508
+ titleText.style.display = 'flex';
509
+ titleText.style.minWidth = '0';
510
+ }
511
+ titleElement.removeAttribute('data-nl-slot');
512
+ titleElement.appendChild(titleText);
513
+ }
514
+ const icon = primitives.iconAction(
515
+ descriptor.name,
516
+ undefined,
517
+ undefined,
518
+ descriptor.tintColor
519
+ );
520
+ icon.className = 'ok-native-list-identity-title-leading-icon';
521
+ if (descriptor.accessibilityLabel)
522
+ icon.setAttribute('aria-label', descriptor.accessibilityLabel);
523
+ else icon.setAttribute('aria-hidden', 'true');
524
+ titleElement.style.display = 'flex';
525
+ titleElement.style.alignItems = 'center';
526
+ // 6px to the title, whatever gap the title line already has.
527
+ const lineGap = Number.parseFloat(titleElement.style.gap || '0') || 0;
528
+ icon.style.marginInlineEnd = `${Math.max(0, 6 - lineGap)}px`;
529
+ titleElement.prepend(icon);
530
+ }
386
531
  if (presentation === 'walletSidebar' && row.badges?.length) {
387
532
  const badges = createElement(
388
533
  document,
@@ -395,6 +540,7 @@ function createIdentityRow(
395
540
  column.appendChild(badges);
396
541
  }
397
542
  for (const key of ['title', 'subtitle'] as const) {
543
+ if (key === 'title' && hasTitleRuns) continue;
398
544
  const lines = row.style?.[key]?.lines ?? row[`${key}Lines`];
399
545
  if (lines !== undefined)
400
546
  column