@lumx/react 2.2.25 → 2.2.26-alpha-a11y-slideshow.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/esm/_internal/ClickAwayProvider.js +9 -5
  2. package/esm/_internal/ClickAwayProvider.js.map +1 -1
  3. package/esm/_internal/List2.js.map +1 -1
  4. package/esm/_internal/ProgressTrackerStepPanel.js +2 -1
  5. package/esm/_internal/ProgressTrackerStepPanel.js.map +1 -1
  6. package/esm/_internal/Slides.js +206 -76
  7. package/esm/_internal/Slides.js.map +1 -1
  8. package/esm/_internal/TabPanel.js +2 -1
  9. package/esm/_internal/TabPanel.js.map +1 -1
  10. package/esm/_internal/progress-tracker.js +2 -1
  11. package/esm/_internal/progress-tracker.js.map +1 -1
  12. package/esm/_internal/slideshow.js +2 -0
  13. package/esm/_internal/slideshow.js.map +1 -1
  14. package/esm/_internal/state.js +145 -0
  15. package/esm/_internal/state.js.map +1 -0
  16. package/esm/_internal/tabs.js +1 -0
  17. package/esm/_internal/tabs.js.map +1 -1
  18. package/esm/_internal/useRovingTabIndex.js +9 -144
  19. package/esm/_internal/useRovingTabIndex.js.map +1 -1
  20. package/esm/index.js +3 -1
  21. package/esm/index.js.map +1 -1
  22. package/package.json +4 -4
  23. package/src/components/slideshow/Slides.tsx +33 -3
  24. package/src/components/slideshow/Slideshow.stories.tsx +90 -2
  25. package/src/components/slideshow/Slideshow.tsx +15 -3
  26. package/src/components/slideshow/SlideshowControls.stories.tsx +1 -1
  27. package/src/components/slideshow/SlideshowControls.tsx +43 -6
  28. package/src/components/slideshow/SlideshowItem.tsx +0 -5
  29. package/src/components/slideshow/SlideshowItemGroup.tsx +56 -0
  30. package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +10 -1
  31. package/src/components/slideshow/useSlideFocusManagement.tsx +67 -0
  32. package/src/hooks/useRovingTabIndex.tsx +9 -0
  33. package/src/utils/focus/constants.ts +5 -0
  34. package/src/utils/focus/getFirstAndLastFocusable.ts +4 -10
  35. package/src/utils/focus/getFocusableElements.test.ts +174 -0
  36. package/src/utils/focus/getFocusableElements.ts +7 -0
  37. package/types.d.ts +19 -5
@@ -8,9 +8,9 @@ import { Emphasis, IconButton, IconButtonProps, Theme } from '@lumx/react';
8
8
  import { Comp, GenericProps, getRootClassName, handleBasicClasses, HasTheme } from '@lumx/react/utils';
9
9
  import { WINDOW } from '@lumx/react/constants';
10
10
  import { useSlideshowControls, DEFAULT_OPTIONS } from '@lumx/react/hooks/useSlideshowControls';
11
+ import { useRovingTabIndex } from '@lumx/react/hooks/useRovingTabIndex';
11
12
 
12
13
  import { useSwipeNavigate } from './useSwipeNavigate';
13
- import { useKeyNavigate } from './useKeyNavigate';
14
14
  import { PAGINATION_ITEM_SIZE, PAGINATION_ITEMS_MAX } from './constants';
15
15
  import { usePaginationVisibleRange } from './usePaginationVisibleRange';
16
16
 
@@ -28,6 +28,8 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
28
28
  /** Props to pass to the previous button (minus those already set by the SlideshowControls props). */
29
29
  previousButtonProps: Pick<IconButtonProps, 'label'> &
30
30
  Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color'>;
31
+ /** Props to pass to the pagination wrapper */
32
+ paginationProps?: Omit<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'role'>;
31
33
  /** Number of slides. */
32
34
  slidesCount: number;
33
35
  /** On next button click callback. */
@@ -38,8 +40,15 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
38
40
  onPreviousClick?(loopback?: boolean): void;
39
41
  /** whether the slideshow is currently playing */
40
42
  isAutoPlaying?: boolean;
41
- /** function to be executed in order to retrieve the label for the pagination item */
43
+ /**
44
+ * function to be executed in order to retrieve the label for the pagination item
45
+ * @deprecated Use paginationItemProps instead.
46
+ * */
42
47
  paginationItemLabel?: (index: number) => string;
48
+ /**
49
+ * function to be executed in order to retrieve the props for a pagination item.
50
+ */
51
+ paginationItemProps?: (itemIndex: number) => React.HTMLAttributes<HTMLButtonElement>;
43
52
  /** Props to pass to the lay button (minus those already set by the SlideshowControls props). */
44
53
  playButtonProps?: Pick<IconButtonProps, 'label'> &
45
54
  Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color'>;
@@ -80,11 +89,13 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
80
89
  onPreviousClick,
81
90
  parentRef,
82
91
  previousButtonProps,
92
+ paginationProps,
83
93
  slidesCount,
84
94
  theme,
85
95
  isAutoPlaying = false,
86
96
  playButtonProps,
87
97
  paginationItemLabel,
98
+ paginationItemProps,
88
99
  ...forwardedProps
89
100
  } = props;
90
101
 
@@ -93,8 +104,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
93
104
  // Checking window object to avoid errors in SSR.
94
105
  parent = parentRef instanceof HTMLElement ? parentRef : parentRef?.current;
95
106
  }
96
- // Listen to keyboard navigate left & right.
97
- useKeyNavigate(parent, onNextClick, onPreviousClick);
107
+ const paginationRef = React.useRef(null);
98
108
  // Listen to touch swipe navigate left & right.
99
109
  useSwipeNavigate(
100
110
  parent,
@@ -104,6 +114,18 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
104
114
  useCallback(() => onPreviousClick?.(false), [onPreviousClick]),
105
115
  );
106
116
 
117
+ /**
118
+ * Add roving tab index pattern to pagination items and activate slide on focus.
119
+ */
120
+ useRovingTabIndex({
121
+ parentRef: paginationRef,
122
+ elementSelector: 'button',
123
+ keepTabIndex: true,
124
+ onElementFocus: (element) => {
125
+ element.click();
126
+ },
127
+ });
128
+
107
129
  // Pagination "bullet" range.
108
130
  const visibleRange = usePaginationVisibleRange(activeIndex as number, slidesCount);
109
131
 
@@ -126,8 +148,13 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
126
148
  emphasis={Emphasis.low}
127
149
  onClick={onPreviousClick}
128
150
  />
129
- <div className={`${CLASSNAME}__pagination`}>
130
- <div className={`${CLASSNAME}__pagination-items`} style={wrapperStyle}>
151
+ <div ref={paginationRef} className={`${CLASSNAME}__pagination`}>
152
+ <div
153
+ className={`${CLASSNAME}__pagination-items`}
154
+ style={wrapperStyle}
155
+ role="tablist"
156
+ {...paginationProps}
157
+ >
131
158
  {useMemo(
132
159
  () =>
133
160
  range(slidesCount).map((index) => {
@@ -137,6 +164,10 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
137
164
  (index === visibleRange.min || index === visibleRange.max);
138
165
  const isActive = activeIndex === index;
139
166
  const isOutRange = index < visibleRange.min || index > visibleRange.max;
167
+ const { className: itemClassName = undefined, ...itemProps } = paginationItemProps
168
+ ? paginationItemProps(index)
169
+ : {};
170
+
140
171
  return (
141
172
  <button
142
173
  className={classNames(
@@ -146,15 +177,20 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
146
177
  isOnEdge,
147
178
  isOutRange,
148
179
  }),
180
+ itemClassName,
149
181
  )}
150
182
  key={index}
151
183
  type="button"
184
+ tabIndex={isActive ? undefined : -1}
185
+ role="tab"
186
+ aria-selected={isActive}
152
187
  onClick={() => onPaginationClick?.(index)}
153
188
  {...{
154
189
  'aria-label': paginationItemLabel
155
190
  ? paginationItemLabel(index)
156
191
  : `${index + 1} / ${slidesCount}`,
157
192
  }}
193
+ {...itemProps}
158
194
  />
159
195
  );
160
196
  }),
@@ -163,6 +199,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
163
199
  visibleRange.min,
164
200
  visibleRange.max,
165
201
  activeIndex,
202
+ paginationItemProps,
166
203
  paginationItemLabel,
167
204
  onPaginationClick,
168
205
  ],
@@ -8,8 +8,6 @@ import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/
8
8
  * Defines the props of the component.
9
9
  */
10
10
  export interface SlideshowItemProps extends GenericProps {
11
- /** whether the slideshow item is currently visible */
12
- isCurrentlyVisible?: boolean;
13
11
  /** interval in which slides are automatically shown */
14
12
  interval?: number;
15
13
  }
@@ -33,7 +31,6 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
33
31
  */
34
32
  export const SlideshowItem: Comp<SlideshowItemProps, HTMLDivElement> = forwardRef((props, ref) => {
35
33
  const { className, children, ...forwardedProps } = props;
36
-
37
34
  return (
38
35
  <div
39
36
  ref={ref}
@@ -43,8 +40,6 @@ export const SlideshowItem: Comp<SlideshowItemProps, HTMLDivElement> = forwardRe
43
40
  prefix: CLASSNAME,
44
41
  }),
45
42
  )}
46
- aria-roledescription="slide"
47
- role="group"
48
43
  {...forwardedProps}
49
44
  >
50
45
  {children}
@@ -0,0 +1,56 @@
1
+ import React, { forwardRef } from 'react';
2
+
3
+ import classNames from 'classnames';
4
+
5
+ import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';
6
+
7
+ /**
8
+ * Defines the props of the component.
9
+ */
10
+ export interface SlideshowItemGroupProps extends GenericProps {
11
+ role?: 'tabpanel' | 'group';
12
+ label?: string;
13
+ }
14
+
15
+ /**
16
+ * Component display name.
17
+ */
18
+ const COMPONENT_NAME = 'SlideshowItemGroup';
19
+
20
+ /**
21
+ * Component default class name and class prefix.
22
+ */
23
+ export const CLASSNAME = getRootClassName(COMPONENT_NAME);
24
+
25
+ export const buildSlideShowGroupId = (slidesId: string, index: number) => `${slidesId}-slide-${index}`;
26
+
27
+ /**
28
+ * SlideshowItemGroup component.
29
+ *
30
+ * @param props Component props.
31
+ * @param ref Component ref.
32
+ * @return React element.
33
+ */
34
+ export const SlideshowItemGroup: Comp<SlideshowItemGroupProps, HTMLDivElement> = forwardRef((props, ref) => {
35
+ const { className, children, role = 'group', label, ...forwardedProps } = props;
36
+ return (
37
+ <div
38
+ ref={ref}
39
+ role={role}
40
+ className={classNames(
41
+ className,
42
+ handleBasicClasses({
43
+ prefix: CLASSNAME,
44
+ }),
45
+ )}
46
+ aria-roledescription="slide"
47
+ aria-label={label}
48
+ {...forwardedProps}
49
+ >
50
+ {children}
51
+ </div>
52
+ );
53
+ });
54
+
55
+ SlideshowItemGroup.displayName = COMPONENT_NAME;
56
+ SlideshowItemGroup.className = CLASSNAME;
@@ -20,7 +20,14 @@ Array [
20
20
  onNextClick={[Function]}
21
21
  onPaginationClick={[Function]}
22
22
  onPreviousClick={[Function]}
23
+ paginationItemProps={[Function]}
23
24
  parentRef={null}
25
+ playButtonProps={
26
+ Object {
27
+ "aria-controls": "slideshow-slides2",
28
+ "onClick": undefined,
29
+ }
30
+ }
24
31
  previousButtonProps={
25
32
  Object {
26
33
  "aria-controls": "slideshow-slides2",
@@ -32,11 +39,13 @@ Array [
32
39
  />
33
40
  </div>
34
41
  }
42
+ aria-label="Simple carousel example"
35
43
  autoPlay={false}
36
44
  groupBy={1}
45
+ hasControls={true}
37
46
  id="slideshow1"
38
- interval={1000}
39
47
  isAutoPlaying={false}
48
+ slideGroupLabel={[Function]}
40
49
  slidesId="slideshow-slides2"
41
50
  style={
42
51
  Object {
@@ -0,0 +1,67 @@
1
+ import React, { useEffect } from 'react';
2
+ import { getFocusableElements } from '@lumx/react/utils/focus/getFocusableElements';
3
+
4
+ import { CLASSNAME as ITEM_GROUP_CLASSNAME } from './SlideshowItemGroup';
5
+
6
+ export interface UseSlideFocusManagementProps {
7
+ activeIndex: number;
8
+ groupBy?: number;
9
+ wrapperRef: React.RefObject<HTMLDivElement>;
10
+ }
11
+
12
+ /**
13
+ * Manage how slides must behave when visible or not.
14
+ * When not visible, they should be hidden from screen readers and not focusable.
15
+ */
16
+ export const useSlideFocusManagement = ({ activeIndex, groupBy = 1, wrapperRef }: UseSlideFocusManagementProps) => {
17
+ useEffect(() => {
18
+ const element = wrapperRef?.current;
19
+ const startIndexVisible = activeIndex;
20
+ const endIndexVisible = startIndexVisible + 1;
21
+
22
+ const slideshowChildren = element?.querySelectorAll<HTMLElement>(`.${ITEM_GROUP_CLASSNAME}`);
23
+
24
+ /**
25
+ * Classname set on elements whose focus was blocked.
26
+ * This is to easily find elements that have been tempered with,
27
+ * and not elements whose focus was already initially blocked.
28
+ * */
29
+ const elementWithBlockedFocusClass = `${ITEM_GROUP_CLASSNAME}__no-focus`;
30
+
31
+ /**
32
+ * Display given slide to screen readers and, if focus was blocked, restore focus on elements..
33
+ */
34
+ const enableSlide = (slide: HTMLElement) => {
35
+ // Hide from screen readers
36
+ slide.setAttribute('aria-hidden', 'false');
37
+ // Find elements we have blocked focus on
38
+ slide.querySelectorAll(`.${elementWithBlockedFocusClass}`).forEach((focusableElement) => {
39
+ focusableElement.removeAttribute('tabindex');
40
+ focusableElement.classList.remove(elementWithBlockedFocusClass);
41
+ });
42
+ };
43
+
44
+ /**
45
+ * Hide given slide from screen readers and block focus on all focusable elements within.
46
+ */
47
+ const blockSlide = (slide: HTMLElement) => {
48
+ slide.setAttribute('aria-hidden', 'true');
49
+ getFocusableElements(slide).forEach((focusableElement) => {
50
+ focusableElement.setAttribute('tabindex', '-1');
51
+ focusableElement.classList.add(elementWithBlockedFocusClass);
52
+ });
53
+ };
54
+
55
+ if (slideshowChildren && slideshowChildren?.length > 0) {
56
+ Array.from(slideshowChildren).forEach((slide, slideIndex) => {
57
+ const slideIsVisible = slideIndex >= startIndexVisible && slideIndex < endIndexVisible;
58
+
59
+ if (slideIsVisible) {
60
+ enableSlide(slide);
61
+ } else {
62
+ blockSlide(slide);
63
+ }
64
+ });
65
+ }
66
+ }, [activeIndex, groupBy, wrapperRef]);
67
+ };
@@ -4,6 +4,8 @@ interface UseRovingTabIndexOptions {
4
4
  parentRef: RefObject<HTMLElement>;
5
5
  elementSelector: string;
6
6
  keepTabIndex?: boolean;
7
+ /** Action to trigger when an element is focused using roving tab index */
8
+ onElementFocus?: (element: HTMLElement) => void;
7
9
  /** List of values to be used as extra dependencies of the useEffect */
8
10
  extraDependencies?: any[];
9
11
  }
@@ -12,6 +14,7 @@ export const useRovingTabIndex = ({
12
14
  parentRef,
13
15
  elementSelector,
14
16
  keepTabIndex,
17
+ onElementFocus,
15
18
  extraDependencies = [],
16
19
  }: UseRovingTabIndexOptions): void => {
17
20
  useEffect(
@@ -47,6 +50,12 @@ export const useRovingTabIndex = ({
47
50
  }
48
51
  const newElement = elements[newTabFocus];
49
52
  newElement?.focus();
53
+
54
+ // When an element is focused using roving tab index, trigger the onElementFocus callback
55
+ if (newElement && onElementFocus) {
56
+ onElementFocus(newElement);
57
+ }
58
+
50
59
  if (keepTabIndex) {
51
60
  (evt.currentTarget as HTMLElement).setAttribute('tabindex', '-1');
52
61
  newElement?.setAttribute('tabindex', '0');
@@ -0,0 +1,5 @@
1
+ /** CSS selector listing all tabbable elements. */
2
+ export const TABBABLE_ELEMENTS_SELECTOR = `a[href], button, textarea, input:not([type="hidden"]):not([hidden]), [tabindex]`;
3
+
4
+ /** CSS selector matching element that are disabled (should not receive focus). */
5
+ export const DISABLED_SELECTOR = `[hidden], [tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])`;
@@ -1,10 +1,4 @@
1
- /** CSS selector listing all tabbable elements. */
2
- const TABBABLE_ELEMENTS_SELECTOR = `a[href], button, textarea, input:not([type="hidden"]):not([hidden]), [tabindex]`;
3
-
4
- /** CSS selector matching element that are disabled (should not receive focus). */
5
- const DISABLED_SELECTOR = `[hidden], [tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])`;
6
-
7
- const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECTOR);
1
+ import { getFocusableElements } from './getFocusableElements';
8
2
 
9
3
  /**
10
4
  * Get first and last elements focusable in an element.
@@ -13,12 +7,12 @@ const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECT
13
7
  * @return first and last focusable elements
14
8
  */
15
9
  export function getFirstAndLastFocusable(parentElement: HTMLElement) {
16
- const focusableElements = Array.from(parentElement.querySelectorAll<HTMLElement>(TABBABLE_ELEMENTS_SELECTOR));
10
+ const focusableElements = getFocusableElements(parentElement);
17
11
 
18
12
  // First non disabled element.
19
- const first = focusableElements.find(isNotDisabled);
13
+ const first = focusableElements[0];
20
14
  // Last non disabled element.
21
- const last = focusableElements.reverse().find(isNotDisabled);
15
+ const last = focusableElements[focusableElements.length - 1];
22
16
 
23
17
  if (last && first) {
24
18
  return { first, last };
@@ -0,0 +1,174 @@
1
+ import { getFocusableElements } from '@lumx/react/utils/focus/getFocusableElements';
2
+
3
+ function htmlToElement(html: string): any {
4
+ const template = document.createElement('template');
5
+ template.innerHTML = html.trim();
6
+ return template.content.firstChild;
7
+ }
8
+
9
+ describe(getFocusableElements.name, () => {
10
+ it('should get empty', () => {
11
+ const element = htmlToElement(`<div></div>`);
12
+ const focusable = getFocusableElements(element);
13
+ expect(focusable).toEqual([]);
14
+ });
15
+
16
+ it('should get single item', () => {
17
+ const element = htmlToElement(`<div><button /></div>`);
18
+ const focusable = getFocusableElements(element);
19
+ expect(focusable).toMatchInlineSnapshot(`
20
+ Array [
21
+ <button />,
22
+ ]
23
+ `);
24
+ });
25
+
26
+ // it('should get first and last', () => {
27
+ // const element = htmlToElement(`
28
+ // <div>
29
+ // <div>Non focusable div</div>
30
+ // <button>Simple button</button>
31
+ // <div>Non focusable div</div>
32
+ // <input />
33
+ // <div>Non focusable div</div>
34
+ // </div>
35
+ // `);
36
+ // const focusable = getFocusableElements(element);
37
+ // expect(focusable.first).toMatchInlineSnapshot(`
38
+ // <button>
39
+ // Simple button
40
+ // </button>
41
+ // `);
42
+ // expect(focusable.first).toMatchInlineSnapshot(`
43
+ // <button>
44
+ // Simple button
45
+ // </button>
46
+ // `);
47
+ // });
48
+
49
+ describe('match focusable elements', () => {
50
+ it('should match input element', () => {
51
+ const element = htmlToElement(`<div><input /></div>`);
52
+ const focusable = getFocusableElements(element);
53
+ expect(focusable).toMatchInlineSnapshot(`
54
+ Array [
55
+ <input />,
56
+ ]
57
+ `);
58
+ });
59
+
60
+ it('should match link element', () => {
61
+ const element = htmlToElement(`<div><a href="#" /></div>`);
62
+ const focusable = getFocusableElements(element);
63
+ expect(focusable).toMatchInlineSnapshot(`
64
+ Array [
65
+ <a
66
+ href="#"
67
+ />,
68
+ ]
69
+ `);
70
+ });
71
+
72
+ it('should match textarea element', () => {
73
+ const element = htmlToElement(`<div><textarea /></div>`);
74
+ const focusable = getFocusableElements(element);
75
+ expect(focusable).toMatchInlineSnapshot(`
76
+ Array [
77
+ <textarea>
78
+ &lt;/div&gt;
79
+ </textarea>,
80
+ ]
81
+ `);
82
+ });
83
+
84
+ it('should match element with tabindex', () => {
85
+ const element = htmlToElement(`<div><span tabindex="0" /></div>`);
86
+ const focusable = getFocusableElements(element);
87
+ expect(focusable).toMatchInlineSnapshot(`
88
+ Array [
89
+ <span
90
+ tabindex="0"
91
+ />,
92
+ ]
93
+ `);
94
+ });
95
+
96
+ it('should keep disabled=false', () => {
97
+ const element = htmlToElement(`<div><button disabled="false" /><button /></div>`);
98
+ const focusable = getFocusableElements(element);
99
+ expect(focusable).toMatchInlineSnapshot(`
100
+ Array [
101
+ <button
102
+ disabled="false"
103
+ />,
104
+ <button />,
105
+ ]
106
+ `);
107
+ });
108
+
109
+ it('should keep aria-disabled=false', () => {
110
+ const element = htmlToElement(`<div><button aria-disabled="false" /><button /></div>`);
111
+ const focusable = getFocusableElements(element);
112
+ expect(focusable).toMatchInlineSnapshot(`
113
+ Array [
114
+ <button
115
+ aria-disabled="false"
116
+ />,
117
+ <button />,
118
+ ]
119
+ `);
120
+ });
121
+ });
122
+
123
+ describe('skip disabled elements', () => {
124
+ it('should skip disabled', () => {
125
+ const element = htmlToElement(`<div><button disabled /><button /></div>`);
126
+ const focusable = getFocusableElements(element);
127
+ expect(focusable).toMatchInlineSnapshot(`
128
+ Array [
129
+ <button />,
130
+ ]
131
+ `);
132
+ });
133
+
134
+ it('should skip aria-disabled', () => {
135
+ const element = htmlToElement(`<div><button aria-disabled /><button /></div>`);
136
+ const focusable = getFocusableElements(element);
137
+ expect(focusable).toMatchInlineSnapshot(`
138
+ Array [
139
+ <button />,
140
+ ]
141
+ `);
142
+ });
143
+
144
+ it('should skip tabindex=-1', () => {
145
+ const element = htmlToElement(`<div><button tabindex="-1" /><button /></div>`);
146
+ const focusable = getFocusableElements(element);
147
+ expect(focusable).toMatchInlineSnapshot(`
148
+ Array [
149
+ <button />,
150
+ ]
151
+ `);
152
+ });
153
+
154
+ it('should skip input type hidden', () => {
155
+ const element = htmlToElement(`<div><input type="hidden" /><button /></div>`);
156
+ const focusable = getFocusableElements(element);
157
+ expect(focusable).toMatchInlineSnapshot(`
158
+ Array [
159
+ <button />,
160
+ ]
161
+ `);
162
+ });
163
+
164
+ it('should skip hidden input', () => {
165
+ const element = htmlToElement(`<div><input hidden /><button /></div>`);
166
+ const focusable = getFocusableElements(element);
167
+ expect(focusable).toMatchInlineSnapshot(`
168
+ Array [
169
+ <button />,
170
+ ]
171
+ `);
172
+ });
173
+ });
174
+ });
@@ -0,0 +1,7 @@
1
+ import { DISABLED_SELECTOR, TABBABLE_ELEMENTS_SELECTOR } from './constants';
2
+
3
+ const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECTOR);
4
+
5
+ export function getFocusableElements(element: HTMLElement): HTMLElement[] {
6
+ return Array.from(element.querySelectorAll<HTMLElement>(TABBABLE_ELEMENTS_SELECTOR)).filter(isNotDisabled);
7
+ }
package/types.d.ts CHANGED
@@ -2135,13 +2135,13 @@ export declare const clamp: (value: number, min: number, max: number) => number;
2135
2135
  /**
2136
2136
  * Defines the props of the component.
2137
2137
  */
2138
- export interface SlideshowProps extends GenericProps, Pick<SlidesProps, "autoPlay" | "slidesId" | "id" | "theme" | "fillHeight" | "groupBy"> {
2138
+ export interface SlideshowProps extends GenericProps, Pick<SlidesProps, "autoPlay" | "slidesId" | "id" | "theme" | "fillHeight" | "groupBy" | "slideGroupLabel"> {
2139
2139
  /** current slide active */
2140
2140
  activeIndex?: SlidesProps["activeIndex"];
2141
2141
  /** Interval between each slide when automatic rotation is enabled. */
2142
2142
  interval?: number;
2143
2143
  /** Props to pass to the slideshow controls (minus those already set by the Slideshow props). */
2144
- slideshowControlsProps?: Pick<SlideshowControlsProps, "nextButtonProps" | "previousButtonProps"> & Omit<SlideshowControlsProps, "activeIndex" | "onPaginationClick" | "onNextClick" | "onPreviousClick" | "slidesCount" | "parentRef" | "theme">;
2144
+ slideshowControlsProps?: Pick<SlideshowControlsProps, "nextButtonProps" | "previousButtonProps" | "paginationItemProps"> & Omit<SlideshowControlsProps, "activeIndex" | "onPaginationClick" | "onNextClick" | "onPreviousClick" | "slidesCount" | "parentRef" | "theme">;
2145
2145
  /** Callback when slide changes */
2146
2146
  onChange?(index: number): void;
2147
2147
  }
@@ -2157,8 +2157,6 @@ export declare const Slideshow: Comp<SlideshowProps, HTMLDivElement>;
2157
2157
  * Defines the props of the component.
2158
2158
  */
2159
2159
  export interface SlideshowItemProps extends GenericProps {
2160
- /** whether the slideshow item is currently visible */
2161
- isCurrentlyVisible?: boolean;
2162
2160
  /** interval in which slides are automatically shown */
2163
2161
  interval?: number;
2164
2162
  }
@@ -2242,6 +2240,8 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
2242
2240
  parentRef?: RefObject<HTMLDivElement> | HTMLDivElement | null;
2243
2241
  /** Props to pass to the previous button (minus those already set by the SlideshowControls props). */
2244
2242
  previousButtonProps: Pick<IconButtonProps, "label"> & Omit<IconButtonProps, "label" | "onClick" | "icon" | "emphasis" | "color">;
2243
+ /** Props to pass to the pagination wrapper */
2244
+ paginationProps?: Omit<React.HTMLAttributes<HTMLDivElement>, "className" | "style" | "role">;
2245
2245
  /** Number of slides. */
2246
2246
  slidesCount: number;
2247
2247
  /** On next button click callback. */
@@ -2252,8 +2252,15 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
2252
2252
  onPreviousClick?(loopback?: boolean): void;
2253
2253
  /** whether the slideshow is currently playing */
2254
2254
  isAutoPlaying?: boolean;
2255
- /** function to be executed in order to retrieve the label for the pagination item */
2255
+ /**
2256
+ * function to be executed in order to retrieve the label for the pagination item
2257
+ * @deprecated Use paginationItemProps instead.
2258
+ * */
2256
2259
  paginationItemLabel?: (index: number) => string;
2260
+ /**
2261
+ * function to be executed in order to retrieve the props for a pagination item.
2262
+ */
2263
+ paginationItemProps?: (itemIndex: number) => React.HTMLAttributes<HTMLButtonElement>;
2257
2264
  /** Props to pass to the lay button (minus those already set by the SlideshowControls props). */
2258
2265
  playButtonProps?: Pick<IconButtonProps, "label"> & Omit<IconButtonProps, "label" | "onClick" | "icon" | "emphasis" | "color">;
2259
2266
  }
@@ -2280,6 +2287,13 @@ export interface SlidesProps extends GenericProps, HasTheme {
2280
2287
  toggleAutoPlay: () => void;
2281
2288
  /** component to be rendered after the slides */
2282
2289
  afterSlides?: React.ReactNode;
2290
+ /** Whether the slides have controls linked */
2291
+ hasControls?: boolean;
2292
+ /**
2293
+ * Accessible label to set on a slide group.
2294
+ * Receives the group position starting from 1 and the total number of groups.
2295
+ * */
2296
+ slideGroupLabel?: (groupPosition: number, groupTotal: number) => string;
2283
2297
  }
2284
2298
  /**
2285
2299
  * Slides component.