@qoretechnologies/reqraft 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/components/form/fields/Field.js +24 -31
  2. package/dist/components/form/fields/Field.js.map +1 -1
  3. package/dist/components/form/fields/boolean/Boolean.js +5 -9
  4. package/dist/components/form/fields/boolean/Boolean.js.map +1 -1
  5. package/dist/components/form/fields/color/Color.js +7 -14
  6. package/dist/components/form/fields/color/Color.js.map +1 -1
  7. package/dist/components/form/fields/cron/Cron.js +12 -19
  8. package/dist/components/form/fields/cron/Cron.js.map +1 -1
  9. package/dist/components/form/fields/long-string/LongString.js +5 -9
  10. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  11. package/dist/components/form/fields/markdown/Markdown.js +13 -20
  12. package/dist/components/form/fields/markdown/Markdown.js.map +1 -1
  13. package/dist/components/form/fields/number/Number.js +5 -9
  14. package/dist/components/form/fields/number/Number.js.map +1 -1
  15. package/dist/components/form/fields/radio-group/RadioGroup.js +7 -11
  16. package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
  17. package/dist/components/form/fields/string/String.js +4 -8
  18. package/dist/components/form/fields/string/String.js.map +1 -1
  19. package/dist/components/form/index.js +8 -24
  20. package/dist/components/form/index.js.map +1 -1
  21. package/dist/components/menu/Menu.js +23 -31
  22. package/dist/components/menu/Menu.js.map +1 -1
  23. package/dist/contexts/FetchContext.js +2 -5
  24. package/dist/contexts/FetchContext.js.map +1 -1
  25. package/dist/contexts/ReqraftContext.js +2 -5
  26. package/dist/contexts/ReqraftContext.js.map +1 -1
  27. package/dist/contexts/StorageContext.js +2 -5
  28. package/dist/contexts/StorageContext.js.map +1 -1
  29. package/dist/hooks/useFetch/useFetch.js +10 -14
  30. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  31. package/dist/hooks/useReqraftProperty.js +4 -8
  32. package/dist/hooks/useReqraftProperty.js.map +1 -1
  33. package/dist/hooks/useStorage/useStorage.d.ts.map +1 -1
  34. package/dist/hooks/useStorage/useStorage.js +13 -13
  35. package/dist/hooks/useStorage/useStorage.js.map +1 -1
  36. package/dist/hooks/useValidation.js +1 -5
  37. package/dist/hooks/useValidation.js.map +1 -1
  38. package/dist/index.js +6 -30
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers/FetchProvider.js +11 -15
  41. package/dist/providers/FetchProvider.js.map +1 -1
  42. package/dist/providers/ReqraftProvider.js +12 -17
  43. package/dist/providers/ReqraftProvider.js.map +1 -1
  44. package/dist/providers/StorageProvider.d.ts +1 -1
  45. package/dist/providers/StorageProvider.d.ts.map +1 -1
  46. package/dist/providers/StorageProvider.js +17 -21
  47. package/dist/providers/StorageProvider.js.map +1 -1
  48. package/dist/types/Form.js +1 -2
  49. package/dist/types.js +1 -2
  50. package/dist/utils/fetch.js +12 -17
  51. package/dist/utils/fetch.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/components/form/fields/Field.stories.tsx +165 -0
  54. package/src/components/form/fields/Field.tsx +172 -0
  55. package/src/components/form/fields/boolean/Boolean.stories.tsx +46 -0
  56. package/src/components/form/fields/boolean/Boolean.tsx +34 -0
  57. package/src/components/form/fields/color/Color.stories.tsx +60 -0
  58. package/src/components/form/fields/color/Color.tsx +43 -0
  59. package/src/components/form/fields/cron/Cron.stories.tsx +60 -0
  60. package/src/components/form/fields/cron/Cron.tsx +77 -0
  61. package/src/components/form/fields/long-string/LongString.stories.tsx +62 -0
  62. package/src/components/form/fields/long-string/LongString.tsx +35 -0
  63. package/src/components/form/fields/markdown/Markdown.stories.tsx +47 -0
  64. package/src/components/form/fields/markdown/Markdown.tsx +106 -0
  65. package/src/components/form/fields/number/Number.stories.tsx +74 -0
  66. package/src/components/form/fields/number/Number.tsx +53 -0
  67. package/src/components/form/fields/radio-group/RadioGroup.stories.tsx +79 -0
  68. package/src/components/form/fields/radio-group/RadioGroup.tsx +46 -0
  69. package/src/components/form/fields/radio-group/images/java-96x128.png +0 -0
  70. package/src/components/form/fields/radio-group/images/python-129x128.png +0 -0
  71. package/src/components/form/fields/radio-group/images/qore-106x128.png +0 -0
  72. package/src/components/form/fields/string/String.stories.tsx +70 -0
  73. package/src/components/form/fields/string/String.tsx +43 -0
  74. package/src/components/form/index.tsx +8 -0
  75. package/src/components/menu/Menu.stories.tsx +73 -0
  76. package/src/components/menu/Menu.tsx +244 -0
  77. package/src/contexts/FetchContext.tsx +25 -0
  78. package/src/contexts/ReqraftContext.tsx +9 -0
  79. package/src/contexts/StorageContext.tsx +33 -0
  80. package/src/global.d.ts +4 -0
  81. package/src/hooks/useFetch/useFetch.stories.tsx +123 -0
  82. package/src/hooks/useFetch/useFetch.tsx +71 -0
  83. package/src/hooks/useReqraftProperty.ts +16 -0
  84. package/src/hooks/useStorage/useStorage.stories.tsx +85 -0
  85. package/src/hooks/useStorage/useStorage.ts +43 -0
  86. package/src/hooks/useValidation.ts +9 -0
  87. package/src/index.tsx +17 -0
  88. package/src/providers/FetchProvider.tsx +40 -0
  89. package/src/providers/ReqraftProvider.tsx +52 -0
  90. package/src/providers/StorageProvider.tsx +85 -0
  91. package/src/types/Form.ts +46 -0
  92. package/src/types.ts +14 -0
  93. package/src/utils/fetch.ts +121 -0
@@ -0,0 +1,60 @@
1
+ import { StoryObj } from '@storybook/react';
2
+ import { expect, fn, userEvent } from '@storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { StoryMeta } from '../../../../types';
6
+ import { ColorFormField } from './Color';
7
+
8
+ const meta = {
9
+ component: ColorFormField,
10
+ title: 'Components/Form/Color',
11
+ args: {
12
+ onChange: fn(),
13
+ },
14
+ } as StoryMeta<typeof ColorFormField>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Default: Story = {
20
+ args: {
21
+ value: { r: 0, g: 0, b: 0, a: 1 },
22
+ },
23
+ render(args) {
24
+ const [value, setValue] = useState(args.value);
25
+
26
+ return (
27
+ <ColorFormField
28
+ {...args}
29
+ value={value}
30
+ onChange={(value) => {
31
+ args.onChange?.(value);
32
+ setValue(value);
33
+ }}
34
+ />
35
+ );
36
+ },
37
+ async play({ canvasElement, args }) {
38
+ const picker = canvasElement.querySelector('.sketch-picker');
39
+ const colorPanel = canvasElement.querySelector('.saturation-white');
40
+ const valueInput = canvasElement.querySelector('input');
41
+
42
+ await expect(picker).toBeInTheDocument();
43
+ await expect(colorPanel).toBeInTheDocument();
44
+ await expect(valueInput).toBeInTheDocument();
45
+ await expect(valueInput).toHaveValue('000000');
46
+
47
+ await userEvent.pointer({
48
+ keys: '[MouseLeft]',
49
+ target: colorPanel,
50
+ coords: { x: 0, y: 0 },
51
+ });
52
+ await expect(valueInput).toHaveValue('FFFFFF');
53
+ await expect(args.onChange).toHaveBeenLastCalledWith({
54
+ r: 255,
55
+ g: 255,
56
+ b: 255,
57
+ a: 1,
58
+ });
59
+ },
60
+ };
@@ -0,0 +1,43 @@
1
+ import { SketchPicker, SketchPickerProps } from 'react-color';
2
+ import styled from 'styled-components';
3
+
4
+ export interface IColorFormFieldProps extends Omit<SketchPickerProps, 'onChange'> {
5
+ value: SketchPickerProps['color'];
6
+ onChange(value: IColorFormFieldProps['value']);
7
+ }
8
+
9
+ export const StyledSketchPicker = styled(SketchPicker)`
10
+ background-color: transparent !important;
11
+ width: 100% !important;
12
+ max-width: 400px !important;
13
+ box-shadow: none !important;
14
+ padding: 0 !important;
15
+
16
+ > div:first-child {
17
+ padding-bottom: unset !important;
18
+ height: 100px !important;
19
+ }
20
+
21
+ label {
22
+ color: #fff !important;
23
+ }
24
+ input {
25
+ width: 100% !important;
26
+ }
27
+ .flexbox-fix {
28
+ border: none !important;
29
+ }
30
+ `;
31
+
32
+ export const ColorFormField = ({ value, onChange, ...rest }: IColorFormFieldProps) => {
33
+ return (
34
+ <StyledSketchPicker
35
+ onChange={(color) => onChange(color.rgb)}
36
+ color={value}
37
+ disableAlpha
38
+ {...rest}
39
+ />
40
+ );
41
+ };
42
+
43
+ export default ColorFormField;
@@ -0,0 +1,60 @@
1
+ import { StoryObj } from '@storybook/react';
2
+ import { expect, fn, userEvent, within } from '@storybook/test';
3
+ import { useState } from 'react';
4
+ import { StoryMeta } from '../../../../types';
5
+ import { CronFormField } from './Cron';
6
+
7
+ const meta = {
8
+ component: CronFormField,
9
+ title: 'Components/Form/Cron',
10
+ args: {
11
+ wrapperProps: {
12
+ 'aria-label': 'Cron',
13
+ },
14
+ onChange: fn(),
15
+ },
16
+ render(args) {
17
+ const [value, setValue] = useState(args.value);
18
+
19
+ return (
20
+ <CronFormField
21
+ {...args}
22
+ value={value}
23
+ onChange={(value) => {
24
+ args.onChange?.(value);
25
+ setValue(value);
26
+ }}
27
+ />
28
+ );
29
+ },
30
+ } as StoryMeta<typeof CronFormField>;
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof meta>;
34
+
35
+ export const Default: Story = {
36
+ args: {
37
+ value: '1 1 1 1 1',
38
+ inputProps: ['Minute', 'Hour', 'Day', 'Month', 'Weekday'].map((label) => ({
39
+ 'aria-label': label,
40
+ })),
41
+ },
42
+
43
+ async play({ args, canvasElement }) {
44
+ const canvas = within(canvasElement);
45
+ const wrapper = canvas.getByLabelText('Cron');
46
+ await expect(wrapper).toBeInTheDocument();
47
+
48
+ await Promise.all(
49
+ args.inputProps
50
+ .map((p) => p['aria-label'])
51
+ .map((label) => expect(canvas.getByLabelText(label)).toBeInTheDocument())
52
+ );
53
+ await userEvent.clear(canvas.getByLabelText('Minute'));
54
+ await userEvent.type(canvas.getByLabelText('Minute'), '30');
55
+ await expect(args.onChange).toHaveBeenLastCalledWith('30 1 1 1 1');
56
+
57
+ await userEvent.click(canvas.getByRole('button'));
58
+ await expect(args.onChange).toHaveBeenLastCalledWith('');
59
+ },
60
+ };
@@ -0,0 +1,77 @@
1
+ import {
2
+ ReqoreButton,
3
+ ReqoreControlGroup,
4
+ ReqoreInput,
5
+ ReqoreMessage,
6
+ } from '@qoretechnologies/reqore';
7
+ import { IReqoreControlGroupProps } from '@qoretechnologies/reqore/dist/components/ControlGroup';
8
+ import { IReqoreInputProps } from '@qoretechnologies/reqore/dist/components/Input';
9
+ import cronstrue from 'cronstrue';
10
+ import { useMemo } from 'react';
11
+
12
+ export interface ICronFormFieldProps {
13
+ value?: string;
14
+ onChange?(value: string): void;
15
+ wrapperProps?: Partial<IReqoreControlGroupProps>;
16
+ inputProps?: IReqoreInputProps[];
17
+ }
18
+
19
+ export const CronFormField = ({
20
+ onChange,
21
+ value,
22
+ wrapperProps,
23
+ inputProps,
24
+ }: ICronFormFieldProps) => {
25
+ const { message, isError } = useMemo(() => {
26
+ try {
27
+ return { message: cronstrue.toString(value, {}) };
28
+ } catch (message: any) {
29
+ return {
30
+ message,
31
+ isError: true,
32
+ };
33
+ }
34
+ }, [value]);
35
+
36
+ return (
37
+ <ReqoreControlGroup fluid stack={false} vertical gapSize='big' {...wrapperProps}>
38
+ <ReqoreControlGroup fluid stack>
39
+ {['Minute', 'Hour', 'Day', 'Month', 'Weekday'].map((label, index) => {
40
+ return (
41
+ <ReqoreInput
42
+ {...inputProps[index]}
43
+ key={index}
44
+ aria-label={label}
45
+ placeholder={label}
46
+ onChange={(event) => {
47
+ const cronData: Record<string, string> = {};
48
+ [
49
+ cronData.minute = '',
50
+ cronData.hour = '',
51
+ cronData.day = '',
52
+ cronData.month = '',
53
+ cronData.weekday = '',
54
+ ] = value.split(' ');
55
+ cronData[label.toLowerCase() as any] = event.currentTarget.value;
56
+
57
+ onChange?.(
58
+ `${cronData.minute} ${cronData.hour} ${cronData.day} ${cronData.month} ${cronData.weekday}`
59
+ );
60
+ }}
61
+ value={value.split(' ')?.[index] ?? ''}
62
+ />
63
+ );
64
+ })}
65
+
66
+ <ReqoreButton fixed onClick={() => onChange('')} icon={'CloseLine'} />
67
+ </ReqoreControlGroup>
68
+ {value && (
69
+ <ReqoreMessage intent={isError ? 'danger' : 'info'} opaque={false}>
70
+ {message}
71
+ </ReqoreMessage>
72
+ )}
73
+ </ReqoreControlGroup>
74
+ );
75
+ };
76
+
77
+ export default CronFormField;
@@ -0,0 +1,62 @@
1
+ import { StoryObj } from '@storybook/react';
2
+ import { expect, fn, userEvent, within } from '@storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { longStringText } from '../../../../../mock/fields';
6
+ import { StoryMeta } from '../../../../types';
7
+ import { LongStringFormField } from './LongString';
8
+
9
+ const meta = {
10
+ component: LongStringFormField,
11
+ title: 'Components/Form/LongString',
12
+ args: {
13
+ onChange: fn(),
14
+ onClearClick: fn(),
15
+ 'aria-label': `LongString`,
16
+ value: longStringText,
17
+ },
18
+ render(args) {
19
+ const [value, setValue] = useState(args.value);
20
+ return (
21
+ <LongStringFormField
22
+ {...args}
23
+ value={value}
24
+ onChange={(value) => {
25
+ args.onChange?.(value);
26
+ setValue(value);
27
+ }}
28
+ />
29
+ );
30
+ },
31
+ } as StoryMeta<typeof LongStringFormField>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ export const Default: Story = {
37
+ play: async ({ canvasElement, args, step }) => {
38
+ const canvas = within(canvasElement);
39
+ const textarea = canvas.getByLabelText('LongString');
40
+
41
+ await step('Initial asserts', async () => {
42
+ await expect(textarea).toBeInTheDocument();
43
+ await expect(textarea).toHaveValue(args.value);
44
+ });
45
+
46
+ await step('Clear Longstring field', async () => {
47
+ await userEvent.click(textarea.nextElementSibling);
48
+ await expect(textarea).toHaveValue('');
49
+ await expect(args.onChange).toHaveBeenLastCalledWith('');
50
+ await expect(args.onClearClick).toHaveBeenCalledOnce();
51
+ });
52
+
53
+ await step('Type in the input', async () => {
54
+ await userEvent.type(textarea, 'Qore');
55
+ await expect(textarea).toHaveValue('Qore');
56
+ await expect(args.onChange).toHaveBeenLastCalledWith('Qore');
57
+
58
+ await userEvent.clear(textarea);
59
+ await userEvent.type(textarea, args.value, { delay: null });
60
+ });
61
+ },
62
+ };
@@ -0,0 +1,35 @@
1
+ import { ReqoreTextarea } from '@qoretechnologies/reqore';
2
+ import { IReqoreTextareaProps } from '@qoretechnologies/reqore/dist/components/Textarea';
3
+ import { TFormFieldValueType } from '../../../../types/Form';
4
+
5
+ export interface ILongStringFormFieldProps extends Omit<IReqoreTextareaProps, 'onChange'> {
6
+ onChange?: (
7
+ value?: TFormFieldValueType<'string'>,
8
+ event?: React.FormEvent<HTMLTextAreaElement>
9
+ ) => void;
10
+ }
11
+
12
+ export const LongStringFormField = ({
13
+ onChange,
14
+ onClearClick,
15
+ ...rest
16
+ }: ILongStringFormFieldProps) => {
17
+ return (
18
+ <ReqoreTextarea
19
+ scaleWithContent
20
+ fluid
21
+ wrapperStyle={{
22
+ width: '100%',
23
+ }}
24
+ onClearClick={() => {
25
+ onClearClick?.();
26
+ onChange?.('');
27
+ }}
28
+ onChange={(event) => onChange(event.currentTarget.value, event)}
29
+ rows={4}
30
+ {...rest}
31
+ />
32
+ );
33
+ };
34
+
35
+ export default LongStringFormField;
@@ -0,0 +1,47 @@
1
+ import { StoryObj } from '@storybook/react';
2
+ import { expect, fn, within } from '@storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { markdown } from '../../../../../mock/fields';
6
+ import { StoryMeta } from '../../../../types';
7
+ import { MarkdownFormField } from './Markdown';
8
+
9
+ const meta = {
10
+ component: MarkdownFormField,
11
+ title: 'Components/Form/Markdown',
12
+ args: {
13
+ onChange: fn(),
14
+ value: markdown,
15
+ },
16
+ render(args) {
17
+ const [value, setValue] = useState(args.value);
18
+ return (
19
+ <MarkdownFormField
20
+ {...args}
21
+ value={value}
22
+ onChange={(value) => {
23
+ args.onChange?.(value);
24
+ setValue(value);
25
+ }}
26
+ />
27
+ );
28
+ },
29
+ } as StoryMeta<typeof MarkdownFormField>;
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof meta>;
33
+
34
+ export const Default: Story = {
35
+ args: {
36
+ 'aria-label': 'MarkdownEditor',
37
+ },
38
+ async play({ canvasElement, args }) {
39
+ const canvas = within(canvasElement);
40
+ const editor = canvas.getByLabelText('MarkdownEditor');
41
+ const preview = canvas.getByLabelText('Preview');
42
+
43
+ await expect(editor).toBeInTheDocument();
44
+ await expect(preview).toBeInTheDocument();
45
+ await expect(editor).toHaveValue(args.value);
46
+ },
47
+ };
@@ -0,0 +1,106 @@
1
+ import { ReqoreColumn, ReqoreColumns, ReqoreMessage } from '@qoretechnologies/reqore';
2
+ import { IReqoreMessageProps } from '@qoretechnologies/reqore/dist/components/Message';
3
+ import { ComponentProps } from 'react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import styled from 'styled-components';
6
+ import LongStringFormField, { ILongStringFormFieldProps } from '../long-string/LongString';
7
+
8
+ export interface IMarkdownFormFieldProps extends ILongStringFormFieldProps {
9
+ markdownPreviewProps?: Partial<ComponentProps<typeof ReactMarkdown>>;
10
+ }
11
+
12
+ const StyledWrapper = styled(ReqoreColumns)<ComponentProps<typeof ReqoreColumns>>`
13
+ width: 100%;
14
+ `;
15
+
16
+ const StyledLongStringWrapper = styled(ReqoreColumn)`
17
+ .reqore-control-wrapper {
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+ `;
22
+
23
+ const StyledMarkdown = styled(ReactMarkdown)<ComponentProps<typeof ReactMarkdown>>`
24
+ p {
25
+ font-size: 14px;
26
+
27
+ &:first-child {
28
+ margin-top: 0;
29
+ }
30
+ &:last-child {
31
+ margin-bottom: 0;
32
+ }
33
+ }
34
+ h1,
35
+ h2,
36
+ p,
37
+ i,
38
+ a {
39
+ color: rgba(255, 255, 255, 0.84);
40
+ }
41
+
42
+ h1 {
43
+ text-align: left;
44
+ }
45
+
46
+ h2 {
47
+ font-weight: 700;
48
+ padding: 0;
49
+ text-align: left;
50
+ line-height: 34.5px;
51
+ letter-spacing: -0.45px;
52
+ }
53
+
54
+ p,
55
+ i,
56
+ a {
57
+ letter-spacing: -0.03px;
58
+ line-height: 1.58;
59
+ }
60
+
61
+ a {
62
+ text-decoration: underline;
63
+ }
64
+
65
+ blockquote {
66
+ font-style: italic;
67
+ letter-spacing: -0.36px;
68
+ line-height: 44.4px;
69
+ overflow-wrap: break-word;
70
+ color: rgba(255, 255, 255, 0.68);
71
+ padding: 0 0 0 50px;
72
+ }
73
+
74
+ code {
75
+ background: rgba(255, 255, 255, 0.05);
76
+ border-radius: 2px;
77
+ padding: 34px 6px;
78
+ }
79
+ `;
80
+
81
+ const StyledPreviewColumn = styled(ReqoreColumn)`
82
+ width: 100%;
83
+ `;
84
+
85
+ const StyledPreviewWrapper = styled(ReqoreMessage)<IReqoreMessageProps>`
86
+ & div div {
87
+ justify-content: start;
88
+ }
89
+ `;
90
+
91
+ export const MarkdownFormField = ({ markdownPreviewProps, ...rest }: IMarkdownFormFieldProps) => {
92
+ return (
93
+ <StyledWrapper columnsGap='10px'>
94
+ <StyledLongStringWrapper flexFlow='column'>
95
+ <LongStringFormField {...rest} />
96
+ </StyledLongStringWrapper>
97
+ <StyledPreviewColumn>
98
+ <StyledPreviewWrapper size='small' aria-label='Preview' flat fluid>
99
+ <StyledMarkdown {...markdownPreviewProps}>{rest.value ?? ''}</StyledMarkdown>
100
+ </StyledPreviewWrapper>
101
+ </StyledPreviewColumn>
102
+ </StyledWrapper>
103
+ );
104
+ };
105
+
106
+ export default MarkdownFormField;
@@ -0,0 +1,74 @@
1
+ import { StoryObj } from '@storybook/react';
2
+ import { expect, fn, userEvent, within } from '@storybook/test';
3
+ import { useState } from 'react';
4
+ import { StoryMeta } from '../../../../types';
5
+ import { NumberFormField } from './Number';
6
+
7
+ const meta = {
8
+ component: NumberFormField,
9
+ title: 'Components/Form/Number',
10
+ args: {
11
+ 'aria-label': 'Number',
12
+ onChange: fn(),
13
+ },
14
+ render(args) {
15
+ const [value, setValue] = useState(args.value);
16
+
17
+ return (
18
+ <NumberFormField
19
+ {...args}
20
+ value={value}
21
+ onChange={(value) => {
22
+ args.onChange?.(value);
23
+ setValue(value);
24
+ }}
25
+ />
26
+ );
27
+ },
28
+ } as StoryMeta<typeof NumberFormField>;
29
+
30
+ export default meta;
31
+ type Story = StoryObj<typeof meta>;
32
+
33
+ export const Default: Story = {
34
+ args: {
35
+ value: 0,
36
+ },
37
+
38
+ async play({ args, canvasElement }) {
39
+ const canvas = within(canvasElement);
40
+ const input = canvas.getByLabelText('Number');
41
+
42
+ await expect(input).toBeInTheDocument();
43
+ await expect(input).toHaveValue(0);
44
+ await expect(input).toHaveAttribute('type', 'number');
45
+
46
+ await userEvent.type(input, '10');
47
+ await expect(input).toHaveValue(10);
48
+ await expect(args.onChange).toHaveBeenLastCalledWith(10);
49
+
50
+ await userEvent.click(input.nextElementSibling);
51
+ await expect(args.onChange).toHaveBeenLastCalledWith(undefined);
52
+
53
+ await userEvent.type(input, '10.5');
54
+ await expect(input).toHaveValue(10);
55
+ await expect(args.onChange).toHaveBeenLastCalledWith(10);
56
+ },
57
+ };
58
+
59
+ export const Float: Story = {
60
+ args: {
61
+ value: 0,
62
+ type: 'float',
63
+ },
64
+
65
+ async play({ args, canvasElement }) {
66
+ const canvas = within(canvasElement);
67
+ const input = canvas.getByLabelText('Number');
68
+
69
+ await userEvent.clear(input);
70
+ await userEvent.type(input, '10.9');
71
+ await expect(input).toHaveValue(10.9);
72
+ await expect(args.onChange).toHaveBeenLastCalledWith(10.9);
73
+ },
74
+ };
@@ -0,0 +1,53 @@
1
+ import { ReqoreInput } from '@qoretechnologies/reqore';
2
+ import { IReqoreInputProps } from '@qoretechnologies/reqore/dist/components/Input';
3
+ import { ChangeEvent, ChangeEventHandler } from 'react';
4
+
5
+ export interface INumberFormFieldProps
6
+ extends Omit<IReqoreInputProps, 'value' | 'onChange' | 'type'> {
7
+ value?: number;
8
+ onChange?(value: number): void;
9
+ type?: 'int' | 'float';
10
+ }
11
+
12
+ export const NumberFormField = ({
13
+ onChange,
14
+ autoFocus,
15
+ type = 'int',
16
+ value,
17
+ ...rest
18
+ }: INumberFormFieldProps) => {
19
+ const handleInputChange: ChangeEventHandler<HTMLInputElement> = (
20
+ event: ChangeEvent<HTMLInputElement>
21
+ ): void => {
22
+ const value = type === 'int' ? parseInt(event.target.value) : parseFloat(event.target.value);
23
+ onChange?.(value ?? undefined);
24
+ };
25
+
26
+ const handleResetClick = (): void => {
27
+ onChange(undefined);
28
+ };
29
+
30
+ return (
31
+ <ReqoreInput
32
+ wrapperStyle={{
33
+ width: '100%',
34
+ }}
35
+ value={value ?? ''}
36
+ onChange={handleInputChange}
37
+ type='number'
38
+ onClearClick={handleResetClick}
39
+ focusRules={
40
+ autoFocus ?
41
+ {
42
+ type: 'auto',
43
+ viewportOnly: true,
44
+ }
45
+ : undefined
46
+ }
47
+ step={type === 'int' ? 1 : 0.1}
48
+ {...rest}
49
+ />
50
+ );
51
+ };
52
+
53
+ export default NumberFormField;
@@ -0,0 +1,79 @@
1
+ import { StoryObj } from '@storybook/react';
2
+ import { expect, fn, userEvent, within } from '@storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { StoryMeta } from '../../../../types';
6
+ import { RadioGroupFormField } from './RadioGroup';
7
+
8
+ import java from './images/java-96x128.png';
9
+ import python from './images/python-129x128.png';
10
+ import qore from './images/qore-106x128.png';
11
+
12
+ const meta = {
13
+ component: RadioGroupFormField,
14
+ title: 'Components/Form/Radio',
15
+ args: {
16
+ onChange: fn(),
17
+ },
18
+ render(args) {
19
+ const [value, setValue] = useState(args.value);
20
+ return (
21
+ <RadioGroupFormField
22
+ {...args}
23
+ value={value}
24
+ onChange={(value) => {
25
+ args.onChange(value);
26
+ setValue(value);
27
+ }}
28
+ />
29
+ );
30
+ },
31
+ } as StoryMeta<typeof RadioGroupFormField>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ export const Default: Story = {
37
+ args: {
38
+ value: 'Qore',
39
+ items: [
40
+ { label: 'Qore', value: 'Qore', 'aria-label': 'Qore' },
41
+ { label: 'Java', value: 'Java', 'aria-label': 'Java' },
42
+ { label: 'Python', value: 'Python', 'aria-label': 'Python' },
43
+ ],
44
+ },
45
+ async play({ canvasElement, args }) {
46
+ const canvas = within(canvasElement);
47
+ const java = canvas.getByLabelText('Java');
48
+ await userEvent.click(java);
49
+ await expect(args.onChange).toHaveBeenLastCalledWith('Java');
50
+ },
51
+ };
52
+
53
+ export const WithImages: Story = {
54
+ args: {
55
+ value: 'Qore',
56
+ items: [
57
+ { label: 'Qore', value: 'Qore', image: qore },
58
+ { label: 'Java', value: 'Java', image: java },
59
+ { label: 'Python', value: 'Python', image: python },
60
+ ],
61
+ },
62
+ async play({ canvasElement }) {
63
+ await expect(canvasElement.querySelector(`img[src="${qore}"]`)).toBeInTheDocument();
64
+ await expect(canvasElement.querySelector(`img[src="${java}"]`)).toBeInTheDocument();
65
+ await expect(canvasElement.querySelector(`img[src="${python}"]`)).toBeInTheDocument();
66
+ },
67
+ };
68
+
69
+ export const Disabled: Story = {
70
+ args: { ...Default.args, disabled: true },
71
+ async play({ canvasElement, args }) {
72
+ const canvas = within(canvasElement);
73
+ const java = canvas.getByLabelText('Java');
74
+
75
+ // expect rejection caused by clicking an element with pointer-events:none
76
+ await expect(() => userEvent.click(java)).rejects.toBeTruthy();
77
+ await expect(args.onChange).not.toHaveBeenCalled();
78
+ },
79
+ };