@@ -3,8 +3,10 @@ import type { Listbox } from '../listbox/listbox.js';
3
3
import { isListbox } from '../listbox/listbox.options.js' ;
4
4
import type { DropdownOption } from '../option/option.js' ;
5
5
import { isDropdownOption } from '../option/option.options.js' ;
6
+ import { getDirection } from '../utils/direction.js' ;
6
7
import { toggleState } from '../utils/element-internals.js' ;
7
8
import { getLanguage } from '../utils/language.js' ;
9
+ import { AnchorPositioningCSSSupported } from '../utils/support.js' ;
8
10
import { uniqueId } from '../utils/unique-id.js' ;
9
11
import { DropdownType } from './dropdown.options.js' ;
10
12
import { dropdownButtonTemplate , dropdownInputTemplate } from './dropdown.template.js' ;
@@ -239,6 +241,14 @@ export class BaseDropdown extends FASTElement {
239
241
240
242
this . setValidity ( ) ;
241
243
} ) ;
244
+
245
+ if ( AnchorPositioningCSSSupported ) {
246
+ // The `anchor-name` property seems to not be isolated between instances in Safari Technology Preview 220 (18.4).
247
+ // It's unclear if the spec requires the `anchor-name` to be unique when styled on the `:host`.
248
+ const anchorName = uniqueId ( '--dropdown-anchor-' ) ;
249
+ this . style . setProperty ( 'anchor-name' , anchorName ) ;
250
+ this . listbox . style . setProperty ( 'position-anchor' , anchorName ) ;
251
+ }
242
252
}
243
253
}
244
254
@@ -320,12 +330,9 @@ export class BaseDropdown extends FASTElement {
320
330
this . elementInternals . ariaExpanded = next ? 'true' : 'false' ;
321
331
this . activeIndex = this . selectedIndex ?? - 1 ;
322
332
323
- if ( next ) {
324
- BaseDropdown . AnchorPositionFallbackObserver ?. observe ( this . listbox ) ;
325
- return ;
333
+ if ( ! AnchorPositioningCSSSupported ) {
334
+ this . anchorPositionFallback ( next ) ;
326
335
}
327
-
328
- BaseDropdown . AnchorPositionFallbackObserver ?. unobserve ( this . listbox ) ;
329
336
}
330
337
331
338
/**
@@ -386,6 +393,18 @@ export class BaseDropdown extends FASTElement {
386
393
*/
387
394
public controlSlot ! : HTMLSlotElement ;
388
395
396
+ /**
397
+ * Event handler for scroll and resize events. Used when the browser does not support CSS anchor positioning.
398
+ *
399
+ * @internal
400
+ */
401
+ private debouncedReposition = ( ) => {
402
+ if ( this . frameId ) {
403
+ cancelAnimationFrame ( this . frameId ) ;
404
+ }
405
+ this . frameId = requestAnimationFrame ( this . repositionListbox ) ;
406
+ } ;
407
+
389
408
/**
390
409
* The internal {@link https://developer.mozilla.org/docs/Web/API/ElementInternals | `ElementInternals`} instance for the component.
391
410
*
@@ -410,27 +429,11 @@ export class BaseDropdown extends FASTElement {
410
429
public static formAssociated = true ;
411
430
412
431
/**
413
- * Resets the form value to its initial value when the form is reset .
432
+ * The ID of the frame used for repositioning the listbox when the browser does not support CSS anchor positioning .
414
433
*
415
434
* @internal
416
435
*/
417
- formResetCallback ( ) : void {
418
- this . enabledOptions . forEach ( ( x , i ) => {
419
- if ( this . multiple ) {
420
- x . selected = ! ! x . defaultSelected ;
421
- return ;
422
- }
423
-
424
- if ( ! x . defaultSelected ) {
425
- x . selected = false ;
426
- return ;
427
- }
428
-
429
- this . selectOption ( i ) ;
430
- } ) ;
431
-
432
- this . setValidity ( ) ;
433
- }
436
+ private frameId ?: number ;
434
437
435
438
/**
436
439
* A reference to the first freeform option, if present.
@@ -482,6 +485,31 @@ export class BaseDropdown extends FASTElement {
482
485
return this . listbox ?. options ?? [ ] ;
483
486
}
484
487
488
+ /**
489
+ * Repositions the listbox to align with the control element. Used when the browser does not support CSS anchor positioning.
490
+ *
491
+ * @internal
492
+ */
493
+ private repositionListbox = ( ) => {
494
+ const controlRect = this . getBoundingClientRect ( ) ;
495
+ const right = window . innerWidth - controlRect . right ;
496
+ const left = controlRect . left ;
497
+
498
+ this . listbox . style . minWidth = `${ controlRect . width } px` ;
499
+ this . listbox . style . top = `${ controlRect . top } px` ;
500
+
501
+ if (
502
+ left + controlRect . width > window . innerWidth ||
503
+ ( getDirection ( this ) === 'rtl' && right - controlRect . width > 0 )
504
+ ) {
505
+ this . listbox . style . right = `${ right } px` ;
506
+ this . listbox . style . left = 'unset' ;
507
+ } else {
508
+ this . listbox . style . left = `${ left } px` ;
509
+ this . listbox . style . right = 'unset' ;
510
+ }
511
+ } ;
512
+
485
513
/**
486
514
* The index of the first selected option, scoped to the enabled options.
487
515
*
@@ -716,6 +744,29 @@ export class BaseDropdown extends FASTElement {
716
744
return true ;
717
745
}
718
746
747
+ /**
748
+ * Resets the form value to its initial value when the form is reset.
749
+ *
750
+ * @internal
751
+ */
752
+ formResetCallback ( ) : void {
753
+ this . enabledOptions . forEach ( ( x , i ) => {
754
+ if ( this . multiple ) {
755
+ x . selected = ! ! x . defaultSelected ;
756
+ return ;
757
+ }
758
+
759
+ if ( ! x . defaultSelected ) {
760
+ x . selected = false ;
761
+ return ;
762
+ }
763
+
764
+ this . selectOption ( i ) ;
765
+ } ) ;
766
+
767
+ this . setValidity ( ) ;
768
+ }
769
+
719
770
/**
720
771
* Ensures the active index is within bounds of the enabled options. Out-of-bounds indices are wrapped to the opposite
721
772
* end of the range.
@@ -948,13 +999,11 @@ export class BaseDropdown extends FASTElement {
948
999
this . freeformOption . hidden = false ;
949
1000
}
950
1001
951
- connectedCallback ( ) : void {
952
- super . connectedCallback ( ) ;
953
- this . anchorPositionFallback ( ) ;
954
- }
955
-
956
1002
disconnectedCallback ( ) : void {
957
- BaseDropdown . AnchorPositionFallbackObserver ?. unobserve ( this . listbox ) ;
1003
+ BaseDropdown . AnchorPositionFallbackObserver ?. disconnect ( ) ;
1004
+
1005
+ window . removeEventListener ( 'scroll' , this . debouncedReposition , { capture : true } ) ;
1006
+ window . removeEventListener ( 'resize' , this . debouncedReposition ) ;
958
1007
959
1008
super . disconnectedCallback ( ) ;
960
1009
}
@@ -979,25 +1028,43 @@ export class BaseDropdown extends FASTElement {
979
1028
*
980
1029
* @internal
981
1030
*/
982
- private anchorPositionFallback ( ) : void {
983
- BaseDropdown . AnchorPositionFallbackObserver =
984
- BaseDropdown . AnchorPositionFallbackObserver ??
985
- new IntersectionObserver (
1031
+ private anchorPositionFallback ( shouldObserve ?: boolean ) : void {
1032
+ if ( ! BaseDropdown . AnchorPositionFallbackObserver ) {
1033
+ BaseDropdown . AnchorPositionFallbackObserver = new IntersectionObserver (
986
1034
( entries : IntersectionObserverEntry [ ] ) : void => {
987
1035
entries . forEach ( ( { boundingClientRect, isIntersecting, target } ) => {
988
- if ( isListbox ( target ) && ! isIntersecting ) {
1036
+ if ( isListbox ( target ) ) {
989
1037
if ( boundingClientRect . bottom > window . innerHeight ) {
990
- toggleState ( target . dropdown ! . elementInternals , 'flip-block' , true ) ;
1038
+ toggleState ( target . elementInternals , 'flip-block' , true ) ;
991
1039
return ;
992
1040
}
993
1041
994
1042
if ( boundingClientRect . top < 0 ) {
995
- toggleState ( target . dropdown ! . elementInternals , 'flip-block' , false ) ;
1043
+ toggleState ( target . elementInternals , 'flip-block' , false ) ;
996
1044
}
997
1045
}
998
1046
} ) ;
999
1047
} ,
1000
1048
{ threshold : 1 } ,
1001
1049
) ;
1050
+ }
1051
+
1052
+ if ( shouldObserve ) {
1053
+ BaseDropdown . AnchorPositionFallbackObserver . observe ( this . listbox ) ;
1054
+
1055
+ window . addEventListener ( 'scroll' , this . debouncedReposition , { passive : true , capture : true } ) ;
1056
+ window . addEventListener ( 'resize' , this . debouncedReposition , { passive : true } ) ;
1057
+ this . debouncedReposition ( ) ;
1058
+ return ;
1059
+ }
1060
+
1061
+ BaseDropdown . AnchorPositionFallbackObserver . unobserve ( this . listbox ) ;
1062
+
1063
+ window . removeEventListener ( 'scroll' , this . debouncedReposition , { capture : true } ) ;
1064
+ window . removeEventListener ( 'resize' , this . debouncedReposition ) ;
1065
+ if ( this . frameId ) {
1066
+ cancelAnimationFrame ( this . frameId ) ;
1067
+ this . frameId = undefined ;
1068
+ }
1002
1069
}
1003
1070
}
0 commit comments