@quicktvui/web-renderer 1.0.9 → 1.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quicktvui/web-renderer",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Web renderer for QuickTVUI - provides web browser rendering support",
5
5
  "author": "QuickTVUI Team",
6
6
  "license": "Apache-2.0",
@@ -21,4 +21,4 @@
21
21
  "peerDependencies": {
22
22
  "vue": "^3.0.0"
23
23
  }
24
- }
24
+ }
@@ -306,6 +306,12 @@ export class QtFastListView extends QtBaseComponent {
306
306
 
307
307
  // Override beforeChildMount - called before a child is mounted
308
308
  async beforeChildMount(child, childPosition) {
309
+ console.log(
310
+ '[QtFastListView] beforeChildMount called, child:',
311
+ child?.dom?.getAttribute?.('data-component-name'),
312
+ 'type:',
313
+ child?.dom?.getAttribute?.('type')
314
+ )
309
315
  if (child && child.dom) {
310
316
  // Check if this child's parent is NOT this FastListView
311
317
  if (child.pId && child.pId !== this.id) {
@@ -388,23 +394,39 @@ export class QtFastListView extends QtBaseComponent {
388
394
 
389
395
  // Set list data and render items
390
396
  setListData(data) {
397
+ console.log(
398
+ '[QtFastListView] setListData called, data length:',
399
+ data?.length,
400
+ 'this.dom:',
401
+ this.dom.getAttribute?.('data-component-name')
402
+ )
391
403
  // Handle wrapped params - sometimes data comes as [dataArray]
392
404
  if (Array.isArray(data) && data.length === 1 && Array.isArray(data[0])) {
393
405
  data = data[0]
394
406
  }
395
407
 
396
408
  if (!Array.isArray(data)) {
409
+ console.log('[QtFastListView] setListData: data is not array, returning')
397
410
  return
398
411
  }
399
412
  this._listData = data
400
413
 
401
414
  // Wait for templates to be ready, then render
402
415
  const tryRender = (attempts = 0) => {
416
+ console.log(
417
+ '[QtFastListView] tryRender, attempts:',
418
+ attempts,
419
+ 'templateChildren:',
420
+ this._templateChildren.length,
421
+ 'singletonTemplates:',
422
+ this._singletonTemplates.length
423
+ )
403
424
  if (
404
425
  this._templateChildren.length > 0 ||
405
426
  this._singletonTemplates.length > 0 ||
406
427
  attempts >= 10
407
428
  ) {
429
+ console.log('[QtFastListView] calling _renderItems')
408
430
  this._renderItems()
409
431
  } else {
410
432
  setTimeout(() => tryRender(attempts + 1), 50)
@@ -475,6 +497,13 @@ export class QtFastListView extends QtBaseComponent {
475
497
  }
476
498
  })
477
499
 
500
+ console.log(
501
+ '[QtFastListView] _renderItems complete, itemContainer children:',
502
+ this._itemContainer.children.length,
503
+ 'dom:',
504
+ this.dom.getAttribute?.('data-component-name')
505
+ )
506
+
478
507
  // Initialize showOnState visibility for dynamically created elements
479
508
  requestAnimationFrame(() => {
480
509
  const focusManager = global.__TV_FOCUS_MANAGER__
@@ -525,6 +554,8 @@ export class QtFastListView extends QtBaseComponent {
525
554
  const item = document.createElement('div')
526
555
  item.setAttribute('data-index', index)
527
556
  item.setAttribute('data-position', index)
557
+ // 添加 FastListView 实例引用,用于焦点导航
558
+ item.__fastListViewInstance = this
528
559
 
529
560
  // Get the item's type
530
561
  const itemType = itemData.type !== undefined ? itemData.type : null
@@ -865,9 +896,23 @@ export class QtFastListView extends QtBaseComponent {
865
896
  }
866
897
 
867
898
  notifyItemFocusChanged(currentElement, hasFocus) {
899
+ console.log(
900
+ '[QtFastListView] notifyItemFocusChanged called, hasFocus:',
901
+ hasFocus,
902
+ 'element:',
903
+ currentElement?.getAttribute?.('data-component-name'),
904
+ 'this.dom:',
905
+ this.dom?.getAttribute?.('data-component-name')
906
+ )
868
907
  if (!currentElement || !this._itemContainer) return
869
908
  const items = this._itemContainer.children
870
909
  const itemCount = items.length
910
+ console.log(
911
+ '[QtFastListView] itemCount:',
912
+ itemCount,
913
+ 'itemContainer:',
914
+ this._itemContainer.className
915
+ )
871
916
  if (itemCount === 0) return
872
917
 
873
918
  let currentIndex = -1
@@ -879,6 +924,14 @@ export class QtFastListView extends QtBaseComponent {
879
924
  break
880
925
  }
881
926
  }
927
+ console.log(
928
+ '[QtFastListView] currentIndex:',
929
+ currentIndex,
930
+ 'focusedIndex:',
931
+ this._focusedIndex,
932
+ 'selectedPosition:',
933
+ this._selectedPosition
934
+ )
882
935
  if (currentIndex === -1) return
883
936
 
884
937
  const itemData = this._listData[currentIndex]
@@ -909,6 +962,13 @@ export class QtFastListView extends QtBaseComponent {
909
962
  if (hasFocus) {
910
963
  if (this._focusedIndex === currentIndex) return
911
964
  this._focusedIndex = currentIndex
965
+
966
+ // 自动设置 selected 状态(模拟原生端行为)
967
+ // 当获取焦点时,自动将当前 item 设为选中状态
968
+ if (this._selectedPosition !== currentIndex) {
969
+ console.log('[QtFastListView] calling setSelectChildPosition for index:', currentIndex)
970
+ this.setSelectChildPosition(currentIndex, false)
971
+ }
912
972
  } else {
913
973
  if (this._focusedIndex !== currentIndex) return
914
974
  this._focusedIndex = -1
@@ -1005,10 +1065,28 @@ export class QtFastListView extends QtBaseComponent {
1005
1065
  }
1006
1066
 
1007
1067
  // Request focus on item (called by tv-list requestFocus)
1008
- requestChildFocus(position) {
1009
- console.log('[QtFastListView] requestChildFocus called with position:', position)
1068
+ requestChildFocus(position, retryCount = 0) {
1069
+ // 处理 Native.callUIFunction 传递的数组参数
1070
+ if (Array.isArray(position)) {
1071
+ position = position[0]
1072
+ }
1073
+ console.log(
1074
+ '[QtFastListView] requestChildFocus called with position:',
1075
+ position,
1076
+ 'retry:',
1077
+ retryCount,
1078
+ 'this.dom:',
1079
+ this.dom?.getAttribute?.('data-component-name')
1080
+ )
1010
1081
  const items = this._itemContainer.children
1082
+ console.log('[QtFastListView] requestChildFocus: items.length:', items.length)
1011
1083
  if (position < 0 || position >= items.length) {
1084
+ // 如果 items 还没有渲染完成,重试几次
1085
+ if (retryCount < 10 && items.length === 0) {
1086
+ console.log('[QtFastListView] requestChildFocus: items not ready, retrying...')
1087
+ setTimeout(() => this.requestChildFocus(position, retryCount + 1), 100)
1088
+ return
1089
+ }
1012
1090
  console.warn(
1013
1091
  '[QtFastListView] requestChildFocus: invalid position',
1014
1092
  position,
@@ -1035,6 +1113,211 @@ export class QtFastListView extends QtBaseComponent {
1035
1113
  console.log('[QtFastListView] requestChildFocus: focused item at position', position)
1036
1114
  }
1037
1115
 
1116
+ // Set selected child position (called by Native.callUIFunction)
1117
+ setSelectChildPosition(position, changeFocusTarget = false) {
1118
+ console.log(
1119
+ '[QtFastListView] setSelectChildPosition called with position:',
1120
+ position,
1121
+ 'this.dom:',
1122
+ this.dom?.getAttribute?.('data-component-name')
1123
+ )
1124
+
1125
+ // Get TVFocusManager instance from global
1126
+ const focusManager = global.__TV_FOCUS_MANAGER__
1127
+
1128
+ // Helper to get select style from multiple sources (data attribute, CSS variable)
1129
+ const getSelectStyle = (element, attrName, cssVarName) => {
1130
+ return (
1131
+ element.getAttribute(attrName) ||
1132
+ element.getAttribute('data-' + attrName) ||
1133
+ window.getComputedStyle(element).getPropertyValue(cssVarName)?.trim()
1134
+ )
1135
+ }
1136
+
1137
+ // Clear previous selected item
1138
+ if (this._selectedPosition !== undefined && this._selectedPosition >= 0) {
1139
+ console.log(
1140
+ '[QtFastListView] setSelectChildPosition: clearing previous selected position:',
1141
+ this._selectedPosition
1142
+ )
1143
+ const prevItems = this._itemContainer.querySelectorAll('[selected="true"], [selected=""]')
1144
+ console.log(
1145
+ '[QtFastListView] setSelectChildPosition: found',
1146
+ prevItems.length,
1147
+ 'items with selected attribute'
1148
+ )
1149
+ prevItems.forEach((item) => {
1150
+ console.log(
1151
+ '[QtFastListView] setSelectChildPosition: removing selected from:',
1152
+ item.getAttribute?.('data-component-name'),
1153
+ 'text:',
1154
+ item.getAttribute?.('text')
1155
+ )
1156
+ item.removeAttribute('selected')
1157
+
1158
+ // 检查是否有焦点
1159
+ const hasFocus =
1160
+ item.classList.contains('focused') ||
1161
+ (focusManager && focusManager.focusedElement === item)
1162
+
1163
+ // 如果没有焦点,恢复原始背景色
1164
+ if (!hasFocus && focusManager) {
1165
+ const originalStyles = focusManager.focusedElementOriginalStyles?.get(item)
1166
+ // 获取 select 背景色,用于判断保存的背景色是否是 select 颜色
1167
+ const selectBg = getSelectStyle(
1168
+ item,
1169
+ 'select-background-color',
1170
+ '--select-background-color'
1171
+ )
1172
+ if (originalStyles && originalStyles.backgroundColor !== undefined) {
1173
+ // 如果保存的背景色是 select 颜色,则恢复为空字符串
1174
+ const savedBg = originalStyles.backgroundColor
1175
+ const isSelectColor =
1176
+ savedBg === selectBg ||
1177
+ savedBg === selectBg?.replace(/\s/g, '') ||
1178
+ savedBg === selectBg?.replace('rgba', 'rgb').replace(/,([^,]+)\)$/, '$1)')
1179
+ const bgToRestore = isSelectColor ? '' : savedBg
1180
+ console.log(
1181
+ '[QtFastListView] setSelectChildPosition: restoring background color to:',
1182
+ bgToRestore,
1183
+ 'savedBg:',
1184
+ savedBg,
1185
+ 'selectBg:',
1186
+ selectBg,
1187
+ 'isSelectColor:',
1188
+ isSelectColor
1189
+ )
1190
+ item.style.backgroundColor = bgToRestore
1191
+ } else {
1192
+ // 如果没有保存原始样式,清除背景色
1193
+ console.log('[QtFastListView] setSelectChildPosition: clearing background color')
1194
+ item.style.backgroundColor = ''
1195
+ }
1196
+ // 触发 TVFocusManager 移除 selected 样式
1197
+ focusManager._removeDuplicateParentStateStyles(item)
1198
+ }
1199
+ })
1200
+ }
1201
+
1202
+ // Store new selected position
1203
+ this._selectedPosition = position
1204
+
1205
+ // Set new selected item
1206
+ if (position >= 0) {
1207
+ const items = this._itemContainer.children
1208
+ console.log(
1209
+ '[QtFastListView] setSelectChildPosition: items.length:',
1210
+ items.length,
1211
+ 'position:',
1212
+ position
1213
+ )
1214
+ console.log('[QtFastListView] setSelectChildPosition: items[position]:', items[position])
1215
+ console.log('[QtFastListView] setSelectChildPosition: items[0]:', items[0])
1216
+ if (position < items.length) {
1217
+ const item = items[position]
1218
+ console.log(
1219
+ '[QtFastListView] setSelectChildPosition: item:',
1220
+ item,
1221
+ 'tagName:',
1222
+ item?.tagName,
1223
+ 'children:',
1224
+ item?.children?.length
1225
+ )
1226
+ if (item) {
1227
+ // Set selected attribute on the wrapper div
1228
+ item.setAttribute('selected', 'true')
1229
+
1230
+ // Set selected on all direct children (QtView, etc.)
1231
+ const directChildren = item.children
1232
+ for (let i = 0; i < directChildren.length; i++) {
1233
+ console.log(
1234
+ '[QtFastListView] setSelectChildPosition: setting selected on child:',
1235
+ i,
1236
+ directChildren[i].getAttribute?.('data-component-name')
1237
+ )
1238
+ directChildren[i].setAttribute('selected', 'true')
1239
+ }
1240
+
1241
+ // Also set selected on all descendants with duplicateParentState
1242
+ const duplicateChildren = item.querySelectorAll('[duplicateparentstate]')
1243
+ duplicateChildren.forEach((child) => {
1244
+ child.setAttribute('selected', 'true')
1245
+ })
1246
+
1247
+ console.log('[QtFastListView] setSelectChildPosition: set selected on position', position)
1248
+
1249
+ // 应用 selected 样式(只对无焦点元素)
1250
+ if (focusManager) {
1251
+ // 检查当前元素是否有焦点
1252
+ const hasFocus =
1253
+ focusManager.focusedElement &&
1254
+ (item.contains(focusManager.focusedElement) || focusManager.focusedElement === item)
1255
+
1256
+ // 对 QtView 本身应用 select-background-color(只在没有焦点时)
1257
+ for (let i = 0; i < directChildren.length; i++) {
1258
+ const child = directChildren[i]
1259
+ const childHasFocus =
1260
+ child.classList.contains('focused') || focusManager.focusedElement === child
1261
+
1262
+ // 只有在没有焦点时才设置 select 样式
1263
+ if (!childHasFocus) {
1264
+ const selectBg = getSelectStyle(
1265
+ child,
1266
+ 'select-background-color',
1267
+ '--select-background-color'
1268
+ )
1269
+ if (selectBg) {
1270
+ // 保存原始背景色(在应用 select 样式之前)
1271
+ if (!focusManager.focusedElementOriginalStyles) {
1272
+ focusManager.focusedElementOriginalStyles = new Map()
1273
+ }
1274
+ if (!focusManager.focusedElementOriginalStyles.has(child)) {
1275
+ const currentBg = child.style.backgroundColor
1276
+ // 如果当前背景色已经是 select 颜色,说明之前已经被错误地设置了
1277
+ // 此时应该保存空字符串作为原始背景色
1278
+ const isSelectColor =
1279
+ currentBg === selectBg ||
1280
+ currentBg === selectBg.replace(/\s/g, '') ||
1281
+ currentBg === selectBg.replace('rgba', 'rgb').replace(/,([^,]+)\)$/, '$1)')
1282
+ const bgToSave = isSelectColor ? '' : currentBg
1283
+ console.log(
1284
+ '[QtFastListView] setSelectChildPosition: saving original background color:',
1285
+ bgToSave,
1286
+ 'currentBg:',
1287
+ currentBg,
1288
+ 'selectBg:',
1289
+ selectBg,
1290
+ 'isSelectColor:',
1291
+ isSelectColor
1292
+ )
1293
+ focusManager.focusedElementOriginalStyles.set(child, {
1294
+ backgroundColor: bgToSave,
1295
+ })
1296
+ }
1297
+ console.log(
1298
+ '[QtFastListView] setSelectChildPosition: setting select background color:',
1299
+ selectBg,
1300
+ 'for child:',
1301
+ child.getAttribute?.('data-component-name')
1302
+ )
1303
+ child.style.backgroundColor = selectBg
1304
+ }
1305
+ // 对 QtView 的子元素(如 QtText)应用 duplicateParentState 样式
1306
+ // 现在可以安全调用,因为 _applyDuplicateParentStateStyles 只在 hasFocus=true 时保存原始样式
1307
+ focusManager._applyDuplicateParentStateStyles(child, false)
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ }
1313
+ }
1314
+
1315
+ // Optionally request focus on the selected item
1316
+ if (changeFocusTarget) {
1317
+ this.requestChildFocus(position)
1318
+ }
1319
+ }
1320
+
1038
1321
  // Set span count for grid layout
1039
1322
  setSpanCount(count) {
1040
1323
  if (count > 0) {
@@ -104,30 +104,14 @@ export class QtText extends QtBaseComponent {
104
104
  }
105
105
 
106
106
  // Handle duplicateParentState and focus style attributes
107
- const tvFocusAttrs = ['duplicateParentState', 'focusable', 'showOnState']
108
-
109
- const focusStyleAttrs = [
110
- 'focusColor',
111
- 'focusBackgroundColor',
112
- 'focusTextColor',
113
- 'focusBorderColor',
114
- 'focusBorderWidth',
115
- 'focusBorderRadius',
116
- 'focusOpacity',
117
- 'focusScale',
118
- ]
107
+ const tvFocusAttrs = ['duplicateParentState', 'focusable', 'showOnState', 'select']
108
+
109
+ // QtText 支持的颜色属性:textColor, focusColor, selectColor
110
+ // 注意:QtText 没有 selectBackgroundColor,只有 selectColor
111
+ const textStyleAttrs = ['textColor', 'focusColor', 'selectColor']
119
112
 
120
113
  // Also handle kebab-case versions
121
- const kebabFocusStyleAttrs = [
122
- 'focus-color',
123
- 'focus-background-color',
124
- 'focus-text-color',
125
- 'focus-border-color',
126
- 'focus-border-width',
127
- 'focus-border-radius',
128
- 'focus-opacity',
129
- 'focus-scale',
130
- ]
114
+ const kebabTextStyleAttrs = ['text-color', 'focus-color', 'select-color']
131
115
 
132
116
  if (tvFocusAttrs.includes(key)) {
133
117
  if (value !== null && value !== undefined) {
@@ -145,7 +129,14 @@ export class QtText extends QtBaseComponent {
145
129
  return
146
130
  }
147
131
 
148
- if (focusStyleAttrs.includes(key)) {
132
+ // Handle textColor directly - apply to DOM
133
+ if (key === 'textColor' || key === 'text-color') {
134
+ this.setTextColor(value)
135
+ return
136
+ }
137
+
138
+ // Handle focusColor and selectColor - store as data attributes for TVFocusManager
139
+ if (textStyleAttrs.includes(key)) {
149
140
  if (value !== null && value !== undefined) {
150
141
  const dataKey = 'data-' + key.replace(/([A-Z])/g, '-$1').toLowerCase()
151
142
  this.dom.setAttribute(dataKey, String(value))
@@ -156,7 +147,7 @@ export class QtText extends QtBaseComponent {
156
147
  return
157
148
  }
158
149
 
159
- if (kebabFocusStyleAttrs.includes(key)) {
150
+ if (kebabTextStyleAttrs.includes(key)) {
160
151
  if (value !== null && value !== undefined) {
161
152
  this.dom.setAttribute('data-' + key, String(value))
162
153
  } else {
@@ -139,19 +139,16 @@ export class QtView extends QtBaseComponent {
139
139
  'duplicateParentState',
140
140
  'blockFocusDirections',
141
141
  'blockfocusdirections',
142
+ 'selected',
142
143
  ]
143
144
 
144
145
  // Focus style attributes (for duplicateParentState)
145
- // These can be set via :focusColor="..." or focus-color in CSS
146
+ // QtView 支持:focus-background-color, focus-border-color, focus-border-width, focus-border-radius, focusScale
146
147
  const focusStyleAttrs = [
147
- 'focusColor',
148
148
  'focusBackgroundColor',
149
- 'focusBackground',
150
- 'focusTextColor',
151
149
  'focusBorderColor',
152
150
  'focusBorderWidth',
153
151
  'focusBorderRadius',
154
- 'focusOpacity',
155
152
  'focusScale',
156
153
  ]
157
154
 
@@ -172,14 +169,10 @@ export class QtView extends QtBaseComponent {
172
169
 
173
170
  // Also handle kebab-case versions (from CSS or direct assignment)
174
171
  const kebabFocusStyleAttrs = [
175
- 'focus-color',
176
172
  'focus-background-color',
177
- 'focus-background',
178
- 'focus-text-color',
179
173
  'focus-border-color',
180
174
  'focus-border-width',
181
175
  'focus-border-radius',
182
- 'focus-opacity',
183
176
  'focus-scale',
184
177
  ]
185
178
 
@@ -1503,6 +1503,16 @@ export class TVFocusManager {
1503
1503
  props.focusColor = focusColor
1504
1504
  }
1505
1505
 
1506
+ // Select text color - for QtText when select="true"
1507
+ const selectColor =
1508
+ element.getAttribute('selectColor') ||
1509
+ element.getAttribute('select-color') ||
1510
+ element.getAttribute('data-select-color') ||
1511
+ computedStyle.getPropertyValue('--select-color')?.trim()
1512
+ if (selectColor) {
1513
+ props.selectColor = selectColor
1514
+ }
1515
+
1506
1516
  return props
1507
1517
  }
1508
1518
 
@@ -1723,17 +1733,90 @@ export class TVFocusManager {
1723
1733
 
1724
1734
  _maybeNotifyFastListItemFocusChanged(element, hasFocus) {
1725
1735
  if (!element) return
1736
+ console.log(
1737
+ '[TVFocusManager] _maybeNotifyFastListItemFocusChanged, element:',
1738
+ element.getAttribute?.('data-component-name'),
1739
+ 'hasFocus:',
1740
+ hasFocus,
1741
+ 'data-position:',
1742
+ element.getAttribute?.('data-position'),
1743
+ 'data-index:',
1744
+ element.getAttribute?.('data-index')
1745
+ )
1746
+ console.log(
1747
+ '[TVFocusManager] element.__fastListViewInstance:',
1748
+ !!element.__fastListViewInstance,
1749
+ 'parent:',
1750
+ element.parentElement?.getAttribute?.('data-component-name')
1751
+ )
1752
+
1753
+ // 首先检查元素自身是否有 __fastListViewInstance(新创建的 item wrapper 会有)
1754
+ if (element.__fastListViewInstance) {
1755
+ const instance = element.__fastListViewInstance
1756
+ console.log(
1757
+ '[TVFocusManager] Found __fastListViewInstance on element itself, itemContainer children:',
1758
+ instance._itemContainer?.children?.length
1759
+ )
1760
+ if (instance && typeof instance.notifyItemFocusChanged === 'function') {
1761
+ instance.notifyItemFocusChanged(element, hasFocus)
1762
+ return
1763
+ }
1764
+ }
1765
+
1766
+ // 检查父元素是否有 __fastListViewInstance(item wrapper 的子元素)
1726
1767
  let parent = element.parentElement
1768
+ if (parent && parent.__fastListViewInstance) {
1769
+ const instance = parent.__fastListViewInstance
1770
+ console.log(
1771
+ '[TVFocusManager] Found __fastListViewInstance on direct parent, itemContainer children:',
1772
+ instance._itemContainer?.children?.length
1773
+ )
1774
+ if (instance && typeof instance.notifyItemFocusChanged === 'function') {
1775
+ instance.notifyItemFocusChanged(parent, hasFocus)
1776
+ return
1777
+ }
1778
+ }
1779
+
1727
1780
  let fastListViewInstance = null
1781
+ // 向上查找最近的 __fastListViewInstance,但要确保元素确实在该 FastListView 的 _itemContainer 内部
1728
1782
  while (parent) {
1729
1783
  if (parent.__fastListViewInstance) {
1730
1784
  fastListViewInstance = parent.__fastListViewInstance
1731
- break
1785
+ console.log(
1786
+ '[TVFocusManager] Found __fastListViewInstance on parent:',
1787
+ parent.getAttribute?.('data-component-name'),
1788
+ 'itemContainer children:',
1789
+ fastListViewInstance._itemContainer?.children?.length
1790
+ )
1791
+ // 检查 element 是否在这个 FastListView 的 _itemContainer 内部
1792
+ const itemContainer = fastListViewInstance._itemContainer
1793
+ if (itemContainer && (itemContainer === element || itemContainer.contains(element))) {
1794
+ console.log('[TVFocusManager] element is in itemContainer, using this FastListView')
1795
+ break // 找到正确的实例
1796
+ }
1797
+ // 额外检查:如果 element 有 data-position 或 data-index,检查它是否在 itemContainer 的直接子元素中
1798
+ const pos = element.getAttribute?.('data-position') ?? element.getAttribute?.('data-index')
1799
+ if (pos !== null && pos !== undefined) {
1800
+ const posNum = Number(pos)
1801
+ const item = itemContainer?.children?.[posNum]
1802
+ if (item && (item === element || item.contains(element))) {
1803
+ console.log(
1804
+ '[TVFocusManager] element matched by data-position, using this FastListView'
1805
+ )
1806
+ break
1807
+ }
1808
+ }
1809
+ console.log('[TVFocusManager] element NOT in itemContainer, continuing search...')
1810
+ // 如果不在,继续向上查找
1811
+ fastListViewInstance = null
1732
1812
  }
1733
1813
  parent = parent.parentElement
1734
1814
  }
1735
1815
  if (fastListViewInstance && typeof fastListViewInstance.notifyItemFocusChanged === 'function') {
1816
+ console.log('[TVFocusManager] Calling notifyItemFocusChanged')
1736
1817
  fastListViewInstance.notifyItemFocusChanged(element, hasFocus)
1818
+ } else {
1819
+ console.log('[TVFocusManager] No FastListView found for element')
1737
1820
  }
1738
1821
  }
1739
1822
 
@@ -1842,12 +1925,14 @@ export class TVFocusManager {
1842
1925
  }
1843
1926
 
1844
1927
  // Apply focusColor if specified (for elements that have text)
1928
+ // 焦点优先:获得焦点时总是使用 focus 样式,不管是否有 selected 状态
1845
1929
  if (props.focusColor) {
1846
1930
  element.style.color = props.focusColor
1847
1931
  }
1848
1932
 
1849
1933
  // Handle duplicateParentState children - apply focus styles to children with duplicateParentState="true"
1850
- this._applyDuplicateParentStateStyles(element)
1934
+ // 传递 hasFocus=true 表示当前元素有焦点
1935
+ this._applyDuplicateParentStateStyles(element, true)
1851
1936
 
1852
1937
  // Handle showOnState - update visibility based on focused state
1853
1938
  this._updateShowOnStateVisibility(element, 'focused')
@@ -1857,10 +1942,21 @@ export class TVFocusManager {
1857
1942
 
1858
1943
  // Apply focus styles to children with duplicateParentState attribute
1859
1944
  // Supports: duplicateParentState, duplicateParentState="true", duplicateParentState=""
1860
- _applyDuplicateParentStateStyles(parentElement) {
1945
+ // hasFocus: 是否有焦点(焦点优先于选中状态)
1946
+ _applyDuplicateParentStateStyles(parentElement, hasFocus = false) {
1861
1947
  // Match elements with duplicateParentState attribute present (regardless of value)
1862
1948
  // This handles both <div duplicateParentState> and <div duplicateParentState="true">
1863
1949
  const duplicateChildren = parentElement.querySelectorAll('[duplicateparentstate]')
1950
+ console.log(
1951
+ '[TVFocusManager] _applyDuplicateParentStateStyles: parentElement:',
1952
+ parentElement.getAttribute?.('data-component-name'),
1953
+ 'text:',
1954
+ parentElement.getAttribute?.('text'),
1955
+ 'hasFocus:',
1956
+ hasFocus,
1957
+ 'children count:',
1958
+ duplicateChildren.length
1959
+ )
1864
1960
 
1865
1961
  duplicateChildren.forEach((child) => {
1866
1962
  // Skip if explicitly set to false
@@ -1869,6 +1965,13 @@ export class TVFocusManager {
1869
1965
  return
1870
1966
  }
1871
1967
 
1968
+ console.log(
1969
+ '[TVFocusManager] _applyDuplicateParentStateStyles: processing child:',
1970
+ child.getAttribute?.('data-component-name'),
1971
+ 'text:',
1972
+ child.getAttribute?.('text')
1973
+ )
1974
+
1872
1975
  // Get focus styles from multiple sources:
1873
1976
  // 1. Child's own focus attributes
1874
1977
  // 2. Data attributes (from style extraction)
@@ -1896,35 +1999,105 @@ export class TVFocusManager {
1896
1999
  const focusBorderWidth = getFocusAttr('focusBorderWidth', 'focus-border-width')
1897
2000
  const focusBorderColor = getFocusAttr('focusBorderColor', 'focus-border-color')
1898
2001
 
2002
+ // Get select style attributes (for selected state)
2003
+ const selectColor = getFocusAttr('selectColor', 'select-color')
2004
+ const selectBackgroundColor = getFocusAttr('selectBackgroundColor', 'select-background-color')
2005
+
2006
+ // Check if parentElement itself is in selected state
2007
+ // duplicateParentState 的意思是复制父元素的状态,所以只检查直接父元素
2008
+ // 不要向上查找祖先,否则会错误地找到其他列表项的 selected 状态
2009
+ const isParentSelect =
2010
+ (parentElement.hasAttribute('select') &&
2011
+ parentElement.getAttribute('select') !== 'false') ||
2012
+ (parentElement.hasAttribute('selected') &&
2013
+ parentElement.getAttribute('selected') !== 'false')
2014
+
2015
+ if (isParentSelect) {
2016
+ console.log(
2017
+ '[TVFocusManager] _applyDuplicateParentStateStyles: parentElement is selected:',
2018
+ parentElement.getAttribute?.('data-component-name'),
2019
+ 'text:',
2020
+ parentElement.getAttribute?.('text')
2021
+ )
2022
+ }
2023
+
2024
+ // Determine which colors to use: focus takes priority over select
2025
+ // 焦点优先:有焦点时用 focus 样式,无焦点但有选中时用 select 样式
2026
+ const effectiveColor = hasFocus
2027
+ ? focusColor
2028
+ : isParentSelect && selectColor
2029
+ ? selectColor
2030
+ : null
2031
+ const effectiveBackgroundColor = hasFocus
2032
+ ? focusBackgroundColor
2033
+ : isParentSelect && selectBackgroundColor
2034
+ ? selectBackgroundColor
2035
+ : null
2036
+
1899
2037
  // Check if enableFocusBorder is set
1900
2038
  const enableFocusBorder =
1901
2039
  child.getAttribute('enableFocusBorder') || child.getAttribute('enable-focus-border')
1902
2040
  const shouldShowBorder =
1903
2041
  enableFocusBorder === 'true' || enableFocusBorder === '1' || enableFocusBorder === ''
1904
2042
 
1905
- // Store original styles
1906
- const originalStyles = {
1907
- backgroundColor: child.style.backgroundColor,
1908
- color: child.style.color,
1909
- borderRadius: child.style.borderRadius,
1910
- transform: child.style.transform,
1911
- zIndex: child.style.zIndex,
1912
- transition: child.style.transition,
1913
- outline: child.style.outline,
1914
- outlineOffset: child.style.outlineOffset,
2043
+ // Store original styles - 只在有焦点时保存,避免覆盖 setSelectChildPosition 中保存的原始样式
2044
+ // hasFocus=false 时,我们只是在应用 select 样式,不需要保存原始样式
2045
+ if (hasFocus && !this.focusedElementOriginalStyles.has(child)) {
2046
+ // 如果内联样式为空,从 computedStyle 获取计算后的样式值
2047
+ const computedStyle = window.getComputedStyle(child)
2048
+ const originalStyles = {
2049
+ backgroundColor: child.style.backgroundColor || computedStyle.backgroundColor,
2050
+ color: child.style.color || computedStyle.color,
2051
+ borderRadius: child.style.borderRadius,
2052
+ transform: child.style.transform,
2053
+ zIndex: child.style.zIndex,
2054
+ transition: child.style.transition,
2055
+ outline: child.style.outline,
2056
+ outlineOffset: child.style.outlineOffset,
2057
+ }
2058
+ this.focusedElementOriginalStyles.set(child, originalStyles)
2059
+ console.log(
2060
+ '[TVFocusManager] _applyDuplicateParentStateStyles: saving original styles for focus state - color:',
2061
+ originalStyles.color,
2062
+ 'bg:',
2063
+ originalStyles.backgroundColor
2064
+ )
2065
+ }
2066
+
2067
+ // 当 hasFocus=false 且要应用 select 样式时,也需要保存原始样式
2068
+ // 这样在清除 select 状态时才能正确恢复
2069
+ if (!hasFocus && isParentSelect && !this.focusedElementOriginalStyles.has(child)) {
2070
+ // 如果内联样式为空,从 computedStyle 获取计算后的样式值
2071
+ const computedStyle = window.getComputedStyle(child)
2072
+ const originalStyles = {
2073
+ backgroundColor: child.style.backgroundColor || computedStyle.backgroundColor,
2074
+ color: child.style.color || computedStyle.color,
2075
+ }
2076
+ this.focusedElementOriginalStyles.set(child, originalStyles)
2077
+ console.log(
2078
+ '[TVFocusManager] _applyDuplicateParentStateStyles: saving original styles for select state - color:',
2079
+ originalStyles.color,
2080
+ 'bg:',
2081
+ originalStyles.backgroundColor
2082
+ )
1915
2083
  }
1916
- this.focusedElementOriginalStyles.set(child, originalStyles)
1917
2084
 
1918
2085
  // Apply transition
1919
2086
  child.style.transition = this.defaultFocusStyle.transition
1920
2087
 
1921
- // Apply focus styles
1922
- if (focusBackgroundColor) {
1923
- child.style.backgroundColor = focusBackgroundColor
2088
+ // Apply focus/select styles
2089
+ if (effectiveBackgroundColor) {
2090
+ child.style.backgroundColor = effectiveBackgroundColor
1924
2091
  }
1925
2092
 
1926
- if (focusColor) {
1927
- child.style.color = focusColor
2093
+ if (effectiveColor) {
2094
+ child.style.color = effectiveColor
2095
+ } else if (hasFocus) {
2096
+ // 当有焦点但没有 focusColor 时,恢复原始颜色
2097
+ const savedOriginalStyles = this.focusedElementOriginalStyles.get(child)
2098
+ if (savedOriginalStyles && savedOriginalStyles.color !== undefined) {
2099
+ child.style.color = savedOriginalStyles.color
2100
+ }
1928
2101
  }
1929
2102
 
1930
2103
  if (focusBorderRadius) {
@@ -1942,8 +2115,9 @@ export class TVFocusManager {
1942
2115
  }
1943
2116
  }
1944
2117
 
1945
- // Apply border if enableFocusBorder is set
1946
- if (shouldShowBorder) {
2118
+ // Apply border if enableFocusBorder is set AND has focus
2119
+ // 只有在获得焦点时才显示 focus border,selected 状态不显示边框
2120
+ if (shouldShowBorder && hasFocus) {
1947
2121
  const borderWidth = focusBorderWidth ? parseFloat(focusBorderWidth) : 3
1948
2122
  const borderColor = focusBorderColor || '#FFFFFF'
1949
2123
  const borderRadius = focusBorderRadius ? parseFloat(focusBorderRadius) : 8
@@ -2006,6 +2180,21 @@ export class TVFocusManager {
2006
2180
  // Remove focus styles from duplicateParentState children
2007
2181
  this._removeDuplicateParentStateStyles(element)
2008
2182
 
2183
+ // Check if element is in selected state, apply select styles
2184
+ const isSelected =
2185
+ element.hasAttribute('selected') && element.getAttribute('selected') !== 'false'
2186
+ if (isSelected) {
2187
+ const computedStyle = window.getComputedStyle(element)
2188
+ const selectBackgroundColor =
2189
+ element.getAttribute('selectBackgroundColor') ||
2190
+ element.getAttribute('select-background-color') ||
2191
+ element.getAttribute('data-select-background-color') ||
2192
+ computedStyle.getPropertyValue('--select-background-color')?.trim()
2193
+ if (selectBackgroundColor) {
2194
+ element.style.backgroundColor = selectBackgroundColor
2195
+ }
2196
+ }
2197
+
2009
2198
  // Handle showOnState - update visibility based on normal state
2010
2199
  this._updateShowOnStateVisibility(element, 'normal')
2011
2200
 
@@ -2023,6 +2212,14 @@ export class TVFocusManager {
2023
2212
  return
2024
2213
  }
2025
2214
  const originalStyles = this.focusedElementOriginalStyles.get(child)
2215
+ console.log(
2216
+ '[TVFocusManager] _removeDuplicateParentStateStyles: child:',
2217
+ child.getAttribute?.('data-component-name'),
2218
+ 'text:',
2219
+ child.getAttribute?.('text'),
2220
+ 'originalStyles:',
2221
+ originalStyles
2222
+ )
2026
2223
  if (originalStyles) {
2027
2224
  child.style.backgroundColor = originalStyles.backgroundColor
2028
2225
  child.style.color = originalStyles.color
@@ -2032,15 +2229,8 @@ export class TVFocusManager {
2032
2229
  child.style.transition = originalStyles.transition
2033
2230
  child.style.outline = originalStyles.outline
2034
2231
  child.style.outlineOffset = originalStyles.outlineOffset
2035
- } else {
2036
- child.style.backgroundColor = ''
2037
- child.style.color = ''
2038
- child.style.borderRadius = ''
2039
- child.style.transform = ''
2040
- child.style.zIndex = ''
2041
- child.style.outline = ''
2042
- child.style.outlineOffset = ''
2043
2232
  }
2233
+ // 如果没有 originalStyles,不要清除样式,保持当前状态
2044
2234
  child.classList.remove('focused-child')
2045
2235
 
2046
2236
  // Handle showOnState for duplicateParentState children
@@ -2054,7 +2244,38 @@ export class TVFocusManager {
2054
2244
  }
2055
2245
  }
2056
2246
 
2057
- this.focusedElementOriginalStyles.delete(child)
2247
+ // 不要在这里删除 originalStyles,因为可能还需要用于后续恢复
2248
+ // this.focusedElementOriginalStyles.delete(child)
2249
+
2250
+ // Check if parentElement itself is in selected state
2251
+ // 只检查直接父元素,不向上查找祖先
2252
+ const isParentSelect =
2253
+ (parentElement.hasAttribute('select') &&
2254
+ parentElement.getAttribute('select') !== 'false') ||
2255
+ (parentElement.hasAttribute('selected') &&
2256
+ parentElement.getAttribute('selected') !== 'false')
2257
+
2258
+ // If parent is selected, apply select styles
2259
+ if (isParentSelect) {
2260
+ const computedStyle = window.getComputedStyle(child)
2261
+ const selectColor =
2262
+ child.getAttribute('selectColor') ||
2263
+ child.getAttribute('select-color') ||
2264
+ child.getAttribute('data-select-color') ||
2265
+ computedStyle.getPropertyValue('--select-color')?.trim()
2266
+ const selectBackgroundColor =
2267
+ child.getAttribute('selectBackgroundColor') ||
2268
+ child.getAttribute('select-background-color') ||
2269
+ child.getAttribute('data-select-background-color') ||
2270
+ computedStyle.getPropertyValue('--select-background-color')?.trim()
2271
+
2272
+ if (selectBackgroundColor) {
2273
+ child.style.backgroundColor = selectBackgroundColor
2274
+ }
2275
+ if (selectColor) {
2276
+ child.style.color = selectColor
2277
+ }
2278
+ }
2058
2279
  })
2059
2280
  }
2060
2281
 
@@ -2106,10 +2327,22 @@ export class TVFocusManager {
2106
2327
  let target = e.target
2107
2328
  const componentRegistry = window.__HIPPY_COMPONENT_REGISTRY__
2108
2329
 
2330
+ console.log(
2331
+ '[TVFocusManager] handleClick called, target:',
2332
+ target?.getAttribute?.('data-component-name'),
2333
+ 'text:',
2334
+ target?.getAttribute?.('text')
2335
+ )
2336
+
2109
2337
  while (target && target !== document.body) {
2110
2338
  // Check if this element is focusable
2111
2339
  if (target.hasAttribute && target.hasAttribute('focusable')) {
2112
- // console.log('[TVFocus] Mouse click on focusable element:', target)
2340
+ console.log(
2341
+ '[TVFocusManager] handleClick: found focusable element:',
2342
+ target?.getAttribute?.('data-component-name'),
2343
+ 'text:',
2344
+ target?.getAttribute?.('text')
2345
+ )
2113
2346
 
2114
2347
  // Update focus to this element
2115
2348
  if (this.focusableElements.includes(target)) {
@@ -648,6 +648,8 @@ const FOCUS_STYLE_PROPS = [
648
648
  'focusBorderRadius',
649
649
  'focusOpacity',
650
650
  'focusScale',
651
+ 'selectColor',
652
+ 'selectBackgroundColor',
651
653
  ]
652
654
 
653
655
  // Properties that are numeric values, not colors (should not be converted via argbToCssColor)
@@ -828,6 +830,8 @@ function patchFocusCssVars() {
828
830
  'focus-border-radius',
829
831
  'focus-opacity',
830
832
  'focus-scale',
833
+ 'select-color',
834
+ 'select-background-color',
831
835
  ]
832
836
 
833
837
  const patchRuleStyle = (style) => {