@datametria/vue-components
Version:
DATAMETRIA Vue.js 3 Component Library with Multi-Brand Theming - 51 components + 10 composables with theming support, WCAG 2.2 AA, dark mode, responsive system
440 lines (364 loc) • 10.9 kB
text/typescript
import { ref, computed } from 'vue'
type HapticType = 'light' | 'medium' | 'heavy' | 'selection' | 'impact' | 'notification'
type NotificationType = 'success' | 'warning' | 'error'
interface HapticOptions {
enabled?: boolean
fallbackToVisual?: boolean
visualDuration?: number
}
/**
* Composable para feedback háptico e visual
* Suporta dispositivos iOS/Android com fallback visual
*/
export function useHapticFeedback(options: HapticOptions = {}) {
const {
enabled = true,
fallbackToVisual = true,
visualDuration = 150
} = options
const isSupported = ref(false)
const isEnabled = ref(enabled)
const activeVisualFeedback = ref<string | null>(null)
// Detectar suporte a haptic feedback
const detectHapticSupport = () => {
// Haptic Feedback API (iOS/Android)
if ('hapticFeedback' in navigator && (navigator as any).hapticFeedback) {
return true
}
// Web Vibration API
if ('vibrate' in navigator && typeof (navigator as any).vibrate === 'function') {
return true
}
return false
}
// Executar vibração nativa
const executeNativeVibration = (pattern: number | number[]) => {
if (!('vibrate' in navigator) || typeof (navigator as any).vibrate !== 'function') return false
try {
return (navigator as any).vibrate(pattern)
} catch (error) {
console.warn('Haptic feedback failed:', error)
return false
}
}
// Feedback visual como fallback
const executeVisualFeedback = (type: HapticType, element?: HTMLElement) => {
if (!fallbackToVisual) return
const feedbackId = `visual-${Date.now()}`
activeVisualFeedback.value = feedbackId
// Aplicar efeito visual ao elemento
if (element) {
const originalTransform = element.style.transform
const originalTransition = element.style.transition
element.style.transition = 'transform 75ms ease-out'
switch (type) {
case 'light':
element.style.transform = 'scale(0.98)'
break
case 'medium':
element.style.transform = 'scale(0.95)'
break
case 'heavy':
element.style.transform = 'scale(0.92)'
break
case 'selection':
element.style.transform = 'scale(1.02)'
break
default:
element.style.transform = 'scale(0.97)'
}
setTimeout(() => {
element.style.transform = originalTransform
setTimeout(() => {
element.style.transition = originalTransition
if (activeVisualFeedback.value === feedbackId) {
activeVisualFeedback.value = null
}
}, 75)
}, 75)
}
// Limpar feedback visual após duração
setTimeout(() => {
if (activeVisualFeedback.value === feedbackId) {
activeVisualFeedback.value = null
}
}, visualDuration)
}
// Feedback leve (toque suave)
const light = (element?: HTMLElement) => {
if (!isEnabled.value) return
let success = false
if (isSupported.value) {
success = executeNativeVibration(10)
}
if (!success && fallbackToVisual) {
executeVisualFeedback('light', element)
}
}
// Feedback médio (toque moderado)
const medium = (element?: HTMLElement) => {
if (!isEnabled.value) return
let success = false
if (isSupported.value) {
success = executeNativeVibration(20)
}
if (!success && fallbackToVisual) {
executeVisualFeedback('medium', element)
}
}
// Feedback pesado (toque forte)
const heavy = (element?: HTMLElement) => {
if (!isEnabled.value) return
let success = false
if (isSupported.value) {
success = executeNativeVibration(40)
}
if (!success && fallbackToVisual) {
executeVisualFeedback('heavy', element)
}
}
// Feedback de seleção
const selection = (element?: HTMLElement) => {
if (!isEnabled.value) return
let success = false
if (isSupported.value) {
success = executeNativeVibration(5)
}
if (!success && fallbackToVisual) {
executeVisualFeedback('selection', element)
}
}
// Feedback de impacto
const impact = (intensity: 'light' | 'medium' | 'heavy' = 'medium', element?: HTMLElement) => {
switch (intensity) {
case 'light':
light(element)
break
case 'medium':
medium(element)
break
case 'heavy':
heavy(element)
break
}
}
// Feedback de notificação
const notification = (type: NotificationType, element?: HTMLElement) => {
if (!isEnabled.value) return
let pattern: number[]
switch (type) {
case 'success':
pattern = [10, 50, 10]
break
case 'warning':
pattern = [20, 100, 20, 100, 20]
break
case 'error':
pattern = [50, 100, 50, 100, 50]
break
}
let success = false
if (isSupported.value) {
success = executeNativeVibration(pattern)
}
if (!success && fallbackToVisual) {
executeVisualFeedback('notification', element)
}
}
// Padrão customizado
const custom = (pattern: number | number[], element?: HTMLElement) => {
if (!isEnabled.value) return
let success = false
if (isSupported.value) {
success = executeNativeVibration(pattern)
}
if (!success && fallbackToVisual) {
executeVisualFeedback('medium', element)
}
}
// Habilitar/desabilitar feedback
const enable = () => {
isEnabled.value = true
}
const disable = () => {
isEnabled.value = false
}
const toggle = () => {
isEnabled.value = !isEnabled.value
}
// Computed
const canVibrate = computed(() => isSupported.value && isEnabled.value)
const hasVisualFeedback = computed(() => activeVisualFeedback.value !== null)
// Método principal para feedback háptico
const triggerHaptic = (type: HapticType | NotificationType = 'light') => {
if (!isEnabled.value) return false
// Tentar haptic feedback nativo primeiro
if ('hapticFeedback' in navigator && (navigator as any).hapticFeedback) {
try {
return (navigator as any).hapticFeedback.impact(type)
} catch (error) {
console.warn('Haptic feedback failed:', error)
}
}
// Fallback para vibration API
if ('vibrate' in navigator && typeof (navigator as any).vibrate === 'function') {
let pattern: number | number[]
switch (type) {
case 'light':
pattern = 10
break
case 'medium':
pattern = 20
break
case 'heavy':
pattern = 30
break
case 'success':
pattern = [10, 50, 10]
break
case 'warning':
pattern = [20, 100, 20]
break
case 'error':
pattern = [50, 100, 50, 100, 50]
break
default:
pattern = 10
}
try {
return (navigator as any).vibrate(pattern)
} catch (error) {
console.warn('Vibration failed:', error)
return false
}
}
return false
}
// Injetar CSS para animações visuais
const injectVisualCSS = () => {
const styleId = 'haptic-keyframes'
if (document.getElementById(styleId)) return
const style = document.createElement('style')
style.id = styleId
style.textContent = `
@keyframes haptic-pulse-light {
0% { transform: scale(1); }
50% { transform: scale(0.98); }
100% { transform: scale(1); }
}
@keyframes haptic-pulse-medium {
0% { transform: scale(1); }
50% { transform: scale(0.95); }
100% { transform: scale(1); }
}
@keyframes haptic-pulse-heavy {
0% { transform: scale(1); }
50% { transform: scale(0.92); }
100% { transform: scale(1); }
}
@keyframes haptic-success {
0% { transform: scale(1); filter: brightness(1); }
50% { transform: scale(1.02); filter: brightness(1.1); }
100% { transform: scale(1); filter: brightness(1); }
}
@keyframes haptic-error {
0% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
@keyframes haptic-pulse-light,
@keyframes haptic-pulse-medium,
@keyframes haptic-pulse-heavy,
@keyframes haptic-success,
@keyframes haptic-error {
0%, 100% { transform: none; filter: none; }
}
}
`
document.head.appendChild(style)
}
// Feedback visual
const triggerVisualFeedback = (element: HTMLElement | null, type: HapticType | NotificationType) => {
if (!element) return
injectVisualCSS()
const originalAnimation = element.style.animation
let animation: string
let duration: number
switch (type) {
case 'light':
animation = 'haptic-pulse-light 0.15s ease-out'
duration = 150
break
case 'medium':
animation = 'haptic-pulse-medium 0.2s ease-out'
duration = 200
break
case 'heavy':
animation = 'haptic-pulse-heavy 0.25s ease-out'
duration = 250
break
case 'success':
animation = 'haptic-success 0.3s ease-out'
duration = 300
break
case 'warning':
animation = 'haptic-pulse-medium 0.2s ease-out'
duration = 200
break
case 'error':
animation = 'haptic-error 0.4s ease-out'
duration = 400
break
default:
animation = 'haptic-pulse-light 0.15s ease-out'
duration = 150
}
element.style.animation = animation
setTimeout(() => {
element.style.animation = originalAnimation
}, duration)
}
// Feedback combinado (háptico + visual)
const feedback = (element: HTMLElement | null, type: HapticType | NotificationType) => {
const hapticResult = triggerHaptic(type)
triggerVisualFeedback(element, type)
return hapticResult
}
// Verificar suporte (método público)
const checkSupport = () => {
const hasSupport = detectHapticSupport()
isSupported.value = hasSupport
return hasSupport
}
// Inicializar detecção de suporte
checkSupport()
return {
// Estado
isSupported,
isEnabled,
canVibrate,
hasVisualFeedback,
activeVisualFeedback,
// Métodos principais (compatíveis com testes)
triggerHaptic,
triggerVisualFeedback,
feedback,
checkSupport,
// Métodos de feedback específicos
light,
medium,
heavy,
selection,
impact,
notification,
custom,
// Controles
enable,
disable,
toggle,
// Utilitários
detectHapticSupport
}
}