-
Notifications
You must be signed in to change notification settings - Fork 49
Expand file tree
/
Copy pathChannelHeaderRelationButton.vue
More file actions
122 lines (101 loc) · 2.79 KB
/
Copy pathChannelHeaderRelationButton.vue
File metadata and controls
122 lines (101 loc) · 2.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<template>
<button
ref="trigger"
type="button"
aria-haspopup="true"
:aria-expanded="isOpen"
:aria-controls="popupId"
title="関連チャンネルを表示"
:class="$style.trigger"
@click="toggle"
>
<AIcon :size="20" name="rounded-triangle" :class="$style.icon" />
</button>
<!-- NOTE: ボタンから Tab 移動した際に popup のはじめに飛べるように Focus を管理する -->
<div v-if="isOpen" tabindex="0" @focus="focusPopup" />
<ChannelHeaderRelationPopup
v-if="isOpen"
ref="popup"
:popup-id="popupId"
:right-position="triggerBottomRightPosition"
:channel-id="props.channelId"
@outside-click="close"
@focus-return="focusTrigger"
/>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useEventListener } from '@vueuse/core'
import AIcon from '/@/components/UI/AIcon.vue'
import type { Point } from '/@/lib/basic/point'
import { randomString } from '/@/lib/basic/randomString'
import ChannelHeaderRelationPopup from './ChannelHeaderRelationPopup.vue'
const props = defineProps<{
channelId: string
}>()
const trigger = ref<HTMLElement | null>(null)
const popup = ref<InstanceType<typeof ChannelHeaderRelationPopup> | null>(null)
const popupId = randomString()
const isOpen = ref(false)
const toggle = () => {
updateTriggerPosition()
isOpen.value = !isOpen.value
}
const close = (e: Event) => {
// NOTE: popup の外側かつボタンをクリックしたときに、うまく閉じないため抑制する
// 具体的には close -> toggle と呼ばれて閉じなくなる
if (trigger.value !== null && e.composedPath().includes(trigger.value)) return
isOpen.value = false
}
const triggerBottomRightPosition = reactive<Point>({
x: 0,
y: 0
})
const updateTriggerPosition = () => {
if (trigger.value === null) return
const rect = trigger.value.getBoundingClientRect()
triggerBottomRightPosition.x = rect.right
triggerBottomRightPosition.y = rect.bottom
}
useEventListener(
computed(() => (isOpen.value ? window : null)),
'resize',
updateTriggerPosition
)
useEventListener(window, 'resize', updateTriggerPosition)
onMounted(() => {
updateTriggerPosition()
})
const focusPopup = () => {
popup.value?.focus()
}
const focusTrigger = () => {
trigger.value?.focus()
}
</script>
<style lang="scss" module>
.trigger {
@include color-ui-secondary;
@include background-primary;
cursor: pointer;
overflow: hidden;
height: 100%;
width: 24px;
margin: 0 8px;
display: grid;
place-items: center;
flex-shrink: 0;
position: sticky;
right: -1px;
transition: transform 0.1s;
&:hover {
transform: scale(1.1);
}
.icon {
transition: transform 0.5s;
}
&[aria-expanded='true'] .icon {
transform: rotate(180deg);
}
}
</style>