Update useModal hook
All checks were successful
forgejo/Procyon/procyon/pipeline/head This commit looks good

This commit is contained in:
Roman Jaroš 2023-11-19 22:26:02 +01:00 committed by Roman Jaroš
parent 4e84d18090
commit bc7aeb3f01
16 changed files with 88 additions and 115 deletions

View file

@ -11,14 +11,14 @@
"vscode": { "vscode": {
"extensions": [ "extensions": [
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"rvest.vs-code-prettier-eslint",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"unifiedjs.vscode-mdx", "unifiedjs.vscode-mdx",
"naumovs.color-highlight", "naumovs.color-highlight",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"hediet.tasks-statusbar", "hediet.tasks-statusbar",
"BeardedBear.beardedicons", "BeardedBear.beardedicons",
"mia-hall.vscode-git-branch-sidebar" "mia-hall.vscode-git-branch-sidebar",
"esbenp.prettier-vscode"
] ]
} }
} }

View file

@ -3,7 +3,6 @@ import { omit } from 'ramda';
import { format as formatFn } from 'date-fns'; import { format as formatFn } from 'date-fns';
import { TodayIcon } from '@procyon/components/Icons/Today'; import { TodayIcon } from '@procyon/components/Icons/Today';
import { ModalId } from '@procyon/components/Modal/context';
import { useModal } from '@procyon/components/Modal/hooks'; import { useModal } from '@procyon/components/Modal/hooks';
import { InputElementType } from '@procyon/types/form'; import { InputElementType } from '@procyon/types/form';
@ -22,7 +21,10 @@ export type DatePickerProps<V = Date> = Omit<
export const DatePicker: React.FC<DatePickerProps> = (props) => { export const DatePicker: React.FC<DatePickerProps> = (props) => {
const { onChange, label, name, format, ...others } = props; const { onChange, label, name, format, ...others } = props;
const { closeModal, openModal, registerModal } = useModal(); const [openModal, { closeModal }] = useModal(name, {
title: label,
style: { width: 400 },
});
const [value, setValue] = useState<Date>(); const [value, setValue] = useState<Date>();
@ -32,7 +34,7 @@ export const DatePicker: React.FC<DatePickerProps> = (props) => {
} }
}, [props.value]); }, [props.value]);
const handleCloseModal = (id: ModalId) => closeModal(id); const handleCloseModal = () => closeModal(name);
const handleDateClick = (value: Date): void => { const handleDateClick = (value: Date): void => {
setValue(value); setValue(value);
@ -50,17 +52,13 @@ export const DatePicker: React.FC<DatePickerProps> = (props) => {
{...props} {...props}
/> />
), ),
[format, name, value] [format, name, value],
); );
const handleOpenModal = (): void => { const handleOpenModal = (): void => {
registerModal({ openModal({
id: name, Component: modalComponent,
title: label,
style: { width: 400 },
Component: modalComponent
}); });
openModal(name);
}; };
return ( return (

View file

@ -4,7 +4,7 @@ import { Calendar } from '../../Calendar';
import { ModalId } from '../../Modal/context'; import { ModalId } from '../../Modal/context';
export type DatePickerModalProps = { export type DatePickerModalProps = {
closeModal: (id: ModalId) => void; closeModal: () => void;
format: ModalId; format: ModalId;
name: ModalId; name: ModalId;
onChange: (value: Date) => void; onChange: (value: Date) => void;
@ -12,14 +12,14 @@ export type DatePickerModalProps = {
}; };
export const DatePickerModal: FC<DatePickerModalProps> = (props) => { export const DatePickerModal: FC<DatePickerModalProps> = (props) => {
const { onChange, closeModal, name, value } = props; const { onChange, closeModal, value } = props;
const setActiveOption = useCallback( const setActiveOption = useCallback(
(value: Date) => { (value: Date) => {
onChange?.(value); onChange?.(value);
closeModal(name); closeModal();
}, },
[onChange, closeModal, name] [onChange, closeModal],
); );
return <Calendar selected={value ?? undefined} value={value} onClick={setActiveOption} enableHover />; return <Calendar selected={value ?? undefined} value={value} onClick={setActiveOption} enableHover />;

View file

@ -1,4 +1,4 @@
import React, { FC, useEffect } from 'react'; import React, { FC } from 'react';
import { StatusEnum } from '@procyon/types/enums'; import { StatusEnum } from '@procyon/types/enums';
@ -7,29 +7,15 @@ import { Modal } from '../context';
import { useModal } from '../hooks'; import { useModal } from '../hooks';
export type AddModalButtonProps = { export type AddModalButtonProps = {
Component: FC;
modalTitle: string; modalTitle: string;
props?: { [key: string]: any };
status?: StatusEnum; status?: StatusEnum;
title: string; title: string;
} & Modal<any>; } & Modal<any>;
export const AddModalButton: FC<AddModalButtonProps> = ({ id, title, Component, style, status, props }) => { export const AddModalButton: FC<AddModalButtonProps> = ({ status, title, ...props }) => {
const { openModal, registerModal } = useModal(); const [openModal] = useModal(props.id, {
...props,
useEffect(() => { title: props.modalTitle,
registerModal({ });
Component, return <Button label={title} onClick={openModal} status={status} />;
id,
props,
style,
title,
});
}, []);
const handleClick = () => {
openModal(id);
};
return <Button label={title} onClick={handleClick} status={status} />;
}; };

View file

@ -6,7 +6,7 @@ import { ModalId } from '../context';
import { useModal } from '../hooks'; import { useModal } from '../hooks';
export const Modals = () => { export const Modals = () => {
const { modals, opened, closeModal } = useModal(); const [, { modals, opened, closeModal }] = useModal();
const handleCloseModal = (modalId: ModalId) => { const handleCloseModal = (modalId: ModalId) => {
modals[modalId]?.props?.onClose?.(); modals[modalId]?.props?.onClose?.();
@ -43,11 +43,11 @@ export const Modals = () => {
className="modal-container" className="modal-container"
style={{ style={{
width: style?.width, width: style?.width,
...(style?.overrideMaxWidth ? { maxWidth: 'initial' } : {}) ...(style?.overrideMaxWidth ? { maxWidth: 'initial' } : {}),
}}> }}>
<div className="modal-title">{title}</div> <div className="modal-title">{title}</div>
<div className="modal-body" onClick={(e: MouseEvent) => e.stopPropagation()}> <div className="modal-body" onClick={(e: MouseEvent) => e.stopPropagation()}>
<Component {...props} /> {Component && <Component {...props} />}
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,7 +2,7 @@ import React, { createContext, FC, ReactNode, useEffect, useMemo, useState } fro
import { omit } from 'ramda'; import { omit } from 'ramda';
export type Modal<P> = { export type Modal<P> = {
Component: FC<P>; Component?: FC<P>;
id: string; id: string;
props?: Partial<P>; props?: Partial<P>;
style?: { style?: {
@ -29,7 +29,7 @@ export const ModalContext = createContext<ModalContextType>({
open: () => {}, open: () => {},
close: () => {}, close: () => {},
register: () => {}, register: () => {},
unregister: () => {} unregister: () => {},
}); });
export const ModalWrapper: FC<{ children: ReactNode }> = ({ children }) => { export const ModalWrapper: FC<{ children: ReactNode }> = ({ children }) => {
@ -41,8 +41,8 @@ export const ModalWrapper: FC<{ children: ReactNode }> = ({ children }) => {
...modals, ...modals,
[modal.id]: { [modal.id]: {
...modals[modal.id], ...modals[modal.id],
...modal ...modal,
} },
}); });
}; };
@ -77,9 +77,9 @@ export const ModalWrapper: FC<{ children: ReactNode }> = ({ children }) => {
unregister, unregister,
register, register,
open, open,
close close,
}), }),
[modals, opened] [modals, opened],
); );
return <ModalContext.Provider value={value}>{children}</ModalContext.Provider>; return <ModalContext.Provider value={value}>{children}</ModalContext.Provider>;

View file

@ -2,22 +2,28 @@ import { useContext, useEffect } from 'react';
import { Modal, ModalContext } from './context'; import { Modal, ModalContext } from './context';
export const useModal = <P>(modal?: Modal<P>) => { export const useModal = <P>(id?: Modal<P>['id'], modal?: Omit<Modal<P>, 'id'>) => {
const { registered, close, open, opened, register, unregister } = useContext(ModalContext); const { registered, close, open, opened, register, unregister } = useContext(ModalContext);
useEffect(() => { useEffect(() => {
if (modal) { if (id && modal) {
register(modal); register({
return () => unregister(modal.id); id,
...modal,
});
return () => unregister(id);
} }
}, []); }, []);
return { return [
modals: registered, (modal?: Omit<Modal<P>, 'id'>) => open(id!, modal),
opened: opened, {
openModal: open, modals: registered,
closeModal: close, opened: opened,
registerModal: register, openModal: open,
unregisterModal: unregister, closeModal: close,
}; registerModal: register,
unregisterModal: unregister,
},
] as const;
}; };

View file

@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useState } from 'react';
import { find, propEq, propOr } from 'ramda'; import { find, propEq, propOr } from 'ramda';
import { ButtonCursorIcon } from '@procyon/components/Icons/ButtonCursor'; import { ButtonCursorIcon } from '@procyon/components/Icons/ButtonCursor';
import { ModalId } from '@procyon/components/Modal/context';
import { useModal } from '@procyon/components/Modal/hooks'; import { useModal } from '@procyon/components/Modal/hooks';
import { InputElementType } from '@procyon/types/form'; import { InputElementType } from '@procyon/types/form';
import { Option } from '@procyon/types/options'; import { Option } from '@procyon/types/options';
@ -21,7 +20,10 @@ export type SelectBoxProps<V = Option> = Omit<
export const Selectbox: React.FC<SelectBoxProps> = (props) => { export const Selectbox: React.FC<SelectBoxProps> = (props) => {
const { value, options, onChange, name, label, autoComplete, ...others } = props; const { value, options, onChange, name, label, autoComplete, ...others } = props;
const { openModal, closeModal, registerModal } = useModal(); const [openModal, { closeModal }] = useModal(name, {
title: label,
style: { width: 300 },
});
const [selectedOption, setSelectedOption] = useState<Option | null>(null); const [selectedOption, setSelectedOption] = useState<Option | null>(null);
@ -42,10 +44,6 @@ export const Selectbox: React.FC<SelectBoxProps> = (props) => {
onChange?.(undefined, null); onChange?.(undefined, null);
}; };
const handleCloseModal = (id: ModalId) => {
closeModal(id);
};
const modalComponent = useCallback( const modalComponent = useCallback(
(props) => ( (props) => (
<SelectboxModal <SelectboxModal
@ -53,23 +51,19 @@ export const Selectbox: React.FC<SelectBoxProps> = (props) => {
name={name} name={name}
options={options} options={options}
selectedOption={selectedOption} selectedOption={selectedOption}
closeModal={handleCloseModal} closeModal={closeModal}
onChange={handleOptionClick} onChange={handleOptionClick}
onRemove={handleOptionRemove} onRemove={handleOptionRemove}
{...props} {...props}
/> />
), ),
[autoComplete, name, options, selectedOption] [autoComplete, name, options, selectedOption],
); );
const handleOpenModal = (): void => { const handleOpenModal = (): void => {
registerModal({ openModal({
id: name, Component: modalComponent,
title: label,
style: { width: 300 },
Component: modalComponent
}); });
openModal(name);
}; };
return ( return (

View file

@ -42,7 +42,7 @@ const TimeList: React.FC<TimeListProps> = ({ title, onChange, min, max, step, se
selectedCode={selectedCode} selectedCode={selectedCode}
scrollToCode={scrollToCode} scrollToCode={scrollToCode}
enableAutoScroll enableAutoScroll
className="h-60" className="h-60 text-center"
/> />
</> </>
); );

View file

@ -29,8 +29,6 @@ export type TimePickerProps<V = Date> = Omit<
export const TimePicker: FC<TimePickerProps> = (props) => { export const TimePicker: FC<TimePickerProps> = (props) => {
const { onChange, label, name, hour, minute, scrollToCode, ...others } = props; const { onChange, label, name, hour, minute, scrollToCode, ...others } = props;
const { openModal, registerModal } = useModal();
const [value, setValue] = useState<Date>(); const [value, setValue] = useState<Date>();
useEffect(() => { useEffect(() => {
@ -42,15 +40,10 @@ export const TimePicker: FC<TimePickerProps> = (props) => {
onChange?.(value, null); onChange?.(value, null);
}; };
const handleOpenModal = (): void => { const [openModal] = useModal(name, {
registerModal({ title: label,
id: name, style: { width: 220 },
title: label, });
style: { width: 200 },
Component: modalComponent
});
openModal(name);
};
const modalComponent = useCallback( const modalComponent = useCallback(
(props) => ( (props) => (
@ -64,9 +57,13 @@ export const TimePicker: FC<TimePickerProps> = (props) => {
{...props} {...props}
/> />
), ),
[value, scrollToCode, name, minute, hour] [value, scrollToCode, name, minute, hour],
); );
const handleOpenModal = () => {
openModal({ Component: modalComponent });
};
return ( return (
<TextField <TextField
{...others} {...others}

View file

@ -33,7 +33,7 @@ const { base } = GridSizeEnum;
export const TimePickerModal: React.FC<TimePickerModalProps> = (props) => { export const TimePickerModal: React.FC<TimePickerModalProps> = (props) => {
const { onChange, modalName, scrollToCode } = props; const { onChange, modalName, scrollToCode } = props;
const { closeModal } = useModal(); const [, { closeModal }] = useModal(props.modalName);
const [hour, setHour] = useState<number | null>(); const [hour, setHour] = useState<number | null>();
const [minute, setMinute] = useState<number | null>(); const [minute, setMinute] = useState<number | null>();

View file

@ -15,7 +15,7 @@ const creator: PluginCreator = ({ addComponents, theme }) =>
borderRadius: theme('borderRadius.md'), borderRadius: theme('borderRadius.md'),
border: `1px solid ${theme('colors.list.item.border')}`, border: `1px solid ${theme('colors.list.item.border')}`,
'&-compact': { '&-compact': {
padding: '1px 1px 1px 8px', padding: '1px',
}, },
'&--focus': { '&--focus': {
backgroundColor: theme('colors.focus.background'), backgroundColor: theme('colors.focus.background'),

View file

@ -29,6 +29,9 @@ const creator: PluginCreator = ({ addComponents, theme }) =>
lineHeight: '24px', lineHeight: '24px',
padding: '0.875rem', padding: '0.875rem',
height: '54px', height: '54px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}, },
'&-cross': { '&-cross': {
position: 'absolute', position: 'absolute',

View file

@ -37,24 +37,15 @@ return <AddModalButton />;
```tsx ```tsx
import { useModal } from '@procyon/components/Modal/hooks'; import { useModal } from '@procyon/components/Modal/hooks';
const { registerModal } = useModal(); const [openModal] = useModal({
registerModal({
component: <div>Modal content</div>, component: <div>Modal content</div>,
id: 'modal1', id: 'modal1',
props: { onClick: () => {} }, props: { onClick: () => {} },
style: { width: '300px' }, style: { width: '300px' },
title: 'Modal Title', title: 'Modal Title',
}); });
```
Then, call `openModal` hook. openModal();
```ts
import { openModal } from '@procyon/components/Modal/hooks';
const { openModal } = useModal();
openModal('modal1');
``` ```
<Canvas of={ModalStories.Hook} withToolbar /> <Canvas of={ModalStories.Hook} withToolbar />

View file

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
@ -43,15 +43,11 @@ export const Hook: Story = {
name: 'Hook', name: 'Hook',
render: () => { render: () => {
const Comp = () => { const Comp = () => {
const { registerModal, openModal } = useModal(); const [openModal] = useModal('modal2', {
useEffect(() => { Component: () => <div>Hi there</div>,
registerModal({ title: 'Modal Title',
Component: () => <div>Hi there</div>, });
id: 'modal2', return <Button label="Open Modal" onClick={openModal} />;
title: 'Modal Title',
});
}, []);
return <Button label="Open Modal" onClick={() => openModal('modal2')} />;
}; };
return <Comp />; return <Comp />;
}, },

View file

@ -25,10 +25,12 @@ export default {
tags: ['autodocs'], tags: ['autodocs'],
decorators: [ decorators: [
(Story) => ( (Story) => (
<ModalWrapper> <div style={{ height: '600px' }}>
<Story /> <ModalWrapper>
<Modals /> <Story />
</ModalWrapper> <Modals />
</ModalWrapper>
</div>
), ),
], ],
} as Meta<typeof TimePicker>; } as Meta<typeof TimePicker>;
@ -37,7 +39,7 @@ const date = new Date();
export const Default: Story = { export const Default: Story = {
args: { args: {
name: 'datePicker', name: 'timePicker1',
label: 'Choose time', label: 'Choose time',
value: setHours(date, 20), value: setHours(date, 20),
}, },
@ -45,14 +47,14 @@ export const Default: Story = {
export const WithoutLabel: Story = { export const WithoutLabel: Story = {
args: { args: {
name: 'datePicker', name: 'timePicker2',
value: setHours(date, 20), value: setHours(date, 20),
}, },
}; };
export const RestrictedTimes: Story = { export const RestrictedTimes: Story = {
args: { args: {
name: 'datePicker', name: 'timePicker3',
label: 'Choose time', label: 'Choose time',
value: setHours(date, 20), value: setHours(date, 20),
minute: { minute: {