@olenbetong/synergi-react 1.0.5 → 2.0.1

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 (67) hide show
  1. package/dist/esm/ob.react.css +216 -146
  2. package/dist/esm/ob.react.js +227 -131
  3. package/dist/esm/ob.react.min.css +1 -1
  4. package/dist/esm/ob.react.min.css.map +3 -3
  5. package/dist/esm/ob.react.min.js +1 -1
  6. package/dist/esm/ob.react.min.js.map +4 -4
  7. package/dist/iife/ob.react.css +216 -146
  8. package/dist/iife/ob.react.js +230 -134
  9. package/dist/iife/ob.react.min.css +1 -1
  10. package/dist/iife/ob.react.min.css.map +3 -3
  11. package/dist/iife/ob.react.min.js +1 -1
  12. package/dist/iife/ob.react.min.js.map +4 -4
  13. package/dist/styles/styles.css +1 -1
  14. package/dist/styles/styles.css.map +1 -1
  15. package/es/Checkbox/index.css +1 -1
  16. package/es/Checkbox/index.d.ts +11 -0
  17. package/es/Checkbox/index.js +8 -5
  18. package/es/ColorCard/index.d.ts +5 -2
  19. package/es/ColorCard/index.js +6 -0
  20. package/es/DateNavigator/index.css +1 -1
  21. package/es/DateNavigator/index.d.ts +3 -1
  22. package/es/DateNavigator/index.js +82 -28
  23. package/es/LinkedCardList/index.css +1 -1
  24. package/es/LinkedCardList/index.d.ts +2 -2
  25. package/es/LinkedCardList/index.js +2 -2
  26. package/es/PageBanner/index.js +2 -1
  27. package/es/Portal/index.js +5 -7
  28. package/es/ProgressBar/index.css +1 -1
  29. package/es/Sidebar/index.js +1 -0
  30. package/es/Spinner/index.d.ts +1 -1
  31. package/es/SplitContainer/index.d.ts +4 -2
  32. package/es/SplitContainer/index.js +45 -13
  33. package/es/ValueToggleGroup/index.d.ts +18 -0
  34. package/es/ValueToggleGroup/index.js +12 -0
  35. package/es/index.css +1 -1
  36. package/es/index.d.ts +8 -8
  37. package/es/index.js +3 -3
  38. package/es/useTranslation/index.js +2 -1
  39. package/package.json +88 -90
  40. package/src/Checkbox/index.scss +12 -5
  41. package/src/Checkbox/index.tsx +25 -7
  42. package/src/ColorCard/{index.scss → index.css} +50 -59
  43. package/src/ColorCard/index.tsx +21 -9
  44. package/src/DateNavigator/index.scss +66 -33
  45. package/src/DateNavigator/index.tsx +98 -33
  46. package/src/LinkedCardList/index.scss +8 -1
  47. package/src/LinkedCardList/index.tsx +2 -2
  48. package/src/PageBanner/index.tsx +2 -1
  49. package/src/Portal/index.tsx +8 -7
  50. package/src/ProgressBar/index.scss +2 -2
  51. package/src/Sidebar/index.tsx +1 -0
  52. package/src/Spinner/{index.scss → index.css} +13 -10
  53. package/src/Spinner/index.tsx +2 -2
  54. package/src/SplitContainer/index.tsx +75 -17
  55. package/src/ValueToggleGroup/index.css +79 -0
  56. package/src/ValueToggleGroup/index.tsx +52 -0
  57. package/src/index.scss +3 -2
  58. package/src/index.ts +12 -12
  59. package/src/useTranslation/index.ts +2 -1
  60. package/LICENSE +0 -21
  61. package/es/ColorCard/index.css +0 -1
  62. package/es/Spinner/index.css +0 -1
  63. package/es/ValueToggle/index.css +0 -1
  64. package/es/ValueToggle/index.d.ts +0 -18
  65. package/es/ValueToggle/index.js +0 -38
  66. package/src/ValueToggle/index.scss +0 -50
  67. package/src/ValueToggle/index.tsx +0 -75
@@ -3,58 +3,91 @@
3
3
  $date-select-height: 3rem;
4
4
 
5
5
  .ObDateNavigator-icon {
6
- display: inline-block;
6
+ display: inline-block;
7
7
 
8
- svg {
9
- width: 0.625em;
10
- position: relative;
11
- top: -1px;
12
- }
8
+ svg {
9
+ width: 0.625em;
10
+ position: relative;
11
+ top: -1px;
12
+ }
13
13
  }
14
14
 
15
15
  .ObDateNavigator-root {
16
- background-color: hsl(220, 20%, 96%);
17
- background-color: var(--block-header-color-bg);
18
- display: flex;
19
- font-size: 1rem;
16
+ background-color: hsl(220, 20%, 96%);
17
+ background-color: var(--block-header-color-bg);
18
+ display: flex;
19
+ font-size: 1rem;
20
+ outline: none;
21
+
22
+ &:focus-visible {
23
+ outline: 2px solid var(--brand-dark, #336699);
24
+ outline-offset: 2px;
25
+ }
20
26
  }
21
27
 
22
28
  .ObDateNavigator-arrow {
23
- @include mixins.btn-reset;
24
- display: flex;
25
- align-items: center;
26
- flex: 0 0 $date-select-height;
27
- justify-content: center;
29
+ @include mixins.btn-reset;
30
+ display: flex;
31
+ align-items: center;
32
+ flex: 0 0 $date-select-height;
33
+ justify-content: center;
28
34
 
29
- height: $date-select-height;
35
+ height: $date-select-height;
30
36
 
31
- cursor: pointer;
32
- text-align: center;
37
+ cursor: pointer;
38
+ text-align: center;
33
39
 
34
- &:hover {
35
- background-color: #e3e3e3;
36
- background-color: var(--block-header-button-hover);
37
- }
40
+ &:hover {
41
+ background-color: #e3e3e3;
42
+ background-color: var(--block-header-button-hover);
43
+ }
38
44
  }
39
45
 
40
46
  .ObDateNavigator-arrowLeft {
41
- border-right: 1px solid #dddddd;
42
- order: 1;
47
+ border-right: 1px solid #dddddd;
48
+ order: 1;
43
49
  }
44
50
 
45
51
  .ObDateNavigator-arrowRight {
46
- border-left: 1px solid #dddddd;
47
- order: 3;
52
+ border-left: 1px solid #dddddd;
53
+ order: 3;
48
54
  }
49
55
 
50
56
  .ObDateNavigator-date {
51
- display: flex;
52
- flex: 1 1 100%;
53
- flex-direction: column;
54
- justify-content: center;
55
- order: 2;
57
+ display: flex;
58
+ flex: 1 1 100%;
59
+ flex-direction: column;
60
+ justify-content: center;
61
+ order: 2;
56
62
 
57
- height: $date-select-height;
63
+ height: $date-select-height;
58
64
 
59
- text-align: center;
65
+ text-align: center;
60
66
  }
67
+
68
+ .ObDateNavigator-today {
69
+ @include mixins.btn-reset;
70
+ order: 4;
71
+ padding: 0 1rem;
72
+ height: $date-select-height;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ font-size: 0.875rem;
77
+ color: var(--brand-dark, #336699);
78
+ text-transform: uppercase;
79
+ cursor: pointer;
80
+
81
+ text-wrap: nowrap;
82
+
83
+ border-left: 1px solid #dddddd;
84
+
85
+ &:hover {
86
+ background-color: var(--block-header-button-hover);
87
+ }
88
+
89
+ &:focus-visible {
90
+ outline: 2px solid var(--brand-dark, #336699);
91
+ outline-offset: 2px;
92
+ }
93
+ }
@@ -1,35 +1,36 @@
1
1
  import "./index.scss";
2
2
 
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
-
5
4
  import clsx from "clsx";
6
- import { useState } from "react";
5
+ import { useEffect, useRef, useState } from "react";
7
6
 
8
7
  function getUiCulture() {
9
8
  return af?.userSession?.culture ?? "default";
10
9
  }
11
10
 
12
- function Arrow(props) {
13
- const { direction, ...rest } = props;
14
-
11
+ function Arrow({
12
+ direction,
13
+ ...rest
14
+ }: { direction: "left" | "right" } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
15
15
  return (
16
16
  <button
17
17
  className={clsx(
18
18
  "ObDateNavigator-arrow",
19
19
  direction === "left" ? "ObDateNavigator-arrowLeft" : "ObDateNavigator-arrowRight",
20
20
  )}
21
+ aria-label={direction === "left" ? getLocalizedString("Previous day") : getLocalizedString("Next day")}
21
22
  {...rest}
22
23
  >
23
24
  <span className="ObDateNavigator-icon">
24
25
  {direction === "left" ? (
25
- <svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 320 512">
26
+ <svg aria-hidden="true" viewBox="0 0 320 512">
26
27
  <path
27
28
  fill="currentColor"
28
29
  d="M34.52 239.03L228.87 44.69c9.37-9.37 24.57-9.37 33.94 0l22.67 22.67c9.36 9.36 9.37 24.52.04 33.9L131.49 256l154.02 154.75c9.34 9.38 9.32 24.54-.04 33.9l-22.67 22.67c-9.37 9.37-24.57 9.37-33.94 0L34.52 272.97c-9.37-9.37-9.37-24.57 0-33.94z"
29
30
  />
30
31
  </svg>
31
32
  ) : (
32
- <svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 320 512">
33
+ <svg aria-hidden="true" viewBox="0 0 320 512">
33
34
  <path
34
35
  fill="currentColor"
35
36
  d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"
@@ -48,45 +49,109 @@ const formatter = new Intl.DateTimeFormat(getUiCulture(), {
48
49
  year: "numeric",
49
50
  timeZone: "utc",
50
51
  });
51
- const today = new Date();
52
- const todayUtc = Date.UTC(today.getFullYear(), today.getMonth(), today.getDate());
53
52
 
54
53
  const oneDay = 24 * 60 * 60 * 1000;
55
54
 
56
- export function DateNavigator({ date: dateProp, onChange }: { date?: Date; onChange?: (date: Date) => void }) {
55
+ export function DateNavigator({
56
+ date: dateProp,
57
+ onChange,
58
+ minDate,
59
+ maxDate,
60
+ }: {
61
+ date?: Date;
62
+ onChange?: (date: Date) => void;
63
+ minDate?: Date;
64
+ maxDate?: Date;
65
+ }) {
57
66
  let [internalDate, setInternalDate] = useState(new Date());
58
- const date = dateProp ?? internalDate;
59
- const dateUtc = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());
60
- let dateValue = new Date(dateUtc);
61
-
62
- let text = formatter.format(dateValue);
63
- let altText;
64
-
65
- if (todayUtc === dateUtc) {
66
- altText = getLocalizedString("Today");
67
- } else if (todayUtc === dateUtc + oneDay) {
68
- altText = getLocalizedString("Yesterday");
69
- } else if (todayUtc === dateUtc - oneDay) {
70
- altText = getLocalizedString("Tomorrow");
67
+ let date = dateProp ?? internalDate;
68
+ let dateUtc = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());
69
+ let today = new Date();
70
+ let todayUtc = Date.UTC(today.getFullYear(), today.getMonth(), today.getDate());
71
+ let liveRef = useRef<HTMLDivElement>(null);
72
+
73
+ let text = (() => {
74
+ let base = formatter.format(new Date(dateUtc));
75
+ if (todayUtc === dateUtc) return `${getLocalizedString("Today")}, ${base}`;
76
+ if (todayUtc === dateUtc + oneDay) return `${getLocalizedString("Yesterday")}, ${base}`;
77
+ if (todayUtc === dateUtc - oneDay) return `${getLocalizedString("Tomorrow")}, ${base}`;
78
+ return base;
79
+ })();
80
+
81
+ let disablePrev = !!minDate && dateUtc <= Date.UTC(minDate.getFullYear(), minDate.getMonth(), minDate.getDate());
82
+ let disableNext = !!maxDate && dateUtc >= Date.UTC(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate());
83
+
84
+ function updateDate(newDate: Date) {
85
+ if (minDate && newDate < minDate) {
86
+ newDate = new Date(minDate); // Ensure we don't go below minDate
87
+ } else if (maxDate && newDate > maxDate) {
88
+ newDate = new Date(maxDate); // Ensure we don't go above maxDate
89
+ }
90
+
91
+ if (onChange) onChange(newDate);
92
+ else setInternalDate(newDate);
71
93
  }
72
94
 
73
- if (altText) {
74
- text = `${altText}, ${text}`;
95
+ function jumpToToday() {
96
+ updateDate(new Date());
75
97
  }
76
98
 
77
- function handleDateChanged(date: Date) {
78
- if (typeof onChange === "function") {
79
- onChange(date);
80
- } else {
81
- setInternalDate(date);
99
+ useEffect(() => {
100
+ if (liveRef.current) {
101
+ liveRef.current.textContent = text;
102
+ }
103
+ }, [text]);
104
+
105
+ function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
106
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
107
+ if (event.defaultPrevented) return;
108
+ if (event.altKey || event.metaKey) return; // Ignore if alt or meta key is pressed
109
+
110
+ if (disablePrev && event.key === "ArrowLeft") return;
111
+ if (disableNext && event.key === "ArrowRight") return;
112
+
113
+ if (event.ctrlKey && event.shiftKey) {
114
+ // If both ctrl and shift are pressed, navigate by 1 year
115
+ let newDate = new Date(dateUtc);
116
+ if (event.key === "ArrowLeft") {
117
+ newDate.setFullYear(newDate.getFullYear() - 1);
118
+ } else if (event.key === "ArrowRight") {
119
+ newDate.setFullYear(newDate.getFullYear() + 1);
120
+ }
121
+ updateDate(newDate);
122
+ return;
123
+ }
124
+
125
+ if (event.ctrlKey) {
126
+ // Navigate by month
127
+ let newDate = new Date(dateUtc);
128
+ if (event.key === "ArrowLeft") {
129
+ newDate.setMonth(newDate.getMonth() - 1);
130
+ } else if (event.key === "ArrowRight") {
131
+ newDate.setMonth(newDate.getMonth() + 1);
132
+ }
133
+ updateDate(newDate);
134
+ return;
135
+ }
136
+
137
+ let modifier = event.shiftKey ? 7 : 1;
138
+ if (event.key === "ArrowLeft") {
139
+ updateDate(new Date(dateUtc - oneDay * modifier));
140
+ } else if (event.key === "ArrowRight") {
141
+ updateDate(new Date(dateUtc + oneDay * modifier));
82
142
  }
83
143
  }
84
144
 
85
145
  return (
86
- <div className="ObDateNavigator-root">
87
- <Arrow direction="left" onClick={() => handleDateChanged(new Date(dateUtc - oneDay))} />
146
+ // biome-ignore lint/a11y/noNoninteractiveTabindex: I hate this component
147
+ <div className="ObDateNavigator-root" onKeyDown={handleKeyDown} tabIndex={0} aria-label="Date navigator">
148
+ <Arrow direction="left" onClick={() => updateDate(new Date(dateUtc - oneDay))} disabled={disablePrev} />
88
149
  <div className="ObDateNavigator-date">{text}</div>
89
- <Arrow direction="right" onClick={() => handleDateChanged(new Date(dateUtc + oneDay))} />
150
+ <Arrow direction="right" onClick={() => updateDate(new Date(dateUtc + oneDay))} disabled={disableNext} />
151
+ <button type="button" className="ObDateNavigator-today" onClick={jumpToToday}>
152
+ {getLocalizedString("Today")}
153
+ </button>
154
+ <div aria-live="polite" ref={liveRef} style={{ position: "absolute", left: "-9999px" }} />
90
155
  </div>
91
156
  );
92
157
  }
@@ -3,7 +3,13 @@
3
3
  $bullet-size: 0.7em;
4
4
  $line-width: 0.2em;
5
5
 
6
+ :root {
7
+ --ob-linked-list-bullet-size: 0.7em;
8
+ --ob-linked-list-line-width: 0.2em;
9
+ }
10
+
6
11
  .ObLinkedList-root {
12
+
7
13
  list-style-type: none;
8
14
  margin-left: 0;
9
15
  padding-left: 0;
@@ -47,10 +53,11 @@ $line-width: 0.2em;
47
53
  left: 0;
48
54
  top: 50%;
49
55
 
50
- transform: translate(calc(-1 * ($bullet-size / 2) + ($line-width / 2)), -50%);
56
+ transform: translate(calc(-1 * (var(--ob-linked-list-bullet-size) / 2) + (var(--ob-linked-list-line-width) / 2)), -50%);
51
57
  }
52
58
 
53
59
  .ObLinkedList-card {
60
+ display: block;
54
61
  background: white;
55
62
  border: 1px solid;
56
63
  border-color: variables.$color-border-top variables.$color-border-side variables.$color-border-bottom;
@@ -2,7 +2,7 @@ import "./index.scss";
2
2
 
3
3
  import clsx from "clsx";
4
4
 
5
- export function List({ children, className, ...other }: React.HTMLProps<HTMLUListElement>) {
5
+ export function LinkedList({ children, className, ...other }: React.HTMLProps<HTMLUListElement>) {
6
6
  return (
7
7
  <ul className={clsx("ObLinkedList-root", className)} {...other}>
8
8
  {children}
@@ -10,7 +10,7 @@ export function List({ children, className, ...other }: React.HTMLProps<HTMLULis
10
10
  );
11
11
  }
12
12
 
13
- export function ListItem({
13
+ export function LinkedListItem({
14
14
  children,
15
15
  className,
16
16
  label,
@@ -26,7 +26,8 @@ export const BannerImage = React.forwardRef<HTMLImageElement, BannerImageProps>(
26
26
  ref,
27
27
  ) {
28
28
  const { className, ...other } = props;
29
- return <img ref={ref} className={clsx("ObPageBanner-bannerImage", className)} {...other} />;
29
+ // biome-ignore lint/a11y/useAltText: should be provided by the user
30
+ return <img ref={ref} className={clsx("ObPageBanner-bannerImage", className)} alt="" {...other} />;
30
31
  });
31
32
 
32
33
  export type TabListProps = Pick<React.HTMLProps<HTMLDivElement>, "id" | "className" | "children"> & {
@@ -10,15 +10,16 @@ function useForceUpdate() {
10
10
  export function Portal({ children, root, type = "div" }) {
11
11
  const forceUpdate = useForceUpdate();
12
12
  const mountNode = useRef<HTMLDivElement>(null);
13
- const portalNode = useRef<HTMLElement>(null);
13
+ const portalNode = useRef<HTMLElement | null>(null);
14
14
 
15
+ // biome-ignore lint/correctness/useExhaustiveDependencies: because reasons
15
16
  useEffect(() => {
16
17
  const ownerDocument = mountNode.current?.ownerDocument;
17
- let portalElem = portalNode.current;
18
- if (ownerDocument && portalElem) {
18
+ if (ownerDocument && !portalNode.current) {
19
19
  let parent = root ? ownerDocument.querySelector(root) || ownerDocument.body : ownerDocument.body;
20
+
20
21
  const node = ownerDocument.createElement(type);
21
- portalElem = node;
22
+ portalNode.current = node;
22
23
  parent.appendChild(node);
23
24
  forceUpdate();
24
25
 
@@ -26,11 +27,11 @@ export function Portal({ children, root, type = "div" }) {
26
27
  parent.removeChild(node);
27
28
  };
28
29
  }
29
- }, []);
30
+ }, [root, type]);
30
31
 
31
32
  if (!portalNode.current) {
32
33
  return <div ref={mountNode} />;
33
- } else {
34
- return createPortal(children, portalNode.current);
35
34
  }
35
+
36
+ return createPortal(children, portalNode.current);
36
37
  }
@@ -28,7 +28,7 @@
28
28
  transparent 66%
29
29
  ),
30
30
  linear-gradient(top, rgba(255, 255, 255, 0.25), rgba(0, 0, 0, 0.2)),
31
- linear-gradient(left, var(--brand-primary, variables.$brand-primary), var(--brand-light, variables.$brand-light));
31
+ linear-gradient(left, var(--brand-primary), var(--brand-light));
32
32
  background-image:
33
33
  -webkit-linear-gradient(
34
34
  135deg,
@@ -39,7 +39,7 @@
39
39
  transparent 66%
40
40
  ),
41
41
  -webkit-linear-gradient(top, rgba(255, 255, 255, 0.25), rgba(0, 0, 0, 0.2)),
42
- -webkit-linear-gradient(left, var(--brand-primary), variables.$brand-primary, var(--brand-light, variables.$brand-light));
42
+ -webkit-linear-gradient(left, var(--brand-primary), var(--brand-light));
43
43
  background-size:
44
44
  35px 20px,
45
45
  100% 100%,
@@ -49,6 +49,7 @@ export function Sidebar(props: SidebarProps) {
49
49
  return () => element?.removeEventListener("transitionend", toggleHiddenContent);
50
50
  }, [open]);
51
51
 
52
+ // biome-ignore lint/correctness/useExhaustiveDependencies: TODO
52
53
  useEffect(() => {
53
54
  function handleCloseSidebar(evt) {
54
55
  if (evt.key === "Escape") {
@@ -1,4 +1,7 @@
1
- $bullet-size: 0.5em;
1
+
2
+ .ObSpinner-wrapper, .ObSpinner-fullPage {
3
+ --bullet-size: 0.5em;
4
+ }
2
5
 
3
6
  .ObSpinner-root {
4
7
  display: inline-block;
@@ -15,38 +18,38 @@ $bullet-size: 0.5em;
15
18
 
16
19
  .ObSpinner-spinner {
17
20
  display: inline-block;
18
- height: $bullet-size * 1.5;
21
+ height: calc(var(--bullet-size) * 1.5);
19
22
  position: relative;
20
- width: $bullet-size * 5;
23
+ width: calc(var(--bullet-size) * 5);
21
24
  }
22
25
 
23
26
  .ObSpinner-bullet {
24
27
  animation-timing-function: cubic-bezier(0, 1, 1, 0);
25
28
  background: #666;
26
29
  border-radius: 50%;
27
- height: $bullet-size;
30
+ height: var(--bullet-size);
28
31
  position: absolute;
29
32
  top: 0.2rem;
30
- width: $bullet-size;
33
+ width: var(--bullet-size);
31
34
 
32
35
  &:nth-child(1) {
33
36
  animation: scaleAnimation 0.6s infinite ease-in-out;
34
- left: $bullet-size * 0.5;
37
+ left: calc(var(--bullet-size) * 0.5);
35
38
  }
36
39
 
37
40
  &:nth-child(2) {
38
41
  animation: translateAnimation 0.6s infinite ease-in-out;
39
- left: $bullet-size * 0.5;
42
+ left: calc(var(--bullet-size) * 0.5);
40
43
  }
41
44
 
42
45
  &:nth-child(3) {
43
46
  animation: translateAnimation 0.6s infinite ease-in-out;
44
- left: $bullet-size * 2.4;
47
+ left: calc(var(--bullet-size) * 2.4);
45
48
  }
46
49
 
47
50
  &:nth-child(4) {
48
51
  animation: scaleAnimation 0.6s infinite ease-in-out reverse;
49
- left: $bullet-size * 4;
52
+ left: calc(var(--bullet-size) * 4);
50
53
  }
51
54
  }
52
55
 
@@ -66,6 +69,6 @@ $bullet-size: 0.5em;
66
69
  }
67
70
 
68
71
  to {
69
- transform: translate($bullet-size * 1.65, 0);
72
+ transform: translate(calc(var(--bullet-size) * 1.65), 0);
70
73
  }
71
74
  }
@@ -1,7 +1,7 @@
1
- import "./index.scss";
1
+ import "./index.css";
2
2
 
3
3
  import clsx from "clsx";
4
- import React from "react";
4
+ import type React from "react";
5
5
 
6
6
  export type SpinnerProps = React.HTMLProps<HTMLDivElement> & {
7
7
  fullPage?: boolean;
@@ -2,7 +2,8 @@ import "./index.scss";
2
2
 
3
3
  import clsx from "clsx";
4
4
  import localforage from "localforage";
5
- import React, { Children, forwardRef, useEffect, useRef, useState } from "react";
5
+ import type React from "react";
6
+ import { Children, forwardRef, useEffect, useRef, useState } from "react";
6
7
 
7
8
  const article = globalThis.af?.article?.id ?? globalThis.window?.location.pathname.substring(1).split("/")[0];
8
9
  const settingsStore = localforage.createInstance({
@@ -59,11 +60,24 @@ type DividerProps = {
59
60
  className?: string;
60
61
  direction: "horizontal" | "vertical";
61
62
  isDragging: boolean;
63
+ min: number;
64
+ max: number;
65
+ size: number;
62
66
  };
63
67
 
64
68
  const Divider = forwardRef<HTMLDivElement, DividerProps>(function Divider(props: DividerProps, ref) {
65
69
  return (
66
- <div tabIndex={0} className={clsx("ObSplitContainer-divider", props.className)} ref={ref}>
70
+ <div
71
+ className={clsx("ObSplitContainer-divider", props.className)}
72
+ tabIndex={0}
73
+ ref={ref}
74
+ // biome-ignore lint/a11y/useSemanticElements: this is allowed according to WAI-ARIA
75
+ role="separator"
76
+ aria-orientation={props.direction}
77
+ aria-valuemin={props.min}
78
+ aria-valuemax={props.max}
79
+ aria-valuenow={props.size}
80
+ >
67
81
  <span>•••</span>
68
82
  </div>
69
83
  );
@@ -114,6 +128,8 @@ export type SplitContainerProps = {
114
128
  direction?: "horizontal" | "vertical";
115
129
  fixedPanel?: 1 | 2;
116
130
  defaultFixedSize?: number;
131
+ min?: number;
132
+ max?: number;
117
133
  /**
118
134
  * When the fixed size is persisted, changing the layoutVersion will force clients
119
135
  * to reset to the default value.
@@ -133,6 +149,8 @@ export function SplitContainer({
133
149
  panelClassName,
134
150
  defaultFixedSize = 400,
135
151
  layoutVersion = 1,
152
+ min = 0,
153
+ max,
136
154
  id,
137
155
  }: SplitContainerProps) {
138
156
  let [fixedSize, setFixedSize] = useState<number>(defaultFixedSize);
@@ -143,18 +161,23 @@ export function SplitContainer({
143
161
  throw Error("SplitContainer requires exactly 2 children");
144
162
  }
145
163
 
146
- function handleFixedSizeChanged(size: React.SetStateAction<number>) {
147
- setFixedSize((current) => {
148
- let newValue: number = typeof size === "function" ? size(current) : size;
164
+ let containerRef = useRef<HTMLDivElement>(null);
165
+ let [containerSize, setContainerSize] = useState<number>(0);
149
166
 
150
- if (id) {
151
- let key = `${article}.${id}`;
152
- persistState(key, layoutVersion, newValue);
153
- }
167
+ useEffect(() => {
168
+ if (!containerRef.current) return;
154
169
 
155
- return newValue;
170
+ let resizeObserver = new ResizeObserver((entries) => {
171
+ for (let entry of entries) {
172
+ if (entry.contentRect) {
173
+ setContainerSize(direction === "horizontal" ? entry.contentRect.width : entry.contentRect.height);
174
+ }
175
+ }
156
176
  });
157
- }
177
+
178
+ resizeObserver.observe(containerRef.current);
179
+ return () => resizeObserver.disconnect();
180
+ }, [direction]);
158
181
 
159
182
  useEffect(() => {
160
183
  if (id) {
@@ -186,6 +209,17 @@ export function SplitContainer({
186
209
  fixedPanel === 1 ? divider.previousElementSibling : divider.nextElementSibling
187
210
  ) as HTMLElement;
188
211
 
212
+ function clampSize(size: number) {
213
+ let adjustedContainerSize = containerSize - 10; // make sure divider will not be stuck outside the container
214
+ let maxSize = Math.min(max ?? adjustedContainerSize, adjustedContainerSize);
215
+ if (size < min) {
216
+ size = min;
217
+ } else if (size > maxSize) {
218
+ size = maxSize;
219
+ }
220
+ return size;
221
+ }
222
+
189
223
  function getNewSize(event: PointerEvent) {
190
224
  let deltaSize =
191
225
  direction === "horizontal"
@@ -194,7 +228,20 @@ export function SplitContainer({
194
228
 
195
229
  deltaSize *= fixedPanel === 1 ? -1 : 1;
196
230
 
197
- return initialPosition.size - deltaSize;
231
+ return clampSize(initialPosition.size - deltaSize);
232
+ }
233
+
234
+ function handleFixedSizeChanged(size: React.SetStateAction<number>) {
235
+ setFixedSize((current) => {
236
+ let newValue: number = clampSize(typeof size === "function" ? size(current) : size);
237
+
238
+ if (id) {
239
+ let key = `${article}.${id}`;
240
+ persistState(key, layoutVersion, newValue);
241
+ }
242
+
243
+ return newValue;
244
+ });
198
245
  }
199
246
 
200
247
  function handlePointerUp(event: PointerEvent) {
@@ -210,6 +257,10 @@ export function SplitContainer({
210
257
  event.preventDefault();
211
258
 
212
259
  let newSize = getNewSize(event);
260
+ let divider = dividerRef.current;
261
+ if (divider) {
262
+ divider.setAttribute("aria-valuenow", String(newSize));
263
+ }
213
264
  movingPanel.style.flex = `0 0 ${newSize}px`;
214
265
  }
215
266
 
@@ -229,8 +280,8 @@ export function SplitContainer({
229
280
  }
230
281
 
231
282
  function handleKeyDown(event: KeyboardEvent) {
232
- let reduce: boolean = true;
233
- let shouldHandle: boolean = false;
283
+ let reduce = true;
284
+ let shouldHandle = false;
234
285
  if (!event.altKey && !event.ctrlKey && !event.metaKey) {
235
286
  if (direction === "horizontal" && ["ArrowLeft", "ArrowRight"].includes(event.key)) {
236
287
  shouldHandle = true;
@@ -260,14 +311,21 @@ export function SplitContainer({
260
311
  divider?.removeEventListener("keydown", handleKeyDown);
261
312
  };
262
313
  }
263
- }, [direction, fixedPanel]);
314
+ }, [direction, fixedPanel, layoutVersion, id, min, max, containerSize]);
264
315
 
265
316
  return (
266
- <SplitRoot direction={direction} className={className}>
317
+ <SplitRoot direction={direction} className={className} ref={containerRef}>
267
318
  <SplitPanel className={clsx(isDragging && "dragging", panelClassName)} fixed={fixedPanel === 1} size={fixedSize}>
268
319
  {childrenArray[0]}
269
320
  </SplitPanel>
270
- <Divider ref={dividerRef} direction={direction} isDragging={isDragging} />
321
+ <Divider
322
+ ref={dividerRef}
323
+ direction={direction}
324
+ isDragging={isDragging}
325
+ size={fixedSize}
326
+ min={min}
327
+ max={max ?? containerSize}
328
+ />
271
329
  <SplitPanel className={clsx(isDragging && "dragging", panelClassName)} fixed={fixedPanel === 2} size={fixedSize}>
272
330
  {childrenArray[1]}
273
331
  </SplitPanel>