:has() की मदद से पैरंट एलिमेंट को स्टाइल करना

वेब की सुविधाएं: :user-valid और :user-invalid

समस्या

अक्सर, गड़बड़ी वाले पेज की स्थिति के लिए, इनपुट के बाहर मौजूद एलिमेंट को स्टाइल करने की ज़रूरत होती है. उदाहरण के लिए, पैरंट fieldset के बॉर्डर का रंग बदलना, <label> को हाइलाइट करना या कार्ड हेडर में ग्लोबल गड़बड़ी का आइकॉन दिखाना. पहले, पैरंट एलिमेंट पर क्लास को टॉगल करने के लिए, JavaScript की ज़रूरत होती थी.

समस्या का हल

:has() को :user-invalid के साथ जोड़कर, हम किसी भी पैरंट एलिमेंट को उसके चाइल्ड एलिमेंट की वैलिडिटी के आधार पर स्टाइल कर सकते हैं. इससे सीएसएस में सभी प्रज़ेंटेशन लॉजिक बने रहते हैं.

लागू करने की रणनीति

  1. सिलेक्टर: कंटेनर को टारगेट करने के लिए, .parent:has(:user-invalid) का इस्तेमाल करें.
  2. स्कोप: परफ़ॉर्मेंस की समस्याओं से बचने के लिए, सटीक जानकारी दें. body के बजाय .field-group को टारगेट करें.
  3. फ़ॉलबैक: अगर :has() काम नहीं करता है, तो पैरंट पर क्लास को टॉगल करने के लिए JS की ज़रूरत होती है.

लागू करने के बारे में जानकारी

1. एचटीएमएल स्ट्रक्चर

<form>
  <div class="card-section">
    <div class="header">
      <h3>Profile Settings</h3>
      <span class="status-icon"></span>
    </div>

    <div class="field">
      <label for="username">Username</label>
      <input type="text" id="username" required>
    </div>
  </div>
</form>

2. सीएसएस

/* Default State */
.card-section {
  border: 1px solid #ccc;
  border-left: 4px solid #ccc;
}

/*
  Parent Styling Logic:
  If the card contains ANY user-invalid input, turn the whole card's edge red.
*/
.card-section:has(:user-invalid) {
  border-left-color: #d93025;
  background-color: #fff8f8;
}

/* Change the icon too */
.card-section:has(:user-invalid) .status-icon::after {
  content: "⚠️";
}

फ़ॉलबैक और ब्राउज़र से जुड़ी सहायता

फ़ॉलबैक के लिए सीएसएस

हम पैरंट पर क्लास .has-error का इस्तेमाल करते हैं, ताकि :has() के व्यवहार की नकल की जा सके.

/* Native */
.card-section:has(:user-invalid) {
  border-left-color: #d93025;
}

/* Fallback */
.card-section.has-error-fallback {
  border-left-color: #d93025;
}

JavaScript फ़ॉलबैक

फिर से इस्तेमाल की जा सकने वाली ऐसी यूटिलिटी का इस्तेमाल करें जो WeakMap का इस्तेमाल करके, इंटरैक्शन की स्थिति को ट्रैक करती है. इससे डीओएम को "डर्टी" क्लास या डेटा एट्रिब्यूट से प्रदूषित होने से बचाया जा सकता है.

const UserInvalidFallback = (() => {
  const dirtyState = new WeakMap();

  const updateState = (input) => {
    const isValid = input.checkValidity();

    // Update both visual and ARIA state
    input.classList.toggle('user-invalid-fallback', !isValid);
    input.classList.toggle('user-valid-fallback', isValid);

    if (!isValid) {
      input.setAttribute('aria-invalid', 'true');
    } else {
      input.removeAttribute('aria-invalid');
    }
  };

  const handleEvent = (event) => {
    const input = event.target;

    if (event.type === 'reset') {
      const controls = input.elements || [];
      for (const control of controls) {
        dirtyState.delete(control);
        control.classList.remove('user-invalid-fallback');
        control.classList.remove('user-valid-fallback');
        control.removeAttribute('aria-invalid');
      }
      return;
    }

    if (!input.checkValidity) return;

    if (event.type === 'input' || event.type === 'change') {
      const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
      state.hasInteracted = true;
      dirtyState.set(input, state);
      if (state.hasBlurred) {
        updateState(input);
      }
    } else if (event.type === 'blur') {
      const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
      state.hasBlurred = true;
      dirtyState.set(input, state);
      if (state.hasInteracted) {
        updateState(input);
      }
    }
  };

  const init = (root = document) => {
    if (CSS.supports('selector(:user-invalid)')) return;

    root.addEventListener('blur', handleEvent, true); // Capture phase
    root.addEventListener('input', handleEvent);
    root.addEventListener('change', handleEvent);
    root.addEventListener('reset', handleEvent, true); // Capture resets
  };

  return { init };
})();

// Initialize for a specific form
const form = document.querySelector('#demo-form');
UserInvalidFallback.init(form);
// 1. Initialize the generic fallback
const form = document.querySelector('#demo-form');
UserInvalidFallback.init(form);

// 2. Add specialized "parent styling" logic (Separate from fallback)
// Listen for changes to form validity after interaction
form.addEventListener('blur', (e) => {
  if (!e.target.matches('input, select, textarea')) return;

  // Find the container we want to style (sync with CSS)
  const container = e.target.closest('.card-section');
  if (!container) return;

  // Check if ANY fallbacked input in this container is invalid
  const hasError = container.querySelector('.user-invalid-fallback');
  container.classList.toggle('has-error-fallback', !!hasError);
}, true); // Capture phase to ensure we run after the fallback's blur listener

// Also handle input events for immediate cleanup
form.addEventListener('input', (e) => {
  const container = e.target.closest('.card-section');
  if (container) {
    const hasError = container.querySelector('.user-invalid-fallback');
    container.classList.toggle('has-error-fallback', !!hasError);
  }
});

// Handle form resets
form.addEventListener('reset', () => {
  form.querySelectorAll('.has-error-fallback').forEach(el => {
    el.classList.remove('has-error-fallback');
  });
});

ध्यान में रखने लायक अन्य बातें

  1. सुलभता: नेटिव :user-invalid, ARIA एट्रिब्यूट के साथ अपने-आप सिंक नहीं होता. aria-invalid को विज़ुअल स्टेट के साथ सिंक करने के लिए, यह JavaScript जोड़ें:
// Sync aria-invalid with the CSS :user-invalid state
const syncAria = (el) => {
  el.setAttribute?.('aria-invalid', el.matches(':user-invalid') ? 'true' : 'false');
};

// Update on blur (to show error) and input (to clear it)
document.addEventListener('blur', (e) => syncAria(e.target), true);
document.addEventListener('input', (e) => {
  if (e.target.hasAttribute('aria-invalid')) syncAria(e.target);
});