This is an automated email from the ASF dual-hosted git repository. greg-dove pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/royale-asjs.git
commit d224f96cfb4cec35680a7cc42338a1b3681c002c Author: greg-dove <[email protected]> AuthorDate: Fri Apr 24 17:26:01 2026 +1200 Tooltip updates with 2 TooltipBeads. A little trickier than I expected... required a bunch of skin updates, but I believe that is working now. --- .../Style/src/main/resources/style-manifest.xml | 4 + .../main/royale/org/apache/royale/style/Tooltip.as | 42 ++- .../royale/style/beads/AdaptiveTooltipBead.as | 62 ++++ .../org/apache/royale/style/beads/TooltipBead.as | 333 +++++++++++++++++++++ .../org/apache/royale/style/skins/ITooltipSkin.as | 3 + .../org/apache/royale/style/skins/TooltipSkin.as | 143 ++++++--- 6 files changed, 533 insertions(+), 54 deletions(-) diff --git a/frameworks/projects/Style/src/main/resources/style-manifest.xml b/frameworks/projects/Style/src/main/resources/style-manifest.xml index 1ce74e9a98..f78e3be009 100644 --- a/frameworks/projects/Style/src/main/resources/style-manifest.xml +++ b/frameworks/projects/Style/src/main/resources/style-manifest.xml @@ -336,5 +336,9 @@ <component id="ToastSkin" class="org.apache.royale.style.skins.ToastSkin"/> <component id="TooltipSkin" class="org.apache.royale.style.skins.TooltipSkin"/> <component id="ITooltipSkin" class="org.apache.royale.style.skins.ITooltipSkin"/> + + + <component id="TooltipBead" class="org.apache.royale.style.beads.TooltipBead"/> + <component id="AdaptiveTooltipBead" class="org.apache.royale.style.beads.AdaptiveTooltipBead"/> </componentPackage> diff --git a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/Tooltip.as b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/Tooltip.as index ca3c5d4ac3..8a7db62e61 100644 --- a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/Tooltip.as +++ b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/Tooltip.as @@ -23,6 +23,7 @@ package org.apache.royale.style import org.apache.royale.style.elements.Div; import org.apache.royale.style.elements.Span; import org.apache.royale.style.skins.ITooltipSkin; + import org.apache.royale.geom.Rectangle; COMPILE::JS { import org.apache.royale.core.WrappedHTMLElement; @@ -31,9 +32,22 @@ package org.apache.royale.style public class Tooltip extends StyleUIBase implements IHasLabel { - public function Tooltip() + public function Tooltip(forDisplayInPopup:Boolean = false) { super(); + useWrapperStyle = true; + _forDisplayInPopup = forDisplayInPopup; + //set the default direction + toggleAttribute("data-direction-top", true); + } + + override public function getWrapperStyle():String{ + return 'tool-tip'; + } + + private var _forDisplayInPopup:Boolean; + public function get forDisplayInPopup():Boolean{ + return _forDisplayInPopup } private var _contentNode:Div; @@ -171,16 +185,16 @@ package org.apache.royale.style public function set direction(value:String):void { if (value == _direction) return; - /* if(_direction){ + if(_direction){ toggleAttribute("data-direction-" + _direction, false); - }*/ + } if(value){ switch(value){ case "left": case "right": case "bottom": case "top": - // toggleAttribute("data-direction-" + value, true); + toggleAttribute("data-direction-" + value, true); break; default: throw new Error("Invalid direction: " + value); @@ -222,7 +236,7 @@ package org.apache.royale.style } } - private var _isOpen:Boolean; + private var _isOpen:Boolean = false; public function get isOpen():Boolean { @@ -231,7 +245,7 @@ package org.apache.royale.style public function set isOpen(value:Boolean):void { - if(value != !!_isOpen){ + if(value != _isOpen){ toggleAttribute("is-open", value); } _isOpen = value; @@ -261,5 +275,21 @@ package org.apache.royale.style updateFlavorIcon(); positionTip(); } + + public function getFullHeight():Number{ + var h:Number = height; + if (skin) { + h += (skin as ITooltipSkin).getExtraHeight(); + } + return h; + } + + public function getFullHWidth():Number{ + var w:Number = width; + if (skin) { + w += (skin as ITooltipSkin).getExtraWidth(); + } + return w; + } } } diff --git a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/beads/AdaptiveTooltipBead.as b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/beads/AdaptiveTooltipBead.as new file mode 100644 index 0000000000..17e706de3f --- /dev/null +++ b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/beads/AdaptiveTooltipBead.as @@ -0,0 +1,62 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// +package org.apache.royale.style.beads +{ + import org.apache.royale.core.IUIBase; + import org.apache.royale.style.Tooltip; + import org.apache.royale.core.IParentIUIBase; + import org.apache.royale.geom.Point; + /** + * Provides core functionality for managing display of Tooltips. + * AdaptiveTooltipBead supports avoidance of browser edges when the tooltip is displayed + * + * This file includes derived work from Spectrum Royale, + * also licensed under the Apache License, Version 2.0. + * + */ + public class AdaptiveTooltipBead extends TooltipBead + { + override protected function determinePosition(comp:IUIBase, tooltip:Tooltip):Point + { + var margin:int = 0; + var pt:Point = super.determinePosition(comp, tooltip); + var screenWidth:Number = (host.popUpParent as IParentIUIBase).width; + var screenHeight:Number = (host.popUpParent as IParentIUIBase).height; + if (direction == TooltipBead.LEFT && pt.x < margin) + { + direction = TooltipBead.RIGHT; + } else if (direction == TooltipBead.TOP && pt.y < margin) + { + direction = TooltipBead.BOTTOM; + } else if (direction == TooltipBead.RIGHT && (pt.x + tooltip.getFullHWidth() + margin) > screenWidth) + { + direction = TooltipBead.LEFT; + } else if (direction == TooltipBead.BOTTOM && (pt.y + tooltip.getFullHeight() + margin) > screenHeight) + { + direction = TooltipBead.TOP; + } else + { + return pt; + } + return super.determinePosition(comp, tooltip); + } + + } +} + diff --git a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/beads/TooltipBead.as b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/beads/TooltipBead.as new file mode 100644 index 0000000000..7774f0342b --- /dev/null +++ b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/beads/TooltipBead.as @@ -0,0 +1,333 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// +package org.apache.royale.style.beads +{ + import org.apache.royale.core.IBead; + import org.apache.royale.core.IPopUpHost; + import org.apache.royale.core.IStrand; + import org.apache.royale.core.IUIBase; + import org.apache.royale.events.IEventDispatcher; + import org.apache.royale.events.MouseEvent; + import org.apache.royale.geom.Point; + import org.apache.royale.utils.PointUtils; + import org.apache.royale.utils.UIUtils; + import org.apache.royale.style.Tooltip; + + COMPILE::SWF{ + //for compilation support only + import flash.utils.setTimeout; + import flash.utils.clearTimeout; + } + + /** + * Provides core functionality for managing display of Tooltips. + * + * This file includes derived work from Spectrum Royale, + * also licensed under the Apache License, Version 2.0. + * + */ + public class TooltipBead implements IBead + { + public function TooltipBead() + { + } + + + public static const LEFT:String = "left"; + public static const RIGHT:String = "right"; + public static const BOTTOM:String = "bottom"; + public static const TOP:String = "top"; + + private static var activeBead:TooltipBead; + + public static function closeTips():void{ + if(activeBead){ + activeBead.closeTooltip(); + } + } + + protected var tt:Tooltip; + protected var host:IPopUpHost; + + private var _autoClose:Number = 2000; + + /** + * Number of milliseconds to auto-close the tooltip + * 0 means it stays open + * Default is 2000 + */ + public function get autoClose():Number + { + return _autoClose; + } + + public function set autoClose(value:Number):void + { + _autoClose = value; + } + + private var _delay:Number = 0; + /** + * Delay (in ms) until tooltip shows + */ + public function get delay():Number{ + return _delay; + } + + public function set delay(value:Number):void{ + _delay = value; + } + + private var _stayOpen:Number = 0; + /** + * How long (in ms) for the tooltip to stay open after mouse leaves. + */ + public function get stayOpen():Number + { + return _stayOpen; + } + + public function set stayOpen(value:Number):void + { + _stayOpen = value; + } + + private var _coolDown:Number = 0; + /** + * ms since last closed tooltip to ignore delay value + * See https://spectrum.adobe.com/page/tooltip/#Warmup-and-cooldown + */ + public function get coolDown():Number{ + return _coolDown; + } + + public function set coolDown(value:Number):void{ + _coolDown = value; + } + + private var _toolTip:String; + public function get toolTip():String + { + return _toolTip; + } + public function set toolTip(value:String):void + { + _toolTip = value; + if (tt) { + if (value) { + tt.text = value; + } else { + closeTooltip(); + } + } + } + + private var _direction:String = TOP; + [Inspectable(category="General", enumeration="left,right,bottom,top", defaultValue="top")] + public function set direction(value:String):void + { + _direction = value; + if (tt) + { + tt.direction = value; + } + } + + public function get direction():String + { + return _direction; + } + + private var _tipPosition:String; + + /** + * The position of the tip within the tooltip + */ + public function get tipPosition():String{ + return _tipPosition; + } + + [Inspectable(category="General", enumeration="start,end,center",defaultValue="center")] + public function set tipPosition(value:String):void{ + _tipPosition = value; + if (tt) + { + tt.tipPosition = value; + } + } + + private var _flavor:String; + + /** + * The flavor of the Tooltip + * One of info, positive and negative, success and error. + * To set the Tooltip to the default, specify an empty string + */ + public function get flavor():String + { + return _flavor; + } + + [Inspectable(category="General", enumeration="info,positive,negative,success,error")] + public function set flavor(value:String):void + { + _flavor = value; + if(tt){ + tt.flavor = value; + } + } + + protected var _strand:IStrand; + + public function set strand(value:IStrand):void + { + _strand = value; + + (_strand as IEventDispatcher).addEventListener("mouseenter", rollOverHandler, false); + } + + protected function rollOverHandler(event:MouseEvent):void + { + if (!toolTip || tt){ + return; + } + (_strand as IEventDispatcher).addEventListener("mouseleave", rollOutHandler, false); + + // Already open. Just make sure it stays open and closes when it should. + if(activeBead == this && tt && tt.isOpen){ + clearTimeouts(); + if(_autoClose > 0){ + closeTimeoutId = setTimeout(closeTooltip,_autoClose); + } + } + else if(delay == 0 || new Date().getTime() - lastShownTS < coolDown){ + showTooltip(); + } else { + showTimeoutId = setTimeout(showTooltip,delay); + } + + } + private var showTimeoutId:Number = 0; + private var closeTimeoutId:Number = 0; + private var stayOpenTimeoutId:Number = 0; + private function clearTimeouts():void{ + if(showTimeoutId > 0){ + clearTimeout(showTimeoutId); + showTimeoutId = 0; + } + if(closeTimeoutId > 0){ + clearTimeout(closeTimeoutId); + closeTimeoutId = 0; + } + if(stayOpenTimeoutId > 0){ + clearTimeout(stayOpenTimeoutId); + stayOpenTimeoutId = 0; + } + + } + + protected function createTooltip():void{ + if(activeBead && activeBead != this){ + activeBead.closeTooltip(); + } + activeBead = this; + + + var comp:IUIBase = _strand as IUIBase + host = UIUtils.findPopUpHost(comp); + removeTooltip(); + tt = new Tooltip(true); + if(_flavor){ + tt.flavor = _flavor; + } + tt.direction = _direction; + if(tipPosition){ + tt.tipPosition = tipPosition; + } + + tt.text = toolTip; + if(_autoClose > 0){ + closeTimeoutId = setTimeout(closeTooltip,_autoClose); + } + } + + protected function showTooltip():void { + createTooltip(); + host.popUpParent.addElement(tt, false); // don't trigger a layout + var ttWidth:Number = tt.width; + var pt:Point = determinePosition(_strand as IUIBase, tt); + tt.x = pt.x; + tt.y = pt.y; + tt.isOpen = true; + if(ttWidth != tt.width){ + pt = determinePosition(_strand as IUIBase, tt); + tt.x = pt.x; + tt.y = pt.y; + } + } + + protected function determinePosition(comp:IUIBase, tooltip:Tooltip):Point + { + var pt:Point = new Point(); + if (_direction == LEFT) { + pt.x = -tooltip.width; + pt.y = (comp.height - tooltip.height) / 2; + } else if (_direction == TOP) { + pt.x = (comp.width - tooltip.width) / 2; + pt.y = -tooltip.height; + } else if (_direction == RIGHT) { + pt.x = comp.width; + pt.y = (comp.height - tooltip.height) / 2; + } else + { + pt.x = (comp.width - tooltip.width) / 2; + pt.y = comp.height; + } + + pt = PointUtils.localToGlobal(pt, comp); + return pt; + } + + protected function rollOutHandler(event:MouseEvent):void{ + (_strand as IEventDispatcher).removeEventListener("mouseleave", rollOutHandler, false); + clearTimeouts(); + if(stayOpen > 0){ + stayOpenTimeoutId = setTimeout(closeTooltip,stayOpen); + } else { + closeTooltip(); + } + } + protected function closeTooltip():void{ + clearTimeouts(); + activeBead = null; + removeTooltip(); + closeTimeoutId = 0; + showTimeoutId = 0; + lastShownTS = new Date().getTime(); + } + protected function removeTooltip():void{ + if(tt && tt.parent){ + tt.parent.removeElement(tt); + } + tt = null; + } + public static var lastShownTS:Number = 0; + } +} + diff --git a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/skins/ITooltipSkin.as b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/skins/ITooltipSkin.as index a540ba278d..3ad6f55155 100644 --- a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/skins/ITooltipSkin.as +++ b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/skins/ITooltipSkin.as @@ -26,5 +26,8 @@ package org.apache.royale.style.skins function get tooltipContentStyles():Array; function get tipStyles():Array; function getIcon(flavor:String):IStyleUIBase; + + function getExtraHeight():Number; + function getExtraWidth():Number; } } diff --git a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/skins/TooltipSkin.as b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/skins/TooltipSkin.as index 705ed47666..d481c4dbf0 100644 --- a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/skins/TooltipSkin.as +++ b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/skins/TooltipSkin.as @@ -33,6 +33,7 @@ package org.apache.royale.style.skins import org.apache.royale.style.stylebeads.flexgrid.AlignItems; import org.apache.royale.style.stylebeads.flexgrid.Flex; import org.apache.royale.style.stylebeads.flexgrid.Gap; + import org.apache.royale.style.stylebeads.interact.PointerEvents; import org.apache.royale.style.stylebeads.interact.UserSelect; import org.apache.royale.style.stylebeads.layout.Bottom; import org.apache.royale.style.stylebeads.layout.Display; @@ -46,7 +47,9 @@ package org.apache.royale.style.skins import org.apache.royale.style.stylebeads.sizing.WidthStyle; import org.apache.royale.style.stylebeads.spacing.Margin; import org.apache.royale.style.stylebeads.spacing.Padding; + import org.apache.royale.style.stylebeads.states.GroupPseudo; import org.apache.royale.style.stylebeads.states.attribute.AttributeState; + import org.apache.royale.style.stylebeads.states.attribute.DataState; import org.apache.royale.style.stylebeads.transform.Transform; import org.apache.royale.style.stylebeads.typography.FontSize; import org.apache.royale.style.stylebeads.typography.FontWeight; @@ -91,27 +94,46 @@ package org.apache.royale.style.skins private function applyStyles():void { - //transform 130ms ease-in-out, opacity 130ms ease-in-out, visibility 0ms linear 130ms; - + var transition:Transition = new Transition() - transition.property = 'transform, opacity, visibility'; + transition.property = 'transform,opacity'; + transition.duration = '130ms'; + transition.timingFunction ='ease-in-out'; + const inPopup:Boolean = host.forDisplayInPopup; - transition.timingFunction ='ease-in-out,ease-in-out,linear' + var openStates:Array = [ + new Visibility('visible'), + new OpacityStyle(100) + ] + if (inPopup) { + var tipMargin:String = computeSize(2 * border_radius * getMultiplier(),host.unit); + var transformTop:Transform = new Transform(); + transformTop.translateY = '-' + tipMargin; + var transformBottom:Transform = new Transform(); + transformBottom.translateY = tipMargin; + var transformLeft:Transform = new Transform(); + transformLeft.translateX = '-' + tipMargin; + var transformRight:Transform = new Transform(); + transformRight.translateX = tipMargin; + openStates.push(new DataState('direction-top',[transformTop])); + openStates.push(new DataState('direction-left',[transformLeft])); + openStates.push(new DataState('direction-right',[transformRight])); + openStates.push(new DataState('direction-bottom',[transformBottom])); + } - //var multiplier:Number = getMultiplier(); + var positionStyle:String = inPopup ? 'absolute' : 'relative'; var styles:Array = [ new Display('inline-flex'), new AlignItems('center'), - new Position('relative'), + new Position(positionStyle), new UserSelect('none'), + new PointerEvents('none'), transition, new Visibility('hidden'), new OpacityStyle(0), - new AttributeState('is-open',[ - new Visibility('visible'), - new OpacityStyle(100) - ]) + new AttributeState('is-open',openStates) ]; + host.setStyles(styles, true); } @@ -189,13 +211,7 @@ package org.apache.royale.style.skins border.topColor = backgroundColor.colorSpecifier; //location related: - var locationStyles:Array = [] - var tipDirection:String = host.direction; var tipPosition:String = host.tipPosition; - var margin:Margin = new Margin(null,unit); - var transform:Transform = new Transform(); - locationStyles.push(margin); - var sideLocation:String; if (tipPosition == 'center') { sideLocation = '50%'; @@ -203,38 +219,50 @@ package org.apache.royale.style.skins var cornerExclusionZone:String = computeSize(border_radius * 1.5 * multiplier,unit); sideLocation = 'calc(' + (tipPosition == 'start' ? '0% + ' : '100% - ') + cornerExclusionZone + ')'; } - - switch(tipDirection) { - case 'bottom': - margin.left = '-'+tipWidth; - locationStyles.push(new Bottom('100%')); - transform.rotate = '180deg'; - locationStyles.push(transform); - //side location - locationStyles.push(new Left(sideLocation)); - break; - case 'left': - margin.top = '-'+tipWidth; - locationStyles.push(new Right('100%')); - transform.rotate = '90deg'; - locationStyles.push(transform); - //side location - locationStyles.push(new Top(sideLocation)); - break; - case 'right': - margin.top = '-'+tipWidth; - locationStyles.push(new Left('100%')); - transform.rotate = '-90deg'; - locationStyles.push(transform); - //side location - locationStyles.push(new Top(sideLocation)); - break; - default: - margin.left = '-'+tipWidth; - locationStyles.push(new Top('100%')); - //side location - locationStyles.push(new Left(sideLocation)); - } + + var verticalPlacementMarginTop:Margin = new Margin(null,unit); + verticalPlacementMarginTop.left = '-'+tipWidth; + var verticalPlacementMarginBottom:Margin = new Margin(null,unit); + verticalPlacementMarginBottom.left = '-'+tipWidth; + var horizontalPlacementMarginLeft:Margin = new Margin(null,unit); + horizontalPlacementMarginLeft.top = '-'+tipWidth; + var horizontalPlacementMarginRight:Margin = new Margin(null,unit); + horizontalPlacementMarginRight.top = '-'+tipWidth; + + var bottomRotate:Transform = new Transform(); + bottomRotate.rotate = '180deg'; + var leftRotate:Transform = new Transform(); + leftRotate.rotate = '-90deg'; + var rightRotate:Transform = new Transform(); + rightRotate.rotate = '90deg'; + + var locationStyles:Array = [ + new GroupPseudo([ + new DataState('direction-top', [ + verticalPlacementMarginTop, + new Top('100%'), + new Left(sideLocation) + ]), + new DataState('direction-left', [ + horizontalPlacementMarginLeft, + leftRotate, + new Left('100%'), + new Top(sideLocation) + ]), + new DataState('direction-right', [ + horizontalPlacementMarginRight, + rightRotate, + new Right('100%'), + new Top(sideLocation) + ]), + new DataState('direction-bottom', [ + verticalPlacementMarginBottom, + bottomRotate, + new Bottom('100%'), + new Left(sideLocation) + ]) + ], host.getWrapperStyle()) + ] return [ new Position('absolute'), @@ -291,5 +319,24 @@ package org.apache.royale.style.skins default: return 1; } } + + //Q:why are these here? + //A: because the absolutely positioned tip child does not contribute to the host component's measured height and width + //these are needed for adaptive positioning. Putting this support in the skin will mean different skins can do different things internally + public function getExtraHeight():Number{ + var dir:String = host.direction; + if (dir =='bottom' || dir=='top') { + return 2 * border_radius; + } + return 0; + } + + public function getExtraWidth():Number{ + var dir:String = host.direction; + if (dir =='left' || dir=='right') { + return 2 * border_radius; + } + return 0; + } } }
