Skip to content

Commit

Permalink
Merge pull request #5188 from Sage/click-away-wrapper
Browse files Browse the repository at this point in the history
 feat(click-away-wrapper): add ClickAwayWrapper component to internal directory
  • Loading branch information
edleeks87 authored Jun 13, 2022
2 parents bca98d9 + 5c38e7f commit f3ddfde
Show file tree
Hide file tree
Showing 8 changed files with 475 additions and 216 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useEffect } from "react";
import Events from "../utils/helpers/events";

export interface ClickAwayWrapperProps {
children: React.ReactNode;
handleClickAway: (ev: CustomEvent) => void;
eventTypeId?: "mousedown" | "click";
targets: React.RefObject<HTMLElement>[];
}

const ClickAwayWrapper = ({
children,
handleClickAway,
eventTypeId = "click",
targets,
}: ClickAwayWrapperProps) => {
useEffect(() => {
const fnClickAway = (ev: CustomEvent) => {
const clickedElements = targets.filter(
(ref: React.RefObject<HTMLElement>) =>
ref?.current && Events.composedPath(ev).includes(ref.current)
);

if (!clickedElements || !clickedElements.length) {
handleClickAway(ev);
}
};

document.addEventListener(eventTypeId, fnClickAway as EventListener);

return function cleanup() {
document.removeEventListener(eventTypeId, fnClickAway as EventListener);
};
}, [handleClickAway, targets, eventTypeId]);

return <>{children}</>;
};

ClickAwayWrapper.displayName = "ClickAwayWrapper";

export default ClickAwayWrapper;
81 changes: 81 additions & 0 deletions src/__internal__/click-away-wrapper/click-away-wrapper.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useRef } from "react";
import { mount } from "enzyme";
import ClickAwayWrapper, {
ClickAwayWrapperProps,
} from "./click-away-wrapper.component";

const MockComponent = ({
handleClickAway,
eventTypeId,
}: Omit<ClickAwayWrapperProps, "children" | "targets">) => {
const ref = useRef<HTMLDivElement>(null);
return (
<ClickAwayWrapper
targets={[ref]}
handleClickAway={handleClickAway}
eventTypeId={eventTypeId}
>
<div ref={ref}>Child</div>
</ClickAwayWrapper>
);
};

describe("ClickAwayWrapper", () => {
it("adds the event listener on mount", () => {
const addListenerSpy = jest.spyOn(document, "addEventListener");
mount(<MockComponent handleClickAway={jest.fn()} />);
expect(addListenerSpy).toHaveBeenCalled();
});

it("removes the event listener on unmount", () => {
const removeListenerSpy = jest.spyOn(document, "removeEventListener");
const wrapper = mount(<MockComponent handleClickAway={jest.fn()} />);
wrapper.unmount();
expect(removeListenerSpy).toHaveBeenCalled();
});

it("calls handleClickAway when mousedown is outside of wrapper element", () => {
const handleClickAway = jest.fn();
mount(
<MockComponent
handleClickAway={handleClickAway}
eventTypeId="mousedown"
/>
);
document.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
expect(handleClickAway).toHaveBeenCalled();
});

it("does not call handleClickAway when mousedown is inside of wrapper element", () => {
const handleClickAway = jest.fn();
const wrapper = mount(<MockComponent handleClickAway={handleClickAway} />);
document.dispatchEvent(
new CustomEvent("mousedown", {
detail: {
enzymeTestingTarget: wrapper?.find("div").getDOMNode(),
},
})
);
expect(handleClickAway).not.toHaveBeenCalled();
});

it("calls handleClickAway when click is outside of wrapper element", () => {
const handleClickAway = jest.fn();
mount(<MockComponent handleClickAway={handleClickAway} />);
document.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(handleClickAway).toHaveBeenCalled();
});

it("does not call handleClickAway when click is inside of wrapper element", () => {
const handleClickAway = jest.fn();
const wrapper = mount(<MockComponent handleClickAway={handleClickAway} />);
document.dispatchEvent(
new CustomEvent("click", {
detail: {
enzymeTestingTarget: wrapper?.find("div").getDOMNode(),
},
})
);
expect(handleClickAway).not.toHaveBeenCalled();
});
});
2 changes: 2 additions & 0 deletions src/__internal__/click-away-wrapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./click-away-wrapper.component";
export type { ClickAwayWrapperProps } from "./click-away-wrapper.component";
131 changes: 63 additions & 68 deletions src/components/date/date.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import StyledDateInput from "./date.style";
import Textbox from "../textbox";
import DatePicker from "./__internal__/date-picker";
import DateRangeContext from "../date-range/date-range.context";
import ClickAwayWrapper from "../../__internal__/click-away-wrapper";

const marginPropTypes = filterStyledSystemMarginProps(
styledSystemPropTypes.space
Expand Down Expand Up @@ -249,29 +250,6 @@ const DateInput = ({
}
};

useEffect(() => {
const fnClosePicker = (ev) => {
if (
open &&
!Events.composedPath(ev).includes(parentRef.current) &&
!Events.composedPath(ev).includes(pickerRef.current)
) {
alreadyFocused.current = true;
inputRef.current.focus();
isBlurBlocked.current = false;
inputRef.current.blur();
setOpen(false);
alreadyFocused.current = false;
}
};

document.addEventListener("mousedown", fnClosePicker);

return function cleanup() {
document.removeEventListener("mousedown", fnClosePicker);
};
}, [open]);

useEffect(() => {
const [matchedFormat, matchedValue] = findMatchedFormatAndValue(
value,
Expand Down Expand Up @@ -326,60 +304,77 @@ const DateInput = ({
return value;
};

const handleClickAway = () => {
if (open) {
alreadyFocused.current = true;
inputRef.current.focus();
isBlurBlocked.current = false;
inputRef.current.blur();
setOpen(false);
alreadyFocused.current = false;
}
};

return (
<StyledDateInput
ref={wrapperRef}
role="presentation"
size={size}
labelInline={labelInline}
data-component={dataComponent || "date"}
data-element={dataElement}
data-role={dataRole}
{...filterStyledSystemMarginProps(rest)}
<ClickAwayWrapper
handleClickAway={handleClickAway}
eventTypeId="mousedown"
targets={[parentRef, pickerRef]}
>
<Textbox
{...filterOutStyledSystemSpacingProps(rest)}
value={computedValue()}
onBlur={handleBlur}
onChange={handleChange}
onClick={handleClick}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
iconOnClick={handleClick}
onMouseDown={handleMouseDown}
iconOnMouseDown={handleIconMouseDown}
inputIcon="calendar"
labelInline={labelInline}
inputRef={assignInput}
adaptiveLabelBreakpoint={adaptiveLabelBreakpoint}
tooltipPosition={tooltipPosition}
helpAriaLabel={helpAriaLabel}
autoFocus={autoFocus}
<StyledDateInput
ref={wrapperRef}
role="presentation"
size={size}
disabled={disabled}
readOnly={readOnly}
/>
<DatePicker
disablePortal={disablePortal}
inputElement={parentRef}
pickerProps={pickerProps}
selectedDays={selectedDays}
setSelectedDays={setSelectedDays}
onDayClick={handleDayClick}
minDate={minDate}
maxDate={maxDate}
ref={pickerRef}
pickerMouseDown={handlePickerMouseDown}
open={open}
/>
</StyledDateInput>
labelInline={labelInline}
data-component={dataComponent || "date"}
data-element={dataElement}
data-role={dataRole}
{...filterStyledSystemMarginProps(rest)}
>
<Textbox
{...filterOutStyledSystemSpacingProps(rest)}
value={computedValue()}
onBlur={handleBlur}
onChange={handleChange}
onClick={handleClick}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
iconOnClick={handleClick}
onMouseDown={handleMouseDown}
iconOnMouseDown={handleIconMouseDown}
inputIcon="calendar"
labelInline={labelInline}
inputRef={assignInput}
adaptiveLabelBreakpoint={adaptiveLabelBreakpoint}
tooltipPosition={tooltipPosition}
helpAriaLabel={helpAriaLabel}
autoFocus={autoFocus}
size={size}
disabled={disabled}
readOnly={readOnly}
/>
<DatePicker
disablePortal={disablePortal}
inputElement={parentRef}
pickerProps={pickerProps}
selectedDays={selectedDays}
setSelectedDays={setSelectedDays}
onDayClick={handleDayClick}
minDate={minDate}
maxDate={maxDate}
ref={pickerRef}
pickerMouseDown={handlePickerMouseDown}
open={open}
/>
</StyledDateInput>
</ClickAwayWrapper>
);
};

DateInput.propTypes = {
...Textbox.propTypes,
...marginPropTypes,
/** Pass any props that match the [DayPickerProps](https://react-day-picker.js.org/api/DayPicker)
/** Pass any props that match the [DayPickerProps](https://react-day-picker-v7.netlify.app/docs/getting-started/)
* interface to override default behaviors
* */
pickerProps: PropTypes.object,
Expand Down
Loading

0 comments on commit f3ddfde

Please sign in to comment.