@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 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
- var _a;
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 targetIdx = props.atTop ? focusables.length - 1 : 0;
26
- (_a = focusables[targetIdx]) === null || _a === void 0 ? void 0 : _a.focus();
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 itemCount = props.itemCount != null ? props.itemCount : (children || items).length;
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 }, children ||
95
- items.map((item, i) => (
96
- // @ts-expect-error (Can't be arsed...)
97
- react_1.default.createElement(Component, Object.assign({ key: i }, ComponentProps, item))))));
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
- var _a;
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 targetIdx = props.atTop ? focusables.length - 1 : 0;
22
- (_a = focusables[targetIdx]) === null || _a === void 0 ? void 0 : _a.focus();
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 itemCount = props.itemCount != null ? props.itemCount : (children || items).length;
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 }, children ||
91
- items.map((item, i) => (
92
- // @ts-expect-error (Can't be arsed...)
93
- React.createElement(Component, Object.assign({ key: i }, ComponentProps, item))))));
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 }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/hanna-react",
3
- "version": "0.10.128",
3
+ "version": "0.10.130",
4
4
  "author": "Reykjavík (http://www.reykjavik.is)",
5
5
  "contributors": [
6
6
  "Hugsmiðjan ehf (http://www.hugsmidjan.is)",