Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ModalPicker button onClick propagation #706

Merged
merged 1 commit into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/core/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type Props = {
/**
* Optional callback for what needs to happen when the button is clicked.
*/
onClick?: (event: React.MouseEvent<HTMLElement>) => any;
onClick?: React.MouseEventHandler<HTMLButtonElement>;

/**
* Whether the action you are performing is currently in progress.
Expand Down Expand Up @@ -124,15 +124,15 @@ export function Button({
}: Props) {
const showSpinner = useShowSpinner(!!inProgress);

function handleOnClick(event: React.MouseEvent<HTMLElement>) {
const handleOnClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
if (!onClick) {
return;
}

if (!inProgress) {
onClick(event);
}
}
};

// If there are children it will look like a button.
if (children) {
Expand Down
26 changes: 12 additions & 14 deletions src/form/ModalPicker/ModalPickerOpener/ModalPickerOpener.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { MouseEventHandler } from 'react';
import { Button } from 'reactstrap';
import { ModalPickerButtonAlignment } from '../types';
import classNames from 'classnames';
Expand All @@ -16,7 +16,7 @@ type BaseProps = {
/**
* Function to open the modal, called when the button is clicked.
*/
openModal: () => void;
openModal: MouseEventHandler<HTMLButtonElement>;

/**
* The label to display on the button.
Expand Down Expand Up @@ -60,18 +60,16 @@ type Props<T> =
| ModalPickerSingleOpenerProps<T>
| ModalPickerMultipleOpenerProps<T>;

export function ModalPickerOpener<T>(props: Props<T>) {
const {
openModal,
label,
icon,
alignButton,
onClear,
text = {},
renderValue,
value
} = props;

export function ModalPickerOpener<T>({
openModal,
label,
icon,
alignButton,
onClear,
text = {},
renderValue,
value
}: Props<T>) {
const hasValue = !!value;

const wrapperClassName = classNames('d-flex', 'align-items-center', {
Expand Down
8 changes: 5 additions & 3 deletions src/form/ModalPicker/multiple/ModalPickerMultiple.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { MouseEventHandler, useState } from 'react';
import { Col, FormGroup, Input, Label, Row } from 'reactstrap';

import { withJarb } from '../../withJarb/withJarb';
Expand Down Expand Up @@ -176,7 +176,9 @@ export function ModalPickerMultiple<T>(props: Props<T>) {
setIsOpen(false);
}

function openModal() {
const openModal: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();

// Always copy the `value` so the `selected` is a fresh array.
// Otherwise, the selection will be the same as the value, which
// causes values to be committed and the cancel button will not
Expand All @@ -188,7 +190,7 @@ export function ModalPickerMultiple<T>(props: Props<T>) {
setQuery('');
setPageNumber(1);
setUserHasSearched(false);
}
};

function optionClicked(option: T, isSelected: boolean) {
// Always copy the `value` so the `selected` is a fresh array.
Expand Down
218 changes: 117 additions & 101 deletions src/form/ModalPicker/single/ModalPickerSingle.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,105 @@
import React, { useState } from 'react';
import React, { MouseEventHandler, useState } from 'react';
import { FormGroup, Input, Label } from 'reactstrap';
import { FieldCompatibleWithPredeterminedOptions, getKeyForOption, isOptionSelected } from '../../option';
import {
FieldCompatibleWithPredeterminedOptions,
getKeyForOption,
isOptionSelected
} from '../../option';
import { FieldCompatible } from '../../types';
import { useOptions } from '../../useOptions';
import { alwaysTrue } from '../../utils';
import { withJarb } from '../../withJarb/withJarb';
import { ModalPicker, Text } from '../ModalPicker';
import { ModalPickerOpener } from '../ModalPickerOpener/ModalPickerOpener';
import { ModalPickerValueTruncator } from '../ModalPickerValueTruncator/ModalPickerValueTruncator';
import { ModalPickerAddButtonCallback, ModalPickerAddButtonOptions, ModalPickerButtonAlignment, ModalPickerRenderOptions } from '../types';
import {
ModalPickerAddButtonCallback,
ModalPickerAddButtonOptions,
ModalPickerButtonAlignment,
ModalPickerRenderOptions
} from '../types';
import { IconType } from '../../../core/Icon';
import { withField } from '../../withField/withField';

export type ModalPickerSingleRenderValue<T> = (value?: T) => React.ReactNode;

type Props<T> = Omit<FieldCompatible<T, T | undefined>,
'placeholder' | 'valid'> &
type Props<T> = Omit<
FieldCompatible<T, T | undefined>,
'placeholder' | 'valid'
> &
FieldCompatibleWithPredeterminedOptions<T> & {
/**
* The placeholder of the form element.
*/
placeholder: string;

/**
* Optionally the icon to display on the button to open the modal picker.
*/
icon?: IconType;

/**
* Optionally whether the user can search.
* Defaults to `true`.
*/
canSearch?: boolean;

/**
* Optionally specify the number of options to show / fetch per
* page in the modal.
*
* When `options` is an array, it will determine how many options
* will be displayed per page.
*
* When `options` is a fetcher, it will determine how many options
* are requested through the fetcher as the `page` parameter.
* This means that when you set the pageSize to `100` that
* `100` items are fetched from the back-end. Beware of
* performance issues when setting the value too high.
*
* Beware that setting the page size too high will cause the UX
* to deteriorate on smaller screens!
*
* Defaults to `10`.
*/
pageSize?: number;

/**
* Optionally an add button to display in the Modal. Can
* be used to dynamically add an option which was not there
* before.
*/
addButton?: ModalPickerAddButtonOptions<T>;

/**
* Optionally the position the button should be aligned to
* within its container.
*/
alignButton?: ModalPickerButtonAlignment;

/**
* Optionally callback to display the selected item.
*/
renderValue?: ModalPickerSingleRenderValue<T>;

/**
* Callback to customize display of options.
*/
renderOptions?: ModalPickerRenderOptions<T>;

/**
* Whether to show a "clear" button.
*
* Defaults to `true`
*/
canClear?: boolean;

/**
* Optionally customized text within the component.
* This text should already be translated.
*/
text?: Text;
};
/**
* The placeholder of the form element.
*/
placeholder: string;

/**
* Optionally the icon to display on the button to open the modal picker.
*/
icon?: IconType;

/**
* Optionally whether the user can search.
* Defaults to `true`.
*/
canSearch?: boolean;

/**
* Optionally specify the number of options to show / fetch per
* page in the modal.
*
* When `options` is an array, it will determine how many options
* will be displayed per page.
*
* When `options` is a fetcher, it will determine how many options
* are requested through the fetcher as the `page` parameter.
* This means that when you set the pageSize to `100` that
* `100` items are fetched from the back-end. Beware of
* performance issues when setting the value too high.
*
* Beware that setting the page size too high will cause the UX
* to deteriorate on smaller screens!
*
* Defaults to `10`.
*/
pageSize?: number;

/**
* Optionally an add button to display in the Modal. Can
* be used to dynamically add an option which was not there
* before.
*/
addButton?: ModalPickerAddButtonOptions<T>;

/**
* Optionally the position the button should be aligned to
* within its container.
*/
alignButton?: ModalPickerButtonAlignment;

/**
* Optionally callback to display the selected item.
*/
renderValue?: ModalPickerSingleRenderValue<T>;

/**
* Callback to customize display of options.
*/
renderOptions?: ModalPickerRenderOptions<T>;

/**
* Whether to show a "clear" button.
*
* Defaults to `true`
*/
canClear?: boolean;

/**
* Optionally customized text within the component.
* This text should already be translated.
*/
text?: Text;
};

/**
* The ModalPickerSingle is a form element which allows the user
Expand Down Expand Up @@ -130,11 +141,11 @@ export function ModalPickerSingle<T>(props: Props<T>) {
text
} = props;

const [ isOpen, setIsOpen ] = useState(false);
const [ pageNumber, setPageNumber ] = useState(1);
const [ query, setQuery ] = useState<string>('');
const [ userHasSearched, setUserHasSearched ] = useState(false);
const [ selected, setSelected ] = useState<T | undefined>(undefined);
const [isOpen, setIsOpen] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [query, setQuery] = useState<string>('');
const [userHasSearched, setUserHasSearched] = useState(false);
const [selected, setSelected] = useState<T | undefined>(undefined);

const { page, loading } = useOptions({
options,
Expand All @@ -158,13 +169,14 @@ export function ModalPickerSingle<T>(props: Props<T>) {
setIsOpen(false);
}

function openModal() {
const openModal: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();
setIsOpen(true);
setSelected(value);
setQuery('');
setPageNumber(1);
setUserHasSearched(false);
}
};

async function addButtonClicked(callback: ModalPickerAddButtonCallback<T>) {
setIsOpen(false);
Expand All @@ -189,11 +201,11 @@ export function ModalPickerSingle<T>(props: Props<T>) {
renderValue: renderValue
? renderValue
: (value: T) => (
<ModalPickerValueTruncator
value={value}
labelForOption={labelForOption}
/>
),
<ModalPickerValueTruncator
value={value}
labelForOption={labelForOption}
/>
),
onClear: canClear ? () => onChange(undefined) : undefined,
value
};
Expand All @@ -219,9 +231,9 @@ export function ModalPickerSingle<T>(props: Props<T>) {
function renderModal() {
const addButtonOptions = addButton
? {
label: addButton.label,
onClick: () => addButtonClicked(addButton.onClick)
}
label: addButton.label,
onClick: () => addButtonClicked(addButton.onClick)
}
: undefined;

return (
Expand All @@ -243,13 +255,13 @@ export function ModalPickerSingle<T>(props: Props<T>) {
renderOptionsConfig={
renderOptions
? {
labelForOption,
isOptionEqual,
keyForOption,
isOptionEnabled,
renderOptions,
onChange: setSelected
}
labelForOption,
isOptionEqual,
keyForOption,
isOptionEnabled,
renderOptions,
onChange: setSelected
}
: undefined
}
text={text}
Expand Down Expand Up @@ -292,9 +304,13 @@ export function ModalPickerSingle<T>(props: Props<T>) {
/**
* Variant of the ModalPickerSingle which can be used in a Jarb context.
*/
export const JarbModalPickerSingle = withJarb<any, any | null, Props<any>>(ModalPickerSingle);
export const JarbModalPickerSingle = withJarb<any, any | null, Props<any>>(
ModalPickerSingle
);

/**
* Variant of the ModalPickerSingle which can be used in a final form.
*/
export const FieldModalPickerSingle = withField<any, any | null, Props<any>>(ModalPickerSingle);
export const FieldModalPickerSingle = withField<any, any | null, Props<any>>(
ModalPickerSingle
);
Loading