Skip to content

Commit 0cdde07

Browse files
committed
Add split button menu
1 parent 7c13987 commit 0cdde07

1 file changed

Lines changed: 115 additions & 17 deletions

File tree

Lines changed: 115 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,123 @@
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: 100cqw 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

Comments
 (0)