DEV Community

HarmonyOS
HarmonyOS

Posted on

How to Solve the Issue of TextArea's Default Placeholder Overlapping with Newly Entered Text

Read the original article:How to Solve the Issue of TextArea's Default Placeholder Overlapping with Newly Entered Text

Problem Description

The default placeholder in a Text component is intended to remain visible in front of newly entered text within a TextArea. However, when the input text grows long enough to scroll vertically, the placeholder overlaps with the text, creating a visual conflict.

The problem code is as follows:

Stack({ alignContent: Alignment.TopStart }) {
  Text('Placeholder Text')
    .margin({ top: 8, left: 14 })
    .fontColor(Color.Red)
    .fontSize(15)
    .onAreaChange((oldValue: Area, newValue: Area) => {
      this.txtWidth = newValue.width as number
    })

  TextArea({ text: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" })
    .width(300)
    .textIndent(this.txtWidth)
}

Enter fullscreen mode Exit fullscreen mode

Background Knowledge

  • Text component: used to display a piece of text.
  • TextArea component: multi-line text input field.
  • UI container behavior: for example, using a Stack container, child components are stacked in order, with later components overlapping earlier ones.

Troubleshooting Process

  • Observed that the placeholder and input text are inside the same Stack.
  • Noted that the placeholder does not move when the text scrolls.
  • Determined that the solution requires synchronizing the placeholder position with the scrolling of the TextArea.

Analysis Conclusion

The placeholder overlaps the input text because it is fixed inside the same container and does not respond to the TextArea’s scroll events. The placeholder needs to move dynamically according to the scroll offset.

Solution

When the input text exceeds the maximum display lines, the newly entered text can be scrolled. However, since the default placeholder Text and the input TextArea are both inside the same Stack, the placeholder does not move when the text area scrolls.

To solve this issue, the placeholder Text needs to move along with the scrolling input text. By using the onContentScroll property of TextArea, you can get the scroll distance when the text exceeds the maximum display lines and pass it to the offset property of the placeholder Text, so that it scrolls together with the input text.

The sample code is as follows:

@Entry
@Component
struct Page240911220842047 {
  @State txt: string = 'Placeholder Text'
  @State text: string = ' '
  @State textValue: string = ''
  @State txtWidth: number = 0
  @State offsetY: number = 0
  controller: TextAreaController = new TextAreaController()

  build() {
    Row() {
      Column() {
        Stack({ alignContent: Alignment.TopStart }) {
          Column() {
            Text(this.txt)
              .margin({ left: 14 })
              .fontSize(15)
              .fontColor(Color.Red)
              .onAreaChange((oldValue: Area, newValue: Area) => {
                this.txtWidth = newValue.width as number
              })
              .offset({ x: 0, y: this.offsetY })
          }
          .width(300)
          .height(92)
          .alignItems(HorizontalAlign.Start)
          .border({ width: 1 })
          .margin({ top: 8 })
          .clip(true)

          TextArea({ text: this.text, controller: this.controller })
            .width(300)
            .height(100)
            .textIndent(this.txtWidth)
            .onChange((info) => {
              this.text = info
              this.textValue = this.text.trim()
            })
            .onContentScroll((totalOffsetX: number, totalOffsetY: number) => {
              if ((px2vp(totalOffsetY) - 8) > 0) {
                this.offsetY = 0
              } else if ((px2vp(totalOffsetY) - 8) < -16) {
                this.offsetY = -16
              } else {
                this.offsetY = (px2vp(totalOffsetY) - 8)
              }
            })
            .onWillDelete((info: DeleteValue) => {
              if (info.deleteOffset === 1 || this.text === ' ') {
                return false
              }
              return true;
            })
            .onDidDelete((info: DeleteValue) => {
              if (info.deleteOffset === 0) {
                this.text = ' '
              }
            })
            .onTextSelectionChange((selectionStart: number, selectionEnd: number) => {
              if (selectionStart === 0) {
                selectionStart === selectionEnd ? this.controller.caretPosition(1) : this.controller.setTextSelection(1, selectionEnd)
              }
            })
        }
      }
      .width('100%')
    }
    .height('100%')
  }
}

Enter fullscreen mode Exit fullscreen mode

The previous sections provided a solution for text input when both the default placeholder and newly entered text coexist. Another common approach for handling placeholders and text input is to keep the placeholder separate from the input text. In this method, the placeholder text is independent of the TextArea content and remains visible, which is useful for certain types of text input. This effect can be achieved by setting the padding property for the TextArea.

The code is as follows:

Stack() {
  Column() {
    Text(this.txt)
      .margin({ top: 8, left: 14 })
      .fontColor(Color.Red)
      .fontSize(15)
      .onAreaChange((oldValue: Area, newValue: Area) => {
        this.txtWidth = newValue.width as number
      })
  }
  .width(300)
  .alignItems(HorizontalAlign.Start)
  .justifyContent(FlexAlign.Start)

  TextArea({ text: "I am a TextArea, I am a TextArea, I am a TextArea, I am a TextArea" })
    .width(300)
    .padding({ left: this.txtWidth + 15 })
}
.alignContent(Alignment.TopStart)

Enter fullscreen mode Exit fullscreen mode

If you want to display the default prompt when there is no input and remove it when there is input, you can use the existing placeholder property of the multi-line input text component TextArea to set the prompt text when there is no input. After entering the content, the prompt text will not be displayed. As shown in the figure:

The code is as follows:

TextArea({
        text: this.text,
        placeholder: 'The text area can hold an unlimited amount of text. input your word...',
        controller: this.controller
      })
        .placeholderFont({ size: 16, weight: 400 })
Enter fullscreen mode Exit fullscreen mode

This property cannot satisfy the requirement of always displaying the default prompt. In this case, customized scene design needs to be completed based on the component events and the characteristics of the overall UI container component.

Verification Result

After implementing the scroll synchronization, the placeholder now moves with the input text, preventing overlap. Both short and long input texts behave correctly, preserving visual clarity.

Related Documents or Links

https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-container-stack

https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-basic-components-text

https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-basic-components-textarea

Written by Emine Inan

Top comments (0)