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

adapter.mddocs/

Adapter Interface

The MDCRippleAdapter interface defines the required methods for integrating ripples with different frameworks and DOM environments. It abstracts DOM operations, event handling, and CSS manipulation to enable custom implementations.

Capabilities

MDCRippleAdapter Interface

Integration interface for custom frameworks and components.

/**
 * Adapter interface defining required methods for ripple integration
 * Implement this interface to integrate ripples with custom frameworks
 */
interface MDCRippleAdapter {
  /** Check if browser supports CSS custom properties */
  browserSupportsCssVars(): boolean;
  
  /** Check if this ripple instance is unbounded */
  isUnbounded(): boolean;
  
  /** Check if the surface is currently in active state (:active pseudo-class) */
  isSurfaceActive(): boolean;
  
  /** Check if the surface is disabled */
  isSurfaceDisabled(): boolean;
  
  /** Add CSS class to the ripple surface */
  addClass(className: string): void;
  
  /** Remove CSS class from the ripple surface */
  removeClass(className: string): void;
  
  /** Check if ripple surface contains the given event target */
  containsEventTarget(target: EventTarget | null): boolean;
  
  /** Register event handler on the ripple surface */
  registerInteractionHandler<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void;
  
  /** Remove event handler from the ripple surface */
  deregisterInteractionHandler<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void;
  
  /** Register event handler on document.documentElement */
  registerDocumentInteractionHandler<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void;
  
  /** Remove event handler from document.documentElement */
  deregisterDocumentInteractionHandler<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void;
  
  /** Register window resize event handler */
  registerResizeHandler(handler: SpecificEventListener<'resize'>): void;
  
  /** Remove window resize event handler */
  deregisterResizeHandler(handler: SpecificEventListener<'resize'>): void;
  
  /** Update CSS custom property on the ripple surface */
  updateCssVariable(varName: string, value: string | null): void;
  
  /** Get bounding rectangle of the ripple surface */
  computeBoundingRect(): DOMRect;
  
  /** Get current window scroll offset coordinates */
  getWindowPageOffset(): MDCRipplePoint;
}

Browser Support Detection

browserSupportsCssVars

Detects CSS custom property support with Safari 9 workarounds.

/**
 * Check if browser supports CSS custom properties
 * Should handle Safari 9 compatibility issues with CSS variables
 * @returns True if CSS custom properties are supported
 */
browserSupportsCssVars(): boolean;

Usage Example:

// Standard implementation using the provided utility
import { supportsCssVariables } from "@material/ripple/util";

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

Surface State Detection

isUnbounded

Determines if the ripple should extend beyond element bounds.

/**
 * Check if this ripple instance is unbounded
 * Unbounded ripples extend beyond the element's boundaries
 * @returns True if ripple is unbounded
 */
isUnbounded(): boolean;

isSurfaceActive

Detects if the surface is in an active state (typically :active pseudo-class).

/**
 * Check if the surface is currently in active state
 * Used for keyboard activation detection and proper animation timing
 * @returns True if surface is active (e.g., :active pseudo-class)
 */
isSurfaceActive(): boolean;

isSurfaceDisabled

Checks if the surface should not respond to interactions.

/**
 * Check if the surface is disabled
 * Disabled surfaces should not show ripple effects
 * @returns True if surface is disabled
 */
isSurfaceDisabled(): boolean;

DOM Manipulation

addClass / removeClass

CSS class management for ripple states.

/**
 * Add CSS class to the ripple surface
 * @param className - CSS class name to add
 */
addClass(className: string): void;

/**
 * Remove CSS class from the ripple surface  
 * @param className - CSS class name to remove
 */
removeClass(className: string): void;

containsEventTarget

Event target containment check for nested element handling.

/**
 * Check if ripple surface contains the given event target
 * Used to determine if events should be handled by this ripple instance
 * @param target - Event target to check
 * @returns True if target is within the ripple surface
 */
containsEventTarget(target: EventTarget | null): boolean;

Event Management

registerInteractionHandler / deregisterInteractionHandler

Surface-level event handler management.

/**
 * Register event handler on the ripple surface
 * @param evtType - Event type (e.g., 'click', 'touchstart')
 * @param handler - Event handler function
 */
registerInteractionHandler<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void;

/**
 * Remove event handler from the ripple surface
 * @param evtType - Event type
 * @param handler - Event handler function to remove
 */
deregisterInteractionHandler<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void;

registerDocumentInteractionHandler / deregisterDocumentInteractionHandler

Document-level event handler management for deactivation events.

/**
 * Register event handler on document.documentElement
 * Used for capturing deactivation events outside the surface
 * @param evtType - Event type
 * @param handler - Event handler function
 */
registerDocumentInteractionHandler<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void;

/**
 * Remove event handler from document.documentElement
 * @param evtType - Event type
 * @param handler - Event handler function to remove
 */
deregisterDocumentInteractionHandler<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void;

registerResizeHandler / deregisterResizeHandler

Window resize event management for layout updates.

/**
 * Register window resize event handler
 * Used for unbounded ripples that need layout updates on resize
 * @param handler - Resize event handler function
 */
registerResizeHandler(handler: SpecificEventListener<'resize'>): void;

/**
 * Remove window resize event handler
 * @param handler - Resize event handler function to remove
 */
deregisterResizeHandler(handler: SpecificEventListener<'resize'>): void;

CSS and Layout

updateCssVariable

CSS custom property management for ripple animations.

/**
 * Update CSS custom property on the ripple surface
 * @param varName - CSS custom property name (e.g., '--mdc-ripple-fg-scale')
 * @param value - CSS property value or null to remove
 */
updateCssVariable(varName: string, value: string | null): void;

computeBoundingRect

Element dimension and position calculation.

/**
 * Get bounding rectangle of the ripple surface
 * Used for ripple size calculations and positioning
 * @returns DOMRect with element dimensions and position
 */
computeBoundingRect(): DOMRect;

getWindowPageOffset

Window scroll position for coordinate calculations.

/**
 * Get current window scroll offset coordinates
 * Used for accurate event coordinate calculations
 * @returns Object with x and y scroll offsets
 */
getWindowPageOffset(): MDCRipplePoint;

Implementation Examples

Standard DOM Implementation

import { MDCRippleAdapter, MDCRipplePoint } from "@material/ripple";
import { supportsCssVariables } from "@material/ripple/util";
import { applyPassive } from "@material/dom/events";
import { matches } from "@material/dom/ponyfill";

class StandardRippleAdapter implements MDCRippleAdapter {
  constructor(private element: HTMLElement, private surface: { unbounded?: boolean; disabled?: boolean }) {}
  
  browserSupportsCssVars(): boolean {
    return supportsCssVariables(window);
  }
  
  isUnbounded(): boolean {
    return Boolean(this.surface.unbounded);
  }
  
  isSurfaceActive(): boolean {
    return matches(this.element, ':active');
  }
  
  isSurfaceDisabled(): boolean {
    return Boolean(this.surface.disabled);
  }
  
  addClass(className: string): void {
    this.element.classList.add(className);
  }
  
  removeClass(className: string): void {
    this.element.classList.remove(className);
  }
  
  containsEventTarget(target: EventTarget | null): boolean {
    return this.element.contains(target as Node);
  }
  
  registerInteractionHandler(evtType: string, handler: EventListener): void {
    this.element.addEventListener(evtType, handler, applyPassive());
  }
  
  deregisterInteractionHandler(evtType: string, handler: EventListener): void {
    this.element.removeEventListener(evtType, handler, applyPassive());
  }
  
  registerDocumentInteractionHandler(evtType: string, handler: EventListener): void {
    document.documentElement.addEventListener(evtType, handler, applyPassive());
  }
  
  deregisterDocumentInteractionHandler(evtType: string, handler: EventListener): void {
    document.documentElement.removeEventListener(evtType, handler, applyPassive());
  }
  
  registerResizeHandler(handler: EventListener): void {
    window.addEventListener('resize', handler);
  }
  
  deregisterResizeHandler(handler: EventListener): void {
    window.removeEventListener('resize', handler);
  }
  
  updateCssVariable(varName: string, value: string | null): void {
    this.element.style.setProperty(varName, value);
  }
  
  computeBoundingRect(): DOMRect {
    return this.element.getBoundingClientRect();
  }
  
  getWindowPageOffset(): MDCRipplePoint {
    return { x: window.pageXOffset, y: window.pageYOffset };
  }
}

React Integration Example

import React, { useRef, useEffect } from 'react';
import { MDCRippleFoundation, MDCRippleAdapter } from '@material/ripple';

interface RippleProps {
  unbounded?: boolean;
  disabled?: boolean;
  children: React.ReactNode;
}

export const RippleComponent: React.FC<RippleProps> = ({ 
  unbounded = false, 
  disabled = false, 
  children 
}) => {
  const elementRef = useRef<HTMLDivElement>(null);
  const foundationRef = useRef<MDCRippleFoundation | null>(null);
  
  useEffect(() => {
    if (!elementRef.current) return;
    
    const adapter: MDCRippleAdapter = {
      browserSupportsCssVars: () => CSS.supports('--css-vars', 'yes'),
      isUnbounded: () => unbounded,
      isSurfaceActive: () => elementRef.current?.matches(':active') ?? false,
      isSurfaceDisabled: () => disabled,
      addClass: (className) => elementRef.current?.classList.add(className),
      removeClass: (className) => elementRef.current?.classList.remove(className),
      containsEventTarget: (target) => elementRef.current?.contains(target as Node) ?? false,
      registerInteractionHandler: (evtType, handler) => {
        elementRef.current?.addEventListener(evtType, handler);
      },
      deregisterInteractionHandler: (evtType, handler) => {
        elementRef.current?.removeEventListener(evtType, handler);
      },
      registerDocumentInteractionHandler: (evtType, handler) => {
        document.documentElement.addEventListener(evtType, handler);
      },
      deregisterDocumentInteractionHandler: (evtType, handler) => {
        document.documentElement.removeEventListener(evtType, handler);
      },
      registerResizeHandler: (handler) => {
        window.addEventListener('resize', handler);
      },
      deregisterResizeHandler: (handler) => {
        window.removeEventListener('resize', handler);
      },
      updateCssVariable: (varName, value) => {
        elementRef.current?.style.setProperty(varName, value);
      },
      computeBoundingRect: () => {
        return elementRef.current?.getBoundingClientRect() ?? new DOMRect();
      },
      getWindowPageOffset: () => ({
        x: window.pageXOffset,
        y: window.pageYOffset
      })
    };
    
    foundationRef.current = new MDCRippleFoundation(adapter);
    foundationRef.current.init();
    
    return () => {
      foundationRef.current?.destroy();
    };
  }, [unbounded, disabled]);
  
  return (
    <div ref={elementRef} className="ripple-surface">
      {children}
    </div>
  );
};

Custom Framework Adapter

// Example for a hypothetical framework
class CustomFrameworkRippleAdapter implements MDCRippleAdapter {
  constructor(
    private component: CustomFrameworkComponent,
    private element: FrameworkElement
  ) {}
  
  browserSupportsCssVars(): boolean {
    return this.component.framework.supportsCssVars();
  }
  
  isUnbounded(): boolean {
    return this.component.getProperty('unbounded');
  }
  
  isSurfaceActive(): boolean {
    return this.component.hasState('active');
  }
  
  isSurfaceDisabled(): boolean {
    return this.component.getProperty('disabled');
  }
  
  addClass(className: string): void {
    this.element.addClass(className);
  }
  
  removeClass(className: string): void {
    this.element.removeClass(className);
  }
  
  containsEventTarget(target: EventTarget | null): boolean {
    return this.element.contains(target);
  }
  
  registerInteractionHandler(evtType: string, handler: EventListener): void {
    this.element.on(evtType, handler);
  }
  
  deregisterInteractionHandler(evtType: string, handler: EventListener): void {
    this.element.off(evtType, handler);
  }
  
  // ... implement remaining methods using framework APIs
}

docs

adapter.md

component.md

foundation.md

index.md

sass.md

utilities.md

tile.json