Hone logo
Hone
Problems

Implement a useContrastMode Hook for Dynamic Contrast Adjustment

This challenge involves creating a custom React hook, useContrastMode, that allows users to toggle between a normal and a high-contrast mode for a web application. This is crucial for accessibility, providing users with visual impairments a more comfortable and readable experience.

Problem Description

You need to implement a custom React hook named useContrastMode that manages and applies a contrast mode to your application. This hook should:

  1. Maintain State: Keep track of whether the high-contrast mode is currently enabled or disabled.
  2. Provide Toggle Functionality: Offer a function to easily switch between the normal and high-contrast modes.
  3. Apply CSS Class: Dynamically add or remove a specific CSS class (e.g., contrast-mode) to a designated element (typically the body or a root container div) based on the current contrast mode state.
  4. Persist State (Optional but Recommended): Ideally, the hook should persist the user's preference across page reloads, possibly using localStorage.

Key Requirements:

  • The hook must be written in TypeScript.
  • It should return an object containing:
    • isContrastModeEnabled: A boolean indicating the current state.
    • toggleContrastMode: A function to flip the state.
  • The hook should accept an optional parameter to specify the localStorage key for persistence.
  • The hook should accept an optional parameter to specify the selector for the element to which the contrast class will be applied. Defaults to document.body.

Expected Behavior:

  • When isContrastModeEnabled is true, the designated HTML element should have the specified contrast class added.
  • When isContrastModeEnabled is false, the contrast class should be removed.
  • Calling toggleContrastMode should invert the isContrastModeEnabled state and update the DOM accordingly.
  • If localStorage is used, the chosen mode should be restored when the hook is initialized on subsequent visits.

Edge Cases:

  • No localStorage support: The hook should gracefully handle environments where localStorage is not available (e.g., private browsing modes or certain browser configurations).
  • Initial Load: Ensure the correct contrast class is applied immediately on the first render if a preference is found in localStorage.
  • Dynamic Target Element: If the target element is not the body, ensure it's correctly identified and the class is applied to it.

Examples

Example 1: Basic Usage

Assume you have a simple app structure and some CSS:

/* styles.css */
.contrast-mode {
  filter: invert(1) hue-rotate(180deg); /* A common high-contrast effect */
  background-color: #f0f0f0;
  color: #000;
}

body {
  background-color: #fff;
  color: #333;
}
// App.tsx
import React from 'react';
import useContrastMode from './useContrastMode'; // Assuming hook is in this file

function App() {
  const { isContrastModeEnabled, toggleContrastMode } = useContrastMode();

  return (
    <div>
      <h1>My Awesome App</h1>
      <p>This is some content that will change appearance.</p>
      <button onClick={toggleContrastMode}>
        {isContrastModeEnabled ? 'Disable High Contrast' : 'Enable High Contrast'}
      </button>
      <p>Current Contrast Mode: {isContrastModeEnabled ? 'Enabled' : 'Disabled'}</p>
    </div>
  );
}

export default App;

Input (User Action): User clicks the "Enable High Contrast" button.

Expected Output (DOM State): The body element will have the class contrast-mode added. The button text will change to "Disable High Contrast". The paragraph showing the current mode will update.

Explanation: The useContrastMode hook initializes isContrastModeEnabled to false. Clicking the button calls toggleContrastMode, which sets isContrastModeEnabled to true, adds the contrast-mode class to the body, and updates the UI.

Example 2: With localStorage Persistence

Consider the same setup as Example 1, but useContrastMode is initialized with a localStorage key.

// useContrastMode.ts
// ... (hook implementation using localStorage)

// App.tsx
import React from 'react';
import useContrastMode from './useContrastMode';

function App() {
  // Persist preference using localStorage key 'myAppContrastPref'
  const { isContrastModeEnabled, toggleContrastMode } = useContrastMode({
    localStorageKey: 'myAppContrastPref',
  });

  return (
    <div>
      <h1>Accessible App</h1>
      <button onClick={toggleContrastMode}>
        {isContrastModeEnabled ? 'Disable High Contrast' : 'Enable High Contrast'}
      </button>
    </div>
  );
}

export default App;

Input:

  1. User enables high contrast mode. localStorage.setItem('myAppContrastPref', 'true') is called internally.
  2. User refreshes the page.

Expected Output: The body element will immediately have the contrast-mode class applied upon page load, and the button will display "Disable High Contrast".

Explanation: On the first interaction, the hook updates the localStorage. On subsequent loads, the hook reads the value from localStorage and initializes isContrastModeEnabled accordingly, applying the class before any user interaction.

Example 3: Targeting a Specific Element

Imagine a dashboard where only a specific content area should be affected by contrast mode.

// App.tsx
import React from 'react';
import useContrastMode from './useContrastMode';

function App() {
  const { isContrastModeEnabled, toggleContrastMode } = useContrastMode({
    targetSelector: '#main-content', // Target a specific div
    contrastClassName: 'high-contrast-content', // Use a different class name
    localStorageKey: 'dashboardContrast'
  });

  return (
    <div>
      <header>Header Content</header>
      <button onClick={toggleContrastMode}>
        {isContrastModeEnabled ? 'Disable Contrast' : 'Enable Contrast'}
      </button>
      <main id="main-content" className="content-area"> {/* Element to be targeted */}
        <h2>Main Content Area</h2>
        <p>This text inside the main content will be affected.</p>
      </main>
      <footer>Footer Content</footer>
    </div>
  );
}

export default App;
/* styles.css */
.high-contrast-content {
  filter: sepia(100%) saturate(150%); /* Different contrast effect */
  border: 2px solid darkred;
}

.content-area {
  padding: 20px;
  border: 1px solid #ccc;
}

Input (User Action): User clicks "Enable Contrast".

Expected Output (DOM State): The <main id="main-content"> element will have the class high-contrast-content added. The button text will change.

Explanation: The hook identifies the element with id="main-content" using targetSelector and applies the high-contrast-content class to it, instead of the body.

Constraints

  • The hook must be written in TypeScript.
  • The hook should not rely on any specific CSS framework.
  • The core functionality (state management, toggle) should be independent of localStorage.
  • localStorage should only be used for persistence and should be handled in a way that doesn't break the app if localStorage is unavailable.
  • The targetSelector should resolve to a single DOM element. If multiple elements match, the hook may choose to apply the class to the first one or throw an error (clarify chosen behavior in implementation).

Notes

  • Consider how you will handle the initial state. Should it default to false or try to read from localStorage immediately?
  • Think about the order of operations: when the class is added/removed (e.g., useEffect is appropriate here).
  • For the localStorage persistence, ensure you are converting the string value from localStorage back to a boolean correctly.
  • The contrastClassName should be configurable to allow for flexibility in styling.
  • Consider adding a cleanup function in useEffect to remove the class when the component unmounts, although for a global state like contrast mode applied to body, this might not be strictly necessary if the hook is intended to be used at the root of the application.
Loading editor...
typescript