From 490d426531cae145281525298250b7f6818a3dfd Mon Sep 17 00:00:00 2001 From: Sayed Risat Date: Sat, 23 May 2026 09:34:41 +0600 Subject: [PATCH 1/2] Add accessibility props to scroll container --- README.md | 1 + src/__tests__/index.test.tsx | 21 +++++++++++++++++++++ src/index.tsx | 9 ++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 584baba..2d547f3 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,7 @@ function PostList() { | `onScroll` | `(e: UIEvent) => void` | no | - | Callback fired on every scroll event on the container. Receives the native `UIEvent`. Useful for syncing UI state with scroll position. | | `className` | `string` | no | `''` | CSS class name applied to the inner scroll container div. | | `style` | `CSSProperties` | no | - | Inline style object applied to the inner scroll container div. Merged with the component's default layout styles. | +| Any other div attribute | HTML div attributes | no | - | Additional attributes such as `role`, `aria-label`, and `tabIndex` are applied to the inner scroll container div. | | `hasChildren` | `boolean` | no | - | Set to `true` when `children` is a single element or a fragment rather than an array. Helps the component detect whether visible content exists to determine scroll state. | | `initialScrollY` | `number` | no | - | Scrolls the window to this Y offset on mount. Useful for restoring a user's scroll position when navigating back to a page. | diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index 99881cb..9421e7a 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -45,6 +45,27 @@ describe('React Infinite Scroll Component', () => { expect(container.querySelectorAll('.custom-class').length).toBe(1); }); + it('passes accessibility attributes to the scroll container', () => { + const { container } = render( + {}} + role="list" + aria-label="Loaded items" + > +
+ + ); + + const scrollContainer = container.querySelector( + '.infinite-scroll-component' + ); + expect(scrollContainer?.getAttribute('role')).toBe('list'); + expect(scrollContainer?.getAttribute('aria-label')).toBe('Loaded items'); + }); + it('renders children when passed in', () => { const { container } = render( any; -export interface Props { +export interface Props + extends Omit< + HTMLAttributes, + 'children' | 'style' | 'className' | 'onScroll' + > { /** * Total number of items currently rendered. Unlocks the next load when it * changes. Always pass the length of your full accumulated list, not just @@ -140,6 +145,7 @@ export default function InfiniteScroll({ dataLength, initialScrollY, className = '', + ...containerProps }: Props) { const [showLoader, setShowLoader] = useState(false); const [pullToRefreshThresholdBreached, setPullToRefreshThresholdBreached] = @@ -410,6 +416,7 @@ export default function InfiniteScroll({ return (
Date: Mon, 25 May 2026 11:30:38 +0600 Subject: [PATCH 2/2] Scope container props to accessibility attributes --- README.md | 44 ++++++++++++++++++------------------ src/__tests__/index.test.tsx | 4 ++++ src/index.tsx | 25 +++++++++++++------- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 2d547f3..4e40856 100644 --- a/README.md +++ b/README.md @@ -340,28 +340,28 @@ function PostList() { ## Props, `InfiniteScroll` -| Prop | Type | Required | Default | Description | -| ---------------------------- | ------------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `dataLength` | `number` | yes | - | Current count of rendered items. The component resets its load guard each time this value changes, which allows `next()` to fire again on the next scroll. | -| `next` | `() => void` | yes | - | Called once when the sentinel enters the viewport. Append new items to your list state inside this callback; do not replace the existing items. | -| `hasMore` | `boolean` | yes | - | When `false`, the observer is disconnected and `next()` will not be called again. Set it to `false` when your data source has no more pages. | -| `loader` | `ReactNode` | yes | - | Rendered below the list while the next page is loading. Displayed between the last item and the bottom sentinel. | -| `endMessage` | `ReactNode` | no | - | Rendered below the list when `hasMore` is `false`. Use it for an "all caught up" or "no more items" message. | -| `height` | `number \| string` | no | - | Creates a fixed-height scroll container wrapping the list. Accepts a pixel number or any CSS length string. Omit this prop to scroll the window instead. | -| `scrollableTarget` | `HTMLElement \| string \| null` | no | - | The scrollable ancestor that already provides overflow scrollbars. Pass the element's `id` string or a direct `HTMLElement` reference. Required when the scroll container is neither the window nor the `height` wrapper. | -| `scrollThreshold` | `number \| string` | no | `0.8` | How close to the bottom the user must scroll before `next()` is called. A fraction like `0.8` means 80% scrolled; a string like `"200px"` means within 200 px of the bottom edge. | -| `inverse` | `boolean` | no | `false` | Reverse scroll direction for chat or messaging UIs. The sentinel moves to the top of the list. Use together with `flexDirection: column-reverse` on the scroll container. | -| `pullDownToRefresh` | `boolean` | no | `false` | Enable pull-to-refresh gesture on touch and mouse. Requires `refreshFunction` to also be set. | -| `refreshFunction` | `() => void` | no | - | Called once when the user pulls down past `pullDownToRefreshThreshold` pixels and releases. Only active when `pullDownToRefresh` is `true`. | -| `pullDownToRefreshThreshold` | `number` | no | `100` | How many pixels the user must pull down before `refreshFunction` is triggered on release. | -| `pullDownToRefreshContent` | `ReactNode` | no | - | Content shown inside the pull-to-refresh area while the user is pulling but has not yet reached the threshold. | -| `releaseToRefreshContent` | `ReactNode` | no | - | Content shown inside the pull-to-refresh area once the threshold is passed and the user can release to trigger a refresh. | -| `onScroll` | `(e: UIEvent) => void` | no | - | Callback fired on every scroll event on the container. Receives the native `UIEvent`. Useful for syncing UI state with scroll position. | -| `className` | `string` | no | `''` | CSS class name applied to the inner scroll container div. | -| `style` | `CSSProperties` | no | - | Inline style object applied to the inner scroll container div. Merged with the component's default layout styles. | -| Any other div attribute | HTML div attributes | no | - | Additional attributes such as `role`, `aria-label`, and `tabIndex` are applied to the inner scroll container div. | -| `hasChildren` | `boolean` | no | - | Set to `true` when `children` is a single element or a fragment rather than an array. Helps the component detect whether visible content exists to determine scroll state. | -| `initialScrollY` | `number` | no | - | Scrolls the window to this Y offset on mount. Useful for restoring a user's scroll position when navigating back to a page. | +| Prop | Type | Required | Default | Description | +| ---------------------------- | ---------------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `dataLength` | `number` | yes | - | Current count of rendered items. The component resets its load guard each time this value changes, which allows `next()` to fire again on the next scroll. | +| `next` | `() => void` | yes | - | Called once when the sentinel enters the viewport. Append new items to your list state inside this callback; do not replace the existing items. | +| `hasMore` | `boolean` | yes | - | When `false`, the observer is disconnected and `next()` will not be called again. Set it to `false` when your data source has no more pages. | +| `loader` | `ReactNode` | yes | - | Rendered below the list while the next page is loading. Displayed between the last item and the bottom sentinel. | +| `endMessage` | `ReactNode` | no | - | Rendered below the list when `hasMore` is `false`. Use it for an "all caught up" or "no more items" message. | +| `height` | `number \| string` | no | - | Creates a fixed-height scroll container wrapping the list. Accepts a pixel number or any CSS length string. Omit this prop to scroll the window instead. | +| `scrollableTarget` | `HTMLElement \| string \| null` | no | - | The scrollable ancestor that already provides overflow scrollbars. Pass the element's `id` string or a direct `HTMLElement` reference. Required when the scroll container is neither the window nor the `height` wrapper. | +| `scrollThreshold` | `number \| string` | no | `0.8` | How close to the bottom the user must scroll before `next()` is called. A fraction like `0.8` means 80% scrolled; a string like `"200px"` means within 200 px of the bottom edge. | +| `inverse` | `boolean` | no | `false` | Reverse scroll direction for chat or messaging UIs. The sentinel moves to the top of the list. Use together with `flexDirection: column-reverse` on the scroll container. | +| `pullDownToRefresh` | `boolean` | no | `false` | Enable pull-to-refresh gesture on touch and mouse. Requires `refreshFunction` to also be set. | +| `refreshFunction` | `() => void` | no | - | Called once when the user pulls down past `pullDownToRefreshThreshold` pixels and releases. Only active when `pullDownToRefresh` is `true`. | +| `pullDownToRefreshThreshold` | `number` | no | `100` | How many pixels the user must pull down before `refreshFunction` is triggered on release. | +| `pullDownToRefreshContent` | `ReactNode` | no | - | Content shown inside the pull-to-refresh area while the user is pulling but has not yet reached the threshold. | +| `releaseToRefreshContent` | `ReactNode` | no | - | Content shown inside the pull-to-refresh area once the threshold is passed and the user can release to trigger a refresh. | +| `onScroll` | `(e: UIEvent) => void` | no | - | Callback fired on every scroll event on the container. Receives the native `UIEvent`. Useful for syncing UI state with scroll position. | +| `className` | `string` | no | `''` | CSS class name applied to the inner scroll container div. | +| `style` | `CSSProperties` | no | - | Inline style object applied to the inner scroll container div. Merged with the component's default layout styles. | +| Accessibility attributes | `aria-*`, `role`, `tabIndex`, `id` | no | - | Accessibility attributes applied to the inner scroll container div for labelling, semantics, and keyboard focus management. | +| `hasChildren` | `boolean` | no | - | Set to `true` when `children` is a single element or a fragment rather than an array. Helps the component detect whether visible content exists to determine scroll state. | +| `initialScrollY` | `number` | no | - | Scrolls the window to this Y offset on mount. Useful for restoring a user's scroll position when navigating back to a page. | ## Props, `useInfiniteScroll` diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index 9421e7a..b07bf5d 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -52,7 +52,9 @@ describe('React Infinite Scroll Component', () => { loader={'Loading...'} hasMore={false} next={() => {}} + id="loaded-items" role="list" + tabIndex={0} aria-label="Loaded items" >
@@ -62,7 +64,9 @@ describe('React Infinite Scroll Component', () => { const scrollContainer = container.querySelector( '.infinite-scroll-component' ); + expect(scrollContainer?.getAttribute('id')).toBe('loaded-items'); expect(scrollContainer?.getAttribute('role')).toBe('list'); + expect(scrollContainer?.getAttribute('tabindex')).toBe('0'); expect(scrollContainer?.getAttribute('aria-label')).toBe('Loaded items'); }); diff --git a/src/index.tsx b/src/index.tsx index 8d9a774..6d7377d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,7 +5,8 @@ import { useCallback, ReactNode, CSSProperties, - HTMLAttributes, + AriaAttributes, + AriaRole, } from 'react'; import { buildRootMargin } from './utils/buildRootMargin'; @@ -17,11 +18,7 @@ export type { type Fn = () => any; -export interface Props - extends Omit< - HTMLAttributes, - 'children' | 'style' | 'className' | 'onScroll' - > { +export interface Props extends AriaAttributes { /** * Total number of items currently rendered. Unlocks the next load when it * changes. Always pass the length of your full accumulated list, not just @@ -122,6 +119,12 @@ export interface Props initialScrollY?: number; /** CSS class name added to the inner scroll container element. */ className?: string; + /** Accessibility role applied to the inner scroll container element. */ + role?: AriaRole; + /** Tab order for keyboard focus management on the inner scroll container. */ + tabIndex?: number; + /** DOM id for labelling or controlling the inner scroll container. */ + id?: string; } export default function InfiniteScroll({ @@ -145,7 +148,10 @@ export default function InfiniteScroll({ dataLength, initialScrollY, className = '', - ...containerProps + role, + tabIndex, + id, + ...ariaProps }: Props) { const [showLoader, setShowLoader] = useState(false); const [pullToRefreshThresholdBreached, setPullToRefreshThresholdBreached] = @@ -416,7 +422,10 @@ export default function InfiniteScroll({ return (