Skip to content

Commit

Permalink
feat: Makes number inputs always clearable when in focus irrespective…
Browse files Browse the repository at this point in the history
… of upstream rules (#946)
  • Loading branch information
amir-zahedi authored Oct 30, 2023
1 parent 7740c43 commit 9542838
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 73 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-sloths-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@autoguru/overdrive': patch
---

NumberInput gets to be cleared when upstream enforeces values
23 changes: 15 additions & 8 deletions packages/overdrive/lib/components/EditableText/EditableText.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import clsx from 'clsx';
import * as React from 'react';
import { ComponentProps, forwardRef, InputHTMLAttributes, useEffect, useRef, useState } from 'react';
import {
ComponentProps,
forwardRef,
InputHTMLAttributes,
useEffect,
useRef,
useState,
} from 'react';

import { Box } from '../Box';
import { Text, useTextStyles } from '../Text';
import * as inputStyles from '../private/InputBase/withEnhancedInput.css';

import * as styles from './EditableText.css';

type BoxProps = Pick<ComponentProps<typeof Box>, 'display' | 'onFocus' | 'onBlur' | 'onKeyDown'>;
type BoxProps = Pick<
ComponentProps<typeof Box>,
'display' | 'onFocus' | 'onBlur' | 'onKeyDown'
>;
type TextProps = Pick<
ComponentProps<typeof Text>,
'is' | 'colour' | 'size' | 'children' | 'noWrap'
Expand Down Expand Up @@ -79,18 +89,15 @@ export const EditableText = forwardRef<HTMLAnchorElement, Props>(
className={styles.root}
onClick={() => onRequestModeChange('INPUT')}
onFocus={(e) => {
if (typeof onFocus === 'function')
onFocus(e);
if (typeof onFocus === 'function') onFocus(e);
onRequestModeChange('INPUT');
}}
onBlur={(e) => {
if (typeof onBlur === 'function')
onBlur(e);
if (typeof onBlur === 'function') onBlur(e);
onRequestModeChange('TEXT');
}}
onKeyDown={(e) => {
if (typeof onKeyDown === 'function')
onKeyDown(e);
if (typeof onKeyDown === 'function') onKeyDown(e);
if (e.key === 'Enter' || e.key === 'Escape') {
onRequestModeChange('TEXT');
}
Expand Down
16 changes: 8 additions & 8 deletions packages/overdrive/lib/components/MinimalModal/MinimalModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { ComponentProps, FunctionComponent, MouseEventHandler } from 'react';
import type {
ComponentProps,
FunctionComponent,
MouseEventHandler,
} from 'react';
import * as React from 'react';
import { ReactNode, useLayoutEffect, useRef } from 'react';

Expand All @@ -20,8 +24,8 @@ export const MinimalModal: FunctionComponent<Props> = ({
alignItems = 'center',
className = '',
children,
onRequestClose,
...modalProps
onRequestClose,
...modalProps
}) => {
const titleId = useId();
const locked = useRef<boolean>(true);
Expand Down Expand Up @@ -51,11 +55,7 @@ export const MinimalModal: FunctionComponent<Props> = ({
}

return (
<Modal
isOpen={isOpen}
onRequestClose={onRequestClose}
{...modalProps}
>
<Modal isOpen={isOpen} onRequestClose={onRequestClose} {...modalProps}>
<Box
className={[styles.container, className]}
height="full"
Expand Down
8 changes: 6 additions & 2 deletions packages/overdrive/lib/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const reducer: Reducer<State, Action> = (prevState, action) => {
export const Modal: FunctionComponent<Props> = ({
isOpen,
hideBackdrop = false,
disableBackdropClick = false,
disableBackdropClick = false,
ref,
noThemedWrapper,
container,
Expand Down Expand Up @@ -124,7 +124,11 @@ export const Modal: FunctionComponent<Props> = ({
<Box
aria-hidden="true"
position="fixed"
pointerEvents={(disableBackdropClick || state === 'CLOSING') ? 'none' : undefined}
pointerEvents={
disableBackdropClick || state === 'CLOSING'
? 'none'
: undefined
}
opacity={state === 'OPEN' ? undefined : 0}
backgroundColour={
hideBackdrop ? 'transparent' : 'neutral'
Expand Down
76 changes: 24 additions & 52 deletions packages/overdrive/lib/components/NumberInput/NumberInput.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import * as React from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { FocusEventHandler, FormEventHandler } from 'react';

import { Box } from '../Box';
import { withEnhancedInput } from '../private/InputBase';

import { useNumberInputBehaviours } from './useNumberInputBehaviours';

const isEdge: boolean =
typeof navigator !== 'undefined' && /edge/i.test(navigator.userAgent);

const type = isEdge ? 'text' : 'number';

interface Props
extends Partial<Pick<HTMLInputElement, 'min' | 'max' | 'step'>> {
extends Partial<Pick<HTMLInputElement, 'min' | 'max' | 'step'>>,
Pick<HTMLInputElement, 'value'> {
preventMouseWheel?: boolean;
onChange?: FormEventHandler<HTMLInputElement>;
onFocus?: FocusEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
}

export const NumberInput = withEnhancedInput<Props>(
Expand All @@ -24,68 +30,34 @@ export const NumberInput = withEnhancedInput<Props>(
prefixed,
preventMouseWheel = false,
size,
onChange: incomingOnChange,
onFocus: incomingOnFocus,
onBlur: incomingOnBlur,
...rest
}) => {
const inputRef = useRef<HTMLInputElement>(ref?.current);

const preventWheel = useCallback<EventListener>((e) => {
e.preventDefault();
e.stopPropagation();
}, []);

useEffect(() => {
let mouseWheelListener;
let onWheelListener;
let wheelListener;
if (preventMouseWheel && inputRef?.current) {
mouseWheelListener = inputRef.current.addEventListener(
'mousewheel',
preventWheel,
{ passive: false },
);
onWheelListener = inputRef.current.addEventListener(
'onwheel',
preventWheel,
{ passive: false },
);
wheelListener = inputRef.current.addEventListener(
'wheel',
preventWheel,
{ passive: false },
);
}

return () => {
if (mouseWheelListener)
// Standard
inputRef.current.removeEventListener(
'mousewheel',
mouseWheelListener,
);
if (onWheelListener)
// Chrome
inputRef.current.removeEventListener(
'onwheel',
onWheelListener,
);
if (wheelListener)
// Safari
inputRef.current.removeEventListener(
'wheel',
wheelListener,
);
};
}, [preventMouseWheel, inputRef.current]);
const { value, inputRef, onFocus, onBlur, onChange } =
useNumberInputBehaviours({
ref,
preventMouseWheel,
onFocus: eventHandlers.onFocus,
onBlur: eventHandlers.onBlur,
onChange: eventHandlers.onChange,
value: incomingFieldProps.value,
});

return (
<Box
is="input"
ref={inputRef}
{...eventHandlers}
onFocus={onFocus}
onBlur={onBlur}
onChange={onChange}
{...incomingFieldProps}
{...rest}
autoComplete="off"
type={type}
value={value}
/>
);
},
Expand Down
22 changes: 19 additions & 3 deletions packages/overdrive/lib/components/NumberInput/stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,24 @@ const argTypes: ArgTypes = {
},
};

const Template: ComponentStory<typeof NumberInput> = (args) => (
<NumberInput {...args} />
);
const Template: ComponentStory<typeof NumberInput> = ({
value: initialValue,
onChange: incomingOnChange,
...args
}) => {
const [value, setValue] = React.useState<string | undefined>(initialValue);

return (
<NumberInput
{...args}
value={value}
onChange={(e) => {
setValue(e.currentTarget.value);
incomingOnChange(e);
}}
/>
);
};

const sharedProps: ComponentProps<typeof NumberInput> = {
disabled: false,
Expand All @@ -97,6 +112,7 @@ const sharedProps: ComponentProps<typeof NumberInput> = {
reserveHintSpace: false,
hintText: '',
notch: true,
preventMouseWheel: true,
prefixIcon: null,
onChange: action('onChange'),
onFocus: action('onFocus'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
FocusEventHandler,
FormEventHandler,
RefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';

import { EnhanceInputPrimitiveProps } from '../private/InputBase/withEnhancedInput';

interface Props {
value: EnhanceInputPrimitiveProps['value'];
ref: RefObject<HTMLInputElement>;
preventMouseWheel: boolean;
onFocus?: FocusEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
onChange?: FormEventHandler<HTMLInputElement>;
}

interface Returns {
value: EnhanceInputPrimitiveProps['value'];
inputRef: RefObject<HTMLInputElement>;
onFocus: FocusEventHandler<HTMLInputElement>;
onBlur: FocusEventHandler<HTMLInputElement>;
onChange: FormEventHandler<HTMLInputElement>;
}

export const useNumberInputBehaviours = ({
ref,
preventMouseWheel,
onBlur: incomingOnBlur,
onFocus: incomingOnFocus,
onChange: incomingOnChange,
value,
}: Props): Returns => {
const inputRef = useRef<HTMLInputElement>(ref?.current);
const [isFocused, setIsFocused] = useState(false);
const [displayValue, setDisplayValue] = useState<string | undefined>(value);

const onBlur = useCallback<FocusEventHandler<HTMLInputElement>>(
(e) => {
if (typeof incomingOnBlur === 'function') incomingOnBlur(e);
setIsFocused(false);
setDisplayValue(void 0);
},
[incomingOnBlur],
);
const onFocus = useCallback<FocusEventHandler<HTMLInputElement>>(
(e) => {
if (typeof incomingOnFocus === 'function') incomingOnFocus(e);
if (typeof value === 'string' && displayValue !== value)
setDisplayValue(value);
setIsFocused(true);
},
[incomingOnFocus, value, displayValue],
);
const onChange = useCallback<FormEventHandler<HTMLInputElement>>(
(e) => {
if (isFocused) {
setDisplayValue(e.currentTarget.value);
}
if (typeof incomingOnChange === 'function') incomingOnChange(e);
},
[incomingOnChange, isFocused, value, displayValue],
);

const preventWheel = useCallback<EventListener>((e) => {
e.preventDefault();
e.stopPropagation();
}, []);

useEffect(() => {
let mouseWheelListener;
let onWheelListener;
let wheelListener;
if (preventMouseWheel && inputRef?.current) {
mouseWheelListener = inputRef.current.addEventListener(
'mousewheel',
preventWheel,
{ passive: false },
);
onWheelListener = inputRef.current.addEventListener(
'onwheel',
preventWheel,
{ passive: false },
);
wheelListener = inputRef.current.addEventListener(
'wheel',
preventWheel,
{ passive: false },
);
}

return () => {
if (mouseWheelListener && inputRef.current)
// Standard
inputRef.current.removeEventListener(
'mousewheel',
mouseWheelListener,
);
if (onWheelListener && inputRef.current)
// Chrome
inputRef.current.removeEventListener(
'onwheel',
onWheelListener,
);
if (wheelListener && inputRef.current)
// Safari
inputRef.current.removeEventListener('wheel', wheelListener);
};
}, [preventMouseWheel, inputRef.current]);

return {
inputRef,
onBlur,
onFocus,
onChange,
value: typeof displayValue === 'string' ? displayValue : value,
};
};

0 comments on commit 9542838

Please sign in to comment.