11<script setup lang="ts">
2- const { popoverButtonAriaLabel , accented } = defineProps <{
3- /** The `aria-label` of the button that opens the popover . */
4- popoverButtonAriaLabel : string ;
2+ const { menuLabel , accented } = defineProps <{
3+ /** The `aria-label` of the menu and the menu button . */
4+ menuLabel : string ;
55
66 /** Whether to use accent colors for the button. */
77 accented? : boolean ;
88}>();
9+
10+ const isOpen = ref (false );
11+
12+ function toggleMenu() {
13+ if (! isOpen .value ) {
14+ isOpen .value = true ;
15+ return ;
16+ }
17+
18+ closeAndRestoreFocus ();
19+ }
20+
21+ function preventMenuBlur(event : MouseEvent ) {
22+ if (isOpen .value ) {
23+ event .preventDefault ();
24+ }
25+ }
26+
27+ const menuButton = useTemplateRef (" menu-button" );
28+
29+ function closeAndRestoreFocus() {
30+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The menu button must be mounted if its menu is being closed.
31+ menuButton .value ! .$el .focus ();
32+ }
33+
34+ const menu = useTemplateRef (" menu" );
35+
36+ async function handleBlur() {
37+ // Wait for the next element to focus.
38+ await timeout ();
39+
40+ if (menu .value ?.$el .contains (document .activeElement )) {
41+ return ;
42+ }
43+
44+ isOpen .value = false ;
45+ }
46+
47+ function handleClick(event : MouseEvent ) {
48+ if (
49+ event .target instanceof HTMLElement &&
50+ event .target .role === " menuitem" &&
51+ ! (
52+ event .target .ariaHasPopup === " menu" ||
53+ event .target .ariaHasPopup === " true"
54+ )
55+ ) {
56+ closeAndRestoreFocus ();
57+ }
58+ }
59+
60+ const menuFocus = useTemplateRef (" menu-focus" );
61+
62+ function focusFirstItem() {
63+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The menu's focus manager must be mounted if an item in it is being focused.
64+ menuFocus .value ! .focusFirst ();
65+ }
966 </script >
1067
1168<template >
12- <fieldset class =" split-button" :class =" { accented }" >
13- <slot name =" button" ></slot >
69+ <fieldset class =" split-button-wrapper" >
70+ <div class =" split-button" :class =" { accented }" >
71+ <slot name =" button" ></slot >
1472
15- <div class =" popover-button-wrapper" >
16- <Button class="popover-button" :aria-label =" popoverButtonAriaLabel " >
17- <IconChevronDown />
18- </Button >
73+ <div class =" menu-button-wrapper" >
74+ <Button
75+ ref="menu-button"
76+ class="menu-button"
77+ :aria-label =" menuLabel "
78+ aria-haspopup="menu"
79+ :aria-expanded =" isOpen "
80+ @click =" toggleMenu "
81+ @mousedown =" preventMenuBlur "
82+ >
83+ <IconChevronDown />
84+ </Button >
85+ </div >
1986 </div >
87+
88+ <MenuPanel
89+ v-if =" isOpen "
90+ ref="menu"
91+ class="menu"
92+ role="menu"
93+ :aria-label =" menuLabel "
94+ tabindex="-1"
95+ @keydown .esc =" closeAndRestoreFocus "
96+ @blur .capture =" handleBlur "
97+ @click .capture =" handleClick "
98+ @focus =" focusFirstItem "
99+ >
100+ <KeyboardFocus ref="menu-focus" arrows="all" home-and-end>
101+ <slot ></slot >
102+ </KeyboardFocus >
103+ </MenuPanel >
20104 </fieldset >
21105</template >
22106
23107<style scoped lang="scss">
24108$gap : 1px ;
25109$inner-border-radius : 2px ;
26- $popover -button-padding-x : 0.6em ;
27- $popover -button-width : calc (2 * $popover -button-padding-x + 1em );
110+ $menu -button-padding-x : 0.6em ;
111+ $menu -button-width : calc (2 * $menu -button-padding-x + 1em );
28112
29- .split-button {
113+ .split-button-wrapper {
30114 position : relative ;
115+ }
116+
117+ .split-button {
31118 display : inline-flex ;
32- padding-right : calc ($popover-button-width + $gap );
119+ width : 100% ;
120+ padding-right : calc ($menu-button-width + $gap );
33121
34122 // Prevent outside elements from covering any `z-index: -1` descendants.
35123 isolation : isolate ;
@@ -49,10 +137,10 @@ $popover-button-width: calc(2 * $popover-button-padding-x + 1em);
49137 border-top-right-radius : $inner-border-radius ;
50138 border-bottom-right-radius : $inner-border-radius ;
51139
52- background-size : calc (100% + $popover -button-width ) 100% ;
140+ background-size : calc (100% + $menu -button-width ) 100% ;
53141}
54142
55- .popover -button-wrapper {
143+ .menu -button-wrapper {
56144 container : split- button / size ;
57145 position : absolute ;
58146 inset : 0 ;
@@ -61,14 +149,24 @@ $popover-button-width: calc(2 * $popover-button-padding-x + 1em);
61149 text-align : right ;
62150}
63151
64- .popover -button {
152+ .menu -button {
65153 pointer-events : auto ;
66- padding : 0 $popover -button-padding-x ;
154+ padding : 0 $menu -button-padding-x ;
67155
68156 border-top-left-radius : $inner-border-radius ;
69157 border-bottom-left-radius : $inner-border-radius ;
70158
71159 background-size : 100 cqw 100% ;
72160 background-position : right ;
73161}
162+
163+ .menu {
164+ position : absolute ;
165+ z-index : 1 ;
166+
167+ min-width : 100% ;
168+ margin-top : 4px ;
169+
170+ font-size : 1rem ;
171+ }
74172 </style >
0 commit comments