CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-material--ripple

Material Design ink ripple effect component for web element interactions with JavaScript and CSS-only implementations

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

utilities.mddocs/

Utilities

The utilities module provides helper functions for CSS variable support detection and event coordinate normalization. These functions handle browser compatibility issues and provide consistent behavior across different input types.

Capabilities

CSS Variable Support Detection

supportsCssVariables

Detects CSS custom property support with Safari 9 compatibility handling.

/**
 * Detect CSS custom property support with Safari 9 compatibility handling
 * Includes workarounds for WebKit CSS variable bugs and caches results for performance
 * @param windowObj - Window object to test CSS support on
 * @param forceRefresh - Force re-detection instead of using cached result (default: false)
 * @returns True if CSS custom properties are fully supported
 */
function supportsCssVariables(windowObj: typeof globalThis, forceRefresh?: boolean): boolean;

Usage Examples:

import { supportsCssVariables } from "@material/ripple/util";

// Basic usage
const supportsVars = supportsCssVariables(window);
if (supportsVars) {
  // Use CSS variable-based ripple
  console.log("CSS variables supported");
} else {
  // Fall back to simpler CSS-only ripple
  console.log("CSS variables not supported");
}

// Force fresh detection (mainly for testing)
const freshResult = supportsCssVariables(window, true);

// Use in adapter implementation
const adapter = {
  browserSupportsCssVars: () => supportsCssVariables(window),
  // ... other methods
};

Safari 9 Compatibility:

This function includes special handling for Safari 9, which has partial CSS variable support that doesn't work correctly with pseudo-elements. The function detects this specific case and returns false for Safari 9 to ensure proper fallback behavior.

// The function tests both:
// 1. CSS.supports('--css-vars', 'yes') - basic CSS variable support
// 2. CSS.supports('color', '#00000000') - 8-digit hex color support
// 
// Safari 10+ supports both, Safari 9 only supports the first
// This allows detection of Safari 9's broken CSS variable implementation

Event Coordinate Normalization

getNormalizedEventCoords

Normalizes touch and mouse event coordinates relative to the target element.

/**
 * Get normalized event coordinates relative to target element
 * Handles both touch and mouse events with proper coordinate calculation
 * @param evt - Touch or mouse event (undefined returns {x: 0, y: 0})
 * @param pageOffset - Current window scroll offset
 * @param clientRect - Target element's bounding rectangle
 * @returns Normalized coordinates relative to element
 */
function getNormalizedEventCoords(
  evt: Event | undefined, 
  pageOffset: MDCRipplePoint, 
  clientRect: DOMRect
): MDCRipplePoint;

Usage Examples:

import { getNormalizedEventCoords } from "@material/ripple/util";

// In an event handler
function handleActivation(evt: Event) {
  const pageOffset = { x: window.pageXOffset, y: window.pageYOffset };
  const clientRect = element.getBoundingClientRect();
  
  const coords = getNormalizedEventCoords(evt, pageOffset, clientRect);
  
  console.log(`Ripple origin: ${coords.x}, ${coords.y}`);
  
  // Use coordinates for ripple positioning
}

// Touch event handling
element.addEventListener('touchstart', (evt: TouchEvent) => {
  const coords = getNormalizedEventCoords(
    evt,
    { x: window.pageXOffset, y: window.pageYOffset },
    element.getBoundingClientRect()
  );
  // coords.x and coords.y are relative to element's top-left corner
});

// Mouse event handling  
element.addEventListener('mousedown', (evt: MouseEvent) => {
  const coords = getNormalizedEventCoords(
    evt,
    { x: window.pageXOffset, y: window.pageYOffset },
    element.getBoundingClientRect()
  );
  // Same interface for both touch and mouse events
});

// Handle undefined events (programmatic activation)
const defaultCoords = getNormalizedEventCoords(undefined, pageOffset, clientRect);
console.log(defaultCoords); // { x: 0, y: 0 }

Coordinate Calculation:

The function performs different calculations based on event type:

// For touch events: uses changedTouches[0].pageX/pageY
// For mouse events: uses pageX/pageY  
// For undefined events: returns { x: 0, y: 0 }

// Final calculation:
// normalizedX = event.pageX - (pageOffset.x + clientRect.left)
// normalizedY = event.pageY - (pageOffset.y + clientRect.top)

Advanced Usage

Custom CSS Variable Detection

import { supportsCssVariables } from "@material/ripple/util";

class CustomRippleImplementation {
  private cssVarsSupported: boolean;
  
  constructor(private element: HTMLElement) {
    this.cssVarsSupported = supportsCssVariables(window);
    
    if (this.cssVarsSupported) {
      this.setupAdvancedRipple();
    } else {
      this.setupFallbackRipple();
    }
  }
  
  private setupAdvancedRipple() {
    // Use CSS variables for dynamic ripple effects
    this.element.style.setProperty('--ripple-color', 'rgba(0, 0, 0, 0.1)');
    console.log('Using CSS variable-based ripple');
  }
  
  private setupFallbackRipple() {
    // Use static CSS classes for ripple effects
    this.element.classList.add('ripple-fallback');
    console.log('Using CSS-only fallback ripple');
  }
}

Event Coordinate Processing

import { getNormalizedEventCoords } from "@material/ripple/util";

class RippleAnimationController {
  constructor(private element: HTMLElement) {
    this.setupEventHandlers();
  }
  
  private setupEventHandlers() {
    // Handle multiple event types with unified coordinate processing
    const activationEvents = ['mousedown', 'touchstart', 'pointerdown'];
    
    activationEvents.forEach(eventType => {
      this.element.addEventListener(eventType, (evt) => {
        this.handleActivation(evt);
      });
    });
  }
  
  private handleActivation(evt: Event) {
    const pageOffset = this.getPageOffset();
    const clientRect = this.element.getBoundingClientRect();
    
    // Get normalized coordinates for any event type
    const coords = getNormalizedEventCoords(evt, pageOffset, clientRect);
    
    // Calculate ripple properties based on coordinates
    const centerX = clientRect.width / 2;
    const centerY = clientRect.height / 2;
    
    const distanceFromCenter = Math.sqrt(
      Math.pow(coords.x - centerX, 2) + Math.pow(coords.y - centerY, 2)
    );
    
    // Use coordinates for animation positioning
    this.animateRipple(coords, distanceFromCenter);
  }
  
  private getPageOffset() {
    return {
      x: window.pageXOffset || document.documentElement.scrollLeft,
      y: window.pageYOffset || document.documentElement.scrollTop
    };
  }
  
  private animateRipple(origin: { x: number; y: number }, distance: number) {
    // Custom ripple animation using calculated coordinates
    console.log(`Animating ripple from ${origin.x}, ${origin.y} with distance ${distance}`);
  }
}

Framework Integration Utilities

import { supportsCssVariables, getNormalizedEventCoords } from "@material/ripple/util";

// React hook for ripple utilities
function useRippleUtils() {
  const cssVarsSupported = React.useMemo(
    () => supportsCssVariables(window), 
    []
  );
  
  const normalizeEventCoords = React.useCallback(
    (evt: React.SyntheticEvent, element: HTMLElement) => {
      const nativeEvent = evt.nativeEvent;
      const pageOffset = { x: window.pageXOffset, y: window.pageYOffset };
      const clientRect = element.getBoundingClientRect();
      
      return getNormalizedEventCoords(nativeEvent, pageOffset, clientRect);
    },
    []
  );
  
  return { cssVarsSupported, normalizeEventCoords };
}

// Vue.js composition function
function useRipple() {
  const cssVarsSupported = supportsCssVariables(window);
  
  function handleRippleEvent(evt: Event, element: HTMLElement) {
    const coords = getNormalizedEventCoords(
      evt,
      { x: window.pageXOffset, y: window.pageYOffset },
      element.getBoundingClientRect()
    );
    
    return coords;
  }
  
  return { cssVarsSupported, handleRippleEvent };
}

Testing and Development

import { supportsCssVariables } from "@material/ripple/util";

// Testing CSS variable support in different environments
function testCssVariableSupport() {
  // Test in current environment
  const currentSupport = supportsCssVariables(window);
  console.log(`Current environment supports CSS vars: ${currentSupport}`);
  
  // Force fresh detection (useful for testing)
  const freshSupport = supportsCssVariables(window, true);
  console.log(`Fresh detection result: ${freshSupport}`);
  
  // Mock window object for testing
  const mockWindow = {
    CSS: {
      supports: (property: string, value: string) => {
        if (property === '--css-vars' && value === 'yes') return true;
        if (property === 'color' && value === '#00000000') return false; // Simulate Safari 9
        return false;
      }
    }
  };
  
  const mockSupport = supportsCssVariables(mockWindow as any);
  console.log(`Mock Safari 9 result: ${mockSupport}`); // Should be false
}

// Testing coordinate normalization
function testCoordinateNormalization() {
  const mockEvent = new MouseEvent('mousedown', {
    clientX: 100,
    clientY: 200
  });
  
  // Mock page offset and client rect
  const pageOffset = { x: 50, y: 75 };
  const clientRect = new DOMRect(25, 30, 200, 150);
  
  const coords = getNormalizedEventCoords(mockEvent, pageOffset, clientRect);
  console.log(`Normalized coordinates: ${coords.x}, ${coords.y}`);
  
  // Test with undefined event
  const defaultCoords = getNormalizedEventCoords(undefined, pageOffset, clientRect);
  console.log(`Default coordinates: ${defaultCoords.x}, ${defaultCoords.y}`); // 0, 0
}

Browser Compatibility

CSS Variable Support Matrix

  • Chrome 49+: Full support
  • Firefox 31+: Full support
  • Safari 10+: Full support
  • Safari 9.1: Partial support (detected and disabled by supportsCssVariables)
  • Edge 16+: Full support
  • IE: No support (falls back to CSS-only implementation)

Event Handling Compatibility

  • Touch Events: Supported in all mobile browsers and modern desktop browsers
  • Mouse Events: Universal support
  • Coordinate Normalization: Handles all event types consistently across browsers

The utility functions ensure consistent behavior across all supported browsers by detecting capabilities and providing appropriate fallbacks.

docs

adapter.md

component.md

foundation.md

index.md

sass.md

utilities.md

tile.json