@@ -715,6 +715,127 @@ describe("legend", () => {
715715 expect ( iconY + iconHeight / 2 ) . toBeLessThan ( ( labelY * 2 + labelHeight ) * 0.6 ) ;
716716 } ) ;
717717
718+ describe ( "Centered vertical legend overflow (regression for Math.max(0, ...) clamp)" , ( ) => {
719+ function parseTranslateY ( transform : string | null ) : number {
720+ if ( ! transform ) {
721+ return 0 ;
722+ }
723+ const match = / t r a n s l a t e \( \s * [ - + 0 - 9 . e E ] + \s * [ , ] \s * ( [ - + 0 - 9 . e E ] + ) / . exec ( transform ) ;
724+ return match ? parseFloat ( match [ 1 ] ) : 0 ;
725+ }
726+
727+ function expectGroupNotClipped ( position : LegendPosition ) : void {
728+ const legendData = getLotsOfLegendData ( ) ;
729+
730+ legend . changeOrientation ( position ) ;
731+ // Force overflow: tall list of items in a short vertical area.
732+ legend . drawLegend ( { dataPoints : legendData } , { height : 50 , width : 200 } ) ;
733+
734+ flushAllD3Transitions ( ) ;
735+
736+ const group = element . querySelector ( "#legendGroup" ) ;
737+ expect ( group ) . not . toBeNull ( ) ;
738+
739+ const translateY = parseTranslateY ( group . getAttribute ( "transform" ) ) ;
740+ // Before the fix this value went negative (group shifted above the
741+ // SVG, clipping the title and "previous" arrow).
742+ expect ( translateY ) . toBeGreaterThanOrEqual ( 0 ) ;
743+ }
744+
745+ it ( "LeftCenter with overflowing items does not translate group above 0" , ( ) => {
746+ expectGroupNotClipped ( LegendPosition . LeftCenter ) ;
747+ } ) ;
748+
749+ it ( "RightCenter with overflowing items does not translate group above 0" , ( ) => {
750+ expectGroupNotClipped ( LegendPosition . RightCenter ) ;
751+ } ) ;
752+ } ) ;
753+
754+ describe ( "Right-aligned horizontal legend (TopRight / BottomRight)" , ( ) => {
755+ function parseTranslate ( transform : string | null ) : { x : number ; y : number } {
756+ if ( ! transform ) {
757+ return { x : 0 , y : 0 } ;
758+ }
759+ const match = / t r a n s l a t e \( \s * ( [ - + 0 - 9 . e E ] + ) \s * [ , ] \s * ( [ - + 0 - 9 . e E ] + ) ? / . exec ( transform ) ;
760+ if ( ! match ) {
761+ return { x : 0 , y : 0 } ;
762+ }
763+ return { x : parseFloat ( match [ 1 ] ) , y : match [ 2 ] ? parseFloat ( match [ 2 ] ) : 0 } ;
764+ }
765+
766+ function fittingDataPoints ( ) : LegendDataPoint [ ] {
767+ return [
768+ { label : "A" , color : "#ff0000" , identity : createSelectionIdentity ( "a" ) , selected : false } ,
769+ { label : "B" , color : "#00ff00" , identity : createSelectionIdentity ( "b" ) , selected : false } ,
770+ ] ;
771+ }
772+
773+ it ( "TopRight: when items fit, the legend group is translated to the right edge and no nav arrow is rendered" , ( ) => {
774+ legend . changeOrientation ( LegendPosition . TopRight ) ;
775+ legend . drawLegend ( { dataPoints : fittingDataPoints ( ) } , { height : 100 , width : 1000 } ) ;
776+
777+ flushAllD3Transitions ( ) ;
778+
779+ const group = element . querySelector ( "#legendGroup" ) ;
780+ expect ( group ) . not . toBeNull ( ) ;
781+
782+ const translate = parseTranslate ( group . getAttribute ( "transform" ) ) ;
783+ // Items fit, so the group should be shifted right by a positive amount.
784+ expect ( translate . x ) . toBeGreaterThan ( 0 ) ;
785+ expect ( translate . y ) . toBe ( 0 ) ;
786+
787+ // No overflow -> no navigation arrows.
788+ expect ( element . querySelectorAll ( ".navArrow" ) . length ) . toBe ( 0 ) ;
789+ } ) ;
790+
791+ it ( "BottomRight: when items fit, the legend group is translated to the right edge" , ( ) => {
792+ legend . changeOrientation ( LegendPosition . BottomRight ) ;
793+ legend . drawLegend ( { dataPoints : fittingDataPoints ( ) } , { height : 100 , width : 1000 } ) ;
794+
795+ flushAllD3Transitions ( ) ;
796+
797+ const group = element . querySelector ( "#legendGroup" ) ;
798+ const translate = parseTranslate ( group . getAttribute ( "transform" ) ) ;
799+ expect ( translate . x ) . toBeGreaterThan ( 0 ) ;
800+ expect ( element . querySelectorAll ( ".navArrow" ) . length ) . toBe ( 0 ) ;
801+ } ) ;
802+
803+ it ( "TopRight: when items overflow, falls back to left-aligned with a navigation arrow" , ( ) => {
804+ const legendData = getLotsOfLegendData ( ) ;
805+
806+ legend . changeOrientation ( LegendPosition . TopRight ) ;
807+ // Narrow viewport forces overflow.
808+ legend . drawLegend ( { dataPoints : legendData } , { height : 100 , width : 200 } ) ;
809+
810+ flushAllD3Transitions ( ) ;
811+
812+ const group = element . querySelector ( "#legendGroup" ) ;
813+ const translate = parseTranslate ( group . getAttribute ( "transform" ) ) ;
814+
815+ // Fallback: group is not translated to the right edge anymore.
816+ expect ( translate . x ) . toBe ( 0 ) ;
817+
818+ // The "next" navigation arrow should be visible at the right side.
819+ const arrows = element . querySelectorAll ( ".navArrow" ) ;
820+ expect ( arrows . length ) . toBeGreaterThan ( 0 ) ;
821+ } ) ;
822+
823+ it ( "BottomRight: when items overflow, falls back to left-aligned with a navigation arrow" , ( ) => {
824+ const legendData = getLotsOfLegendData ( ) ;
825+
826+ legend . changeOrientation ( LegendPosition . BottomRight ) ;
827+ legend . drawLegend ( { dataPoints : legendData } , { height : 100 , width : 200 } ) ;
828+
829+ flushAllD3Transitions ( ) ;
830+
831+ const group = element . querySelector ( "#legendGroup" ) ;
832+ const translate = parseTranslate ( group . getAttribute ( "transform" ) ) ;
833+
834+ expect ( translate . x ) . toBe ( 0 ) ;
835+ expect ( element . querySelectorAll ( ".navArrow" ) . length ) . toBeGreaterThan ( 0 ) ;
836+ } ) ;
837+ } ) ;
838+
718839 function validateLegendDOM ( expectedData : LegendDataPoint [ ] ) : void {
719840 let len = expectedData . length ;
720841 let labels = element . querySelectorAll ( ".legendText" ) ;
0 commit comments