1
- import React , { forwardRef , RefObject , useCallback , useMemo } from 'react' ;
1
+ import React , { forwardRef , RefObject , useCallback , useState } from 'react' ;
2
2
3
3
import classNames from 'classnames' ;
4
4
import range from 'lodash/range' ;
5
5
6
6
import { mdiChevronLeft , mdiChevronRight , mdiPlayCircleOutline , mdiPauseCircleOutline } from '@lumx/icons' ;
7
- import { Emphasis , IconButton , IconButtonProps , Theme } from '@lumx/react' ;
7
+ import { Emphasis , IconButton , IconButtonProps , Slides , Theme } from '@lumx/react' ;
8
8
import { Comp , GenericProps , HasTheme } from '@lumx/react/utils/type' ;
9
9
import { getRootClassName , handleBasicClasses } from '@lumx/react/utils/className' ;
10
10
import { WINDOW } from '@lumx/react/constants' ;
11
- import { useSlideshowControls , DEFAULT_OPTIONS } from '@lumx/react/hooks/useSlideshowControls ' ;
12
- import { useRovingTabIndex } from '@lumx/react/hooks/useRovingTabIndex ' ;
11
+ import { useKeyNavigate } from '@lumx/react/components/slideshow/useKeyNavigate ' ;
12
+ import { useMergeRefs } from '@lumx/react/utils/mergeRefs ' ;
13
13
14
+ import { buildSlideShowGroupId } from '@lumx/react/components/slideshow/SlideshowItemGroup' ;
15
+ import { DEFAULT_OPTIONS , useSlideshowControls } from './useSlideshowControls' ;
14
16
import { useSwipeNavigate } from './useSwipeNavigate' ;
15
17
import { PAGINATION_ITEM_SIZE , PAGINATION_ITEMS_MAX } from './constants' ;
16
18
import { usePaginationVisibleRange } from './usePaginationVisibleRange' ;
@@ -34,11 +36,11 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
34
36
/** Number of slides. */
35
37
slidesCount : number ;
36
38
/** On next button click callback. */
37
- onNextClick ?( loopback ?: boolean ) : void ;
39
+ onNextClick ?( loopBack ?: boolean ) : void ;
38
40
/** On pagination change callback. */
39
41
onPaginationClick ?( index : number ) : void ;
40
42
/** On previous button click callback. */
41
- onPreviousClick ?( loopback ?: boolean ) : void ;
43
+ onPreviousClick ?( loopBack ?: boolean ) : void ;
42
44
/** whether the slideshow is currently playing */
43
45
isAutoPlaying ?: boolean ;
44
46
/**
@@ -100,7 +102,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
100
102
...forwardedProps
101
103
} = props ;
102
104
103
- let parent ;
105
+ let parent : HTMLElement | null | undefined ;
104
106
if ( WINDOW ) {
105
107
// Checking window object to avoid errors in SSR.
106
108
parent = parentRef instanceof HTMLElement ? parentRef : parentRef ?. current ;
@@ -109,33 +111,30 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
109
111
// Listen to touch swipe navigate left & right.
110
112
useSwipeNavigate (
111
113
parent ,
112
- // Go next without loopback .
114
+ // Go next without loop back .
113
115
useCallback ( ( ) => onNextClick ?.( false ) , [ onNextClick ] ) ,
114
- // Go previous without loopback .
116
+ // Go previous without loop back .
115
117
useCallback ( ( ) => onPreviousClick ?.( false ) , [ onPreviousClick ] ) ,
116
118
) ;
117
119
118
- /**
119
- * Add roving tab index pattern to pagination items and activate slide on focus.
120
- */
121
- useRovingTabIndex ( {
122
- parentRef : paginationRef ,
123
- elementSelector : 'button' ,
124
- keepTabIndex : true ,
125
- onElementFocus : ( element ) => {
126
- element . click ( ) ;
127
- } ,
128
- } ) ;
120
+ const [ focusedIndex , setFocusedIndex ] = useState < number | null > ( null ) ;
121
+ const onButtonFocus = useCallback ( ( index : number ) => ( ) => setFocusedIndex ( index ) , [ setFocusedIndex ] ) ;
122
+ const onFocusOut = useCallback ( ( ) => setFocusedIndex ( null ) , [ setFocusedIndex ] ) ;
129
123
130
124
// Pagination "bullet" range.
131
- const visibleRange = usePaginationVisibleRange ( activeIndex as number , slidesCount ) ;
125
+ const visibleRange = usePaginationVisibleRange ( focusedIndex ?? ( activeIndex as number ) , slidesCount ) ;
132
126
133
127
// Inline style of wrapper element.
134
128
const wrapperStyle = { transform : `translateX(-${ PAGINATION_ITEM_SIZE * visibleRange . min } px)` } ;
135
129
130
+ const controlsRef = React . useRef < HTMLDivElement > ( null ) ;
131
+ useKeyNavigate ( controlsRef . current , onNextClick , onPreviousClick ) ;
132
+
133
+ const slideshowSlidesId = React . useMemo ( ( ) => parent ?. querySelector ( `.${ Slides . className } __slides` ) ?. id , [ parent ] ) ;
134
+
136
135
return (
137
136
< div
138
- ref = { ref }
137
+ ref = { useMergeRefs ( ref , controlsRef ) }
139
138
{ ...forwardedProps }
140
139
className = { classNames ( className , handleBasicClasses ( { prefix : CLASSNAME , theme } ) , {
141
140
[ `${ CLASSNAME } --has-infinite-pagination` ] : slidesCount > PAGINATION_ITEMS_MAX ,
@@ -148,64 +147,53 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
148
147
color = { theme === Theme . dark ? 'light' : 'dark' }
149
148
emphasis = { Emphasis . low }
150
149
onClick = { onPreviousClick }
150
+ aria-controls = { slideshowSlidesId }
151
151
/>
152
+
152
153
< div ref = { paginationRef } className = { `${ CLASSNAME } __pagination` } >
153
154
< div
154
155
className = { `${ CLASSNAME } __pagination-items` }
155
156
style = { wrapperStyle }
156
- role = "tablist"
157
157
{ ...paginationProps }
158
+ onBlur = { onFocusOut }
158
159
>
159
- { useMemo (
160
- ( ) =>
161
- range ( slidesCount ) . map ( ( index ) => {
162
- const isOnEdge =
163
- index !== 0 &&
164
- index !== slidesCount - 1 &&
165
- ( index === visibleRange . min || index === visibleRange . max ) ;
166
- const isActive = activeIndex === index ;
167
- const isOutRange = index < visibleRange . min || index > visibleRange . max ;
168
- const {
169
- className : itemClassName = undefined ,
170
- label = undefined ,
171
- ...itemProps
172
- } = paginationItemProps ? paginationItemProps ( index ) : { } ;
173
-
174
- const ariaLabel =
175
- label || paginationItemLabel ?.( index ) || `${ index + 1 } / ${ slidesCount } ` ;
176
-
177
- return (
178
- < button
179
- className = { classNames (
180
- handleBasicClasses ( {
181
- prefix : `${ CLASSNAME } __pagination-item` ,
182
- isActive,
183
- isOnEdge,
184
- isOutRange,
185
- } ) ,
186
- itemClassName ,
187
- ) }
188
- key = { index }
189
- type = "button"
190
- tabIndex = { isActive ? undefined : - 1 }
191
- role = "tab"
192
- aria-selected = { isActive }
193
- onClick = { ( ) => onPaginationClick ?.( index ) }
194
- aria-label = { ariaLabel }
195
- { ...itemProps }
196
- />
197
- ) ;
198
- } ) ,
199
- [
200
- slidesCount ,
201
- visibleRange . min ,
202
- visibleRange . max ,
203
- activeIndex ,
204
- paginationItemProps ,
205
- paginationItemLabel ,
206
- onPaginationClick ,
207
- ] ,
208
- ) }
160
+ { range ( slidesCount ) . map ( ( index ) => {
161
+ const isOnEdge =
162
+ index !== 0 &&
163
+ index !== slidesCount - 1 &&
164
+ ( index === visibleRange . min || index === visibleRange . max ) ;
165
+ const isActive = activeIndex === index ;
166
+ const isOutRange = index < visibleRange . min || index > visibleRange . max ;
167
+ const {
168
+ className : itemClassName = undefined ,
169
+ label = undefined ,
170
+ ...itemProps
171
+ } = paginationItemProps ? paginationItemProps ( index ) : { } ;
172
+
173
+ const ariaLabel = label || paginationItemLabel ?.( index ) || `${ index + 1 } / ${ slidesCount } ` ;
174
+
175
+ return (
176
+ < button
177
+ className = { classNames (
178
+ handleBasicClasses ( {
179
+ prefix : `${ CLASSNAME } __pagination-item` ,
180
+ isActive,
181
+ isOnEdge,
182
+ isOutRange,
183
+ } ) ,
184
+ itemClassName ,
185
+ ) }
186
+ key = { index }
187
+ type = "button"
188
+ aria-current = { isActive || undefined }
189
+ aria-controls = { buildSlideShowGroupId ( slideshowSlidesId , index ) }
190
+ onClick = { ( ) => onPaginationClick ?.( index ) }
191
+ onFocus = { onButtonFocus ( index ) }
192
+ aria-label = { ariaLabel }
193
+ { ...itemProps }
194
+ />
195
+ ) ;
196
+ } ) }
209
197
</ div >
210
198
</ div >
211
199
@@ -216,6 +204,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
216
204
className = { `${ CLASSNAME } __play` }
217
205
color = { theme === Theme . dark ? 'light' : 'dark' }
218
206
emphasis = { Emphasis . low }
207
+ aria-controls = { slideshowSlidesId }
219
208
/>
220
209
) : null }
221
210
@@ -226,6 +215,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
226
215
color = { theme === Theme . dark ? 'light' : 'dark' }
227
216
emphasis = { Emphasis . low }
228
217
onClick = { onNextClick }
218
+ aria-controls = { slideshowSlidesId }
229
219
/>
230
220
</ div >
231
221
) ;
0 commit comments