টু-ডাইমেনশনাল প্যানিং

ওয়েব ফিচার: scroll-axis-lock overscroll-behavior

ব্যবহারকারী টু-ডাইমেনশনাল স্ক্রল কন্টেনার স্ক্রল করলে, ব্রাউজার সাধারণত স্ক্রল অ্যাক্সিস-লকিং হিউরিস্টিক (যা "রেলিং" নামেও পরিচিত) প্রয়োগ করে। কোনও জেসচার যদি লম্ব অক্ষের তুলনায় একটি অক্ষ বরাবর উল্লেখযোগ্যভাবে বেশি মুভমেন্টের মাধ্যমে শুরু হয়, তাহলে ব্রাউজার সেই একটি অনুভূমিক বা উল্লম্ব অক্ষে স্ক্রল লক করে দেয় এবং লম্ব ডেল্টা উপেক্ষা করে—প্রায়শই জেসচারের বাকি অংশের জন্য।

অ্যাক্সিস-লকিং ফিচারটি এক-মাত্রিক রিডিং ফ্লোতে দুর্ঘটনাবশত সাইডওয়েজ ড্রিফ্ট হওয়া প্রতিরোধ করে, তবে এটি ইন্ট্যার‍্যাক্টিভ ম্যাপ, জুম করা ছবি, ইনফিনিট ক্যানভাস, নোড গ্রাফ এবং বড় ডেটা গ্রিডের মতো দ্বি-মাত্রিক প্যানিং সারফেসে ঘর্ষণ সৃষ্টি করে। যেহেতু জেসচার জুড়ে লকটি বজায় থাকে, তাই এটি দুটি দিক জড়িত এমন যেকোনও প্যানিংকে প্রভাবিত করে—উভয় অক্ষ একসাথে প্যান করা হোক (তির্যক প্যানিং) বা একটি জেসচারের মধ্যে একের পর এক (যেমন বাঁকা, বৃত্তাকার বা L-আকৃতির পথ)। CSS scroll-axis-lock প্রপার্টি সরাসরি নেটিভ স্ক্রল কন্টেনারে এই হিউরিস্টিক বন্ধ করে দেয়, যার ফলে কম্পোজিটর-থ্রেড স্ক্রলিং, মোমেন্টাম ফিজিক্স, নেটিভ স্ক্রলবার ও কীবোর্ড নেভিগেশন বজায় রেখেও বাধাহীনভাবে টু-ডাইমেনশনাল প্যানিং করা যায়।

প্রয়োগ

১. অ্যাক্সিস-লকিং বন্ধ করুন এবং টু-ডাইমেনশনাল স্ক্রল কন্টেনারে ওভারস্ক্রল অন্তর্ভুক্ত করুন

scroll-axis-lock: none সরাসরি সেই এলিমেন্টে প্রয়োগ করুন যা টু-ডাইমেনশনাল স্ক্রল কন্টেনার (overflow: auto বা overflow: scroll) তৈরি করে। 2D স্ক্রল কন্টেনার ডিফল্ট auto ভ্যালুর অধীনে দুটি অক্ষ বরাবর স্ক্রল ডেল্টা আপডেট করতে পারলেও, auto ব্রাউজারের রেলিং হিউরিস্টিককে কাজ শুরু করার অনুমতি দেয় যখন কোনও জেসচার প্রধানত একটি অক্ষ বরাবর শুরু হয়। scroll-axis-lock: none সেটিং টাচস্ক্রিন, ট্র্যাকপ্যাড ও মাউস হুইল জুড়ে রেলিংকে এনগেজ হতে বাধা দেয়।

স্ক্রল কন্টেনারে overscroll-behavior: none (বা overscroll-behavior: contain)-এর সাথে scroll-axis-lock: none পেয়ার করুন যাতে কোনও প্রান্তে পৌঁছালে পেরেন্ট পৃষ্ঠায় স্ক্রলিং চেন না হয় বা ব্রাউজারে ভুল করে ওভারস্ক্রল নেভিগেশন (যেমন, অনুভূমিক ব্যাক/ফরোয়ার্ড সোয়াইপ নেভিগেশন বা রিফ্রেশ করতে নিচের দিকে টানা) ট্রিগার না হয়।

২. কীবোর্ড ফোকাস করার ক্ষমতা ও অ্যাক্সেসিবিলিটি বজায় রাখা

যেহেতু 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 ব্রাউজার টাচ জেসচার আদৌ ম্যানেজ করে কিনা তা নিয়ন্ত্রণ করে (এবং ট্র্যাকপ্যাড বা হুইল ইনপুটের উপর কোনও প্রভাব ফেলে না), তাই এটি সেট করলে নেটিভ কম্পোজিটর টাচ স্ক্রলিং বন্ধ হয়ে যায়। জাভাস্ক্রিপ্ট পয়েন্টার ফলব্যাক প্রয়োগ করার সময় শুধুমাত্র @supports not (scroll-axis-lock: none)-এর মধ্যে এটি প্রয়োগ করুন।
  • scrollLeft / scrollTop-এর মাধ্যমে প্যান করুন, CSS transform-এর মাধ্যমে কখনও নয়: transform-এর মাধ্যমে সারফেস ট্রান্সলেট করলে, এলিমেন্টটি স্ক্রল কন্টেনার হিসেবে কাজ করা বন্ধ করে দেয়—নেটিভ স্ক্রলবার, বাউন্ডারি ক্ল্যাম্পিং, কীবোর্ডের তীরচিহ্ন-কী স্ক্রলিং, scroll ইভেন্ট, IntersectionObserver এবং স্ক্রল-চালিত অ্যানিমেশন ভেঙে যায়।

ফলব্যাক স্ট্র্যাটেজি

আপনার অ্যাপ্লিকেশনের প্রয়োজনীয়তার উপর ভিত্তি করে প্রোগ্রেসিভ এনহ্যান্সমেন্ট এবং জাভাস্ক্রিপ্ট ফলব্যাক-এর মধ্যে থেকে বেছে নিন:

বিকল্প ১: প্রোগ্রেসিভ এনহ্যান্সমেন্ট (জাভাস্ক্রিপ্ট নেই)

নেটিভ কম্পোজিটর-থ্রেড টাচ স্ক্রলিং, ইনার্শিয়াল ফ্লিং মোমেন্টাম এবং পিঞ্চ-টু-জুমের মতো ফিচার বজায় রাখা যখন অসমর্থিত ব্রাউজারে অ্যাক্সিস-লকিং বাদ দেওয়ার চেয়ে বেশি গুরুত্বপূর্ণ, তখন প্রোগ্রেসিভ এনহ্যান্সমেন্ট ব্যবহার করুন:

  • সাপোর্টিং ব্রাউজার: স্ক্রল জেসচার চলাকালীন ব্রাউজারটি 2D স্ক্রল কন্টেনারে তার অ্যাক্সিস-লকিং ("রেলিং") হিউরিস্টিককে কিক করা থেকে বাধা দেয়।
  • কাজ করে না এমন ব্রাউজার: যেসব ব্রাউজার scroll-axis-lock: none শনাক্ত করে না, সেগুলি নিরাপদে ঘোষণা উপেক্ষা করে এবং তাদের ডিফল্ট auto 2D স্ক্রলিং আচরণ বজায় রাখে—নেটিভভাবে দুটি অক্ষ জুড়ে স্ক্রল করে, তবে এখনও রেলিং জেসচার করে যা প্রধানত একটি অক্ষ বরাবর শুরু হয়।

বিকল্প ২: জাভাস্ক্রিপ্ট ফলব্যাক (scrollLeft/scrollTop সহ wheel + pointermove)

সব ব্রাউজার জুড়ে আনরেস্ট্রিক্টেড টু-ডাইমেনশনাল প্যানিংয়ের প্রয়োজন হলে, wheel এবং পয়েন্টার ড্র্যাগ জেসচার শর্তসাপেক্ষে ইন্টারসেপ্ট করতে @supports not (scroll-axis-lock: none) এবং CSS.supports('scroll-axis-lock', 'none') ব্যবহার করুন।

যেহেতু wheel (e.deltaX, e.deltaY) এবং pointermove (e.clientX, e.clientY) ব্রাউজারের স্ক্রলিং ইঞ্জিন অক্ষ-লকিং প্রয়োগ করার আগে কাঁচা, আনলক করা 2D ডেল্টা প্রকাশ করে, আপনি সেই ডেল্টাগুলি সরাসরি scrollLeft এবং scrollTop-এ প্রয়োগ করতে পারেন:

  • CSS 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);
}