• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import { KeyCode } from '@ohos.multimodalInput.keyCode';
17import measure from '@ohos.measure';
18import mediaquery from '@ohos.mediaquery';
19import resourceManager from '@ohos.resourceManager';
20
21export enum ChipSize {
22  NORMAL = "NORMAL",
23  SMALL = "SMALL"
24}
25
26enum BreakPointsType {
27  SM = "SM",
28  MD = "MD",
29  LG = "LG"
30}
31
32export interface IconCommonOptions {
33  src: ResourceStr;
34  size?: SizeOptions;
35  fillColor?: ResourceColor
36}
37
38export interface SuffixIconOptions extends IconCommonOptions {
39  action?: () => void;
40}
41
42export interface PrefixIconOptions extends IconCommonOptions {}
43
44export interface LabelMarginOptions {
45  left?: Dimension;
46  right?: Dimension;
47}
48
49export interface LabelOptions {
50  text: string;
51  fontSize?: Dimension;
52  fontColor?: ResourceColor;
53  fontFamily?: string;
54  labelMargin?: LabelMarginOptions;
55}
56
57interface IconTheme {
58  size: SizeOptions;
59  fillColor: ResourceColor;
60}
61
62interface PrefixIconTheme extends IconTheme {}
63
64interface SuffixIconTheme extends IconTheme {
65  defaultDeleteIcon: ResourceStr;
66  focusable: boolean;
67}
68
69interface LabelTheme {
70  normalFontSize: Dimension;
71  smallFontSize: Dimension;
72  fontColor: ResourceColor;
73  fontFamily: string;
74  normalMargin: Margin;
75  smallMargin: Margin;
76}
77
78interface ChipNodeOpacity {
79  normal: number;
80  hover: number;
81  pressed: number;
82  disabled: number;
83}
84
85interface ChipNodeConstraintWidth {
86  breakPointMinWidth: number,
87  breakPointSmMaxWidth: number,
88  breakPointMdMaxWidth: number,
89  breakPointLgMaxWidth: number,
90}
91
92interface ChipNodeTheme {
93  minLabelWidth: Dimension;
94  normalHeight: Dimension;
95  smallHeight: Dimension;
96  enabled: boolean;
97  backgroundColor: ResourceColor;
98  focusOutlineColor: ResourceColor;
99  normalBorderRadius: Dimension;
100  smallBorderRadius: Dimension;
101  borderWidth: number;
102  normalPadding: Padding;
103  smallPadding: Padding;
104  hoverBlendColor: ResourceColor;
105  pressedBlendColor: ResourceColor;
106  opacity: ChipNodeOpacity;
107  breakPointConstraintWidth: ChipNodeConstraintWidth;
108}
109
110interface ChipTheme {
111  prefixIcon: PrefixIconTheme;
112  label: LabelTheme;
113  suffixIcon: SuffixIconTheme;
114  chipNode: ChipNodeTheme;
115}
116
117export const defaultTheme: ChipTheme = {
118  prefixIcon: {
119    size: { width: 16, height: 16 },
120    fillColor: $r('sys.color.ohos_id_color_secondary'),
121  },
122  label: {
123    normalFontSize: $r('sys.float.ohos_id_text_size_button2'),
124    smallFontSize: $r('sys.float.ohos_id_text_size_button3'),
125    fontColor: $r('sys.color.ohos_id_color_text_primary'),
126    fontFamily: "HarmonyOS Sans",
127    normalMargin: { left: 6, right: 6, top: 0, bottom: 0 },
128    smallMargin: { left: 4, right: 4, top: 0, bottom: 0 },
129  },
130  suffixIcon: {
131    size: { width: 16, height: 16 },
132    fillColor: $r('sys.color.ohos_id_color_primary'),
133    defaultDeleteIcon: $r('sys.media.ohos_ic_public_cancel', 16, 16),
134    focusable: false,
135  },
136  chipNode: {
137    minLabelWidth: 12,
138    normalHeight: 36,
139    smallHeight: 28,
140    enabled: true,
141    backgroundColor: $r('sys.color.ohos_id_color_button_normal'),
142    focusOutlineColor: $r('sys.color.ohos_id_color_focused_outline'),
143    normalBorderRadius: $r('sys.float.ohos_id_corner_radius_tips_instant_tip'),
144    smallBorderRadius: $r('sys.float.ohos_id_corner_radius_piece'),
145    borderWidth: 2,
146    normalPadding: { left: 16, right: 16, top: 0, bottom: 0 },
147    smallPadding: { left: 12, right: 12, top: 0, bottom: 0 },
148    hoverBlendColor: $r('sys.color.ohos_id_color_hover'),
149    pressedBlendColor: $r('sys.color.ohos_id_color_click_effect'),
150    opacity: { normal: 1, hover: 0.95, pressed: 0.9, disabled: 0.4 },
151    breakPointConstraintWidth: {
152      breakPointMinWidth: 128,
153      breakPointSmMaxWidth: 156,
154      breakPointMdMaxWidth: 280,
155      breakPointLgMaxWidth: 400
156    }
157  }
158};
159
160const noop = () => {
161};
162
163interface ChipOptions {
164  prefixIcon?: PrefixIconOptions;
165  label: LabelOptions;
166  suffixIcon?: SuffixIconOptions;
167  allowClose?: boolean;
168  enabled?: boolean;
169  backgroundColor?: ResourceColor;
170  borderRadius?: Dimension;
171  size?: ChipSize | SizeOptions;
172  onClose?: () => void
173}
174
175@Builder
176export function Chip(options: ChipOptions) {
177  ChipComponent({
178    chipSize: options.size,
179    prefixIcon: options.prefixIcon,
180    label: options.label,
181    suffixIcon: options.suffixIcon,
182    allowClose: options.allowClose,
183    chipEnabled: options.enabled,
184    chipNodeBackgroundColor: options.backgroundColor,
185    chipNodeRadius: options.borderRadius,
186    onClose: options.onClose
187  })
188}
189
190@Component
191export struct ChipComponent {
192  private theme: ChipTheme = defaultTheme;
193  @Prop chipSize: ChipSize | SizeOptions = ChipSize.NORMAL
194  @Prop allowClose: boolean = true
195  @Prop prefixIcon: PrefixIconOptions = { src: "" }
196  @Prop label: LabelOptions = { text: "" }
197  @Prop suffixIcon: SuffixIconOptions = { src: "" }
198  @Prop chipNodeBackgroundColor: ResourceColor = this.theme.chipNode.backgroundColor
199  @Prop chipNodeRadius: Dimension | undefined = void (0)
200  @Prop chipEnabled: boolean = true
201  @State isHover: boolean = false
202  @State chipScale: ScaleOptions = { x: 1, y: 1 }
203  @State chipOpacity: number = 1
204  @State chipBlendColor: ResourceColor = Color.Transparent
205  @State deleteChip: boolean = false
206  @State chipNodeOnFocus: boolean = false
207  @State useDefaultSuffixIcon: boolean = false
208  private chipNodeSize: SizeOptions = {}
209  private onClose: () => void = noop
210  @State suffixIconOnFocus: boolean = false
211  @State chipBreakPoints: BreakPointsType = BreakPointsType.SM
212  private smListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync("0vp<width<600vp")
213  private mdListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync("600vp<=width<840vp")
214  private lgListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync("840vp<=width")
215
216  private isChipSizeEnum(): boolean {
217    return typeof (this.chipSize) === 'string'
218  }
219
220  private getLabelFontSize(): Dimension {
221    try {
222      if (this.label?.fontSize !== void (0) && this.toVp(this.label.fontSize) >= 0) {
223        return this.label.fontSize
224      } else {
225        if (this.isChipSizeEnum() && this.chipSize === ChipSize.SMALL) {
226          return resourceManager.getSystemResourceManager()
227            .getNumberByName((((this.theme.label.smallFontSize as Resource).params as string[])[0]).split('.')[2])
228        } else {
229          return resourceManager.getSystemResourceManager()
230            .getNumberByName((((this.theme.label.normalFontSize as Resource).params as string[])[0]).split('.')[2])
231        }
232      }
233    }
234    catch (error) {
235      return 0
236    }
237
238  }
239
240  private getLabelFontColor(): ResourceColor {
241    return this.label?.fontColor ?? this.theme.label.fontColor
242  }
243
244  private getLabelFontFamily(): string {
245    return this.label?.fontFamily ?? this.theme.label.fontFamily
246  }
247
248  private toVp(value: Dimension | Length | undefined): number {
249    if (value === void (0)) {
250      return Number.NEGATIVE_INFINITY
251    }
252    switch (typeof (value)) {
253      case 'number':
254        return value as number
255      case 'object':
256        try {
257          if ((value as Resource).id !== -1) {
258            return px2vp(getContext(this).resourceManager.getNumber((value as Resource).id))
259          } else {
260            return px2vp(getContext(this)
261              .resourceManager
262              .getNumberByName(((value.params as string[])[0]).split('.')[2]))
263          }
264        } catch (error) {
265          return Number.NEGATIVE_INFINITY
266        }
267      case 'string':
268        let regex: RegExp = new RegExp("(-?\\d+(?:\\.\\d+)?)_?(fp|vp|px|lpx|%)?$", "i");
269        let matches: RegExpMatchArray | null = value.match(regex);
270        if (!matches) {
271          return Number.NEGATIVE_INFINITY
272        }
273        let length: number = Number(matches?.[1] ?? 0);
274        let unit: string = matches?.[2] ?? 'vp'
275        switch (unit.toLowerCase()) {
276          case 'px':
277            length = px2vp(length)
278            break
279          case 'fp':
280            length = px2vp(fp2px(length))
281            break
282          case 'lpx':
283            length = px2vp(lpx2px(length))
284            break
285          case '%':
286            length = Number.NEGATIVE_INFINITY
287            break
288          case 'vp':
289            break
290          default:
291            break
292        }
293        return length
294      default:
295        return Number.NEGATIVE_INFINITY
296    }
297  }
298
299  private getLabelMargin(): Margin {
300    let labelMargin: Margin = { left: 0, right: 0 }
301    if (this.label?.labelMargin?.left !== void (0) && this.toVp(this.label.labelMargin.left) >= 0) {
302      labelMargin.left = this.label?.labelMargin?.left
303    } else if (this.prefixIcon?.src) {
304      if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) {
305        labelMargin.left = this.theme.label.smallMargin.left
306      } else {
307        labelMargin.left = this.theme.label.normalMargin.left
308      }
309    }
310    if (this.label?.labelMargin?.right !== void (0) && this.toVp(this.label.labelMargin.right) >= 0) {
311      labelMargin.right = this.label?.labelMargin?.right
312    } else if (this.suffixIcon?.src || this.useDefaultSuffixIcon) {
313      if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) {
314        labelMargin.right = this.theme.label.smallMargin.right
315      } else {
316        labelMargin.right = this.theme.label.normalMargin.right
317      }
318
319    }
320    return labelMargin
321  }
322
323  private getSuffixIconSize(): SizeOptions {
324    let suffixIconSize: SizeOptions = { width: 0, height: 0 }
325    if (this.suffixIcon?.size?.width !== void (0) && this.toVp(this.suffixIcon?.size?.width) >= 0) {
326      suffixIconSize.width = this.suffixIcon?.size?.width
327    } else {
328      if (this.getSuffixIconSrc()) {
329        suffixIconSize.width = this.theme.suffixIcon.size.width
330      } else {
331        suffixIconSize.width = 0
332      }
333    }
334    if (this.suffixIcon?.size?.height !== void (0) && this.toVp(this.suffixIcon?.size?.height) >= 0) {
335      suffixIconSize.height = this.suffixIcon?.size?.height
336    } else {
337      if (this.getSuffixIconSrc()) {
338        suffixIconSize.height = this.theme.suffixIcon.size.height
339      } else {
340        suffixIconSize.height = 0
341      }
342    }
343    return suffixIconSize
344  }
345
346  private getPrefixIconSize(): SizeOptions {
347    let prefixIconSize: SizeOptions = { width: 0, height: 0 }
348    if (this.prefixIcon?.size?.width !== void (0) && this.toVp(this.prefixIcon?.size?.width) >= 0) {
349      prefixIconSize.width = this.prefixIcon?.size?.width
350    } else {
351      if (this.prefixIcon?.src) {
352        prefixIconSize.width = this.theme.prefixIcon.size.width
353      } else {
354        prefixIconSize.width = 0
355      }
356    }
357    if (this.prefixIcon?.size?.height !== void (0) && this.toVp(this.prefixIcon?.size?.height) >= 0) {
358      prefixIconSize.height = this.prefixIcon?.size?.height
359    } else {
360      if (this.prefixIcon?.src) {
361        prefixIconSize.height = this.theme.prefixIcon.size.height
362      } else {
363        prefixIconSize.height = 0
364      }
365    }
366    return prefixIconSize
367  }
368
369  private getPrefixIconFilledColor(): ResourceColor {
370    return this.prefixIcon?.fillColor ?? this.theme.prefixIcon.fillColor
371  }
372
373  private getSuffixIconFilledColor(): ResourceColor {
374    return this.suffixIcon?.fillColor ?? this.theme.suffixIcon.fillColor
375  }
376
377  private getSuffixIconFocusable(): boolean {
378    return (this.useDefaultSuffixIcon && this.allowClose) || this.suffixIcon?.action !== void (0)
379  }
380
381  private getChipNodePadding(): Padding {
382    return (this.isChipSizeEnum() && this.chipSize === ChipSize.SMALL) ? this.theme.chipNode.smallPadding : this.theme.chipNode.normalPadding
383  }
384
385  private getChipNodeRadius(): Dimension {
386    if (this.chipNodeRadius !== void (0) && this.toVp(this.chipNodeRadius) >= 0) {
387      return this.chipNodeRadius as Dimension
388    } else {
389      return ((this.isChipSizeEnum() && this.chipSize === ChipSize.SMALL) ? this.theme.chipNode.smallBorderRadius : this.theme.chipNode.normalBorderRadius)
390    }
391  }
392
393  private getChipNodeBackGroundColor(): ResourceColor {
394    return this.chipNodeBackgroundColor ?? this.theme.chipNode.backgroundColor
395  }
396
397  private getChipNodeHeight(): Length {
398    if (this.isChipSizeEnum()) {
399      return this.chipSize === ChipSize.SMALL ? this.theme.chipNode.smallHeight : this.theme.chipNode.normalHeight
400    } else {
401      this.chipNodeSize = this.chipSize as SizeOptions
402      return (this.chipNodeSize?.height !== void (0) && this.toVp(this.chipNodeSize?.height) >= 0) ? this.toVp(this.chipNodeSize?.height) : this.theme.chipNode.normalHeight
403    }
404  }
405
406  private getLabelWidth(): number {
407    return px2vp(measure.measureText({
408      textContent: this.label?.text ?? "",
409      fontSize: this.getLabelFontSize(),
410      fontFamily: this.label?.fontFamily ?? this.theme.label.fontFamily,
411      maxLines: 1,
412      overflow: TextOverflow.Ellipsis,
413      textAlign: TextAlign.Center
414    }))
415  }
416
417  private getCalculateChipNodeWidth(): number {
418    let calWidth: number = 0
419    calWidth += this.getChipNodePadding().left as number
420    calWidth += this.toVp(this.getPrefixIconSize().width)
421    calWidth += this.toVp(this.getLabelMargin().left)
422    calWidth += this.getLabelWidth()
423    calWidth += this.toVp(this.getLabelMargin().right)
424    calWidth += this.toVp(this.getSuffixIconSize().width)
425    calWidth += this.getChipNodePadding().right as number
426    return calWidth
427  }
428
429  private getReserveChipNodeWidth(): number {
430    return this.getCalculateChipNodeWidth() - this.getLabelWidth() + (this.theme.chipNode.minLabelWidth as number)
431  }
432
433  private getChipEnable(): boolean {
434    return this.chipEnabled || this.chipEnabled === void (0)
435  }
436
437  private getChipNodeOpacity(): number {
438    return this.getChipEnable() ? this.chipOpacity : this.theme.chipNode.opacity.disabled
439  }
440
441  private handleTouch(event: TouchEvent) {
442    if (!this.getChipEnable()) {
443      return
444    }
445    if (this.isHover) {
446      if (event.type === TouchType.Down) {
447        animateTo({ curve: Curve.Sharp, duration: 100 }, () => {
448          this.chipBlendColor = this.theme.chipNode.pressedBlendColor
449          this.chipOpacity = this.theme.chipNode.opacity.pressed
450        })
451      } else if (event.type === TouchType.Up) {
452        animateTo({ curve: Curve.Sharp, duration: 100 }, () => {
453          this.chipBlendColor = this.theme.chipNode.hoverBlendColor
454          this.chipOpacity = this.theme.chipNode.opacity.hover
455        })
456      }
457    } else {
458      if (event.type === TouchType.Down) {
459        animateTo({ curve: Curve.Friction, duration: 350 }, () => {
460          this.chipBlendColor = this.theme.chipNode.pressedBlendColor
461          this.chipOpacity = this.theme.chipNode.opacity.pressed
462        })
463      } else if (event.type === TouchType.Up) {
464        animateTo({ curve: Curve.Friction, duration: 350 }, () => {
465          this.chipBlendColor = Color.Transparent
466          this.chipOpacity = this.theme.chipNode.opacity.normal
467        })
468      }
469    }
470  }
471
472  private hoverAnimate(isHover: boolean) {
473    if (!this.getChipEnable()) {
474      return
475    }
476    this.isHover = isHover
477    animateTo({
478      curve: Curve.Friction,
479      duration: 250
480    }, () => {
481      if (isHover) {
482        this.chipBlendColor = this.theme.chipNode.hoverBlendColor
483        this.chipOpacity = this.theme.chipNode.opacity.hover
484      } else {
485        this.chipBlendColor = Color.Transparent
486        this.chipOpacity = this.theme.chipNode.opacity.normal
487      }
488    })
489  }
490
491  private deleteChipNodeAnimate() {
492    animateTo({ duration: 150, curve: Curve.Sharp }, () => {
493      this.chipOpacity = 0
494      this.chipBlendColor = Color.Transparent
495    })
496    animateTo({ duration: 150, curve: Curve.FastOutLinearIn, onFinish: () => {
497      this.deleteChip = true
498    } },
499      () => {
500        this.chipScale = { x: 0.85, y: 0.85 }
501      })
502  }
503
504  private getSuffixIconSrc(): ResourceStr | undefined {
505    this.useDefaultSuffixIcon = !this.suffixIcon?.src && this.allowClose
506    return this.useDefaultSuffixIcon ? this.theme.suffixIcon.defaultDeleteIcon : (this.suffixIcon?.src ?? void (0))
507  }
508
509  private getChipNodeWidth(): Length {
510    if (!this.isChipSizeEnum()) {
511      this.chipNodeSize = this.chipSize as SizeOptions
512      if (this.chipNodeSize?.width !== void (0) && this.toVp(this.chipNodeSize.width) >= 0) {
513        return this.toVp(this.chipNodeSize.width)
514      }
515    }
516    let constraintWidth: ConstraintSizeOptions = this.getChipConstraintWidth()
517    return Math.min(Math.max(this.getCalculateChipNodeWidth(), constraintWidth.minWidth as number), constraintWidth.maxWidth as number);
518  }
519
520  private getFocusOverlaySize(): SizeOptions {
521    return {
522      width: Math.max(this.getChipNodeWidth() as number, this.getChipConstraintWidth().minWidth as number) + 8,
523      height: this.getChipNodeHeight() as number + 8
524    }
525  }
526
527  private getChipConstraintWidth(): ConstraintSizeOptions {
528    let calcMinWidth: number = this.getReserveChipNodeWidth()
529    if (!this.isChipSizeEnum()) {
530      this.chipNodeSize = this.chipSize as SizeOptions
531      if (this.chipNodeSize?.width !== void (0) && this.toVp(this.chipNodeSize?.width) >= 0) {
532        return { minWidth: calcMinWidth, maxWidth: this.chipNodeSize.width as number }
533      }
534    }
535
536    let constraintWidth: number = this.getCalculateChipNodeWidth()
537    switch (this.chipBreakPoints) {
538      case BreakPointsType.SM:
539        return {
540          minWidth: calcMinWidth,
541          maxWidth: Math.min(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointSmMaxWidth)
542        }
543      case BreakPointsType.MD:
544        return {
545          minWidth: Math.max(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointMinWidth),
546          maxWidth: Math.min(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointMdMaxWidth)
547        }
548
549      case BreakPointsType.LG:
550        return {
551          minWidth: Math.max(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointMinWidth),
552          maxWidth: Math.min(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointLgMaxWidth)
553        }
554      default:
555        return { minWidth: calcMinWidth, maxWidth: constraintWidth }
556    }
557  }
558
559  @Builder
560  focusOverlay() {
561    Stack() {
562      if (this.chipNodeOnFocus && !this.suffixIconOnFocus) {
563        Stack()
564          .borderRadius(this.toVp(this.getChipNodeRadius()) + 4)
565          .size(this.getFocusOverlaySize())
566          .borderColor(this.theme.chipNode.focusOutlineColor)
567          .borderWidth(this.theme.chipNode.borderWidth)
568      }
569    }
570    .size({ width: 1, height: 1 })
571    .align(Alignment.Center)
572  }
573
574  @Styles
575  suffixIconFocusStyles() {
576    .borderColor(this.theme.chipNode.focusOutlineColor)
577    .borderWidth(this.getSuffixIconFocusable() ? this.theme.chipNode.borderWidth : 0)
578  }
579
580  @Styles
581  suffixIconNormalStyles() {
582    .borderColor(Color.Transparent)
583    .borderWidth(0)
584  }
585
586  aboutToAppear() {
587    this.smListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => {
588      if (mediaQueryResult.matches) {
589        this.chipBreakPoints = BreakPointsType.SM
590      }
591    })
592    this.mdListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => {
593      if (mediaQueryResult.matches) {
594        this.chipBreakPoints = BreakPointsType.MD
595      }
596    })
597    this.lgListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => {
598      if (mediaQueryResult.matches) {
599        this.chipBreakPoints = BreakPointsType.LG
600      }
601    })
602  }
603
604  private getVisibility(): Visibility {
605    if (this.toVp(this.getChipNodeHeight()) > 0) {
606      return Visibility.Visible
607    } else {
608      return Visibility.None
609    }
610  }
611
612  aboutToDisappear() {
613    this.smListener.off("change")
614    this.mdListener.off("change")
615    this.lgListener.off("change")
616  }
617
618  @Builder
619  chipBuilder() {
620    Row() {
621      if (this.prefixIcon?.src !== "") {
622        Image(this.prefixIcon?.src)
623          .opacity(this.getChipNodeOpacity())
624          .size(this.getPrefixIconSize())
625          .fillColor(this.getPrefixIconFilledColor())
626          .enabled(this.getChipEnable())
627          .objectFit(ImageFit.Cover)
628          .focusable(false)
629          .flexShrink(0)
630          .visibility(this.getVisibility())
631          .draggable(false)
632      }
633
634      Text(this.label?.text ?? "")
635        .opacity(this.getChipNodeOpacity())
636        .fontSize(this.getLabelFontSize())
637        .fontColor(this.getLabelFontColor())
638        .fontFamily(this.getLabelFontFamily())
639        .margin(this.getLabelMargin())
640        .enabled(this.getChipEnable())
641        .maxLines(1)
642        .textOverflow({ overflow: TextOverflow.Ellipsis })
643        .flexShrink(1)
644        .focusable(true)
645        .textAlign(TextAlign.Center)
646        .visibility(this.getVisibility())
647        .draggable(false)
648        .stateStyles({
649          focused: {}
650        })
651
652      Image(this.getSuffixIconSrc())
653        .opacity(this.getChipNodeOpacity())
654        .size(this.getSuffixIconSize())
655        .fillColor(this.getSuffixIconFilledColor())
656        .enabled(this.getChipEnable())
657        .focusable(this.getSuffixIconFocusable())
658        .objectFit(ImageFit.Cover)
659        .flexShrink(0)
660        .visibility(this.getVisibility())
661        .draggable(false)
662        .onFocus(() => {
663          this.suffixIconOnFocus = true
664        })
665        .onBlur(() => {
666          this.suffixIconOnFocus = false
667        })
668        .onClick(() => {
669          if (!this.getChipEnable()) {
670            return
671          }
672          if (this.suffixIcon?.action) {
673            this.suffixIcon.action()
674            return
675          }
676          if (this.allowClose && this.useDefaultSuffixIcon) {
677            this.onClose()
678            this.deleteChipNodeAnimate()
679            return
680          }
681        })
682    }
683    .alignItems(VerticalAlign.Center)
684    .justifyContent(FlexAlign.Center)
685    .clip(false)
686    .padding(this.getChipNodePadding())
687    .height(this.getChipNodeHeight())
688    .width(this.getChipNodeWidth())
689    .constraintSize(this.getChipConstraintWidth())
690    .backgroundColor(this.getChipNodeBackGroundColor())
691    .borderRadius(this.getChipNodeRadius())
692    .enabled(this.getChipEnable())
693    .scale(this.chipScale)
694    .focusable(true)
695    .colorBlend(this.chipBlendColor)
696    .opacity(this.getChipNodeOpacity())
697    .overlay(this.focusOverlay, { align: Alignment.Center })
698    .onFocus(() => {
699      this.chipNodeOnFocus = true
700    })
701    .onBlur(() => {
702      this.chipNodeOnFocus = false
703    })
704    .onTouch((event) => {
705      this.handleTouch(event)
706    })
707    .onHover((isHover: boolean) => {
708      this.hoverAnimate(isHover)
709    })
710    .onKeyEvent((event) => {
711      if (event.type === KeyType.Down && event.keyCode === KeyCode.KEYCODE_FORWARD_DEL && !this.suffixIconOnFocus) {
712        this.deleteChipNodeAnimate()
713      }
714    })
715  }
716
717  build() {
718    if (!this.deleteChip) {
719      this.chipBuilder()
720    }
721  }
722}