@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.
- package/CHANGELOG.md +6 -0
- package/dist/Surface.js +10 -2
- package/dist/Surface.js.map +1 -1
- package/dist/index.scss +1 -1
- package/dist/labs/DataView/DataView.js +6 -0
- package/dist/labs/DataView/DataView.js.map +1 -1
- package/dist/labs/DataView/componentTypes.js.map +1 -1
- package/dist/labs/SideNav/NavAccordion.js +4 -5
- package/dist/labs/SideNav/NavAccordion.js.map +1 -1
- package/dist/labs/SideNav/SideNav.js +167 -93
- package/dist/labs/SideNav/SideNav.js.map +1 -1
- package/dist/labs/SideNav/SideNavItemContent.js +97 -57
- package/dist/labs/SideNav/SideNavItemContent.js.map +1 -1
- package/dist/labs/SideNav/SideNavItemContentContext.js +1 -0
- package/dist/labs/SideNav/SideNavItemContentContext.js.map +1 -1
- package/dist/labs/SideNav/SideNavItemLinkContent.js +2 -2
- package/dist/labs/SideNav/SideNavItemLinkContent.js.map +1 -1
- package/dist/labs/SideNav/SideNavToggleButton.js +5 -5
- package/dist/labs/SideNav/SideNavToggleButton.js.map +1 -1
- package/dist/labs/SideNav/SortableList/SortableItem.js +162 -0
- package/dist/labs/SideNav/SortableList/SortableItem.js.map +1 -0
- package/dist/labs/SideNav/SortableList/SortableList.js +118 -0
- package/dist/labs/SideNav/SortableList/SortableList.js.map +1 -0
- package/dist/labs/SideNav/SortableList/SortableOverlay.js +30 -0
- package/dist/labs/SideNav/SortableList/SortableOverlay.js.map +1 -0
- package/dist/labs/SideNav/types.js.map +1 -1
- package/dist/labs/TopNav/TopNav.js +1 -1
- package/dist/labs/TopNav/TopNav.js.map +1 -1
- package/dist/labs/UiShell/UiShellContent.js +1 -1
- package/dist/labs/UiShell/UiShellContent.js.map +1 -1
- package/dist/properties/ts/odyssey-react-mui.js +7 -0
- package/dist/properties/ts/odyssey-react-mui.js.map +1 -1
- package/dist/src/OdysseyTranslationProvider.d.ts +1 -1
- package/dist/src/OdysseyTranslationProvider.d.ts.map +1 -1
- package/dist/src/Surface.d.ts.map +1 -1
- package/dist/src/labs/DataView/DataView.d.ts +1 -1
- package/dist/src/labs/DataView/DataView.d.ts.map +1 -1
- package/dist/src/labs/DataView/componentTypes.d.ts +3 -2
- package/dist/src/labs/DataView/componentTypes.d.ts.map +1 -1
- package/dist/src/labs/SideNav/NavAccordion.d.ts +2 -6
- package/dist/src/labs/SideNav/NavAccordion.d.ts.map +1 -1
- package/dist/src/labs/SideNav/SideNav.d.ts +1 -1
- package/dist/src/labs/SideNav/SideNav.d.ts.map +1 -1
- package/dist/src/labs/SideNav/SideNavItemContent.d.ts +37 -1
- package/dist/src/labs/SideNav/SideNavItemContent.d.ts.map +1 -1
- package/dist/src/labs/SideNav/SideNavItemContentContext.d.ts +1 -0
- package/dist/src/labs/SideNav/SideNavItemContentContext.d.ts.map +1 -1
- package/dist/src/labs/SideNav/SideNavToggleButton.d.ts.map +1 -1
- package/dist/src/labs/SideNav/SortableList/SortableItem.d.ts +26 -0
- package/dist/src/labs/SideNav/SortableList/SortableItem.d.ts.map +1 -0
- package/dist/src/labs/SideNav/SortableList/SortableList.d.ts +36 -0
- package/dist/src/labs/SideNav/SortableList/SortableList.d.ts.map +1 -0
- package/dist/src/labs/SideNav/SortableList/SortableOverlay.d.ts +17 -0
- package/dist/src/labs/SideNav/SortableList/SortableOverlay.d.ts.map +1 -0
- package/dist/src/labs/SideNav/types.d.ts +16 -6
- package/dist/src/labs/SideNav/types.d.ts.map +1 -1
- package/dist/src/properties/ts/odyssey-react-mui.d.ts +7 -0
- package/dist/src/properties/ts/odyssey-react-mui.d.ts.map +1 -1
- package/dist/tsconfig.production.tsbuildinfo +1 -1
- package/i18n.config.json +2 -1
- package/package.json +6 -3
- package/src/Surface.tsx +16 -4
- package/src/labs/DataView/DataView.tsx +6 -0
- package/src/labs/DataView/componentTypes.ts +6 -2
- package/src/labs/SideNav/NavAccordion.tsx +5 -10
- package/src/labs/SideNav/SideNav.test.tsx +8 -8
- package/src/labs/SideNav/SideNav.tsx +232 -119
- package/src/labs/SideNav/SideNavItemContent.tsx +114 -61
- package/src/labs/SideNav/SideNavItemContentContext.tsx +2 -0
- package/src/labs/SideNav/SideNavItemLinkContent.tsx +2 -2
- package/src/labs/SideNav/SideNavToggleButton.tsx +5 -9
- package/src/labs/SideNav/SortableList/SortableItem.tsx +202 -0
- package/src/labs/SideNav/SortableList/SortableList.tsx +122 -0
- package/src/labs/SideNav/SortableList/SortableOverlay.tsx +34 -0
- package/src/labs/SideNav/types.ts +16 -6
- package/src/labs/TopNav/TopNav.tsx +1 -1
- package/src/labs/UiShell/UiShellContent.tsx +1 -1
- package/src/properties/odyssey-react-mui.properties +7 -0
- 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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
backgroundColor: !isDisabled
|
|
101
|
-
? odysseyDesignTokens.HueNeutral50
|
|
102
|
-
: "inherit",
|
|
94
|
+
backgroundColor: odysseyDesignTokens.HueNeutral50,
|
|
103
95
|
|
|
104
|
-
...(
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
...(isSelected && {
|
|
97
|
+
backgroundColor: odysseyDesignTokens.HueBlue50,
|
|
98
|
+
color: odysseyDesignTokens.TypographyColorAction,
|
|
107
99
|
}),
|
|
108
100
|
|
|
109
|
-
...(
|
|
110
|
-
"
|
|
111
|
-
backgroundColor: odysseyDesignTokens.HueBlue50,
|
|
112
|
-
},
|
|
101
|
+
...(isDisabled && {
|
|
102
|
+
backgroundColor: "unset",
|
|
113
103
|
}),
|
|
114
104
|
},
|
|
115
105
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
106
|
+
...(isSelected && {
|
|
107
|
+
color: `${odysseyDesignTokens.TypographyColorAction}`,
|
|
108
|
+
fontWeight: odysseyDesignTokens.TypographyWeightBodyBold,
|
|
109
|
+
}),
|
|
120
110
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
111
|
+
...(isDisabled && {
|
|
112
|
+
cursor: "default",
|
|
113
|
+
color: `${odysseyDesignTokens.TypographyColorDisabled} !important`,
|
|
114
|
+
}),
|
|
124
115
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
116
|
+
"&:focus-visible, &:focus": {
|
|
117
|
+
outline: "none",
|
|
118
|
+
boxShadow: `inset 0 0 0 2px ${odysseyDesignTokens.PalettePrimaryMain}`,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
129
121
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
<
|
|
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={
|
|
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
|
-
</
|
|
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.
|
|
36
|
-
marginInlineStart: isIconVisible ? odysseyDesignTokens.
|
|
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.
|
|
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;
|