टू-डाइमेंशनल पैनिंग

वेब सुविधाएं: scroll-axis-lock overscroll-behavior

जब कोई उपयोगकर्ता दो डाइमेंशन वाले स्क्रोल कंटेनर को स्क्रोल करता है, तो ब्राउज़र आम तौर पर स्क्रोल ऐक्सिस-लॉकिंग ह्यूरिस्टिक लागू करते हैं. इसे "रेलिंग" भी कहा जाता है. अगर कोई जेस्चर, लंबवत ऐक्सिस के मुकाबले किसी एक ऐक्सिस पर ज़्यादा मूवमेंट के साथ शुरू होता है, तो ब्राउज़र स्क्रोल को उस हॉरिज़ॉन्टल या वर्टिकल ऐक्सिस पर लॉक कर देता है. साथ ही, लंबवत डेल्टा को अनदेखा कर देता है. आम तौर पर, ऐसा जेस्चर के बाकी हिस्से के लिए किया जाता है.

ऐक्सिस-लॉकिंग की सुविधा, वन-डाइमेंशन वाले रीडिंग फ़्लो में गलती से साइड में ड्रिफ़्ट होने से रोकती है. हालांकि, यह टू-डाइमेंशन वाले पैनिंग प्लैटफ़ॉर्म पर रुकावट पैदा करती है. जैसे, इंटरैक्टिव मैप, ज़ूम की गई इमेज, इनफ़ाइनाइट कैनवस, नोड ग्राफ़, और बड़े डेटा ग्रिड. लॉक होने की वजह से, जेस्चर के दौरान दो दिशाओं में पैन करने पर असर पड़ता है. भले ही, दोनों ऐक्सिस को एक साथ पैन किया जाए (तिरछा पैन करना) या एक ही जेस्चर में एक के बाद एक पैन किया जाए (जैसे, घुमावदार, गोल या L-शेप वाले पाथ). सीएसएस scroll-axis-lock प्रॉपर्टी, इस ह्यूरिस्टिक को सीधे नेटिव स्क्रोल कंटेनर पर बंद कर देती है. इससे कंपोज़िटर-थ्रेड स्क्रोलिंग, मोमेंटम फ़िज़िक्स, नेटिव स्क्रोलबार, और कीबोर्ड नेविगेशन को सुरक्षित रखते हुए, बिना किसी पाबंदी के दो-डाइमेंशन में पैन किया जा सकता है.

लागू करना

1. टू-डाइमेंशनल स्क्रोल कंटेनर पर, ऐक्सिस-लॉकिंग को बंद करना और ओवरस्क्रोल को रोकना

scroll-axis-lock: none को सीधे उस एलिमेंट पर लागू करें जो टू-डाइमेंशनल स्क्रोल कंटेनर (overflow: auto या overflow: scroll) को सेट अप करता है. हालांकि, 2D स्क्रोल कंटेनर, डिफ़ॉल्ट auto वैल्यू के तहत दोनों ऐक्सिस पर स्क्रोल डेल्टा को पहले से ही अपडेट कर सकता है. हालांकि, auto की मदद से ब्राउज़र की रेलिंग ह्यूरिस्टिक तब चालू हो सकती है, जब कोई जेस्चर मुख्य रूप से एक ऐक्सिस पर शुरू होता है. scroll-axis-lock: none को सेट करने से, टचस्क्रीन, ट्रैकपैड, और माउस व्हील पर रेलिंग की सुविधा काम नहीं करती.

स्क्रोल कंटेनर पर scroll-axis-lock: none को overscroll-behavior: none (या overscroll-behavior: contain) के साथ जोड़ें, ताकि किसी किनारे पर पहुंचने से स्क्रोलिंग, पैरंट पेज पर न जाए या ब्राउज़र के ओवरस्क्रोल नेविगेशन गलती से ट्रिगर न हों. जैसे, हॉरिज़ॉन्टल बैक/फ़ॉरवर्ड स्वाइप नेविगेशन या नीचे स्वाइप करके रीफ़्रेश करना.

2. कीबोर्ड फ़ोकस और सुलभता को बनाए रखना

2D पैनिंग सरफ़ेस में अक्सर फ़ोकस किए जा सकने वाले चाइल्ड कंट्रोल नहीं होते. उदाहरण के लिए, कोई मैप या ज़ूम की गई इमेज. इसलिए, पक्का करें कि कीबोर्ड का इस्तेमाल करने वाले लोग, कंटेनर पर फ़ोकस कर सकें और ऐरो बटन की मदद से उसे पैन कर सकें. इसके लिए, tabindex="0", सही role, सुलभ लेबल, और दिखने वाला :focus-visible इंडिकेटर जोड़ें.

कोड का उदाहरण

<!-- Make the 2D scroll container keyboard-focusable and labeled so keyboard
     users can focus the viewport and pan in all directions with Arrow keys. -->
<div class="pan-viewport" tabindex="0" role="region" aria-label="Interactive canvas">
  <div class="pan-surface">
    <!-- 2D overflowing content (map tiles, zoomed image, canvas nodes, or grid) -->
  </div>
</div>
.pan-viewport {
  /* Example viewport dimensions; adjust to fit your layout. */
  inline-size: 100%;
  block-size: 500px;

  /* MANDATORY: Establish a scroll container that can overflow on both axes. */
  overflow: auto;

  /* MANDATORY: Prevent browser scroll axis-locking ("railing") from kicking in
     so 2D gestures never get locked to a single axis. */
  scroll-axis-lock: none;

  /* Contain scrolling and prevent accidental swipe navigations at edges
     (use `none` or `contain`). */
  overscroll-behavior: none;
}

/* Provide a clear focus indicator when keyboard users focus the viewport. */
.pan-viewport:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

.pan-surface {
  /* Example surface dimensions larger than .pan-viewport in both axes. */
  inline-size: 3000px;
  block-size: 3000px;
}

सबसे सही तरीके और पाबंदियां

  • इसे स्क्रोल कंटेनर पर ही लागू करें, न कि ऐंसेस्टर रैपर पर: scroll-axis-lock इनहेरिट नहीं किया जाता. इसलिए, इसे रैपर पर सेट करने से, डिसेंडेंट स्क्रोल कंटेनर पर कोई असर नहीं पड़ता.
  • ओवरस्क्रोल को कंट्रोल करना और गलती से होने वाले नेविगेशन को रोकना: 2D स्क्रोल कंटेनर में overscroll-behavior: none या overscroll-behavior: contain जोड़ें, ताकि किसी बाउंड्री में पैन करने से पैरंट पेज पर न पहुंचा जा सके. इसके अलावा, ब्राउज़र के बैक/फ़ॉरवर्ड स्वाइप नेविगेशन और नीचे स्वाइप करके रीफ़्रेश करने वाले जेस्चर भी ट्रिगर न हों.
  • नेस्ट किए गए वन-डाइमेंशन पैनल, अपने-आप रेलिंग करते रहते हैं: scroll-axis-lock इनहेरिट नहीं होता है. इसलिए, .pan-viewport में नेस्ट किया गया साइडबार या इंस्पेक्टर पैनल, डिफ़ॉल्ट scroll-axis-lock: auto व्यवहार को बनाए रखता है.
  • auto को स्टैंडर्ड वर्टिकल फ़ीड, लेखों, और एक डाइमेंशन वाली सूचियों पर रखें: ऐक्सिस-लॉकिंग की सुविधा, एक दिशा वाले कॉन्टेंट में अनजाने में होने वाले लंबवत ड्रिफ़्ट को रोकती है.
  • touch-action: none को बिना किसी शर्त के सेट न करें: touch-action से यह कंट्रोल किया जाता है कि ब्राउज़र, टच जेस्चर को हैंडल करता है या नहीं. इसका ट्रैकपैड या व्हील इनपुट पर कोई असर नहीं पड़ता. इसलिए, इसे सेट करने से नेटिव कंपोज़िटर टच स्क्रोलिंग की सुविधा बंद हो जाती है. JavaScript पॉइंटर फ़ॉलबैक लागू करते समय, इसे सिर्फ़ @supports not (scroll-axis-lock: none) के अंदर लागू करें.
  • scrollLeft / scrollTop का इस्तेमाल करके पैन करें, सीएसएस transform का इस्तेमाल कभी न करें: transform का इस्तेमाल करके सतह को ट्रांसलेट करने से, एलिमेंट स्क्रोल कंटेनर के तौर पर काम करना बंद कर देता है. इससे नेटिव स्क्रोलबार, बाउंड्री क्लैंपिंग, कीबोर्ड की ऐरो-की से स्क्रोल करने की सुविधा, scroll इवेंट, IntersectionObserver, और स्क्रोल-ड्रिवन ऐनिमेशन काम नहीं करते.

फ़ॉलबैक रणनीतियां

अपने ऐप्लिकेशन की ज़रूरतों के हिसाब से, परतदार वृद्धि और JavaScript फ़ॉलबैक में से कोई एक चुनें:

पहला विकल्प: परतदार वृद्धि (JavaScript के बिना)

परतदार वृद्धि का इस्तेमाल तब करें, जब नेटिव कंपोज़िटर-थ्रेड टच स्क्रोलिंग, इनर्शियल फ़्लिंग मोमेंटम, और पिंच-टू-ज़ूम को बनाए रखना, काम न करने वाले ब्राउज़र में ऐक्सिस-लॉकिंग को हटाने से ज़्यादा ज़रूरी हो:

  • ब्राउज़र के साथ काम करना: ब्राउज़र, स्क्रोल जेस्चर के दौरान 2D स्क्रोल कंटेनर पर, ऐक्सिस-लॉकिंग ("रेलिंग") ह्यूरिस्टिक को चालू होने से रोकता है.
  • काम न करने वाले ब्राउज़र: scroll-axis-lock: none को न पहचानने वाले ब्राउज़र, एलान को सुरक्षित तरीके से अनदेखा करते हैं और अपने डिफ़ॉल्ट auto 2D स्क्रोलिंग व्यवहार को बनाए रखते हैं. इसमें दोनों ऐक्सिस पर स्क्रोल किया जाता है. हालांकि, मुख्य रूप से एक ही ऐक्सिस पर शुरू होने वाले जेस्चर को रेल किया जाता है.

दूसरा विकल्प: JavaScript फ़ॉलबैक (wheel + pointermove के साथ scrollLeft/scrollTop)

अगर सभी ब्राउज़र पर, बिना किसी पाबंदी के टू-डाइमेंशनल पैनिंग की ज़रूरत है, तो @supports not (scroll-axis-lock: none) और CSS.supports('scroll-axis-lock', 'none') का इस्तेमाल करें. इससे, wheel और पॉइंटर को खींचकर छोड़ने के जेस्चर को शर्त के साथ इंटरसेप्ट किया जा सकता है.

wheel (e.deltaX, e.deltaY) और pointermove (e.clientX, e.clientY), ब्राउज़र के स्क्रोलिंग इंजन के ऐक्सिस-लॉकिंग लागू करने से पहले, रॉ और अनलॉक किए गए 2D डेल्टा दिखाते हैं. इसलिए, उन डेल्टा को सीधे scrollLeft और scrollTop पर लागू किया जा सकता है:

  • सीएसएस transform के बजाय scrollLeft/scrollTop का इस्तेमाल क्यों करें: scrollLeft और scrollTop को अपडेट करने से, एलिमेंट एक सही स्क्रोल कंटेनर के तौर पर बना रहता है. ब्राउज़र, स्क्रोल की पोज़िशन को अपने-आप [0, scrollWidth - clientWidth] और [0, scrollHeight - clientHeight] पर क्लैंप कर देता है. साथ ही, नेटिव स्क्रोलबार सिंक रहते हैं और कीबोर्ड पर मौजूद ऐरो-की से स्क्रोल करने की सुविधा काम करती रहती है. इसके अलावा, scroll इवेंट और IntersectionObserver सामान्य रूप से ट्रिगर होते हैं.
  • नुकसान: { passive: false } की मदद से wheel को इंटरसेप्ट करने पर, स्क्रोलिंग मुख्य थ्रेड पर चली जाती है. साथ ही, टचस्क्रीन पर touch-action: none सेट करने से, pointerup के बाद नेटिव इनर्शियल फ़्लिंग मोमेंटम बंद हो जाता है. इसके अलावा, नेटिव पिंच-टू-ज़ूम भी बंद हो जाता है.
/* Only disable native touch scrolling when scroll-axis-lock is unsupported
   and the JavaScript pointer fallback below is active. */
@supports not (scroll-axis-lock: none) {
  .pan-viewport {
    touch-action: none;
  }
}
// Only attach the main-thread gesture fallback when native support is missing
if (!CSS.supports('scroll-axis-lock', 'none')) {
  const viewport = document.querySelector('.pan-viewport');
  let lastPointer = null;

  // 1. Trackpad and mouse wheel fallback
  viewport.addEventListener(
    'wheel',
    (e) => {
      e.preventDefault();
      viewport.scrollLeft += e.deltaX;
      viewport.scrollTop += e.deltaY;
    },
    { passive: false },
  );

  // 2. Touch and pointer drag fallback
  viewport.addEventListener('pointerdown', (e) => {
    // Ignore non-primary buttons and clicks on native scrollbars
    const rect = viewport.getBoundingClientRect();
    if (
      !e.isPrimary ||
      e.button !== 0 ||
      e.clientX - rect.left - viewport.clientLeft >= viewport.clientWidth ||
      e.clientY - rect.top - viewport.clientTop >= viewport.clientHeight
    ) {
      return;
    }
    lastPointer = { x: e.clientX, y: e.clientY };
    viewport.setPointerCapture(e.pointerId);
  });

  viewport.addEventListener('pointermove', (e) => {
    if (!lastPointer) return;
    viewport.scrollLeft -= e.clientX - lastPointer.x;
    viewport.scrollTop -= e.clientY - lastPointer.y;
    lastPointer = { x: e.clientX, y: e.clientY };
  });

  const endPointerPan = () => {
    lastPointer = null;
  };
  viewport.addEventListener('pointerup', endPointerPan);
  viewport.addEventListener('pointercancel', endPointerPan);
}