DEV Community

HarmonyOS
HarmonyOS

Posted on

Landscape-Aware Half-Modal: Slide In from the Right with bindSheet + CustomDialog

Read the original article:Landscape-Aware Half-Modal: Slide In from the Right with bindSheet + CustomDialog

Landscape-Aware Half-Modal: Slide In from the Right with bindSheet + CustomDialog

Requirement Description

When using bindSheet, the half-modal sheet always slides up from the bottom. The goal is to keep this behavior in portrait, but—in landscape—open a right-side sliding panel instead.

Background Knowledge

  • bindSheet binds a half-modal sheet to any component; by default it pops from the bottom. Docs
  • Use media query to detect orientation changes and switch the presentation.

Implementation Steps

  1. Listen for orientation using this.getUIContext().getMediaQuery().matchMediaSync('(orientation: landscape)') and subscribe to its change event.
  2. Keep a small state machine:
    • showType = 0 → no dialog
    • showType = 1 → portrait sheet open (half-modal with bindSheet)
    • showType = 2 → landscape panel open (custom dialog on the right)
  3. Portrait: open the half-modal using bindSheet and isShow = true.
  4. Landscape: close the sheet (if any) and open a right-aligned CustomDialog using alignment: DialogAlignment.CenterEnd, width: '30%', height: '100%'.
  5. On orientation change, swap: close the currently shown presentation and open the other.
  6. Clean up listeners in aboutToDisappear.

Code Snippet / Configuration

import { mediaquery } from '@kit.ArkUI';

@Component
struct MenuExample {
  @State list: string[] = ['Share Screen', 'Chat', 'Raise Hand']

  build() {
    Column() {
      Column() {
        ForEach(this.list, (item: string) => {
          Text(item).fontSize(16).margin({ bottom: 30 })
            .fontColor(Color.Black)
        })
      }
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

// Custom right-side dialog (for landscape)
@CustomDialog
struct CustomDialogExample {
  controller?: CustomDialogController

  build() {
    MenuExample()
  }
}

@Entry
@Component
struct SheetTransitionExample {
  @State isShow: boolean = false // controls the portrait half-sheet
  @State showType: number = 0    // 0: none, 1: portrait sheet open, 2: landscape dialog open
  @State matches: boolean = false // false = portrait

  // Media query listener (use UIContext API; legacy mediaquery.matchMediaSync is deprecated)
  listener = this.getUIContext().getMediaQuery().matchMediaSync('(orientation: landscape)');

  dialogController: CustomDialogController | null = new CustomDialogController({
    builder: CustomDialogExample(),
    onWillDismiss: (dismissDialogAction: DismissDialogAction) => {
      if (dismissDialogAction.reason === DismissReason.TOUCH_OUTSIDE) {
        this.showType = 0
      }
      dismissDialogAction.dismiss(); // register dismiss action
    },
    autoCancel: true,
    alignment: DialogAlignment.CenterEnd, // right side
    cornerRadius: 20,
    width: '30%',
    height: '100%',
  })

  // Swap presentation based on current orientation & state
  ChangeWindow() {
    if (this.showType !== 0) {
      if (this.matches) {
        // Landscape: close portrait sheet, open right-side dialog
        this.isShow = false
        if (this.dialogController != null) {
          this.dialogController.open()
        }
      } else {
        // Portrait: close landscape dialog, open half-sheet
        this.isShow = true
        if (this.dialogController != null) {
          this.dialogController.close()
        }
      }
    }
  }

  onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
    this.matches = mediaQueryResult.matches
    this.ChangeWindow()
  }

  aboutToAppear(): void {
    // Register orientation change callback
    const portraitFunc = (r: mediaquery.MediaQueryResult): void => this.onPortrait(r)
    this.listener.on('change', portraitFunc);
  }

  aboutToDisappear(): void {
    this.listener.off('change')           // unregister callback
    this.dialogController = null          // release dialog controller
  }

  @Builder
  myBuilder() {
    MenuExample()
  }

  build() {
    Column() {
      Button('Show Panel')
        .onClick(() => {
          this.showType = this.matches ? 2 : 1
          this.ChangeWindow()
        })
        .fontSize(20)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .bindSheet($$this.isShow, this.myBuilder(), {
      height: 300,
      onWillDismiss: ((dismissSheetAction: DismissSheetAction) => {
        if (dismissSheetAction.reason === DismissReason.SLIDE_DOWN) {
          this.showType = 0
        }
        dismissSheetAction.dismiss(); // register dismiss action
      }),
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Test Results

  • Verified that portrait shows a bottom half-sheet and landscape shows a right-side panel.
  • Switching device orientation live swaps between the two presentations without stale overlays.

Limitations or Considerations

  • bindSheet cannot slide from the right; the right-side behavior is implemented via CustomDialog.
  • Ensure you close whichever presentation isn’t in use during orientation changes to avoid double overlays.
  • For wearables, landscape orientation is uncommon; however, the panel pattern is still useful for side-docked interactions on larger or rotated screens.

Related Documents or Links

Written by Bunyamin Eymen Alagoz

Top comments (0)