@reykjavik/hanna-react 0.10.128 → 0.10.130
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.
- package/CHANGELOG.md +15 -0
- package/FocusTrap.js +22 -4
- package/_abstract/_AbstractCarousel.d.ts +6 -0
- package/_abstract/_AbstractCarousel.js +27 -7
- package/esm/FocusTrap.js +22 -4
- package/esm/_abstract/_AbstractCarousel.d.ts +6 -0
- package/esm/_abstract/_AbstractCarousel.js +27 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
- ... <!-- Add new lines here. -->
|
|
6
6
|
|
|
7
|
+
## 0.10.130
|
|
8
|
+
|
|
9
|
+
_2024-07-23_
|
|
10
|
+
|
|
11
|
+
- `FocusTrap`:
|
|
12
|
+
- fix: Account for hidden and disabled elements not being focusable
|
|
13
|
+
|
|
14
|
+
## 0.10.129
|
|
15
|
+
|
|
16
|
+
_2024-07-16_
|
|
17
|
+
|
|
18
|
+
- feat: Add prop `childrenHTML` to `Carousel` for Real Nasty Dirty Work™ —
|
|
19
|
+
primary use case is progressive enhancement of static HTML.
|
|
20
|
+
- feat: Gracefully catch (and log) errors in Sprinkles' lifecycle methods
|
|
21
|
+
|
|
7
22
|
## 0.10.128
|
|
8
23
|
|
|
9
24
|
_2024-07-01_
|
package/FocusTrap.js
CHANGED
|
@@ -12,7 +12,7 @@ const react_1 = tslib_1.__importDefault(require("react"));
|
|
|
12
12
|
const FocusTrap = (props) => {
|
|
13
13
|
const Tag = props.Tag || 'span';
|
|
14
14
|
return (react_1.default.createElement(Tag, { className: "FocusTrap", tabIndex: 0, onFocus: (e) => {
|
|
15
|
-
|
|
15
|
+
e.preventDefault();
|
|
16
16
|
let container = e.currentTarget;
|
|
17
17
|
let depth = Math.max(props.depth || 1, 1);
|
|
18
18
|
while (depth-- && container) {
|
|
@@ -21,9 +21,27 @@ const FocusTrap = (props) => {
|
|
|
21
21
|
if (!container) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
-
const focusables = container.querySelectorAll('a, input, select, textarea, button, [tabindex]:not(.FocusTrap):not([tabindex="-1"])');
|
|
25
|
-
const
|
|
26
|
-
|
|
24
|
+
const focusables = container.querySelectorAll('a[href], input, select, textarea, button, [tabindex]:not(.FocusTrap):not([tabindex="-1"])');
|
|
25
|
+
const delta = props.atTop ? -1 : 1;
|
|
26
|
+
let i = delta < 0 ? focusables.length - 1 : 0;
|
|
27
|
+
let newTarget;
|
|
28
|
+
while ((newTarget = focusables[i])) {
|
|
29
|
+
newTarget.focus();
|
|
30
|
+
// See if the focus actually moved to newTarget
|
|
31
|
+
if (document.activeElement === newTarget) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
i += delta;
|
|
35
|
+
}
|
|
36
|
+
// desperationLevel++; // We've failed to find a focusable element.
|
|
37
|
+
// Let's see if we can find a non-interactive focusable to fall back on
|
|
38
|
+
const fallbackFocusElm = container.matches('[tabindex]')
|
|
39
|
+
? container
|
|
40
|
+
: container.querySelector('[tabindex="-1"]') || container.closest('[tabindex');
|
|
41
|
+
if (fallbackFocusElm) {
|
|
42
|
+
fallbackFocusElm.focus();
|
|
43
|
+
}
|
|
44
|
+
// desperationLevel++; // Whatever, we tried. ¯\_(ツ)_/¯
|
|
27
45
|
} }));
|
|
28
46
|
};
|
|
29
47
|
exports.FocusTrap = FocusTrap;
|
|
@@ -20,6 +20,12 @@ export type CarouselProps<I extends Record<string, unknown> = Record<string, nev
|
|
|
20
20
|
* `<Fragment />` or some such.
|
|
21
21
|
*/
|
|
22
22
|
itemCount?: number;
|
|
23
|
+
}, {
|
|
24
|
+
/**
|
|
25
|
+
* HTML content that should be dangerously inserted directly into the
|
|
26
|
+
* `__itemlist` element
|
|
27
|
+
*/
|
|
28
|
+
childrenHTML: string;
|
|
23
29
|
}> & WrapperElmProps & DeprecatedSeenProp;
|
|
24
30
|
type AbstractCarouselProps<I extends Record<string, unknown> = Record<string, unknown>, P extends Record<string, unknown> | undefined = Record<string, unknown>> = CarouselProps<I, P> & BemProps & {
|
|
25
31
|
title?: string;
|
|
@@ -24,17 +24,35 @@ const scrollXBy = (elm, deltaX) => {
|
|
|
24
24
|
};
|
|
25
25
|
// eslint-disable-next-line complexity
|
|
26
26
|
const AbstractCarousel = (props) => {
|
|
27
|
-
const { title, items = [], Component, ComponentProps, bem = 'Carousel', modifier, ssr, className, wrapperProps, } = props;
|
|
27
|
+
const { title, items = [], Component, ComponentProps, childrenHTML, bem = 'Carousel', modifier, ssr, className, wrapperProps, } = props;
|
|
28
|
+
const isBrowser = (0, utils_js_1.useIsBrowserSide)(ssr);
|
|
28
29
|
const children = !props.children
|
|
29
30
|
? undefined
|
|
30
31
|
: Array.isArray(props.children)
|
|
31
32
|
? props.children.filter(hanna_utils_1.notNully)
|
|
32
33
|
: [props.children];
|
|
33
34
|
const [leftOffset, setLeftOffset] = (0, react_1.useState)();
|
|
34
|
-
const
|
|
35
|
+
const htmlChildrenCount = (0, react_1.useMemo)(() => {
|
|
36
|
+
if (!(childrenHTML === null || childrenHTML === void 0 ? void 0 : childrenHTML.trim())) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!isBrowser) {
|
|
40
|
+
return 1; // Return arbitrary non-zero count to avoid early return below.
|
|
41
|
+
// During SSR (and initial hydration render) we want to render the
|
|
42
|
+
// Carousel component, even though we don't know the exact number of
|
|
43
|
+
// items yet.
|
|
44
|
+
}
|
|
45
|
+
const div = document.createElement('div');
|
|
46
|
+
div.innerHTML = childrenHTML;
|
|
47
|
+
return div.children.length;
|
|
48
|
+
}, [childrenHTML, isBrowser]);
|
|
49
|
+
const itemCount = props.itemCount != null
|
|
50
|
+
? props.itemCount
|
|
51
|
+
: childrenHTML
|
|
52
|
+
? htmlChildrenCount
|
|
53
|
+
: (children || items).length;
|
|
35
54
|
const listRef = (0, react_1.useRef)(null);
|
|
36
55
|
const [activeItem, setActiveItem] = (0, react_1.useState)(0);
|
|
37
|
-
const isBrowser = (0, utils_js_1.useIsBrowserSide)(ssr);
|
|
38
56
|
// Since listElm gets unmounted and remounted based on isBrowser, we
|
|
39
57
|
// wait for isBrowser is true before setting scroll and resize events.
|
|
40
58
|
const listElm = isBrowser && listRef.current;
|
|
@@ -91,10 +109,12 @@ const AbstractCarousel = (props) => {
|
|
|
91
109
|
}
|
|
92
110
|
const itemList = (react_1.default.createElement("div", { className: `${bem}__itemlist`, style: leftOffset
|
|
93
111
|
? { '--Carousel--leftOffset': `${leftOffset}px` }
|
|
94
|
-
: undefined, "data-scroll-snapping": leftOffset ? 'true' : undefined, ref: listRef },
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
: undefined, "data-scroll-snapping": leftOffset ? 'true' : undefined, ref: listRef, dangerouslySetInnerHTML: childrenHTML ? { __html: childrenHTML } : undefined }, childrenHTML
|
|
113
|
+
? undefined
|
|
114
|
+
: children ||
|
|
115
|
+
items.map((item, i) => (
|
|
116
|
+
// @ts-expect-error (Can't be arsed...)
|
|
117
|
+
react_1.default.createElement(Component, Object.assign({ key: i }, ComponentProps, item))))));
|
|
98
118
|
return (react_1.default.createElement("div", Object.assign({}, wrapperProps, { className: (0, classUtils_1.modifiedClass)(bem, modifier,
|
|
99
119
|
// Prefer `className` over `wrapperProps.className`
|
|
100
120
|
className || (wrapperProps || {}).className), "data-sprinkled": isBrowser }),
|
package/esm/FocusTrap.js
CHANGED
|
@@ -8,7 +8,7 @@ import React from 'react';
|
|
|
8
8
|
export const FocusTrap = (props) => {
|
|
9
9
|
const Tag = props.Tag || 'span';
|
|
10
10
|
return (React.createElement(Tag, { className: "FocusTrap", tabIndex: 0, onFocus: (e) => {
|
|
11
|
-
|
|
11
|
+
e.preventDefault();
|
|
12
12
|
let container = e.currentTarget;
|
|
13
13
|
let depth = Math.max(props.depth || 1, 1);
|
|
14
14
|
while (depth-- && container) {
|
|
@@ -17,8 +17,26 @@ export const FocusTrap = (props) => {
|
|
|
17
17
|
if (!container) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
const focusables = container.querySelectorAll('a, input, select, textarea, button, [tabindex]:not(.FocusTrap):not([tabindex="-1"])');
|
|
21
|
-
const
|
|
22
|
-
|
|
20
|
+
const focusables = container.querySelectorAll('a[href], input, select, textarea, button, [tabindex]:not(.FocusTrap):not([tabindex="-1"])');
|
|
21
|
+
const delta = props.atTop ? -1 : 1;
|
|
22
|
+
let i = delta < 0 ? focusables.length - 1 : 0;
|
|
23
|
+
let newTarget;
|
|
24
|
+
while ((newTarget = focusables[i])) {
|
|
25
|
+
newTarget.focus();
|
|
26
|
+
// See if the focus actually moved to newTarget
|
|
27
|
+
if (document.activeElement === newTarget) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
i += delta;
|
|
31
|
+
}
|
|
32
|
+
// desperationLevel++; // We've failed to find a focusable element.
|
|
33
|
+
// Let's see if we can find a non-interactive focusable to fall back on
|
|
34
|
+
const fallbackFocusElm = container.matches('[tabindex]')
|
|
35
|
+
? container
|
|
36
|
+
: container.querySelector('[tabindex="-1"]') || container.closest('[tabindex');
|
|
37
|
+
if (fallbackFocusElm) {
|
|
38
|
+
fallbackFocusElm.focus();
|
|
39
|
+
}
|
|
40
|
+
// desperationLevel++; // Whatever, we tried. ¯\_(ツ)_/¯
|
|
23
41
|
} }));
|
|
24
42
|
};
|
|
@@ -20,6 +20,12 @@ export type CarouselProps<I extends Record<string, unknown> = Record<string, nev
|
|
|
20
20
|
* `<Fragment />` or some such.
|
|
21
21
|
*/
|
|
22
22
|
itemCount?: number;
|
|
23
|
+
}, {
|
|
24
|
+
/**
|
|
25
|
+
* HTML content that should be dangerously inserted directly into the
|
|
26
|
+
* `__itemlist` element
|
|
27
|
+
*/
|
|
28
|
+
childrenHTML: string;
|
|
23
29
|
}> & WrapperElmProps & DeprecatedSeenProp;
|
|
24
30
|
type AbstractCarouselProps<I extends Record<string, unknown> = Record<string, unknown>, P extends Record<string, unknown> | undefined = Record<string, unknown>> = CarouselProps<I, P> & BemProps & {
|
|
25
31
|
title?: string;
|
|
@@ -20,17 +20,35 @@ const scrollXBy = (elm, deltaX) => {
|
|
|
20
20
|
};
|
|
21
21
|
// eslint-disable-next-line complexity
|
|
22
22
|
export const AbstractCarousel = (props) => {
|
|
23
|
-
const { title, items = [], Component, ComponentProps, bem = 'Carousel', modifier, ssr, className, wrapperProps, } = props;
|
|
23
|
+
const { title, items = [], Component, ComponentProps, childrenHTML, bem = 'Carousel', modifier, ssr, className, wrapperProps, } = props;
|
|
24
|
+
const isBrowser = useIsBrowserSide(ssr);
|
|
24
25
|
const children = !props.children
|
|
25
26
|
? undefined
|
|
26
27
|
: Array.isArray(props.children)
|
|
27
28
|
? props.children.filter(notNully)
|
|
28
29
|
: [props.children];
|
|
29
30
|
const [leftOffset, setLeftOffset] = useState();
|
|
30
|
-
const
|
|
31
|
+
const htmlChildrenCount = useMemo(() => {
|
|
32
|
+
if (!(childrenHTML === null || childrenHTML === void 0 ? void 0 : childrenHTML.trim())) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!isBrowser) {
|
|
36
|
+
return 1; // Return arbitrary non-zero count to avoid early return below.
|
|
37
|
+
// During SSR (and initial hydration render) we want to render the
|
|
38
|
+
// Carousel component, even though we don't know the exact number of
|
|
39
|
+
// items yet.
|
|
40
|
+
}
|
|
41
|
+
const div = document.createElement('div');
|
|
42
|
+
div.innerHTML = childrenHTML;
|
|
43
|
+
return div.children.length;
|
|
44
|
+
}, [childrenHTML, isBrowser]);
|
|
45
|
+
const itemCount = props.itemCount != null
|
|
46
|
+
? props.itemCount
|
|
47
|
+
: childrenHTML
|
|
48
|
+
? htmlChildrenCount
|
|
49
|
+
: (children || items).length;
|
|
31
50
|
const listRef = useRef(null);
|
|
32
51
|
const [activeItem, setActiveItem] = useState(0);
|
|
33
|
-
const isBrowser = useIsBrowserSide(ssr);
|
|
34
52
|
// Since listElm gets unmounted and remounted based on isBrowser, we
|
|
35
53
|
// wait for isBrowser is true before setting scroll and resize events.
|
|
36
54
|
const listElm = isBrowser && listRef.current;
|
|
@@ -87,10 +105,12 @@ export const AbstractCarousel = (props) => {
|
|
|
87
105
|
}
|
|
88
106
|
const itemList = (React.createElement("div", { className: `${bem}__itemlist`, style: leftOffset
|
|
89
107
|
? { '--Carousel--leftOffset': `${leftOffset}px` }
|
|
90
|
-
: undefined, "data-scroll-snapping": leftOffset ? 'true' : undefined, ref: listRef },
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
: undefined, "data-scroll-snapping": leftOffset ? 'true' : undefined, ref: listRef, dangerouslySetInnerHTML: childrenHTML ? { __html: childrenHTML } : undefined }, childrenHTML
|
|
109
|
+
? undefined
|
|
110
|
+
: children ||
|
|
111
|
+
items.map((item, i) => (
|
|
112
|
+
// @ts-expect-error (Can't be arsed...)
|
|
113
|
+
React.createElement(Component, Object.assign({ key: i }, ComponentProps, item))))));
|
|
94
114
|
return (React.createElement("div", Object.assign({}, wrapperProps, { className: modifiedClass(bem, modifier,
|
|
95
115
|
// Prefer `className` over `wrapperProps.className`
|
|
96
116
|
className || (wrapperProps || {}).className), "data-sprinkled": isBrowser }),
|