Skip to content

Commit 4352dad

Browse files
NarekArshakyanGaroGabrielyanAbrahamyanKarenpetrosyansosAreg Hakobyan
authored
feat(SplitButton): new component (#266)
* fix(SplitButton): test commit * feat(SplitButton): add functionality * fix(SplitButton): component UI styles * fix(SplitButton): classnames duplication * fix(SplitButton): add className and change onClick * feat(Menu): open with enter functionailty * feat(Profile): add accessibility and test fix * fix(GlobalHeader): test * fix(SplitButton): rename and change directory * fix(Menu): children render * fix(SplitButton): code refactor , change structure and logic * fix(SplitButton): add loading logic * fix(SplitButton): loading state and update styles * fix(SplitButton): loader appearance * feat(SplitButton): logic improvement * fix(SplitButton): change type prop to layout * fix(SplitButton): loading state * feat(SplitButton): tests * refactor(SplitButton): docs for props and props default values changes Menu component types --------- Co-authored-by: Galust <[email protected]> Co-authored-by: abrahamyan.karen <[email protected]> Co-authored-by: Sos Petrosyan <[email protected]> Co-authored-by: Areg Hakobyan <[email protected]>
1 parent b83d199 commit 4352dad

File tree

11 files changed

+865
-16
lines changed

11 files changed

+865
-16
lines changed

src/components/molecules/Menu/Menu.tsx

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import React, {
66
FC,
77
FunctionComponentElement,
88
JSX,
9+
KeyboardEvent as ReactKeyboardEvent,
10+
MouseEvent as ReactMouseEvent,
911
MutableRefObject,
1012
ReactElement,
1113
SetStateAction,
14+
useCallback,
1215
useContext,
1316
useEffect,
1417
useMemo,
@@ -52,6 +55,8 @@ type RelativeRefsSetter = (props: {
5255

5356
type SizeType = "large" | "medium" | "small";
5457

58+
type GenericObject = Record<string, unknown>;
59+
5560
interface IMenuContextProps {
5661
onChangeHandler: (props: OnchangeHandlerType) => void;
5762
openSelectedPath?: boolean;
@@ -84,7 +89,7 @@ interface IMenuProps {
8489
/**
8590
* A function for setting additional props for the Popover component that wraps the menu.
8691
*/
87-
setPropsForPopover: Dispatch<SetStateAction<Record<string, unknown>>>;
92+
setPropsForPopover: Dispatch<SetStateAction<GenericObject>>;
8893
/**
8994
* Menu size.<br/>
9095
* Default value is `small`.<br/>
@@ -174,30 +179,97 @@ const Menu: FC<IMenuProps> = ({
174179
setOpenSelectedPathState(openSelectedPath);
175180
}, [openSelectedPath]);
176181

182+
const toggleMenuOpen = useCallback(() => {
183+
if (isMobileBreakpoint) {
184+
setIsOpenState(true);
185+
return;
186+
}
187+
188+
setIsOpenState((prev) => {
189+
if (prev) {
190+
setPaths([]);
191+
}
192+
return !prev;
193+
});
194+
}, [isMobileBreakpoint]);
195+
177196
useClickOutside(
178197
(e) => {
179198
const onMenuTargetClick =
180199
e.target instanceof Node &&
181200
popoverRef.current.referenceElement?.current instanceof Node &&
182201
popoverRef.current.referenceElement.current.contains(e.target);
183202

184-
if (onMenuTargetClick) {
185-
if (isMobileBreakpoint) {
186-
setIsOpenState(true);
187-
} else {
188-
setIsOpenState((prev) => !prev);
189-
if (isOpenState) {
190-
setPaths([]);
191-
}
192-
}
193-
} else if (!isMobileBreakpoint) {
203+
if (!onMenuTargetClick && !isMobileBreakpoint) {
194204
setIsOpenState(false);
195205
setPaths([]);
196206
}
197207
},
198208
[popoverRef.current.floatingElement, ...Object.values(relativeRefs)]
199209
);
200210

211+
const enhanceTriggerPropsRef = useRef<(triggerProps: GenericObject) => GenericObject>();
212+
213+
const enhanceTriggerProps = useCallback(
214+
(triggerProps: GenericObject): GenericObject => {
215+
if (!triggerProps || typeof triggerProps !== "object") {
216+
return triggerProps;
217+
}
218+
219+
const { onClick, onKeyDown, ...rest } = triggerProps as {
220+
onClick?: (event: ReactMouseEvent<HTMLElement>) => void;
221+
onKeyDown?: (event: ReactKeyboardEvent<HTMLElement>) => void;
222+
[key: string]: unknown;
223+
};
224+
225+
return {
226+
...rest,
227+
onClick: (event: ReactMouseEvent<HTMLElement>) => {
228+
onClick?.(event);
229+
if (!event.defaultPrevented) {
230+
toggleMenuOpen();
231+
}
232+
},
233+
onKeyDown: (event: ReactKeyboardEvent<HTMLElement>) => {
234+
onKeyDown?.(event);
235+
if (!event.defaultPrevented && (event.key === "Enter" || event.key === " ")) {
236+
event.preventDefault();
237+
toggleMenuOpen();
238+
}
239+
}
240+
};
241+
},
242+
[toggleMenuOpen]
243+
);
244+
245+
// Keep the ref updated with the latest function
246+
enhanceTriggerPropsRef.current = enhanceTriggerProps;
247+
248+
const setReferenceProps = useCallback(
249+
(value: SetStateAction<GenericObject>) => {
250+
const enhanceFn = enhanceTriggerPropsRef.current;
251+
if (!enhanceFn) {
252+
// Fallback if ref is not set yet
253+
if (typeof value === "function") {
254+
setPropsForPopover((prev) => value(prev));
255+
} else {
256+
setPropsForPopover(value);
257+
}
258+
return;
259+
}
260+
261+
if (typeof value === "function") {
262+
setPropsForPopover((prev) => {
263+
const nextValue = value(prev);
264+
return enhanceFn(nextValue);
265+
});
266+
} else {
267+
setPropsForPopover(enhanceFn(value));
268+
}
269+
},
270+
[setPropsForPopover]
271+
);
272+
201273
useEffect(() => {
202274
const defaultPath = findPathOfSelected(children);
203275
if (defaultPath && isOpenState && openSelectedPathState && !paths.length) {
@@ -293,7 +365,7 @@ const Menu: FC<IMenuProps> = ({
293365
return (
294366
<MenuContext.Provider value={memoizedMenuContextValue}>
295367
<Popover
296-
setProps={setPropsForPopover}
368+
setProps={setReferenceProps}
297369
size={popoverSizeMapping[size]}
298370
position={position}
299371
withArrow={false}

src/components/molecules/Menu/MenuItem.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,15 @@ const MenuItem: FC<IMenuItemProps> = (props) => {
143143
referenceElement: { current: null }
144144
});
145145

146+
const hasNestedContent = useMemo(() => {
147+
if (typeof children === "string") return false;
148+
149+
return !!children;
150+
}, [children]);
151+
146152
const onItemClickHandler = (isBack: boolean) => {
147153
if (onChangeHandler && generatedId) {
148-
const shouldCloseMenu = !!render || typeof children === "string";
154+
const shouldCloseMenu = !!render || !hasNestedContent;
149155
onChangeHandler({
150156
generatedId,
151157
id,
@@ -273,7 +279,7 @@ const MenuItem: FC<IMenuItemProps> = (props) => {
273279

274280
return swappable ? (
275281
<>
276-
{typeof children !== "string" ? (
282+
{hasNestedContent ? (
277283
<>
278284
{isActiveSwappableContent && !popoverOpenState && renderMenuItem("parent")}
279285
<div
@@ -296,7 +302,7 @@ const MenuItem: FC<IMenuItemProps> = (props) => {
296302
</>
297303
) : (
298304
<>
299-
{typeof children !== "string" ? (
305+
{hasNestedContent ? (
300306
<>
301307
{renderMenuItem("parent")}
302308
<Popover

src/components/molecules/Profile/Profile.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ describe("Profile ", () => {
1313
setup = mount(<Profile profileData={profileData} />);
1414
});
1515

16+
afterEach(() => {
17+
if (setup) {
18+
setup.unmount();
19+
}
20+
});
21+
1622
it("renders without crashing", () => {
1723
expect(setup.exists()).toBeTruthy();
1824
});

src/components/molecules/Profile/Profile.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ const Profile: FC<IProfileProps> = ({ className, onToggle, fullName, src, onProf
9898
if (onToggle) {
9999
onToggle(e, !isOpen);
100100
}
101+
// Call the onClick from propsForPopover if it exists (from Menu's enhanced props)
102+
const popoverOnClick = (
103+
propsForPopover as {
104+
onClick?: (event: MouseEvent<HTMLElement>) => void;
105+
}
106+
).onClick;
107+
108+
if (popoverOnClick) {
109+
popoverOnClick(e as MouseEvent<HTMLElement>);
110+
}
101111

102112
setIsOpen((prev) => !prev);
103113
};
@@ -110,9 +120,9 @@ const Profile: FC<IProfileProps> = ({ className, onToggle, fullName, src, onProf
110120
<div className="profile">
111121
<button
112122
type="button"
113-
onClick={onProfileClickHandler}
114123
className={classNames("profile__button", className)}
115124
{...propsForPopover}
125+
onClick={onProfileClickHandler}
116126
>
117127
<Avatar className="profile__avatar" fullName={fullName} Icon={PersonFilled} src={src} />
118128
{!isMobileBreakpoint && (

0 commit comments

Comments
 (0)