@okta/odyssey-react-mui 1.26.0 → 1.27.0

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 (79) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/Surface.js +10 -2
  3. package/dist/Surface.js.map +1 -1
  4. package/dist/index.scss +1 -1
  5. package/dist/labs/DataView/DataView.js +6 -0
  6. package/dist/labs/DataView/DataView.js.map +1 -1
  7. package/dist/labs/DataView/componentTypes.js.map +1 -1
  8. package/dist/labs/SideNav/NavAccordion.js +4 -5
  9. package/dist/labs/SideNav/NavAccordion.js.map +1 -1
  10. package/dist/labs/SideNav/SideNav.js +167 -93
  11. package/dist/labs/SideNav/SideNav.js.map +1 -1
  12. package/dist/labs/SideNav/SideNavItemContent.js +97 -57
  13. package/dist/labs/SideNav/SideNavItemContent.js.map +1 -1
  14. package/dist/labs/SideNav/SideNavItemContentContext.js +1 -0
  15. package/dist/labs/SideNav/SideNavItemContentContext.js.map +1 -1
  16. package/dist/labs/SideNav/SideNavItemLinkContent.js +2 -2
  17. package/dist/labs/SideNav/SideNavItemLinkContent.js.map +1 -1
  18. package/dist/labs/SideNav/SideNavToggleButton.js +5 -5
  19. package/dist/labs/SideNav/SideNavToggleButton.js.map +1 -1
  20. package/dist/labs/SideNav/SortableList/SortableItem.js +162 -0
  21. package/dist/labs/SideNav/SortableList/SortableItem.js.map +1 -0
  22. package/dist/labs/SideNav/SortableList/SortableList.js +118 -0
  23. package/dist/labs/SideNav/SortableList/SortableList.js.map +1 -0
  24. package/dist/labs/SideNav/SortableList/SortableOverlay.js +30 -0
  25. package/dist/labs/SideNav/SortableList/SortableOverlay.js.map +1 -0
  26. package/dist/labs/SideNav/types.js.map +1 -1
  27. package/dist/labs/TopNav/TopNav.js +1 -1
  28. package/dist/labs/TopNav/TopNav.js.map +1 -1
  29. package/dist/labs/UiShell/UiShellContent.js +1 -1
  30. package/dist/labs/UiShell/UiShellContent.js.map +1 -1
  31. package/dist/properties/ts/odyssey-react-mui.js +7 -0
  32. package/dist/properties/ts/odyssey-react-mui.js.map +1 -1
  33. package/dist/src/OdysseyTranslationProvider.d.ts +1 -1
  34. package/dist/src/OdysseyTranslationProvider.d.ts.map +1 -1
  35. package/dist/src/Surface.d.ts.map +1 -1
  36. package/dist/src/labs/DataView/DataView.d.ts +1 -1
  37. package/dist/src/labs/DataView/DataView.d.ts.map +1 -1
  38. package/dist/src/labs/DataView/componentTypes.d.ts +3 -2
  39. package/dist/src/labs/DataView/componentTypes.d.ts.map +1 -1
  40. package/dist/src/labs/SideNav/NavAccordion.d.ts +2 -6
  41. package/dist/src/labs/SideNav/NavAccordion.d.ts.map +1 -1
  42. package/dist/src/labs/SideNav/SideNav.d.ts +1 -1
  43. package/dist/src/labs/SideNav/SideNav.d.ts.map +1 -1
  44. package/dist/src/labs/SideNav/SideNavItemContent.d.ts +37 -1
  45. package/dist/src/labs/SideNav/SideNavItemContent.d.ts.map +1 -1
  46. package/dist/src/labs/SideNav/SideNavItemContentContext.d.ts +1 -0
  47. package/dist/src/labs/SideNav/SideNavItemContentContext.d.ts.map +1 -1
  48. package/dist/src/labs/SideNav/SideNavToggleButton.d.ts.map +1 -1
  49. package/dist/src/labs/SideNav/SortableList/SortableItem.d.ts +26 -0
  50. package/dist/src/labs/SideNav/SortableList/SortableItem.d.ts.map +1 -0
  51. package/dist/src/labs/SideNav/SortableList/SortableList.d.ts +36 -0
  52. package/dist/src/labs/SideNav/SortableList/SortableList.d.ts.map +1 -0
  53. package/dist/src/labs/SideNav/SortableList/SortableOverlay.d.ts +17 -0
  54. package/dist/src/labs/SideNav/SortableList/SortableOverlay.d.ts.map +1 -0
  55. package/dist/src/labs/SideNav/types.d.ts +16 -6
  56. package/dist/src/labs/SideNav/types.d.ts.map +1 -1
  57. package/dist/src/properties/ts/odyssey-react-mui.d.ts +7 -0
  58. package/dist/src/properties/ts/odyssey-react-mui.d.ts.map +1 -1
  59. package/dist/tsconfig.production.tsbuildinfo +1 -1
  60. package/i18n.config.json +2 -1
  61. package/package.json +6 -3
  62. package/src/Surface.tsx +16 -4
  63. package/src/labs/DataView/DataView.tsx +6 -0
  64. package/src/labs/DataView/componentTypes.ts +6 -2
  65. package/src/labs/SideNav/NavAccordion.tsx +5 -10
  66. package/src/labs/SideNav/SideNav.test.tsx +8 -8
  67. package/src/labs/SideNav/SideNav.tsx +232 -119
  68. package/src/labs/SideNav/SideNavItemContent.tsx +114 -61
  69. package/src/labs/SideNav/SideNavItemContentContext.tsx +2 -0
  70. package/src/labs/SideNav/SideNavItemLinkContent.tsx +2 -2
  71. package/src/labs/SideNav/SideNavToggleButton.tsx +5 -9
  72. package/src/labs/SideNav/SortableList/SortableItem.tsx +202 -0
  73. package/src/labs/SideNav/SortableList/SortableList.tsx +122 -0
  74. package/src/labs/SideNav/SortableList/SortableOverlay.tsx +34 -0
  75. package/src/labs/SideNav/types.ts +16 -6
  76. package/src/labs/TopNav/TopNav.tsx +1 -1
  77. package/src/labs/UiShell/UiShellContent.tsx +1 -1
  78. package/src/properties/odyssey-react-mui.properties +7 -0
  79. package/src/properties/ts/odyssey-react-mui.ts +1 -1
@@ -44,17 +44,12 @@ export const StyledSideNavListItem = styled("li", {
44
44
  alignItems: "center",
45
45
  backgroundColor: "unset",
46
46
  borderRadius: odysseyDesignTokens.BorderRadiusMain,
47
- lineHeight: 1.5,
48
47
  transition: `backgroundColor ${odysseyDesignTokens.TransitionDurationMain}, color ${odysseyDesignTokens.TransitionDurationMain}`,
49
48
 
50
49
  ...(isSelected && {
51
50
  color: `${odysseyDesignTokens.TypographyColorAction} !important`,
52
51
  backgroundColor: odysseyDesignTokens.HueBlue50,
53
52
  }),
54
-
55
- "&:last-child": {
56
- marginBottom: odysseyDesignTokens.Spacing2,
57
- },
58
53
  }));
59
54
 
60
55
  const scrollToNode = (node: HTMLElement | null) => {
@@ -71,68 +66,76 @@ type ScrollIntoViewHandle = {
71
66
  scrollIntoView: () => void;
72
67
  };
73
68
 
74
- const GetNavItemContentStyles = ({
69
+ export const getBaseNavItemContentStyles = ({
75
70
  odysseyDesignTokens,
76
- contextValue,
77
71
  isDisabled,
78
72
  isSelected,
79
73
  }: {
80
74
  odysseyDesignTokens: DesignTokens;
81
- contextValue: SideNavItemContentContextValue;
82
75
  isDisabled?: boolean;
83
76
  isSelected?: boolean;
84
- }) => {
85
- return {
86
- display: "flex",
87
- alignItems: "center",
88
- width: "100%",
89
- textDecoration: "none",
90
- color: `${odysseyDesignTokens.TypographyColorHeading} !important`,
91
- minHeight: odysseyDesignTokens.Spacing7,
92
- paddingBlock: odysseyDesignTokens.Spacing4,
93
- paddingInline: `calc(${odysseyDesignTokens.Spacing4} * ${contextValue.depth})`,
94
- borderRadius: odysseyDesignTokens.BorderRadiusMain,
95
- transition: `backgroundColor ${odysseyDesignTokens.TransitionDurationMain}, color ${odysseyDesignTokens.TransitionDurationMain}`,
77
+ }) => ({
78
+ display: "flex",
79
+ alignItems: "center",
80
+ width: "100%",
81
+ textDecoration: "none",
82
+ color: `${odysseyDesignTokens.TypographyColorHeading} !important`,
83
+ minHeight: "unset",
84
+ paddingBlock: odysseyDesignTokens.Spacing3,
85
+ paddingInlineEnd: odysseyDesignTokens.Spacing4,
86
+ borderRadius: odysseyDesignTokens.BorderRadiusMain,
87
+ transition: `backgroundColor ${odysseyDesignTokens.TransitionDurationMain}, color ${odysseyDesignTokens.TransitionDurationMain}`,
88
+ cursor: "pointer",
96
89
 
97
- "&:hover": {
90
+ // `[data-sortable-container='true']:has(button:hover) &` - when the sortable item's drag handle is hovered we want to trigger the same hover behavior as if you were hovering the actual item
91
+ "&:hover, [data-sortable-container='true']:has(button:hover, button:focus, button:focus-visible) &":
92
+ {
98
93
  textDecoration: "none",
99
- cursor: "pointer",
100
- backgroundColor: !isDisabled
101
- ? odysseyDesignTokens.HueNeutral50
102
- : "inherit",
94
+ backgroundColor: odysseyDesignTokens.HueNeutral50,
103
95
 
104
- ...(isDisabled && {
105
- color: "inherit",
106
- cursor: "default",
96
+ ...(isSelected && {
97
+ backgroundColor: odysseyDesignTokens.HueBlue50,
98
+ color: odysseyDesignTokens.TypographyColorAction,
107
99
  }),
108
100
 
109
- ...(isSelected && {
110
- "&:hover": {
111
- backgroundColor: odysseyDesignTokens.HueBlue50,
112
- },
101
+ ...(isDisabled && {
102
+ backgroundColor: "unset",
113
103
  }),
114
104
  },
115
105
 
116
- ...(isSelected && {
117
- color: `${odysseyDesignTokens.TypographyColorAction} !important`,
118
- fontWeight: odysseyDesignTokens.TypographyWeightBodyBold,
119
- }),
106
+ ...(isSelected && {
107
+ color: `${odysseyDesignTokens.TypographyColorAction}`,
108
+ fontWeight: odysseyDesignTokens.TypographyWeightBodyBold,
109
+ }),
120
110
 
121
- ...(isDisabled && {
122
- color: `${odysseyDesignTokens.TypographyColorDisabled} !important`,
123
- }),
111
+ ...(isDisabled && {
112
+ cursor: "default",
113
+ color: `${odysseyDesignTokens.TypographyColorDisabled} !important`,
114
+ }),
124
115
 
125
- ...(contextValue.isCompact && {
126
- paddingBlock: odysseyDesignTokens.Spacing1,
127
- minHeight: odysseyDesignTokens.Spacing6,
128
- }),
116
+ "&:focus-visible, &:focus": {
117
+ outline: "none",
118
+ boxShadow: `inset 0 0 0 2px ${odysseyDesignTokens.PalettePrimaryMain}`,
119
+ },
120
+ });
129
121
 
130
- "&:focus-visible": {
131
- outline: "none",
132
- boxShadow: `inset 0 0 0 3px ${odysseyDesignTokens.PalettePrimaryMain}`,
133
- },
134
- };
135
- };
122
+ export const getNavItemContentStyles = ({
123
+ odysseyDesignTokens,
124
+ contextValue,
125
+ }: {
126
+ odysseyDesignTokens: DesignTokens;
127
+ contextValue: SideNavItemContentContextValue;
128
+ }) => ({
129
+ paddingInlineStart: `calc(${odysseyDesignTokens.Spacing4} * ${contextValue.depth} + ${odysseyDesignTokens.Spacing6})`,
130
+
131
+ ...(contextValue.depth === 1 && {
132
+ paddingInlineStart: odysseyDesignTokens.Spacing4,
133
+ }),
134
+
135
+ ...(contextValue.isCompact && {
136
+ paddingBlock: odysseyDesignTokens.Spacing1,
137
+ }),
138
+ });
136
139
 
137
140
  const NavItemContentContainer = styled("div", {
138
141
  shouldForwardProp: (prop) =>
@@ -140,15 +143,47 @@ const NavItemContentContainer = styled("div", {
140
143
  prop != "contextValue" &&
141
144
  prop !== "isDisabled" &&
142
145
  prop !== "isSelected",
143
- })(GetNavItemContentStyles);
146
+ })<{
147
+ contextValue: SideNavItemContentContextValue;
148
+ odysseyDesignTokens: DesignTokens;
149
+ isSelected?: boolean;
150
+ isDisabled?: boolean;
151
+ }>(({ contextValue, odysseyDesignTokens, isDisabled, isSelected }) => ({
152
+ ...getBaseNavItemContentStyles({
153
+ odysseyDesignTokens,
154
+ isDisabled,
155
+ isSelected,
156
+ }),
144
157
 
145
- const NavItemLinkContainer = styled(NavItemLink, {
158
+ ...getNavItemContentStyles({
159
+ odysseyDesignTokens,
160
+ contextValue,
161
+ }),
162
+ }));
163
+
164
+ const StyledNavItemLink = styled(NavItemLink, {
146
165
  shouldForwardProp: (prop) =>
147
166
  prop !== "odysseyDesignTokens" &&
148
167
  prop != "contextValue" &&
149
168
  prop !== "isDisabled" &&
150
169
  prop !== "isSelected",
151
- })(GetNavItemContentStyles);
170
+ })<{
171
+ contextValue: SideNavItemContentContextValue;
172
+ odysseyDesignTokens: DesignTokens;
173
+ isSelected?: boolean;
174
+ isDisabled?: boolean;
175
+ }>(({ contextValue, odysseyDesignTokens, isDisabled, isSelected }) => ({
176
+ ...getBaseNavItemContentStyles({
177
+ odysseyDesignTokens,
178
+ isDisabled,
179
+ isSelected,
180
+ }),
181
+
182
+ ...getNavItemContentStyles({
183
+ odysseyDesignTokens,
184
+ contextValue,
185
+ }),
186
+ }));
152
187
 
153
188
  const SideNavItemContent = ({
154
189
  count,
@@ -161,9 +196,10 @@ const SideNavItemContent = ({
161
196
  statusLabel,
162
197
  endIcon,
163
198
  onClick,
164
- isSelected,
165
199
  isDisabled,
200
+ isSelected,
166
201
  scrollRef,
202
+ onItemSelected,
167
203
  }: Pick<
168
204
  SideNavItem,
169
205
  | "count"
@@ -176,13 +212,14 @@ const SideNavItemContent = ({
176
212
  | "statusLabel"
177
213
  | "endIcon"
178
214
  | "onClick"
179
- | "isSelected"
180
215
  | "isDisabled"
216
+ | "isSelected"
181
217
  > & {
182
218
  /**
183
219
  * The ref used to scroll to this item
184
220
  */
185
221
  scrollRef?: React.RefObject<ScrollIntoViewHandle>;
222
+ onItemSelected?(selectedItemId: string): void;
186
223
  }) => {
187
224
  const sidenavItemContentContext = useSideNavItemContent();
188
225
  const contextValue = useMemo(
@@ -204,14 +241,25 @@ const SideNavItemContent = ({
204
241
  [],
205
242
  );
206
243
 
244
+ const itemClickHandler = useCallback(
245
+ (id: string) => {
246
+ return () => {
247
+ onItemSelected?.(id);
248
+ onClick?.();
249
+ };
250
+ },
251
+ [onClick, onItemSelected],
252
+ );
253
+
207
254
  const sideNavItemContentKeyHandler = useCallback(
208
- (event: KeyboardEvent<HTMLDivElement>) => {
255
+ (id: string, event: KeyboardEvent<HTMLDivElement>) => {
209
256
  if (event?.key === "Enter") {
210
257
  event.preventDefault();
258
+ onItemSelected?.(id);
211
259
  onClick?.();
212
260
  }
213
261
  },
214
- [onClick],
262
+ [onClick, onItemSelected],
215
263
  );
216
264
 
217
265
  return (
@@ -249,8 +297,12 @@ const SideNavItemContent = ({
249
297
  contextValue={contextValue}
250
298
  isDisabled={isDisabled}
251
299
  tabIndex={0}
252
- onClick={onClick}
253
- onKeyDown={sideNavItemContentKeyHandler}
300
+ role="button"
301
+ onClick={itemClickHandler(id)}
302
+ onKeyDown={(event: KeyboardEvent<HTMLDivElement>) =>
303
+ sideNavItemContentKeyHandler(id, event)
304
+ }
305
+ isSelected={isSelected}
254
306
  >
255
307
  <SideNavItemLinkContent
256
308
  count={count}
@@ -262,14 +314,14 @@ const SideNavItemContent = ({
262
314
  />
263
315
  </NavItemContentContainer>
264
316
  ) : (
265
- <NavItemLinkContainer
317
+ <StyledNavItemLink
266
318
  odysseyDesignTokens={odysseyDesignTokens}
267
319
  contextValue={contextValue}
268
320
  isDisabled={isDisabled}
269
321
  isSelected={isSelected}
270
322
  href={href}
271
323
  target={target}
272
- onClick={onClick}
324
+ onClick={itemClickHandler(id)}
273
325
  >
274
326
  <SideNavItemLinkContent
275
327
  count={count}
@@ -284,12 +336,13 @@ const SideNavItemContent = ({
284
336
  <ExternalLinkIcon />
285
337
  </span>
286
338
  )}
287
- </NavItemLinkContainer>
339
+ </StyledNavItemLink>
288
340
  )
289
341
  }
290
342
  </StyledSideNavListItem>
291
343
  );
292
344
  };
345
+
293
346
  const MemoizedSideNavItemContent = memo(SideNavItemContent);
294
347
  MemoizedSideNavItemContent.displayName = "SideNavItemContent";
295
348
 
@@ -14,12 +14,14 @@ import { createContext, useContext } from "react";
14
14
 
15
15
  export type SideNavItemContentContextValue = {
16
16
  isCompact?: boolean;
17
+ isSortable?: boolean;
17
18
  depth: number;
18
19
  };
19
20
 
20
21
  export const SideNavItemContentContext =
21
22
  createContext<SideNavItemContentContextValue>({
22
23
  isCompact: false,
24
+ isSortable: false,
23
25
  depth: 1,
24
26
  });
25
27
 
@@ -32,8 +32,8 @@ const SideNavItemLabelContainer = styled("div", {
32
32
  display: "flex",
33
33
  flexWrap: "wrap",
34
34
  alignItems: "center",
35
- fontSize: odysseyDesignTokens.TypographyScale0,
36
- marginInlineStart: isIconVisible ? odysseyDesignTokens.Spacing2 : 0,
35
+ fontSize: odysseyDesignTokens.TypographySizeBody,
36
+ marginInlineStart: isIconVisible ? odysseyDesignTokens.Spacing3 : 0,
37
37
  }));
38
38
 
39
39
  const SideNavItemLinkContent = ({
@@ -58,8 +58,7 @@ const StyledToggleButton = styled(MuiButton, {
58
58
  backgroundColor: "transparent",
59
59
 
60
60
  "#lineOne": {
61
- animation:
62
- "lineOne-animate-to-collapse 250ms cubic-bezier(0, 0, 0.2, 1)",
61
+ animation: `lineOne-animate-to-collapse ${odysseyDesignTokens.TransitionDurationMain} cubic-bezier(0, 0, 0.2, 1)`,
63
62
  animationFillMode: "forwards",
64
63
  "@keyframes lineOne-animate-to-collapse": {
65
64
  "0%": {
@@ -75,8 +74,7 @@ const StyledToggleButton = styled(MuiButton, {
75
74
  },
76
75
 
77
76
  "#lineTwo": {
78
- animation:
79
- "lineTwo-animate-to-collapse 250ms cubic-bezier(0, 0, 0.2, 1)",
77
+ animation: `lineTwo-animate-to-collapse ${odysseyDesignTokens.TransitionDurationMain} cubic-bezier(0, 0, 0.2, 1)`,
80
78
  animationFillMode: "forwards",
81
79
  "@keyframes lineTwo-animate-to-collapse": {
82
80
  "0%": {
@@ -93,8 +91,7 @@ const StyledToggleButton = styled(MuiButton, {
93
91
 
94
92
  ...(isSideNavCollapsed && {
95
93
  "#lineOne": {
96
- animation:
97
- "lineOne-animate-to-expand 250ms cubic-bezier(0, 0, 0.2, 1)",
94
+ animation: `lineOne-animate-to-expand ${odysseyDesignTokens.TransitionDurationMain} cubic-bezier(0, 0, 0.2, 1)`,
98
95
  animationFillMode: "forwards",
99
96
  "@keyframes lineOne-animate-to-expand": {
100
97
  "0%": {
@@ -111,8 +108,7 @@ const StyledToggleButton = styled(MuiButton, {
111
108
  },
112
109
 
113
110
  "#lineTwo": {
114
- animation:
115
- "lineTwo-animate-to-expand 250ms cubic-bezier(0, 0, 0.2, 1)",
111
+ animation: `lineTwo-animate-to-expand ${odysseyDesignTokens.TransitionDurationMain} cubic-bezier(0, 0, 0.2, 1)`,
116
112
  animationFillMode: "forwards",
117
113
  "@keyframes lineTwo-animate-to-expand": {
118
114
  "0%": {
@@ -135,7 +131,7 @@ const StyledToggleButton = styled(MuiButton, {
135
131
  left: "50%",
136
132
  width: "2px",
137
133
  height: odysseyDesignTokens.Spacing4,
138
- backgroundColor: odysseyDesignTokens.HueNeutral500,
134
+ backgroundColor: odysseyDesignTokens.HueNeutral600,
139
135
  transform: "translate3d(-50%, -50%, 0)",
140
136
  transition: `transform ${odysseyDesignTokens.TransitionDurationMain}`,
141
137
  },
@@ -0,0 +1,202 @@
1
+ /*!
2
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { createContext, useContext, useMemo } from "react";
14
+ import type { CSSProperties, PropsWithChildren } from "react";
15
+ import type {
16
+ DraggableSyntheticListeners,
17
+ UniqueIdentifier,
18
+ } from "@dnd-kit/core";
19
+ // eslint-disable-next-line import/no-extraneous-dependencies
20
+ import { useSortable } from "@dnd-kit/sortable";
21
+ // eslint-disable-next-line import/no-extraneous-dependencies
22
+ import { CSS } from "@dnd-kit/utilities";
23
+ import styled from "@emotion/styled";
24
+ import {
25
+ DesignTokens,
26
+ useOdysseyDesignTokens,
27
+ } from "../../../OdysseyDesignTokensContext";
28
+ import { useTranslation } from "react-i18next";
29
+
30
+ type ItemProps = {
31
+ id: UniqueIdentifier;
32
+ isDisabled?: boolean;
33
+ isSelected?: boolean;
34
+ };
35
+
36
+ interface Context {
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ attributes: Record<string, any>;
39
+ listeners: DraggableSyntheticListeners;
40
+ ref(node: HTMLElement | null): void;
41
+ }
42
+
43
+ const SortableItemContext = createContext<Context>({
44
+ attributes: {},
45
+ listeners: undefined,
46
+ ref() {},
47
+ });
48
+
49
+ const StyledSortableListItem = styled("li", {
50
+ shouldForwardProp: (prop) =>
51
+ prop !== "odysseyDesignTokens" && prop !== "isSelected",
52
+ })<{
53
+ odysseyDesignTokens: DesignTokens;
54
+ isSelected?: boolean;
55
+ }>(({ odysseyDesignTokens, isSelected }) => ({
56
+ position: "relative",
57
+
58
+ button: {
59
+ top: "50%",
60
+ left: odysseyDesignTokens.Spacing2,
61
+ transform: "translateY(-50%)",
62
+ },
63
+
64
+ svg: {
65
+ path: {
66
+ fill: "currentColor",
67
+ },
68
+ },
69
+
70
+ "&:has(a:hover, button:hover, a:focus, button:focus, a:focus-visible, button:focus-visible, [role='button']:hover, [role='button']:focus, [role='button']:focus-visible)":
71
+ {
72
+ button: {
73
+ opacity: 1,
74
+ outlineWidth: 0,
75
+ },
76
+ },
77
+
78
+ ...(isSelected && {
79
+ svg: {
80
+ path: {
81
+ fill: odysseyDesignTokens.TypographyColorAction,
82
+ },
83
+ },
84
+ }),
85
+ }));
86
+
87
+ const StyledUl = styled("ul")({
88
+ padding: 0,
89
+ listStyle: "none",
90
+ listStyleType: "none",
91
+ });
92
+
93
+ const StyledDragHandleButton = styled("button", {
94
+ shouldForwardProp: (prop) =>
95
+ prop !== "odysseyDesignTokens" && prop !== "isDragging",
96
+ })<{
97
+ odysseyDesignTokens: DesignTokens;
98
+ isDragging?: boolean;
99
+ }>(({ odysseyDesignTokens, isDragging }) => ({
100
+ position: "absolute",
101
+ opacity: 0,
102
+ // paddingInlineStart: odysseyDesignTokens.Spacing4,
103
+ padding: odysseyDesignTokens.Spacing2,
104
+ // paddingBlock: 0,
105
+ border: "none",
106
+ backgroundColor: "transparent",
107
+ cursor: `${isDragging ? "grabbing" : "grab"}`,
108
+ transition: `opacity ${odysseyDesignTokens.TransitionDurationMain}`,
109
+ borderRadius: odysseyDesignTokens.BorderRadiusMain,
110
+
111
+ svg: {
112
+ display: "flex",
113
+ },
114
+
115
+ "&:focus, &:focus-visible": {
116
+ outline: "none",
117
+ boxShadow: `inset 0 0 0 2px ${odysseyDesignTokens.PalettePrimaryMain}`,
118
+ },
119
+ }));
120
+
121
+ type DragHandleProps = {
122
+ isDisabled?: boolean;
123
+ isDragging?: boolean;
124
+ };
125
+
126
+ export const DragHandle = ({ isDragging }: DragHandleProps) => {
127
+ const { attributes, listeners, ref } = useContext(SortableItemContext);
128
+ const odysseyDesignTokens: DesignTokens = useOdysseyDesignTokens();
129
+ const { t } = useTranslation();
130
+
131
+ return (
132
+ <StyledDragHandleButton
133
+ {...attributes}
134
+ {...listeners}
135
+ odysseyDesignTokens={odysseyDesignTokens}
136
+ isDragging={isDragging}
137
+ ref={ref}
138
+ aria-label={t("navigation.drag.handle")}
139
+ >
140
+ <svg
141
+ width="16"
142
+ height="16"
143
+ viewBox="0 0 16 16"
144
+ fill="none"
145
+ xmlns="http://www.w3.org/2000/svg"
146
+ >
147
+ <path
148
+ fillRule="evenodd"
149
+ clipRule="evenodd"
150
+ d="M6 2.33331C6 2.8856 5.55228 3.33331 5 3.33331C4.44772 3.33331 4 2.8856 4 2.33331C4 1.78103 4.44772 1.33331 5 1.33331C5.55228 1.33331 6 1.78103 6 2.33331ZM11 3.33331C11.5523 3.33331 12 2.8856 12 2.33331C12 1.78103 11.5523 1.33331 11 1.33331C10.4477 1.33331 10 1.78103 10 2.33331C10 2.8856 10.4477 3.33331 11 3.33331ZM11 7.11109C11.5523 7.11109 12 6.66338 12 6.11109C12 5.55881 11.5523 5.11109 11 5.11109C10.4477 5.11109 10 5.55881 10 6.11109C10 6.66338 10.4477 7.11109 11 7.11109ZM12 9.88887C12 10.4412 11.5523 10.8889 11 10.8889C10.4477 10.8889 10 10.4412 10 9.88887C10 9.33659 10.4477 8.88887 11 8.88887C11.5523 8.88887 12 9.33659 12 9.88887ZM11 14.6666C11.5523 14.6666 12 14.2189 12 13.6666C12 13.1144 11.5523 12.6666 11 12.6666C10.4477 12.6666 10 13.1144 10 13.6666C10 14.2189 10.4477 14.6666 11 14.6666ZM5 7.11109C5.55228 7.11109 6 6.66338 6 6.11109C6 5.55881 5.55228 5.11109 5 5.11109C4.44772 5.11109 4 5.55881 4 6.11109C4 6.66338 4.44772 7.11109 5 7.11109ZM6 9.88888C6 10.4412 5.55228 10.8889 5 10.8889C4.44772 10.8889 4 10.4412 4 9.88888C4 9.33659 4.44772 8.88888 5 8.88888C5.55228 8.88888 6 9.33659 6 9.88888ZM5 14.6666C5.55228 14.6666 6 14.2189 6 13.6666C6 13.1144 5.55228 12.6666 5 12.6666C4.44772 12.6666 4 13.1144 4 13.6666C4 14.2189 4.44772 14.6666 5 14.6666Z"
151
+ fill="#3F59E4"
152
+ />
153
+ </svg>
154
+ </StyledDragHandleButton>
155
+ );
156
+ };
157
+
158
+ export const SortableItem = ({
159
+ id,
160
+ isDisabled,
161
+ isSelected,
162
+ children,
163
+ }: PropsWithChildren<ItemProps>) => {
164
+ const {
165
+ attributes,
166
+ isDragging,
167
+ listeners,
168
+ setNodeRef,
169
+ setActivatorNodeRef,
170
+ transform,
171
+ transition,
172
+ } = useSortable({ id });
173
+ const context: Context = useMemo(
174
+ () => ({
175
+ attributes,
176
+ listeners,
177
+ ref: setActivatorNodeRef,
178
+ }),
179
+ [attributes, listeners, setActivatorNodeRef],
180
+ );
181
+ const style: CSSProperties = {
182
+ opacity: isDragging ? 0.4 : undefined,
183
+ transform: CSS.Translate.toString(transform),
184
+ transition,
185
+ };
186
+
187
+ const odysseyDesignTokens: DesignTokens = useOdysseyDesignTokens();
188
+ return (
189
+ <SortableItemContext.Provider value={context}>
190
+ <StyledSortableListItem
191
+ data-sortable-container="true"
192
+ ref={setNodeRef}
193
+ style={style}
194
+ odysseyDesignTokens={odysseyDesignTokens}
195
+ isSelected={isSelected}
196
+ >
197
+ {!isDisabled && <DragHandle isDragging={isDragging} />}
198
+ <StyledUl>{children}</StyledUl>
199
+ </StyledSortableListItem>
200
+ </SortableItemContext.Provider>
201
+ );
202
+ };
@@ -0,0 +1,122 @@
1
+ /*!
2
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
3
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
4
+ *
5
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ * Unless required by applicable law or agreed to in writing, software
7
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
8
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ *
10
+ * See the License for the specific language governing permissions and limitations under the License.
11
+ */
12
+
13
+ import React, { useMemo, useState } from "react";
14
+ import type { ReactNode } from "react";
15
+ // eslint-disable-next-line import/no-extraneous-dependencies
16
+ import {
17
+ DndContext,
18
+ KeyboardSensor,
19
+ PointerSensor,
20
+ useSensor,
21
+ useSensors,
22
+ } from "@dnd-kit/core";
23
+ import type { Active, Announcements, UniqueIdentifier } from "@dnd-kit/core";
24
+ // eslint-disable-next-line import/no-extraneous-dependencies
25
+ import {
26
+ SortableContext,
27
+ sortableKeyboardCoordinates,
28
+ } from "@dnd-kit/sortable";
29
+
30
+ import { SortableItem } from "./SortableItem";
31
+ import { SortableOverlay } from "./SortableOverlay";
32
+ import { useTranslation } from "react-i18next";
33
+
34
+ export interface BaseItem {
35
+ id: UniqueIdentifier;
36
+ isDisabled: boolean | undefined;
37
+ isSelected: boolean | undefined;
38
+ navItem: ReactNode;
39
+ }
40
+
41
+ interface ListProps<T extends BaseItem> {
42
+ parentId: string;
43
+ items: T[];
44
+ onChange(parentId: string, activeIndex: number, overIndex: number): void;
45
+ renderItem(item: T): ReactNode;
46
+ }
47
+
48
+ export const SortableList = <T extends BaseItem>({
49
+ parentId,
50
+ items,
51
+ onChange,
52
+ renderItem,
53
+ }: ListProps<T>) => {
54
+ const [active, setActive] = useState<Active | null>(null);
55
+ const activeItem = useMemo(
56
+ () => items.find((item) => item.id === active?.id),
57
+ [active, items],
58
+ );
59
+ const sensors = useSensors(
60
+ useSensor(PointerSensor),
61
+ useSensor(KeyboardSensor, {
62
+ coordinateGetter: sortableKeyboardCoordinates,
63
+ }),
64
+ );
65
+
66
+ const { t } = useTranslation();
67
+ const announcements: Announcements = useMemo(
68
+ () => ({
69
+ onDragStart: ({ active }) => {
70
+ return `${t("sortable.list.drag.start", { activeId: active.id })}`;
71
+ },
72
+ onDragOver: ({ active, over }) => {
73
+ if (over) {
74
+ return `${t("sortable.list.drag.moved.over", { activeId: active.id, overId: over.id })}`;
75
+ }
76
+ return `${t("sortable.list.drag.nolonger.over", { activeId: active.id })}`;
77
+ },
78
+ onDragEnd: ({ active, over }) => {
79
+ if (over) {
80
+ return `${t("sortable.list.drag.end.dropped.over", { activeId: active.id, overId: over.id })}`;
81
+ }
82
+ return `${t("sortable.list.drag.end.dropped", { activeId: active.id })}`;
83
+ },
84
+ onDragCancel: ({ active }) => {
85
+ return `${t("sortable.list.drag.cancel", { activeId: active.id })}`;
86
+ },
87
+ }),
88
+ [t],
89
+ );
90
+
91
+ return (
92
+ <DndContext
93
+ accessibility={{ announcements: announcements }}
94
+ sensors={sensors}
95
+ onDragStart={({ active }) => {
96
+ setActive(active);
97
+ }}
98
+ onDragEnd={({ active, over }) => {
99
+ if (over && active.id !== over?.id) {
100
+ const activeIndex = items.findIndex(({ id }) => id === active.id);
101
+ const overIndex = items.findIndex(({ id }) => id === over.id);
102
+ onChange(parentId, activeIndex, overIndex);
103
+ }
104
+ setActive(null);
105
+ }}
106
+ onDragCancel={() => {
107
+ setActive(null);
108
+ }}
109
+ >
110
+ <SortableContext items={items}>
111
+ {items.map((item) => (
112
+ <React.Fragment key={item.id}>{renderItem(item)}</React.Fragment>
113
+ ))}
114
+ </SortableContext>
115
+ <SortableOverlay>
116
+ {activeItem ? renderItem(activeItem) : null}
117
+ </SortableOverlay>
118
+ </DndContext>
119
+ );
120
+ };
121
+
122
+ SortableList.Item = SortableItem;