@@ -7,158 +7,154 @@ import { useTokenOverflow } from '../../../../../lib/components/internal/compone
77
88import styles from '../../../../../lib/components/internal/components/autosuggest-input/styles.css.js' ;
99
10- let mockContainerWidth : number | null = null ;
11- jest . mock ( '@cloudscape-design/component-toolkit' , ( ) => ( {
12- ...jest . requireActual ( '@cloudscape-design/component-toolkit' ) ,
13- useContainerQuery : ( ) => [ mockContainerWidth , ( ) => { } ] ,
14- } ) ) ;
15-
1610interface Geometry {
17- container : number | null ;
1811 token : number ;
1912 trigger : number ;
20- icon : number ;
21- padding ? : number ;
22- border ?: number ;
13+ // Combined width of the tokens and the input, and how much of it the tokens take.
14+ available : number ;
15+ tokensShare ?: number ;
2316 gap ?: number ;
2417 inputMinWidth ?: string ;
2518}
2619
27- function setGeometry ( {
28- container,
29- token,
30- trigger,
31- icon,
32- padding = 0 ,
33- border = 0 ,
34- gap = 0 ,
35- inputMinWidth = '0px' ,
36- } : Geometry ) {
37- mockContainerWidth = container ;
20+ function setGeometry ( { token, trigger, available, tokensShare = 0.5 , gap = 0 , inputMinWidth = '0px' } : Geometry ) {
21+ const tokensWidth = Math . round ( available * tokensShare ) ;
3822 jest . spyOn ( HTMLElement . prototype , 'offsetWidth' , 'get' ) . mockImplementation ( function ( this : HTMLElement ) {
3923 if ( this . classList . contains ( styles [ 'measurement-token' ] ) ) {
4024 return token ;
4125 }
42- return { trigger, icon } [ this . dataset . part as 'trigger' | 'icon' ] ?? 0 ;
26+ const widths = { trigger, tokens : tokensWidth , input : available - tokensWidth } ;
27+ return widths [ this . dataset . part as keyof typeof widths ] ?? 0 ;
4328 } ) ;
4429 const getComputedStyle = window . getComputedStyle ;
4530 jest . spyOn ( window , 'getComputedStyle' ) . mockImplementation ( element => {
4631 const style = getComputedStyle ( element ) ;
47- const overrides : Record < string , string > = {
48- paddingInlineStart : `${ padding } px` ,
49- paddingInlineEnd : `${ padding } px` ,
50- borderInlineStartWidth : `${ border } px` ,
51- borderInlineEndWidth : `${ border } px` ,
52- columnGap : `${ gap } px` ,
53- minInlineSize : ( element as HTMLElement ) . dataset . part === 'input' ? inputMinWidth : style . minInlineSize ,
54- } ;
32+ const part = ( element as HTMLElement ) . dataset . part ;
33+ const overrides : Record < string , string > =
34+ part === 'tokens' ? { columnGap : `${ gap } px` } : part === 'input' ? { minInlineSize : inputMinWidth } : { } ;
5535 return new Proxy ( style , { get : ( target , prop : string ) => overrides [ prop ] ?? ( target as any ) [ prop ] } ) ;
5636 } ) ;
5737}
5838
5939afterEach ( ( ) => {
6040 jest . restoreAllMocks ( ) ;
61- mockContainerWidth = null ;
6241} ) ;
6342
64- function Harness ( {
65- tokenCount,
66- enabled = true ,
67- layoutKey = '' ,
68- } : {
43+ interface HarnessProps {
6944 tokenCount : number ;
45+ fieldWidth : number | null ;
7046 enabled ?: boolean ;
7147 layoutKey ?: string ;
72- } ) {
48+ }
49+
50+ function Harness ( { tokenCount, fieldWidth, enabled = true , layoutKey = '' } : HarnessProps ) {
7351 const measureRef = React . useRef < HTMLDivElement > ( null ) ;
7452 const measureTriggerRef = React . useRef < HTMLSpanElement > ( null ) ;
75- const fieldRef = React . useRef < HTMLDivElement > ( null ) ;
76- const leadingIconRef = React . useRef < HTMLSpanElement > ( null ) ;
77- const inputWrapperRef = React . useRef < HTMLDivElement > ( null ) ;
78- const { containerRef, visibleCount } = useTokenOverflow ( {
53+ const tokensRef = React . useRef < HTMLDivElement > ( null ) ;
54+ const inputRef = React . useRef < HTMLInputElement > ( null ) ;
55+ const visibleCount = useTokenOverflow ( {
7956 enabled,
8057 tokenCount,
8158 tokenContentKey : String ( tokenCount ) ,
8259 measureRef,
8360 measureTriggerRef,
84- fieldRef ,
85- leadingIconRef ,
86- inputWrapperRef ,
61+ tokensRef ,
62+ inputRef ,
63+ fieldWidth ,
8764 layoutKey,
8865 } ) ;
8966 return (
90- < div ref = { containerRef } >
91- < div ref = { measureRef } >
67+ < div >
68+ < div ref = { measureRef } data-testid = "measure" >
9269 { Array . from ( { length : tokenCount } , ( _ , i ) => (
9370 < span key = { i } className = { styles [ 'measurement-token' ] } />
9471 ) ) }
9572 < span ref = { measureTriggerRef } data-part = "trigger" />
9673 </ div >
97- < div ref = { fieldRef } >
98- < span ref = { leadingIconRef } data-part = "icon " />
99- < div ref = { inputWrapperRef } data-part = "input" />
74+ < div >
75+ { tokenCount > 0 && < div ref = { tokensRef } data-part = "tokens " /> }
76+ < input ref = { inputRef } data-part = "input" />
10077 </ div >
10178 < output > { visibleCount } </ output >
10279 </ div >
10380 ) ;
10481}
10582
106- const visibleCountFor = ( props : { tokenCount : number ; enabled ?: boolean } ) =>
83+ const visibleCountFor = ( props : HarnessProps ) =>
10784 Number ( render ( < Harness { ...props } /> ) . container . querySelector ( 'output' ) ! . textContent ) ;
10885
10986describe ( 'useTokenOverflow' , ( ) => {
110- test ( 'shows all tokens before the container is measured' , ( ) => {
111- setGeometry ( { container : null , token : 100 , trigger : 30 , icon : 20 } ) ;
112- expect ( visibleCountFor ( { tokenCount : 5 } ) ) . toBe ( 5 ) ;
87+ test ( 'shows all tokens before the field is measured' , ( ) => {
88+ setGeometry ( { available : 0 , token : 100 , trigger : 30 } ) ;
89+ expect ( visibleCountFor ( { tokenCount : 5 , fieldWidth : null } ) ) . toBe ( 5 ) ;
11390 } ) ;
11491
11592 test ( 'shows all tokens when disabled' , ( ) => {
116- setGeometry ( { container : 50 , token : 100 , trigger : 30 , icon : 20 } ) ;
117- expect ( visibleCountFor ( { tokenCount : 5 , enabled : false } ) ) . toBe ( 5 ) ;
93+ setGeometry ( { available : 50 , token : 100 , trigger : 30 } ) ;
94+ expect ( visibleCountFor ( { tokenCount : 5 , fieldWidth : 50 , enabled : false } ) ) . toBe ( 5 ) ;
11895 } ) ;
11996
12097 test ( 'shows all tokens when they fit' , ( ) => {
121- setGeometry ( { container : 320 , token : 100 , trigger : 30 , icon : 20 } ) ;
122- expect ( visibleCountFor ( { tokenCount : 3 } ) ) . toBe ( 3 ) ;
98+ setGeometry ( { available : 300 , token : 100 , trigger : 30 } ) ;
99+ expect ( visibleCountFor ( { tokenCount : 3 , fieldWidth : 300 } ) ) . toBe ( 3 ) ;
123100 } ) ;
124101
125102 test ( 'keeps the trailing tokens that fit next to the overflow trigger' , ( ) => {
126- setGeometry ( { container : 260 , token : 100 , trigger : 30 , icon : 20 } ) ;
127- expect ( visibleCountFor ( { tokenCount : 5 } ) ) . toBe ( 2 ) ;
103+ setGeometry ( { available : 240 , token : 100 , trigger : 30 } ) ;
104+ expect ( visibleCountFor ( { tokenCount : 5 , fieldWidth : 240 } ) ) . toBe ( 2 ) ;
128105 } ) ;
129106
130107 test ( 'never shows a partially visible token' , ( ) => {
131- setGeometry ( { container : 249 , token : 100 , trigger : 30 , icon : 20 } ) ;
132- expect ( visibleCountFor ( { tokenCount : 5 } ) ) . toBe ( 1 ) ;
108+ setGeometry ( { available : 229 , token : 100 , trigger : 30 } ) ;
109+ expect ( visibleCountFor ( { tokenCount : 5 , fieldWidth : 229 } ) ) . toBe ( 1 ) ;
133110 } ) ;
134111
135112 test ( 'hides all tokens when none fit next to the trigger' , ( ) => {
136- setGeometry ( { container : 120 , token : 100 , trigger : 30 , icon : 20 } ) ;
137- expect ( visibleCountFor ( { tokenCount : 2 } ) ) . toBe ( 0 ) ;
113+ setGeometry ( { available : 100 , token : 100 , trigger : 30 } ) ;
114+ expect ( visibleCountFor ( { tokenCount : 2 , fieldWidth : 100 } ) ) . toBe ( 0 ) ;
138115 } ) ;
139116
140- test ( 'accounts for padding and gaps' , ( ) => {
141- setGeometry ( { container : 320 , token : 100 , trigger : 30 , icon : 20 , padding : 10 , gap : 4 } ) ;
142- expect ( visibleCountFor ( { tokenCount : 3 } ) ) . toBe ( 2 ) ;
117+ test ( 'accounts for the gaps between tokens ' , ( ) => {
118+ setGeometry ( { available : 300 , token : 100 , trigger : 30 , gap : 4 } ) ;
119+ expect ( visibleCountFor ( { tokenCount : 3 , fieldWidth : 300 } ) ) . toBe ( 2 ) ;
143120 } ) ;
144121
145- test ( 'accounts for the field borders' , ( ) => {
146- setGeometry ( { container : 320 , token : 100 , trigger : 30 , icon : 20 , border : 1 } ) ;
147- expect ( visibleCountFor ( { tokenCount : 3 } ) ) . toBe ( 2 ) ;
122+ test ( 'does not depend on how the field is currently split between tokens and input' , ( ) => {
123+ setGeometry ( { available : 300 , token : 100 , trigger : 30 , tokensShare : 0 } ) ;
124+ expect ( visibleCountFor ( { tokenCount : 3 , fieldWidth : 300 } ) ) . toBe ( 3 ) ;
125+ jest . restoreAllMocks ( ) ;
126+ setGeometry ( { available : 300 , token : 100 , trigger : 30 , tokensShare : 1 } ) ;
127+ expect ( visibleCountFor ( { tokenCount : 3 , fieldWidth : 300 } ) ) . toBe ( 3 ) ;
148128 } ) ;
149129
150130 test ( 're-measures when the field layout changes' , ( ) => {
151- setGeometry ( { container : 320 , token : 100 , trigger : 30 , icon : 20 } ) ;
152- const { container, rerender } = render ( < Harness tokenCount = { 3 } layoutKey = "valid" /> ) ;
131+ setGeometry ( { available : 300 , token : 100 , trigger : 30 } ) ;
132+ const { container, rerender } = render ( < Harness tokenCount = { 3 } fieldWidth = { 300 } layoutKey = "valid" /> ) ;
153133 expect ( container . querySelector ( 'output' ) ) . toHaveTextContent ( '3' ) ;
154134 jest . restoreAllMocks ( ) ;
155- setGeometry ( { container : 320 , token : 100 , trigger : 30 , icon : 20 , padding : 10 } ) ;
156- rerender ( < Harness tokenCount = { 3 } layoutKey = "invalid" /> ) ;
135+ setGeometry ( { available : 280 , token : 100 , trigger : 30 } ) ;
136+ rerender ( < Harness tokenCount = { 3 } fieldWidth = { 300 } layoutKey = "invalid" /> ) ;
137+ expect ( container . querySelector ( 'output' ) ) . toHaveTextContent ( '2' ) ;
138+ } ) ;
139+
140+ test ( 'shows all tokens while the field is hidden and re-measures when it is shown again' , ( ) => {
141+ setGeometry ( { available : 0 , token : 0 , trigger : 0 } ) ;
142+ const { container, rerender } = render ( < Harness tokenCount = { 5 } fieldWidth = { 0 } /> ) ;
143+ expect ( container . querySelector ( 'output' ) ) . toHaveTextContent ( '5' ) ;
144+ jest . restoreAllMocks ( ) ;
145+ setGeometry ( { available : 240 , token : 100 , trigger : 30 } ) ;
146+ rerender ( < Harness tokenCount = { 5 } fieldWidth = { 240 } /> ) ;
157147 expect ( container . querySelector ( 'output' ) ) . toHaveTextContent ( '2' ) ;
158148 } ) ;
159149
150+ test ( 'makes the measurement copy inert even when there are no tokens' , ( ) => {
151+ setGeometry ( { available : 300 , token : 100 , trigger : 30 } ) ;
152+ const { getByTestId } = render ( < Harness tokenCount = { 0 } fieldWidth = { 300 } /> ) ;
153+ expect ( getByTestId ( 'measure' ) . inert ) . toBe ( true ) ;
154+ } ) ;
155+
160156 test ( 'reserves the input minimum width defined in CSS' , ( ) => {
161- setGeometry ( { container : 399 , token : 100 , trigger : 30 , icon : 0 , inputMinWidth : '25%' } ) ;
162- expect ( visibleCountFor ( { tokenCount : 3 } ) ) . toBe ( 2 ) ;
157+ setGeometry ( { available : 399 , token : 100 , trigger : 30 , inputMinWidth : '25%' } ) ;
158+ expect ( visibleCountFor ( { tokenCount : 3 , fieldWidth : 399 } ) ) . toBe ( 2 ) ;
163159 } ) ;
164160} ) ;
0 commit comments