diff --git a/src/components/overflowmenu/README.md b/src/components/overflowmenu/README.md index 0025813..e7aef75 100644 --- a/src/components/overflowmenu/README.md +++ b/src/components/overflowmenu/README.md @@ -3,28 +3,43 @@ ## 介绍 ### overflowmenu:溢出菜单。 + 现有overflowmenu包含一个绑定点击事件的star-button按钮,通过在响应函数closeoverflowmenu控制溢出菜单内容的显示与隐藏。 + + 后续改进:结合active-overlay组件,通过调用overlay-stack类控制溢出菜单的显示与隐藏 -## 新需求(主页面要求——罗 9.5) - -1. 外部颜色控制(思路:使用自定义 css 样式,如: --test-color:XXX p{color: --test-color},通过修改自定义 css 样式的值达到从外部修改组件颜色) -2. 弹出菜单时的越界判断,包括主、副屏切换时的图标定位以及旋转屏幕时的定位 - 思路: - (1)首先获取 button 在屏幕显示的 left、right、top 和 bottom 值以及 menu 的 width 和 height - (2)对于右侧边界:right >= width ? true 则 menu 的 left = button 的 left : false 则 menu 的 right = button 的 right - (3)对于下边界:bottom >= height ? true 则 menu 的 top = button 的 bottom : false 则 menu 的 bottom = button 的 top -3. 外部控制接口,事件还是属性(暂定) -4. 弹出的菜单绑定在父节点上以供调用,减少重复使用(思路:后续通过 overlay 组件实现) - -## 问题(9.6) - -1. 首次点击最右侧的按钮是获取到的菜单宽度和高度与实际不符:(该问题已消失,但不知道为何消失) -2. 点击空白处无法关闭菜单栏(解决方法:将点击事件绑定在父容器中) - -## 新要求:(9.7) - -(1)将不需要修改的“var”变量声明变成“const”(已修改) -(2)变量命名要直观且有解释(已修改变量命名规范并添加对应注释) -(3)点击一个按钮后其余按钮应关闭(方法同(1)) -(4)可以将 slot 增加名称从而将 div 以删除 -(5)定位方式修改为相对定位,将将 fixed 改为 relative,达到适应效果 -(6)控制菜单栏宽度,菜单栏中 star-ul 中的 ul 标签负责扩充大小,修改其 width 值 +```typescript +//现有方案 +closeoverflowmenu(e: any) { + // 获取点击事件所在的标签名字 + const tagName = e.target.tagName.toLowerCase() + // 判断是否点击的star-overflowmenu标签 + if (tagName == 'star-overflowmenu') { + this.state++ + } + // 如果点在空白处则关闭菜单 + if (tagName != 'star-overflowmenu') { + // 获取所有的star-overflowmenu + var menulist = this.shadowRoot!.querySelectorAll('star-overflowmenu') + for (var i = 0; i < menulist.length; i++) { + menulist[i].open = true + var menu = menulist[i].renderRoot.querySelector( + '#menuitem' + ) as HTMLElement + menu.style.display = 'none' + this.state = 0 + } + } + // 通过state判断是否已有展开的菜单,若已有则关闭菜单 + if (this.state > 1) { + var menulist = this.shadowRoot!.querySelectorAll('star-overflowmenu') + for (var i = 0; i < menulist.length; i++) { + menulist[i].open = true + var menu = menulist[i].renderRoot.querySelector( + '#menuitem' + ) as HTMLElement + menu.style.display = 'none' + this.state = 0 + } + } + } +``` \ No newline at end of file diff --git a/src/components/overlay/README.md b/src/components/overlay/README.md index 682a37f..004a984 100644 --- a/src/components/overlay/README.md +++ b/src/components/overlay/README.md @@ -1,5 +1,76 @@ -# 全局 overlay +# Adobe SP组件库——overlay -## 类型包括: +介绍:Adobe的overlay组件是一个可以用于全局调用的叠加层。 -- 置于底部的 overlay,内可填充 ul +## 交互类型 + +- type TriggerInteractions = + | 'click' + | 'custom', + | 'hover' + | 'modal' + +overlay可有多种触发类型,各类型对应情况如下 + +>click将打开一个overlay,该overlay将在下一次单击时立即关闭,该overlay不在overlay内的元素上。 + +>custom允许从外部对显示流程进行定制。 + +>hover一旦指针离开overlay层所连接的触发器,就会关闭overlay层。 + +>modal模态形式,将仅在其内容中捕获标签顺序。 + +## overlay中主要用到的方法 +```typescript +Overlay.open( + (owner: HTMLElement), + (interaction: TriggerInteractions), + (overlayElement: HTMLElement), + (options: OverlayOptions) // +); +``` +Overlay.open() 是一个异步方法,它返回一个用于关闭overlay的函数。 +>owner:HTMLElement 代表要打开叠加层的元素 + +>interaction:TriggerInteractions overlay的触发交互类型 + +>overlayElement:HTMLElement 将要放入overlay中的element元素 + +>options:OverlayOptions overlay定制选项 + +## 作用 +- 置于底部的overlay,内可填充ul,其他组件可通过调用overlay显示信息 +# 组件交互 +- 可与button、menu、popover、picker等联合使用 + +# overlay场景、应有接口 +- 弹出框、溢出菜单、picker、警告框、主页面应用图标长按显示信息框等 +- 接口:position显示位置(top、right、left、bottom)、interaction type交互方式(click、custom、hover、modal、replace)、overlayoptions显示配置(delay:延迟出现overlay;placement:自定义位置;offset:偏移量) + +# overlay包含内容 +> overlay.ts工厂类包含异步函数open(),其通过调用open()和close()函数实现overlay的打开和关闭 +>> open(OverlayOptions),其中OverlayOptions为overlay的一些配置参数,open应有方法两个主要方法:updatePosition()和reparentChildren() + >>> updatePosition()方法实现open过程的overlay显示位置定位 + >>> reparentChildren()方法实现组件中content和overlay显示位置的转换 + +>> close(),关闭overlay,主要分为两步:关闭overlay,将overlay的content替换回原位置 +>>> 移除overlay,removeoverlay节点 +>>> reparentChildren,替换元素 + +# overlay使用 +- 首先获取响应点击事件的节点元素: +```typescript +const targetNode = e.target as HTMLElement +``` +- 获取被点击节点父节点: +```typescript +const originalNode = targetNode?.parentElement as HTMLElement +``` +- 获取要显示在overlay中的内容: +```typescript +const content = targetNode?.nextElementSibling as HTMLElement +``` +- 构造响应函数时通过调用overlayStack中的openOverlay方法创建activeoverlauy: +```typescript +this.overlayStack.openOverlay(originalNode, targetNode, content) +``` \ No newline at end of file diff --git a/src/components/overlay/active-overlay.ts b/src/components/overlay/active-overlay.ts new file mode 100644 index 0000000..3bdcd0b --- /dev/null +++ b/src/components/overlay/active-overlay.ts @@ -0,0 +1,119 @@ +import {LitElement, html, TemplateResult} from 'lit' +import {customElement, property} from 'lit/decorators.js' +import {OverlayOpenDetail, TriggerInteractions} from './overlay-types' + +@customElement('star-activeoverlay') +export class ActiveOverlay extends LitElement { + + // 根节点(占位符所表示的元素的父节点) + public root?: HTMLElement + // 被点击的节点 + public targetNode?: HTMLElement + // 被替换后用以保存要复原的节点集合 + public restoreContent?: HTMLElement + // 显示位置策略 + @property({reflect: true}) + public placement?: String + + public constructor() { + super() + } + + // 调用初始化函数 + private open(openDetail: OverlayOpenDetail): void { + this.extractDetail(openDetail) + } + // 初始化函数 + private extractDetail(detail: OverlayOpenDetail): void { + this.placement = detail.placement + this.offset = detail.offset + this.skidding = detail.skidding || 0 + this.root = detail.root + } + + private stealOverlayContent(element: HTMLElement) {} + // 位置更新函数 + public updatePosition = () => { + // 设置最大显示宽度和高度 + const availableWidth = 200 + const availableHeight = 200 + // 显示内容content宽度和高度——用于越界后的定位计算(右下、右、下) + const contentwidth = this.restoreContent?.offsetWidth as number + const contentheight = this.restoreContent?.offsetHeight as number + // 获取网页宽度——用于越界判断 + const bodywidth = document.documentElement.clientWidth + const bodyheight = document.documentElement.clientHeight + // 被点击的节点 + const targetnode = this.targetNode as HTMLElement + // 获取被点击节点位置——用于越界判断和定位 + const targetNodePosition = targetnode.getBoundingClientRect() + const targettop = Number(targetNodePosition?.top) + const targetbottom = Number(targetNodePosition?.bottom) + const targetleft = Number(targetNodePosition?.left) + const targetright = Number(targetNodePosition?.right) + // 设置样式 + this.style.position = 'relative' + this.style.zIndex = '1' + this.style.maxWidth = availableWidth + 'px' + this.style.maxHeight = availableHeight + 'px' + // 边界判断 + const rightline = targetright + contentwidth > bodywidth ? true : false // 右侧越界条件 + const bottomline = + targetbottom + availableHeight > bodyheight ? true : false //下方越界条件 + // 右下角边界 + if (rightline && bottomline) { + this.style.left = targetleft - (contentwidth - targetnode.offsetWidth) + 'px' + this.style.top = targettop - targetnode.offsetHeight + 'px' + return + } else if (rightline) { + // 右侧边界 + this.style.left = targetleft - (contentwidth - targetnode.offsetWidth) + 'px' + this.style.top = targettop + targetnode.offsetHeight + 'px' + return + } else if (bottomline) { + // 下侧边界 + this.style.left = targetleft + 'px' + this.style.top = targettop - contentheight + 'px' + return + } else { + // 正常情况 + this.style.left = targetleft + 'px' + this.style.top = targettop + targetnode.offsetHeight + 'px' + return + } + } + + private onSlotChange(): void { + this.updatePosition() + } + + public render(): TemplateResult { + const content = html` +
+ +
+ ` + return content + } + + // 创建overlay + public static create( + root?: HTMLElement, + targetnode?: HTMLElement, + content?: HTMLElement + ): ActiveOverlay { + const activeoverlay = new ActiveOverlay() + // 初始化参数 + activeoverlay.root = root + activeoverlay.targetNode = targetnode + activeoverlay.restoreContent = content + + return activeoverlay + } +} + +declare global { + interface HTMLElementTagNameMap { + 'star-activeoverlay': ActiveOverlay + } +} diff --git a/src/components/overlay/overlay-stack.ts b/src/components/overlay/overlay-stack.ts new file mode 100644 index 0000000..3b2e954 --- /dev/null +++ b/src/components/overlay/overlay-stack.ts @@ -0,0 +1,44 @@ +import {ActiveOverlay} from './active-overlay' + +export class OverlayStack { + public overlays: ActiveOverlay[] = [] + public isOpen: Boolean = false + + public openOverlay(root?: HTMLElement, targetnode?: HTMLElement, content?: HTMLElement) { + // 创建activeoverlay对象 + const activeOverlay = ActiveOverlay.create(root, targetnode, content) + // 开启状态 + this.isOpen = true + // 为overlay添加显示内容 + activeOverlay.appendChild(content as HTMLElement) + // 创建注释节点模板——用于替换要展示在overlay中的元素 + const placeholderTemplate: Comment = document.createComment( + 'placeholder for reparented element' + ) + // 占位 + activeOverlay.root?.appendChild(placeholderTemplate) + // 将activeoverlay添加到body底部 + document.body.append(activeOverlay) + // 将activeoverlay添加到已打开overlay数组中 + this.overlays.push(activeOverlay) + } + + /** + * closeOverlay + */ + public closeOverlay(): void { + // 提取要关闭的overlay + const closeactiveoverlay = this.overlays.pop() as unknown as ActiveOverlay + // 获取根节点 + const srcroot = closeactiveoverlay.root + // 获取注释节点 + const placeholder = srcroot?.lastChild as Node + // 获取overlay中的内容 + const content = closeactiveoverlay.restoreContent as HTMLElement + // 替换子节点 + srcroot?.replaceChild(content, placeholder) + // 从body中移除activeoverlay节点 + document.body.removeChild(closeactiveoverlay) + this.isOpen = false + } +} diff --git a/src/components/overlay/overlay-types.ts b/src/components/overlay/overlay-types.ts new file mode 100644 index 0000000..4b95485 --- /dev/null +++ b/src/components/overlay/overlay-types.ts @@ -0,0 +1,58 @@ +// 定义overlay各种参数类型、接口 + +// click用于普通弹窗;longpress可用于主界面图标长按显示详情页;hover可用于解释框的显示; +// custom、inline、replace和modal尚未理解用途,暂定 +export type TriggerInteractions = + | 'click' + | 'longpress' + | 'hover' + | 'custom' + | 'replace' + | 'inline' + | 'modal' + +export type thememode ='light' | 'dark' + +// overlay配置信息 +export type OverlayOptions = { + root?: HTMLElement + mode?: String + // delayed?: boolean + // placement:adobe通过轻量前段工具包floating-ui中的FloatingUIPlacement进行动态定位浮动元素 + // 出现位置 + placement?: String + offset?: number + // receivesFocus?: 'auto' + // notImmediatelyClosable?: boolean + // abortPromise?: Promise + // 为没有元素触发的覆盖提供一个虚拟触发 + // virtualTrigger?: VirtualTrigger +} + +// export interface OverlayOpenDetail { +// content: HTMLElement; +// contentTip?: HTMLElement; +// delayed: boolean; +// offset: number; +// skidding?: number; +// placement?: Placement; +// receivesFocus?: 'auto'; +// virtualTrigger?: VirtualTrigger; +// trigger: HTMLElement; +// root?: HTMLElement; +// interaction: TriggerInteractions; +// theme: ThemeData; +// notImmediatelyClosable?: boolean; +// abortPromise?: Promise; +// } + +export interface OverlayOpenDetail { + content: HTMLElement // 显示内容 + offset: number // 偏移 + skidding?: number // 滑动 + placement?: String //显示位置 + // virtualTrigger?: VirtualTrigger // 虚拟触发 + trigger: HTMLElement // 触发 + root?: HTMLElement // 根节点 + interaction: TriggerInteractions // 交互 +} diff --git a/src/components/overlay/overlay.ts b/src/components/overlay/overlay.ts new file mode 100644 index 0000000..180f6f7 --- /dev/null +++ b/src/components/overlay/overlay.ts @@ -0,0 +1,83 @@ +import { OverlayStack } from './overlay-stack' +import { OverlayOptions, TriggerInteractions } from './overlay-types' + +export interface OverlayDisplayQueryDetail { + overlayRootName?: string + overlayRootElement?: HTMLElement + overlayContentTipElement?: HTMLElement +} + +export class Overlay { + // 基础属性 + private isOpen = false + private overlayElement: HTMLElement + private owner: HTMLElement + private interaction: TriggerInteractions + + private static overlayStack = new OverlayStack() + + /** + * 初始化构造参数 + * @param owner 用于定位overlay显示位置的父元素节点 + * @param interaction 引起overlay显示的交互类型 + * @param overlayElement 用于显示overlay的节点 + */ + constructor( + owner: HTMLElement, + interaction: TriggerInteractions, + overlayElement: HTMLElement + ) { + this.owner = owner + this.overlayElement = overlayElement + this.interaction = interaction + } + + public static async open( + owner: HTMLElement, + interaction: TriggerInteractions, + overlayElement: HTMLElement, + options: OverlayOptions + ): Promise<() => void> { + const overlay = new Overlay(owner, interaction, overlayElement) + await overlay.open(options) + return (): void => { + overlay.close() + } + } + + public static update(): void { + // 自定义事件 + const overlayUpdateEvent = new CustomEvent('star-update-overlays', { + bubbles: true, + composed: true, + cancelable: true, + }) + // 触发事件 + document.dispatchEvent(overlayUpdateEvent) + } + + public async open({ + offset = 0, + placement = 'top', + root, + }: OverlayOptions): Promise { + /* c8 ignore next */ + if (this.isOpen) return true + + await Overlay.overlayStack.openOverlay({ + content: this.overlayElement, + offset: offset, + placement: placement, + trigger: this.owner, + interaction: this.interaction, + root, + }) + this.isOpen = true + return true + } + + // 关闭overlay + public close(): void { + Overlay.overlayStack.closeOverlay(this.overlayElement) + } +} diff --git a/src/index.ts b/src/index.ts index 5c8a6c9..c250661 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,9 +16,9 @@ import './components/picker/picker' import './components/overflowmenu/overflowmenu' import './components/slider/slider' import './components/prompt/prompt' -import './components/prompt/prompt' import './components/digicipher/digicipher' import './components/pattern-view/pattern-view' +import './components/overlay/active-overlay' @customElement('settings-app') export class SettingsApp extends LitElement { diff --git a/src/test/panels/activeoverlay/activeoverlay.ts b/src/test/panels/activeoverlay/activeoverlay.ts new file mode 100644 index 0000000..730f28c --- /dev/null +++ b/src/test/panels/activeoverlay/activeoverlay.ts @@ -0,0 +1,78 @@ +import {html, LitElement, css} from 'lit' +import {customElement, property} from 'lit/decorators.js' +import '../../../components/button/button' +import '../../../components/overlay/active-overlay' +import {OverlayStack} from '../../../components/overlay/overlay-stack' + +@customElement('panel-activeoverlay') +export class PanelActiveOverlay extends LitElement { + constructor() { + super() + } + + public overlayStack = new OverlayStack() + + public test(e: Event) { + if (this.overlayStack.isOpen == false) { + // 获取被点击节点 + const targetNode = e.target as HTMLElement + // 获取被点击节点父节点 + const originalNode = targetNode?.parentElement as HTMLElement + // 获取要显示在overlay中的内容 + const content = targetNode?.nextElementSibling as HTMLElement + if(!content) return + // 调用overlayStack中的openOverlay方法创建节点 + this.overlayStack.openOverlay(originalNode, targetNode, content) + } else { + this.overlayStack.closeOverlay() + } + } + + render() { + return html` +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ ` + } + + static styles = css` + :host { + display: block; + width: 100vw; + height: 100vh; + } + div { + display: block; + width: 100vw; + height: 100vh; + } + ` +} + +declare global { + interface HTMLElementTagNameMap { + 'panel-activeoverlay': PanelActiveOverlay + } +} diff --git a/src/test/panels/root.ts b/src/test/panels/root.ts index 7366785..a892bc3 100644 --- a/src/test/panels/root.ts +++ b/src/test/panels/root.ts @@ -25,7 +25,8 @@ import './pattern-view/pattern-view' import './container/homescreen-container' import './toast/toast' import './picker/picker' -import './prompt/prompt' +import './switch/switch' +import './activeoverlay/activeoverlay' type SEID = string @customElement('panel-root') @@ -186,6 +187,14 @@ export class PanelRoot extends LitElement { href="#overflowmenu" >
+ +