鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 14:大屏弹窗改成侧边面板
前言
我在做材料列表详情查看的时候,最早用的是居中弹窗。外屏下这个写法没什么违和感,用户点一条记录,页面弹出一个详情窗口,确认完再关掉,流程短,注意力也集中。手机空间本来就小,弹窗把当前任务单独拎出来,用户不会被列表里的其他内容干扰。
把同样的交互放到 Pura X Max 展开态里,我开始觉得不太对。列表区域明明还有很多空间,用户也能看到多条材料记录,但居中弹窗一出现,原来的列表和选中项都被遮住了。弹窗本身占据了屏幕中心,左右两边却空着不少区域。用户想对照原列表里的记录,或者继续切换另一条材料,就得先关掉弹窗,再回到列表里重新找。
这种情况在展开态里很常见,尤其是下面这些轻量任务:
- 查看一条记录的补充说明
- 编辑一两个字段
- 临时筛选列表
- 查看备注或处理建议
- 确认一条识别结果
- 给当前记录补充状态或标签
这些任务都有一个共同点,它们需要依附在当前页面上完成,不一定值得跳到完整详情页。小屏里可以用弹窗或底部面板承接;展开态里,右侧面板通常会更符合页面结构。左侧仍然保留列表或原内容,右侧承接当前详情和操作,用户不会丢掉自己是从哪条记录点进来的。
Pura X Max 在外屏、展开态、分屏和自由窗口之间切换时,页面可用宽度变化很大。弹层交互不能只按手机外屏的思路处理,窗口宽起来以后,页面有条件保留上下文,弹层也可以从遮住页面变成贴着页面补充信息。
这次我用一个材料列表页来模拟这个场景。点击“查看详情”后,窄窗口使用底部面板,展开态使用右侧面板。状态和数据仍然是一套,只是面板出现的位置跟着窗口宽度变化。这个处理方式比较适合详情补充、筛选条件、备注编辑这类轻量任务。
一、弹窗在展开态里会遮住上下文
1.1 外屏里弹窗可以集中注意力
外屏里用弹窗处理详情补充,很多时候是可以接受的。比如用户在一个窄屏列表里点开某条材料,弹出一个居中的详情卡片,或者从底部拉起一个面板,用户的注意力会集中到这条记录上。页面空间有限,原列表本来就无法和详情同时展开,弹窗相当于给当前任务临时开出一块区域。
我在手机外屏里也会经常这样写。比如编辑一个标题、确认一个提醒、查看一段识别结果,弹窗或底部面板能把任务收得比较干净。用户看完以后关掉弹层,回到原页面继续操作。这个模式在小屏里并不违和,反而能减少页面跳转。
在代码里,这类写法通常很简单。点一条记录,把showPanel设为 true,再把当前记录 id 存下来。
private openPanel(itemId: number) { this.selectedId = itemId; this.showPanel = true; }这个状态本身可以保留。真正要调整的是弹层在不同窗口宽度下的呈现方式。外屏可以从底部出来,展开态就没必要继续遮住页面中心。
1.2 展开态里弹窗会抢掉参照物
我把这个页面切到展开态后,再点查看详情,第一个感受就是弹窗挡住了列表。原来的材料列表还在背后,但用户已经看不到自己点的是哪条记录,也看不到上下几条记录之间的关系。对于只查看一条详情来说,这还勉强能用;如果用户需要连续切换记录,居中弹窗就开始影响操作。
展开态的价值之一,是能把原页面和补充内容同时放下。比如左侧继续保留列表,右侧显示详情补充。用户在查看详情时,还能看到列表里其他材料,也能确认当前记录的上下文。这个时候居中弹窗反而把展开态空间浪费掉了。
我会把这类弹层分成两种用途来区分。需要强打断、强确认的内容,比如删除确认、支付确认、危险操作提醒,仍然适合弹窗;只是查看详情、筛选条件、备注编辑、轻量补充信息,就更适合放到侧边面板里。它们不需要遮住整个页面,也不需要让用户离开当前上下文。
二、小屏继续用底部面板
2.1 窄窗口更适合聚焦处理
在外屏或较窄窗口里,我仍然会保留底部面板。原因很简单,窄窗口里很难同时放下列表和详情,用户点开一条记录时,页面优先让他处理当前内容。底部面板从屏幕下方出现,覆盖原页面的一部分,用户会自然把注意力放到当前记录上。
这类交互适合短任务。比如查看一条提醒、确认一段识别结果、保存一个处理建议。用户不需要保留大量上下文,只要知道当前处理对象是什么,以及下一步能点哪个按钮。
示例里,小屏下的底部面板只在showPanel为 true 且窗口没有达到展开态时出现。
if (this.showPanel && !this.isExpanded()) { this.BottomSheet() }这个判断看起来很简单,但它决定了小屏里的交互节奏。窄窗口不强行分栏,也不把详情塞在右侧,而是用底部面板集中当前任务。等窗口宽起来以后,同样的详情内容再换到右侧面板。
2.2 底部面板要控制高度
底部面板不能无限长。小屏里高度本来就有限,如果面板展开后把整个屏幕都占满,用户会感觉像跳进了另一个页面,但返回关系又没有完整页面那么清楚。
示例里,底部面板用了固定高度。
.height(430)这个值不是固定标准。真实项目里要根据内容决定,外屏页面里可以略高一点,悬浮窗里要更克制。一般来说,底部面板适合放标题、摘要、少量元信息和一到两个按钮。如果内容继续增长,就要考虑进入完整详情页,而不是让底部面板一直加高。
我在项目里通常会把底部面板看作临时处理区,不会把完整详情全部塞进去。它可以承接当前动作,但不适合承担一个复杂流程。这样后续迁移到展开态侧边面板时,也能保持同一套信息层级。
三、展开态改成右侧面板
3.1 右侧面板保留原页面参照
展开态下,我更愿意把详情补充内容放到右侧。左侧仍然是列表,右侧面板显示当前记录的详情、建议和操作按钮。这样用户打开详情时,不会失去原页面参照,也能继续知道自己是从哪条记录点进来的。
示例里的判断从窗口宽度开始。
private readonly expandedWidth: number = 820; private isExpanded(): boolean { return this.getEffectiveWidth() >= this.expandedWidth; }820vp是这个示例里的门槛。真实项目里要看页面主体宽度、侧边面板宽度、左右 padding 和列表卡片宽度。比如右侧面板需要 360vp,左侧列表至少要保留 420vp,再加上间距和页面边距,阈值就不能设得太低。
大屏下显示侧边面板的判断也很简单。
if (this.showPanel && this.isExpanded()) { this.SidePanel() }我在真实项目里会把这个判断放在页面层,而不是让某个按钮组件自己决定弹出方式。按钮只负责打开详情,至于详情出现在底部还是右侧,交给页面根据窗口宽度处理。
3.2 面板宽度要给主页面留空间
示例里的右侧面板宽度是 360vp。
.width(360)这个宽度适合展示标题、摘要、元信息、补充说明和两个按钮。它不会太窄,正文还可以阅读;也不会太宽,左侧列表仍然能保留足够空间。如果面板内容更少,可以降到 320vp;如果需要展示表单字段,可以增加到 400vp 左右。
这里我会特别关注左侧列表。侧边面板出现以后,左侧仍然应该能看清列表标题、选中态和至少几条记录。如果右侧面板过宽,左侧列表被挤得只剩窄条,那就和居中弹窗一样失去了保留上下文的意义。
四、用 Stack 还原两种形态
4.1 页面层控制遮罩和面板
这里我没有直接调用系统弹窗 API,而是在页面里用Stack叠出遮罩、底部面板和右侧面板。这样做的好处是方便验证布局逻辑,也方便在同一个页面里根据窗口宽度切换不同面板形态。
核心结构放在build()里。
if (this.showPanel) { Column() .width('100%') .height('100%') .backgroundColor(this.isExpanded() ? '#00000000' : '#66000000') if (this.isExpanded()) { this.SidePanel() } else { this.BottomSheet() } }小屏下,遮罩是半透明黑色,点击遮罩可以关闭面板。这个交互更接近普通弹窗,用户知道当前任务是临时打开的。展开态下,遮罩保持透明,面板出现在右侧,左侧列表仍然可见。关闭动作放在面板内部,避免用户误触左侧列表时把面板关掉。
这两个细节很容易被忽略。很多时候我们只处理面板位置,却忘了遮罩也要跟着变化。小屏需要遮罩帮助用户聚焦,大屏更需要保留页面上下文。遮罩颜色、关闭方式、面板位置,其实都应该和窗口状态一起调整。
4.2 详情内容复用同一份
这里底部面板和右侧面板都使用同一个DetailContent()。也就是说,详情内容没有拆成两份,只是外层容器不同。
@Builder private DetailContent(item: MaterialItem) { Column({ space: 16 }) { // 标题、摘要、补充说明、处理建议和按钮 } }这样写的好处是,后面改详情字段时,不需要同时改底部面板和右侧面板两套内容。小屏和大屏的差异主要体现在外层容器:小屏是从底部出现,大屏是贴右侧出现。详情内部的字段结构可以保持一致,再根据宽度做少量字号或行高调整。
真实项目里也建议这样处理。弹层形态可以有两种,内容尽量保持一份。否则后面加字段、改文案、调整按钮状态时,很容易出现小屏和大屏不一致。
五、实际运行效果
这里顶部有外屏和展开态两个演示按钮,方便在同一台模拟器里观察面板形态。真实项目里可以删掉这些按钮,页面直接根据真实窗口宽度判断。
外屏状态下,点击任意一条记录的查看详情,详情会从底部弹出,背景有一层半透明遮罩。这个状态适合窄窗口,用户的注意力集中在当前记录上,底部面板也更贴近小屏操作习惯。
展开态状态下,再点击查看详情,详情会出现在右侧,左侧列表不会被遮住。选中的列表项会保留高亮,用户能看到自己打开的是哪条记录,也可以继续对照列表里的其他材料。
六、如何迁移到实际项目
6.1 演示宽度要删掉
示例里的previewWidth只是为了在同一个模拟器里切换外屏和展开态。真实项目里不需要这些按钮,页面应该直接使用真实窗口宽度。
private getEffectiveWidth(): number { if (this.previewWidth > 0) { return this.previewWidth; } return this.pageWidth; }迁回项目时,可以直接返回pageWidth。
private getEffectiveWidth(): number { return this.pageWidth; }页面宽度可以继续通过onAreaChange写入。这里记录的是页面根容器宽度,而不是设备名称。对 Pura X Max 来说,同一台设备可能处在外屏、展开态、分屏和自由窗口里,面板形态要看当前窗口给了多少空间。
6.2 不是所有弹窗都适合改成侧边面板
侧边面板适合轻量补充任务,比如查看详情、筛选条件、备注编辑、状态确认。它的价值在于保留原页面上下文,让用户不离开列表,也能完成一小段操作。
如果是删除确认、支付确认、权限授权这类需要强提醒的动作,我仍然会用弹窗。它们本来就需要打断用户,让用户明确确认当前操作。侧边面板太轻,反而不适合这类高风险动作。
如果是多步骤表单、长文编辑、图片裁剪、复杂审批流程,我也不会放在侧边面板里。右侧面板宽度有限,复杂流程放进去会让用户一直滚动,还容易丢失表单上下文。这类任务应该进入完整页面,或者使用更大的编辑页面承接。
6.3 面板状态要和选中项保持一致
示例里用了selectedId保存当前选中记录,用showPanel控制面板是否显示。点击不同记录时,面板内容会跟着更新。
private openPanel(itemId: number) { this.selectedId = itemId; this.showPanel = true; }真实项目里也要留意这个状态关系。用户在展开态里点击列表 A,右侧面板显示 A 的详情;继续点击列表 B,右侧面板应该切到 B,而不是重新弹出一个新的弹窗。这样列表和面板之间的关系才是连续的。
我会把这类状态放在页面层,而不是放在单个列表卡片里。列表卡片负责触发打开动作,页面负责保存选中项和面板状态。这样底部面板和右侧面板都能复用同一份状态,不会因为窗口宽度变化导致当前详情丢失。
总结
Pura X Max 展开态里,弹窗继续放在屏幕中间,很多时候会把原页面关系打断。外屏空间小,底部面板可以让用户先处理当前记录;展开态空间变宽以后,详情补充、筛选条件、备注编辑这类内容放到右侧,左侧列表还能留在原位,用户知道自己刚才点的是哪条记录,也能继续对照上下几条材料。
我后面处理这类弹层时,会先看它承担的任务:
- 如果只是查看详情、补充说明、备注编辑、筛选条件,右侧面板更适合展开态。
- 如果是删除、支付、授权这类强确认动作,居中弹窗仍然更合适,因为它需要让用户停下来确认。
- 如果是多步骤表单、长文编辑、图片裁剪这类复杂流程,应该进入完整页面,不适合塞进侧边面板。
- 如果只是外屏上的短任务,比如看一条记录、点一下保存、稍后处理,底部面板已经够用。
弹层的位置要看当前窗口能不能保留原页面参照。窗口窄的时候,先让用户集中处理当前记录;窗口宽的时候,就不要急着遮住列表,把补充内容放到右侧,让原页面和详情内容同时留在视野里。这样处理以后,弹层不再只是一个固定样式,而是会根据任务轻重和窗口宽度换一种呈现方式。
附:完整代码
interface MaterialItem { id: number; title: string; status: string; source: string; time: string; tag: string; summary: string; detail: string; suggestion: string; } @Entry @Component struct Index { // 页面真实宽度,由 onAreaChange 写入 @State private pageWidth: number = 0; // 演示宽度,只用于在同一个模拟器里观察外屏和展开态 @State private previewWidth: number = 0; // 当前选中项。底部面板和右侧面板都读取这个状态 @State private selectedId: number = 1; // 面板是否打开。窗口宽度变化时,面板位置会跟着切换 @State private showPanel: boolean = false; // 模拟保存次数,用来观察面板切换后操作状态是否保留 @State private saveCount: number = 0; private readonly expandedWidth: number = 820; private readonly materials: MaterialItem[] = [ { id: 1, title: '社区物业缴费提醒', status: '待处理', source: '拍照整理', time: '09:20', tag: '通知', summary: '识别到缴费截止日期、金额明细和办理地点。', detail: '这条记录来自一张社区物业缴费通知。内容包含缴费周期、应缴金额、截止日期和办理地点。小屏下适合通过底部面板快速查看,大屏下可以用右侧面板保留列表上下文。', suggestion: '保存为待办提醒,并在截止日期前一天提醒。' }, { id: 2, title: 'Pura X Max 适配会议纪要', status: '待确认', source: '语音转写', time: '10:45', tag: '会议', summary: '整理出弹窗、侧边面板、分屏窗口和横屏结构几类问题。', detail: '会议纪要类记录经常需要对照多个条目。展开态下右侧详情面板能减少页面跳转,列表仍然保留在原位置,切换记录也更方便。', suggestion: '确认适配任务,并同步到开发清单。' }, { id: 3, title: '客户需求变更记录', status: '待处理', source: '文本整理', time: '13:10', tag: '项目', summary: '本次变更涉及首页布局、权限配置和消息提醒。', detail: '需求变更类记录适合在右侧面板里查看补充信息。主页面保留列表,右侧承接详情、处理建议和操作按钮。', suggestion: '同步项目负责人,并拆分到研发排期。' }, { id: 4, title: '活动报名确认单', status: '已保存', source: '相册导入', time: '15:25', tag: '表单', summary: '提取到报名人、联系方式、活动时间和签到地址。', detail: '报名确认类材料通常只是补充查看,不一定需要进入完整详情页。小屏弹出底部面板,宽屏使用侧边面板即可。', suggestion: '保存记录,并在活动前一天提醒。' }, { id: 5, title: '门诊复查预约提示', status: '已整理', source: '拍照整理', time: '16:40', tag: '提醒', summary: '提取到复查时间、科室、楼层和注意事项。', detail: '提醒类信息适合轻量处理。侧边面板可以承接确认、保存、稍后处理等动作,避免把用户带到另一个页面。', suggestion: '加入日程提醒,并保留原始记录。' } ]; // Demo 中优先使用演示宽度,真实项目里可以直接返回 pageWidth private getEffectiveWidth(): number { if (this.previewWidth > 0) { return this.previewWidth; } return this.pageWidth; } private isExpanded(): boolean { return this.getEffectiveWidth() >= this.expandedWidth; } private getContentWidth(): Length { if (this.previewWidth > 0) { return this.previewWidth; } return '100%'; } private getPagePadding(): number { return this.isExpanded() ? 24 : 16; } private getTitleSize(): number { return this.isExpanded() ? 28 : 23; } private getModeText(): string { return this.isExpanded() ? 'expanded · 右侧面板' : 'compact · 底部面板'; } private getModeDesc(): string { if (this.isExpanded()) { return '宽窗口下详情进入右侧面板,左侧列表仍然保留。'; } return '窄窗口下详情从底部弹出,当前任务先聚焦处理。'; } private getSelectedItem(): MaterialItem { const found = this.materials.find((item: MaterialItem) => item.id === this.selectedId); return found ? found : this.materials[0]; } private setPreview(width: number) { this.previewWidth = width; this.showPanel = false; } private openPanel(itemId: number) { this.selectedId = itemId; this.showPanel = true; } private closePanel() { this.showPanel = false; } private save() { this.saveCount += 1; } private getStatusColor(status: string): string { if (status === '待处理') { return '#B25E00'; } if (status === '待确认') { return '#7C3AED'; } return '#276749'; } private getStatusBgColor(status: string): string { if (status === '待处理') { return '#FFF4E5'; } if (status === '待确认') { return '#F1EAFE'; } return '#E7F5EE'; } @Builder private PreviewButton(text: string, width: number) { Text(text) .fontSize(12) .fontColor(this.previewWidth === width ? '#FFFFFF' : '#2F8F83') .textAlign(TextAlign.Center) .padding({ left: 10, right: 10, top: 7, bottom: 7 }) .backgroundColor(this.previewWidth === width ? '#2F8F83' : '#E6F4F1') .borderRadius(999) .onClick(() => { this.setPreview(width); }) } @Builder private StatusPill(status: string) { Text(status) .fontSize(12) .fontColor(this.getStatusColor(status)) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .backgroundColor(this.getStatusBgColor(status)) .borderRadius(999) } @Builder private MetaPill(text: string) { Text(text) .fontSize(12) .fontColor('#4B5563') .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .backgroundColor('#F3F4F6') .borderRadius(999) } @Builder private HeaderPanel() { Column({ space: 10 }) { Row() { Column({ space: 4 }) { Text('大屏弹窗改成侧边面板') .fontSize(this.getTitleSize()) .fontWeight(FontWeight.Bold) .fontColor('#111827') Text(this.getModeText()) .fontSize(14) .fontColor('#2F8F83') } .layoutWeight(1) Text('窗口 ' + Math.round(this.pageWidth).toString() + 'vp') .fontSize(12) .fontColor('#374151') .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor('#FFFFFF') .borderRadius(999) } .width('100%') Text('演示宽度:' + Math.round(this.getEffectiveWidth()).toString() + 'vp。' + this.getModeDesc()) .fontSize(14) .fontColor('#6B7280') .lineHeight(21) Row({ space: 8 }) { this.PreviewButton('自动', 0) this.PreviewButton('外屏', 430) this.PreviewButton('展开态', 960) } .width('100%') } .width('100%') } @Builder private MaterialCard(item: MaterialItem) { Column({ space: 12 }) { Row({ space: 8 }) { this.StatusPill(item.status) this.MetaPill(item.tag) Blank() Text(item.time) .fontSize(12) .fontColor('#6B7280') } .width('100%') Text(item.title) .fontSize(17) .fontWeight(FontWeight.Medium) .fontColor('#111827') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(item.summary) .fontSize(13) .fontColor('#6B7280') .lineHeight(19) .maxLines(this.isExpanded() ? 2 : 1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 10 }) { Text(item.source) .fontSize(12) .fontColor('#4B5563') Blank() Button('查看详情') .fontSize(13) .fontColor('#FFFFFF') .height(34) .padding({ left: 12, right: 12 }) .backgroundColor('#2F8F83') .borderRadius(17) .onClick(() => { this.openPanel(item.id); }) } .width('100%') } .width('100%') .padding(16) .backgroundColor(this.selectedId === item.id && this.showPanel ? '#EEF7F5' : '#FFFFFF') .borderRadius(20) .border({ width: this.selectedId === item.id && this.showPanel ? 1.5 : 1, color: this.selectedId === item.id && this.showPanel ? '#2F8F83' : '#E5E7EB' }) .shadow({ radius: this.selectedId === item.id && this.showPanel ? 12 : 8, color: '#12000000', offsetX: 0, offsetY: 4 }) } @Builder private ListArea() { Scroll() { Column({ space: 12 }) { ForEach(this.materials, (item: MaterialItem) => { this.MaterialCard(item) }, (item: MaterialItem) => item.id.toString()) } .width('100%') .padding({ bottom: 24 }) } .layoutWeight(1) .width('100%') .edgeEffect(EdgeEffect.Spring) } @Builder private DetailContent(item: MaterialItem) { Column({ space: 16 }) { Row() { this.StatusPill(item.status) Blank() Text('关闭') .fontSize(13) .fontColor('#6B7280') .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor('#F3F4F6') .borderRadius(999) .onClick(() => { this.closePanel(); }) } .width('100%') Column({ space: 8 }) { Text(item.title) .fontSize(this.isExpanded() ? 24 : 21) .fontWeight(FontWeight.Bold) .fontColor('#111827') .lineHeight(this.isExpanded() ? 31 : 28) Text(item.summary) .fontSize(14) .fontColor('#4B5563') .lineHeight(22) } .width('100%') .alignItems(HorizontalAlign.Start) Row({ space: 8 }) { this.MetaPill(item.source) this.MetaPill(item.time) this.MetaPill(item.tag) } .width('100%') Column({ space: 8 }) { Text('详情补充') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor('#111827') Text(item.detail) .fontSize(14) .fontColor('#4B5563') .lineHeight(23) } .width('100%') .padding(14) .backgroundColor('#F9FAFB') .borderRadius(16) Column({ space: 8 }) { Text('处理建议') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor('#111827') Text(item.suggestion) .fontSize(14) .fontColor('#4B5563') .lineHeight(23) } .width('100%') .padding(14) .backgroundColor('#F3F8F7') .borderRadius(16) Text('已保存 ' + this.saveCount.toString() + ' 次') .fontSize(13) .fontColor('#6B7280') Button('保存处理结果') .fontSize(15) .fontColor('#FFFFFF') .height(44) .width('100%') .backgroundColor('#2F8F83') .borderRadius(22) .onClick(() => { this.save(); }) Button('稍后处理') .fontSize(15) .fontColor('#2F8F83') .height(42) .width('100%') .backgroundColor('#E6F4F1') .borderRadius(21) if (this.isExpanded()) { Text('展开态下,右侧面板不会遮住左侧列表,用户可以继续保留原页面参照。') .fontSize(13) .fontColor('#6B7280') .lineHeight(20) } } .width('100%') .height('100%') } @Builder private SidePanel() { Row() { Blank() Column() { this.DetailContent(this.getSelectedItem()) } .width(360) .height('100%') .padding(20) .backgroundColor('#FFFFFF') .borderRadius({ topLeft: 24, topRight: 0, bottomLeft: 24, bottomRight: 0 }) .shadow({ radius: 16, color: '#18000000', offsetX: -4, offsetY: 0 }) } .width('100%') .height('100%') } @Builder private BottomSheet() { Column() { Blank() Column() { this.DetailContent(this.getSelectedItem()) } .width('100%') .height(430) .padding(18) .backgroundColor('#FFFFFF') .borderRadius({ topLeft: 24, topRight: 24, bottomLeft: 0, bottomRight: 0 }) .shadow({ radius: 16, color: '#18000000', offsetX: 0, offsetY: -4 }) } .width('100%') .height('100%') } build() { Stack() { Column() { Column({ space: 16 }) { this.HeaderPanel() this.ListArea() } .width(this.getContentWidth()) .height('100%') .padding({ left: this.getPagePadding(), right: this.getPagePadding(), top: 18, bottom: 16 }) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) if (this.showPanel) { Column() .width('100%') .height('100%') .backgroundColor(this.isExpanded() ? '#00000000' : '#66000000') .onClick(() => { if (!this.isExpanded()) { this.closePanel(); } }) if (this.isExpanded()) { this.SidePanel() } else { this.BottomSheet() } } } .width('100%') .height('100%') .backgroundColor('#F6F7F9') .onAreaChange((_: Area, newValue: Area) => { const width = Number(newValue.width); if (!Number.isNaN(width) && width > 0) { this.pageWidth = width; } }) } }