|
| 1 | +<script setup lang="ts"> |
| 2 | +import type { Placement } from "@popperjs/core"; |
| 3 | +import { computed, ref } from "vue"; |
| 4 | +import { RouterLink } from "vue-router"; |
| 5 | +
|
| 6 | +import { useAccessibleHover } from "@/composables/accessibleHover"; |
| 7 | +import { useResolveElement } from "@/composables/resolveElement"; |
| 8 | +import { useUid } from "@/composables/utils/uid"; |
| 9 | +
|
| 10 | +import { type ComponentColor, type ComponentSize, type ComponentVariantClassList, prefix } from "./componentVariants"; |
| 11 | +
|
| 12 | +import GTooltip from "./GTooltip.vue"; |
| 13 | +
|
| 14 | +const props = defineProps<{ |
| 15 | + href?: string; |
| 16 | + to?: string; |
| 17 | + color?: ComponentColor; |
| 18 | + outline?: boolean; |
| 19 | + disabled?: boolean; |
| 20 | + title?: string; |
| 21 | + disabledTitle?: string; |
| 22 | + size?: ComponentSize; |
| 23 | + tooltip?: boolean; |
| 24 | + tooltipPlacement?: Placement; |
| 25 | + inline?: boolean; |
| 26 | + iconOnly?: boolean; |
| 27 | + transparent?: boolean; |
| 28 | + pill?: boolean; |
| 29 | + pressed?: boolean; |
| 30 | +}>(); |
| 31 | +
|
| 32 | +const emit = defineEmits<{ |
| 33 | + (e: "click", event: PointerEvent): void; |
| 34 | + (e: "update:pressed", pressed: boolean): void; |
| 35 | +}>(); |
| 36 | +
|
| 37 | +function onClick(event: PointerEvent) { |
| 38 | + if (!props.disabled) { |
| 39 | + emit("click", event); |
| 40 | + emit("update:pressed", !props.pressed); |
| 41 | + } |
| 42 | +} |
| 43 | +
|
| 44 | +const variantClasses = computed(() => { |
| 45 | + const classObject = {} as ComponentVariantClassList; |
| 46 | + classObject[prefix(props.color ?? "grey")] = true; |
| 47 | + classObject[prefix(props.size ?? "medium")] = true; |
| 48 | + return classObject; |
| 49 | +}); |
| 50 | +
|
| 51 | +const styleClasses = computed(() => { |
| 52 | + return { |
| 53 | + "g-outline": props.outline, |
| 54 | + "g-disabled": props.disabled, |
| 55 | + "g-icon-only": props.iconOnly, |
| 56 | + "g-inline": props.inline, |
| 57 | + "g-pill": props.pill, |
| 58 | + "g-transparent": props.transparent, |
| 59 | + "g-pressed": props.pressed, |
| 60 | + }; |
| 61 | +}); |
| 62 | +
|
| 63 | +const baseComponent = computed(() => { |
| 64 | + if (props.to) { |
| 65 | + return RouterLink; |
| 66 | + } else if (props.href) { |
| 67 | + return "a" as const; |
| 68 | + } else { |
| 69 | + return "button" as const; |
| 70 | + } |
| 71 | +}); |
| 72 | +
|
| 73 | +const currentTooltip = computed(() => { |
| 74 | + if (props.disabled) { |
| 75 | + return props.disabledTitle ?? props.title; |
| 76 | + } else { |
| 77 | + return props.title; |
| 78 | + } |
| 79 | +}); |
| 80 | +
|
| 81 | +const currentTitle = computed(() => { |
| 82 | + if (props.tooltip) { |
| 83 | + return false; |
| 84 | + } else { |
| 85 | + return currentTooltip.value; |
| 86 | + } |
| 87 | +}); |
| 88 | +
|
| 89 | +const tooltipId = useUid("g-tooltip"); |
| 90 | +
|
| 91 | +const describedBy = computed(() => { |
| 92 | + if (props.tooltip) { |
| 93 | + return tooltipId.value; |
| 94 | + } else { |
| 95 | + return false; |
| 96 | + } |
| 97 | +}); |
| 98 | +
|
| 99 | +const buttonRef = ref<HTMLElement | InstanceType<typeof RouterLink> | null>(null); |
| 100 | +const tooltipRef = ref<InstanceType<typeof GTooltip>>(); |
| 101 | +
|
| 102 | +const buttonElementRef = useResolveElement(buttonRef); |
| 103 | +
|
| 104 | +useAccessibleHover( |
| 105 | + buttonElementRef, |
| 106 | + () => { |
| 107 | + tooltipRef.value?.show(); |
| 108 | + }, |
| 109 | + () => { |
| 110 | + tooltipRef.value?.hide(); |
| 111 | + } |
| 112 | +); |
| 113 | +</script> |
| 114 | + |
| 115 | +<template> |
| 116 | + <component |
| 117 | + :is="baseComponent" |
| 118 | + ref="buttonRef" |
| 119 | + class="g-button" |
| 120 | + :data-title="currentTooltip" |
| 121 | + :class="{ ...variantClasses, ...styleClasses }" |
| 122 | + :to="props.to" |
| 123 | + :href="props.to ?? props.href" |
| 124 | + :title="currentTitle" |
| 125 | + :aria-describedby="describedBy" |
| 126 | + v-bind="$attrs" |
| 127 | + @click="onClick"> |
| 128 | + <slot></slot> |
| 129 | + |
| 130 | + <!-- TODO: make tooltip a sibling in Vue 3 --> |
| 131 | + <GTooltip |
| 132 | + v-if="props.tooltip" |
| 133 | + :id="tooltipId" |
| 134 | + ref="tooltipRef" |
| 135 | + :reference="buttonElementRef" |
| 136 | + :text="currentTooltip" |
| 137 | + :placement="props.tooltipPlacement" /> |
| 138 | + </component> |
| 139 | +</template> |
| 140 | + |
| 141 | +<style scoped lang="scss"> |
| 142 | +.g-button { |
| 143 | + display: inline-block; |
| 144 | + margin: 0; |
| 145 | + border: 1px solid; |
| 146 | + border-radius: var(--spacing-1); |
| 147 | + text-decoration: none; |
| 148 | + vertical-align: middle; |
| 149 | + cursor: pointer; |
| 150 | +
|
| 151 | + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, |
| 152 | + box-shadow 0.15s ease-in-out; |
| 153 | +
|
| 154 | + @media (prefers-reduced-motion) { |
| 155 | + transition: none; |
| 156 | + } |
| 157 | +
|
| 158 | + &:focus { |
| 159 | + outline: none; |
| 160 | + box-shadow: 0 0 0 0.2rem rgb(from var(--color-blue-400) r g b / 0.33); |
| 161 | + z-index: 999; |
| 162 | + } |
| 163 | +
|
| 164 | + &:focus-visible { |
| 165 | + outline: none; |
| 166 | + box-shadow: 0 0 0 0.2rem var(--color-blue-400); |
| 167 | + z-index: 999; |
| 168 | + } |
| 169 | +
|
| 170 | + // sizes |
| 171 | + &.g-small { |
| 172 | + font-size: var(--font-size-small); |
| 173 | + padding: var(--spacing-1) var(--spacing-2); |
| 174 | + } |
| 175 | +
|
| 176 | + &.g-medium { |
| 177 | + font-size: var(--font-size-medium); |
| 178 | + padding: var(--spacing-1) var(--spacing-2); |
| 179 | + } |
| 180 | +
|
| 181 | + &.g-large { |
| 182 | + font-size: var(--font-size-large); |
| 183 | + padding: var(--spacing-2) var(--spacing-3); |
| 184 | + } |
| 185 | +
|
| 186 | + // colors |
| 187 | + &.g-grey { |
| 188 | + background-color: var(--color-grey-200); |
| 189 | + border-color: var(--color-grey-300); |
| 190 | + color: var(--color-grey-800); |
| 191 | +
|
| 192 | + &:hover, |
| 193 | + &:focus-visible { |
| 194 | + background-color: var(--color-grey-300); |
| 195 | + border-color: var(--color-grey-400); |
| 196 | +
|
| 197 | + &:active { |
| 198 | + background-color: var(--color-grey-400); |
| 199 | + border-color: var(--color-grey-600); |
| 200 | + } |
| 201 | + } |
| 202 | +
|
| 203 | + &:focus-visible { |
| 204 | + border-color: var(--color-grey-600); |
| 205 | + } |
| 206 | + } |
| 207 | +
|
| 208 | + @each $color in "blue", "green", "red", "yellow", "orange" { |
| 209 | + &.g-#{$color} { |
| 210 | + background-color: var(--color-#{$color}-600); |
| 211 | + border-color: var(--color-#{$color}-600); |
| 212 | + color: var(--color-#{$color}-100); |
| 213 | +
|
| 214 | + &:hover, |
| 215 | + &:focus-visible { |
| 216 | + background-color: var(--color-#{$color}-700); |
| 217 | + border-color: var(--color-#{$color}-700); |
| 218 | +
|
| 219 | + &:active { |
| 220 | + background-color: var(--color-#{$color}-600); |
| 221 | + color: var(--color-#{$color}-100); |
| 222 | + } |
| 223 | + } |
| 224 | +
|
| 225 | + &:focus-visible { |
| 226 | + border-color: var(--color-#{$color}-900); |
| 227 | + } |
| 228 | + } |
| 229 | +
|
| 230 | + &.g-outline:not(.g-pressed).g-#{$color} { |
| 231 | + border-color: var(--color-#{$color}-600); |
| 232 | + color: var(--color-#{$color}-600); |
| 233 | +
|
| 234 | + &:hover { |
| 235 | + background-color: var(--color-#{$color}-600); |
| 236 | + border-color: var(--color-#{$color}-600); |
| 237 | + color: var(--color-#{$color}-100); |
| 238 | + } |
| 239 | +
|
| 240 | + &:focus-visible { |
| 241 | + border-color: var(--color-#{$color}-900); |
| 242 | + background-color: var(--color-#{$color}-200); |
| 243 | + color: var(--color-#{$color}-600); |
| 244 | + } |
| 245 | + } |
| 246 | + } |
| 247 | +
|
| 248 | + &.g-outline:not(.g-pressed) { |
| 249 | + background-color: var(--background-color); |
| 250 | + } |
| 251 | +
|
| 252 | + &.g-disabled { |
| 253 | + background-color: var(--color-grey-100); |
| 254 | + border-color: var(--color-grey-200); |
| 255 | + color: var(--color-grey-500); |
| 256 | +
|
| 257 | + &:hover, |
| 258 | + &:focus-visible { |
| 259 | + background-color: var(--color-grey-100); |
| 260 | + border-color: var(--color-grey-200); |
| 261 | +
|
| 262 | + &:active { |
| 263 | + background-color: var(--color-grey-100); |
| 264 | + border-color: var(--color-grey-200); |
| 265 | + color: var(--color-grey-500); |
| 266 | + } |
| 267 | + } |
| 268 | +
|
| 269 | + &:focus-visible { |
| 270 | + border-color: var(--color-grey-500); |
| 271 | + } |
| 272 | +
|
| 273 | + &.g-outline { |
| 274 | + background-color: var(--background-color); |
| 275 | + border-color: var(--color-grey-400); |
| 276 | + color: var(--color-grey-400); |
| 277 | +
|
| 278 | + &:hover, |
| 279 | + &:focus, |
| 280 | + &:focus-visible { |
| 281 | + background-color: var(--background-color); |
| 282 | + border-color: var(--color-grey-400); |
| 283 | + color: var(--color-grey-400); |
| 284 | + } |
| 285 | +
|
| 286 | + &:focus-visible { |
| 287 | + border-color: var(--color-grey-800); |
| 288 | + background-color: var(--background-color); |
| 289 | + color: var(--color-grey-500); |
| 290 | + } |
| 291 | + } |
| 292 | + } |
| 293 | +
|
| 294 | + // variants |
| 295 | + &.g-inline { |
| 296 | + display: inline; |
| 297 | + padding-top: 0; |
| 298 | + padding-bottom: 0; |
| 299 | + } |
| 300 | +
|
| 301 | + &.g-pill { |
| 302 | + border-radius: 100rem; |
| 303 | + } |
| 304 | +
|
| 305 | + &.g-icon-only { |
| 306 | + aspect-ratio: 1; |
| 307 | + display: inline-flex; |
| 308 | + justify-content: center; |
| 309 | +
|
| 310 | + &.g-small, |
| 311 | + &.g-medium { |
| 312 | + padding: var(--spacing-1); |
| 313 | + } |
| 314 | +
|
| 315 | + &.g-large { |
| 316 | + padding: var(--spacing-2); |
| 317 | + } |
| 318 | +
|
| 319 | + &.g-inline { |
| 320 | + padding: 2px; |
| 321 | + } |
| 322 | + } |
| 323 | +
|
| 324 | + &.g-transparent { |
| 325 | + border: none; |
| 326 | + background-color: rgb(100% 100% 100% / 0); |
| 327 | +
|
| 328 | + @each $color in "blue", "green", "red", "yellow", "orange" { |
| 329 | + &.g-#{$color} { |
| 330 | + color: var(--color-#{$color}-600); |
| 331 | +
|
| 332 | + &:hover, |
| 333 | + &:focus, |
| 334 | + &:focus-visible { |
| 335 | + background-color: var(--color-#{$color}-600); |
| 336 | + color: var(--color-#{$color}-100); |
| 337 | + } |
| 338 | + } |
| 339 | + } |
| 340 | + } |
| 341 | +} |
| 342 | +</style> |
0 commit comments