@marigold/components 0.1.0 → 0.3.2

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 (220) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/dist/ActionGroup/ActionGroup.d.ts +8 -0
  3. package/dist/ActionGroup/ActionGroup.stories.d.ts +5 -0
  4. package/dist/ActionGroup/index.d.ts +1 -0
  5. package/dist/Alert/Alert.d.ts +22 -1
  6. package/dist/Alert/Alert.stories.d.ts +5 -0
  7. package/dist/Badge/Badge.d.ts +5 -0
  8. package/dist/Badge/Badge.stories.d.ts +5 -0
  9. package/dist/Box.d.ts +2 -0
  10. package/dist/Button/Button.d.ts +9 -6
  11. package/dist/Button/Button.stories.d.ts +5 -0
  12. package/dist/Card/Card.d.ts +14 -0
  13. package/dist/Card/Card.stories.d.ts +5 -0
  14. package/dist/Card/index.d.ts +1 -0
  15. package/dist/Checkbox/Checkbox.d.ts +12 -4
  16. package/dist/Checkbox/Checkbox.stories.d.ts +5 -0
  17. package/dist/Checkbox/CheckboxIcons.d.ts +9 -0
  18. package/dist/Column/Column.d.ts +3 -1
  19. package/dist/Column/Column.stories.d.ts +5 -0
  20. package/dist/Columns/Columns.d.ts +2 -2
  21. package/dist/Columns/Columns.stories.d.ts +5 -0
  22. package/dist/Container/Container.stories.d.ts +5 -0
  23. package/dist/Dialog/Dialog.d.ts +12 -2
  24. package/dist/Dialog/Dialog.stories.d.ts +5 -0
  25. package/dist/Dialog/ModalDialog.d.ts +8 -0
  26. package/dist/Divider/Divider.d.ts +7 -3
  27. package/dist/Divider/Divider.stories.d.ts +5 -0
  28. package/dist/Field/Field.d.ts +5 -1
  29. package/dist/Field/Field.stories.d.ts +5 -0
  30. package/dist/Image/Image.d.ts +5 -0
  31. package/dist/Image/Image.stories.d.ts +5 -0
  32. package/dist/Inline/Inline.d.ts +7 -0
  33. package/dist/Inline/Inline.stories.d.ts +5 -0
  34. package/dist/Inline/index.d.ts +1 -0
  35. package/dist/Input/Input.d.ts +5 -0
  36. package/dist/Input/Input.stories.d.ts +5 -0
  37. package/dist/Label/Label.d.ts +14 -2
  38. package/dist/Label/Label.stories.d.ts +5 -0
  39. package/dist/Link/Link.d.ts +3 -0
  40. package/dist/Link/Link.stories.d.ts +5 -0
  41. package/dist/Menu/Menu.d.ts +3 -0
  42. package/dist/Menu/Menu.stories.d.ts +5 -0
  43. package/dist/MenuItem/MenuItem.d.ts +5 -0
  44. package/dist/MenuItem/MenuItem.stories.d.ts +5 -0
  45. package/dist/Message/Message.d.ts +5 -0
  46. package/dist/Message/Message.stories.d.ts +5 -0
  47. package/dist/Provider/MarigoldProvider.d.ts +11 -0
  48. package/dist/Provider/index.d.ts +3 -0
  49. package/dist/Radio/Radio.d.ts +11 -4
  50. package/dist/Radio/Radio.stories.d.ts +5 -0
  51. package/dist/Radio/RadioIcon.d.ts +9 -0
  52. package/dist/Select/ListBox.d.ts +9 -0
  53. package/dist/Select/ListBoxSection.d.ts +9 -0
  54. package/dist/Select/Option.d.ts +9 -0
  55. package/dist/Select/Popover.d.ts +9 -0
  56. package/dist/Select/Select.d.ts +25 -4
  57. package/dist/Select/Select.stories.d.ts +5 -0
  58. package/dist/Slider/Slider.d.ts +5 -0
  59. package/dist/Slider/Slider.stories.d.ts +5 -0
  60. package/dist/Stack/Stack.d.ts +1 -3
  61. package/dist/Stack/Stack.stories.d.ts +5 -0
  62. package/dist/Text/Text.d.ts +5 -0
  63. package/dist/Text/Text.stories.d.ts +5 -0
  64. package/dist/Textarea/Textarea.d.ts +7 -1
  65. package/dist/Textarea/Textarea.stories.d.ts +5 -0
  66. package/dist/ValidationMessage/ValidationMessage.d.ts +5 -0
  67. package/dist/ValidationMessage/ValidationMessage.stories.d.ts +5 -0
  68. package/dist/VisuallyHidden/VisuallyHidden.d.ts +1 -0
  69. package/dist/VisuallyHidden/VisuallyHidden.stories.d.ts +5 -0
  70. package/dist/VisuallyHidden/index.d.ts +1 -0
  71. package/dist/components.cjs.development.js +1068 -601
  72. package/dist/components.cjs.development.js.map +1 -1
  73. package/dist/components.cjs.production.min.js +1 -1
  74. package/dist/components.cjs.production.min.js.map +1 -1
  75. package/dist/components.esm.js +1003 -579
  76. package/dist/components.esm.js.map +1 -1
  77. package/dist/index.d.ts +6 -2
  78. package/dist/theme.d.ts +23 -48
  79. package/package.json +22 -3
  80. package/src/ActionGroup/ActionGroup.stories.tsx +47 -0
  81. package/src/ActionGroup/ActionGroup.test.tsx +83 -0
  82. package/src/ActionGroup/ActionGroup.tsx +32 -0
  83. package/src/ActionGroup/index.ts +1 -0
  84. package/src/Alert/Alert.stories.tsx +32 -0
  85. package/src/Alert/Alert.test.tsx +5 -2
  86. package/src/Alert/Alert.tsx +27 -34
  87. package/src/Badge/Badge.stories.tsx +38 -0
  88. package/src/Badge/Badge.test.tsx +12 -16
  89. package/src/Badge/Badge.tsx +14 -3
  90. package/src/Box.ts +2 -0
  91. package/src/Button/Button.stories.tsx +57 -0
  92. package/src/Button/Button.test.tsx +76 -13
  93. package/src/Button/Button.tsx +61 -18
  94. package/src/Card/Card.stories.tsx +41 -0
  95. package/src/Card/Card.test.tsx +71 -0
  96. package/src/Card/Card.tsx +48 -0
  97. package/src/Card/index.ts +1 -0
  98. package/src/Checkbox/Checkbox.stories.tsx +78 -0
  99. package/src/Checkbox/Checkbox.test.tsx +138 -23
  100. package/src/Checkbox/Checkbox.tsx +67 -68
  101. package/src/Checkbox/CheckboxIcons.tsx +59 -0
  102. package/src/Column/Column.stories.tsx +33 -0
  103. package/src/Column/Column.test.tsx +8 -0
  104. package/src/Column/Column.tsx +12 -2
  105. package/src/Columns/Columns.stories.tsx +75 -0
  106. package/src/Columns/Columns.test.tsx +34 -23
  107. package/src/Columns/Columns.tsx +30 -30
  108. package/src/Container/Container.stories.tsx +14 -0
  109. package/src/Dialog/Dialog.stories.tsx +88 -0
  110. package/src/Dialog/Dialog.test.tsx +129 -18
  111. package/src/Dialog/Dialog.tsx +113 -15
  112. package/src/Dialog/ModalDialog.tsx +76 -0
  113. package/src/Divider/Divider.stories.tsx +30 -0
  114. package/src/Divider/Divider.test.tsx +32 -23
  115. package/src/Divider/Divider.tsx +27 -7
  116. package/src/Field/Field.stories.tsx +110 -0
  117. package/src/Field/Field.test.tsx +74 -33
  118. package/src/Field/Field.tsx +27 -20
  119. package/src/Image/Image.stories.tsx +34 -0
  120. package/src/Image/Image.test.tsx +4 -1
  121. package/src/Image/Image.tsx +13 -1
  122. package/src/Inline/Inline.stories.tsx +39 -0
  123. package/src/Inline/Inline.test.tsx +99 -0
  124. package/src/Inline/Inline.tsx +38 -0
  125. package/src/Inline/index.ts +1 -0
  126. package/src/Input/Input.stories.tsx +54 -0
  127. package/src/Input/Input.test.tsx +7 -3
  128. package/src/Input/Input.tsx +13 -1
  129. package/src/Label/Label.stories.tsx +41 -0
  130. package/src/Label/Label.test.tsx +40 -5
  131. package/src/Label/Label.tsx +54 -8
  132. package/src/Link/Link.stories.tsx +35 -0
  133. package/src/Link/Link.test.tsx +6 -2
  134. package/src/Link/Link.tsx +12 -1
  135. package/src/Menu/Menu.stories.tsx +62 -0
  136. package/src/Menu/Menu.test.tsx +11 -6
  137. package/src/Menu/Menu.tsx +22 -14
  138. package/src/MenuItem/MenuItem.stories.tsx +30 -0
  139. package/src/MenuItem/MenuItem.test.tsx +22 -13
  140. package/src/MenuItem/MenuItem.tsx +19 -10
  141. package/src/Message/Message.stories.tsx +30 -0
  142. package/src/Message/Message.test.tsx +4 -1
  143. package/src/Message/Message.tsx +18 -14
  144. package/src/Provider/MarigoldProvider.test.tsx +136 -0
  145. package/src/Provider/MarigoldProvider.tsx +47 -0
  146. package/src/Provider/index.ts +4 -0
  147. package/src/Radio/Radio.stories.tsx +78 -0
  148. package/src/Radio/Radio.test.tsx +129 -18
  149. package/src/Radio/Radio.tsx +62 -71
  150. package/src/Radio/RadioIcon.tsx +49 -0
  151. package/src/Select/ListBox.tsx +40 -0
  152. package/src/Select/ListBoxSection.tsx +40 -0
  153. package/src/Select/Option.tsx +48 -0
  154. package/src/Select/Popover.tsx +50 -0
  155. package/src/Select/Select.stories.tsx +81 -0
  156. package/src/Select/Select.test.tsx +317 -35
  157. package/src/Select/Select.tsx +162 -18
  158. package/src/Slider/Slider.stories.tsx +24 -0
  159. package/src/Slider/Slider.test.tsx +10 -6
  160. package/src/Slider/Slider.tsx +25 -13
  161. package/src/Stack/Stack.stories.tsx +57 -0
  162. package/src/Stack/Stack.test.tsx +93 -65
  163. package/src/Stack/Stack.tsx +25 -41
  164. package/src/Text/Text.stories.tsx +61 -0
  165. package/src/Text/Text.test.tsx +2 -2
  166. package/src/Text/Text.tsx +25 -14
  167. package/src/Textarea/Textarea.stories.tsx +64 -0
  168. package/src/Textarea/Textarea.test.tsx +11 -8
  169. package/src/Textarea/Textarea.tsx +41 -38
  170. package/src/ValidationMessage/ValidationMessage.stories.tsx +27 -0
  171. package/src/ValidationMessage/ValidationMessage.test.tsx +9 -4
  172. package/src/ValidationMessage/ValidationMessage.tsx +23 -12
  173. package/src/VisuallyHidden/VisuallyHidden.stories.tsx +19 -0
  174. package/src/VisuallyHidden/VisuallyHidden.test.tsx +10 -0
  175. package/src/VisuallyHidden/VisuallyHidden.tsx +1 -0
  176. package/src/VisuallyHidden/index.ts +1 -0
  177. package/src/index.ts +7 -2
  178. package/src/theme.ts +49 -48
  179. package/dist/Box/Box.d.ts +0 -46
  180. package/dist/Box/index.d.ts +0 -1
  181. package/dist/Heading/Heading.d.ts +0 -7
  182. package/dist/Heading/index.d.ts +0 -1
  183. package/dist/Hidden/Hidden.d.ts +0 -5
  184. package/dist/Hidden/index.d.ts +0 -1
  185. package/src/Alert/Alert.stories.mdx +0 -45
  186. package/src/Badge/Badge.stories.mdx +0 -43
  187. package/src/Box/Box.stories.mdx +0 -38
  188. package/src/Box/Box.test.tsx +0 -133
  189. package/src/Box/Box.tsx +0 -157
  190. package/src/Box/index.ts +0 -1
  191. package/src/Button/Button.stories.mdx +0 -176
  192. package/src/Checkbox/Checkbox.stories.mdx +0 -81
  193. package/src/Column/Column.stories.mdx +0 -74
  194. package/src/Columns/Columns.stories.mdx +0 -247
  195. package/src/Container/Container.stories.mdx +0 -36
  196. package/src/Dialog/Dialog.stories.mdx +0 -64
  197. package/src/Divider/Divider.stories.mdx +0 -43
  198. package/src/Field/Field.stories.mdx +0 -57
  199. package/src/Heading/Heading.stories.mdx +0 -91
  200. package/src/Heading/Heading.test.tsx +0 -77
  201. package/src/Heading/Heading.tsx +0 -19
  202. package/src/Heading/index.ts +0 -1
  203. package/src/Hidden/Hidden.stories.mdx +0 -64
  204. package/src/Hidden/Hidden.test.tsx +0 -24
  205. package/src/Hidden/Hidden.tsx +0 -16
  206. package/src/Hidden/index.ts +0 -1
  207. package/src/Image/Image.stories.mdx +0 -40
  208. package/src/Input/Input.stories.mdx +0 -45
  209. package/src/Label/Label.stories.mdx +0 -34
  210. package/src/Link/Link.stories.mdx +0 -38
  211. package/src/Menu/Menu.stories.mdx +0 -49
  212. package/src/MenuItem/MenuItem.stories.mdx +0 -32
  213. package/src/Message/Message.stories.mdx +0 -44
  214. package/src/Radio/Radio.stories.mdx +0 -100
  215. package/src/Select/Select.stories.mdx +0 -44
  216. package/src/Slider/Slider.stories.mdx +0 -58
  217. package/src/Stack/Stack.stories.mdx +0 -105
  218. package/src/Text/Text.stories.mdx +0 -60
  219. package/src/Textarea/Textarea.stories.mdx +0 -65
  220. package/src/ValidationMessage/ValidationMessage.stories.mdx +0 -36
@@ -1,43 +1,154 @@
1
1
  import React from 'react';
2
- import { render, screen } from '@testing-library/react';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
3
  import { Radio } from './Radio';
4
+ import { ThemeProvider } from '@marigold/system';
5
+
6
+ const theme = {
7
+ space: {
8
+ none: 0,
9
+ small: 2,
10
+ },
11
+ colors: {
12
+ disabled: 'gray',
13
+ },
14
+ radio: {
15
+ __default: {
16
+ m: 'small',
17
+ },
18
+ },
19
+ label: {
20
+ above: {
21
+ fontSize: '8px',
22
+ },
23
+ inline: {
24
+ fontSize: '14px',
25
+ },
26
+ },
27
+ };
28
+
29
+ test('supports default labelVariant', () => {
30
+ render(
31
+ <ThemeProvider theme={theme}>
32
+ <Radio id="test" title="checkbox" label="label" />
33
+ </ThemeProvider>
34
+ );
35
+
36
+ const label = screen.getByText(/label/);
37
+ expect(label).toHaveStyle(`font-size: 14px`);
38
+ });
39
+
40
+ test('supports other labelVariant than default', () => {
41
+ render(
42
+ <ThemeProvider theme={theme}>
43
+ <Radio id="test" title="checkbox" label="label" labelVariant="above" />
44
+ </ThemeProvider>
45
+ );
46
+
47
+ const label = screen.getByText(/label/);
48
+ expect(label).toHaveStyle(`font-size: 8px`);
49
+ });
4
50
 
5
51
  test('supports label prop', () => {
6
52
  render(<Radio label="Test" id="test" title="radio" />);
7
- const radio = screen.getByText(/Test/);
8
53
 
9
- expect(radio).toBeDefined();
54
+ const radioLabel = screen.getByText(/Test/);
55
+ expect(radioLabel).toBeDefined();
10
56
  });
11
57
 
12
- test('supports required prop an renders required icon', () => {
58
+ test('supports required prop and renders required icon', () => {
13
59
  render(<Radio label="Test" id="test" required title="radio" />);
14
- const radio = screen.getByText(/Test/);
15
60
 
16
- expect(radio).toContainHTML('path d="M10.8');
61
+ const label = screen.getByText(/Test/);
62
+ expect(label.nextSibling).toContainHTML('path d="M10.8');
17
63
  });
18
64
 
19
65
  test('supports default type', () => {
20
- render(<Radio id="radio" title="radio" />);
21
- const radio = screen.getByTitle(/radio/);
66
+ render(<Radio id="radio" title="radio" label="test" />);
22
67
 
68
+ const radio = screen.getByTitle(/radio/);
23
69
  expect(radio.getAttribute('type')).toEqual('radio');
24
70
  });
25
71
 
26
72
  test('renders <input> element', () => {
27
- render(<Radio id="radio" title="radio" />);
28
- const radio = screen.getByTitle(/radio/);
73
+ render(<Radio id="radio" title="radio" label="test" />);
29
74
 
75
+ const radio = screen.getByTitle(/radio/);
30
76
  expect(radio instanceof HTMLInputElement).toBeTruthy();
31
77
  });
32
78
 
33
- test('renders <SVG> CircleUnchecked element', () => {
34
- render(<Radio id="radio" label="Test" />);
35
- const radio = screen.getByText(/Test/);
36
- expect(radio).toContainHTML('path d="M5.62507');
79
+ test('supports disabled prop', () => {
80
+ render(
81
+ <ThemeProvider theme={theme}>
82
+ <Radio id="test" title="radio" label="label" disabled />
83
+ </ThemeProvider>
84
+ );
85
+
86
+ const radio = screen.getByTitle(/radio/);
87
+ expect(radio).toHaveAttribute('disabled');
88
+ const label = screen.getByText(/label/);
89
+ expect(label).toHaveStyle(`color: gray`);
90
+ });
91
+
92
+ test('supports error and errorMessage prop', () => {
93
+ render(
94
+ <ThemeProvider theme={theme}>
95
+ <Radio id="test" title="radio" label="test" error errorMessage="error" />
96
+ </ThemeProvider>
97
+ );
98
+
99
+ const errorMessage = screen.getByText(/error/);
100
+ expect(errorMessage).toBeDefined();
101
+ });
102
+
103
+ test('supports checked radio', () => {
104
+ render(
105
+ <ThemeProvider theme={theme}>
106
+ <Radio id="test" title="radio" label="test" onChange={() => {}} checked />
107
+ </ThemeProvider>
108
+ );
109
+
110
+ const radio = screen.getByTitle(/radio/);
111
+ expect(radio).toHaveAttribute('checked');
112
+ });
113
+
114
+ test('supports checked and disabled radio', () => {
115
+ render(
116
+ <ThemeProvider theme={theme}>
117
+ <Radio
118
+ id="test"
119
+ title="radio"
120
+ label="test"
121
+ onChange={() => {}}
122
+ checked
123
+ disabled
124
+ />
125
+ </ThemeProvider>
126
+ );
127
+
128
+ const radio = screen.getByTitle(/radio/);
129
+ expect(radio).toHaveAttribute('checked');
130
+ expect(radio).toHaveAttribute('disabled');
37
131
  });
38
132
 
39
- test('renders <SVG> CircleChecked element', () => {
40
- render(<Radio id="radio" checked onChange={() => {}} label="Test" />);
41
- const radio = screen.getByText(/Test/);
42
- expect(radio).toContainHTML('path d="M12');
133
+ test('correctly handles interaction', () => {
134
+ const click = jest.fn();
135
+ const change = jest.fn();
136
+
137
+ render(
138
+ <ThemeProvider theme={theme}>
139
+ <Radio
140
+ id="test"
141
+ title="radio"
142
+ label="Test"
143
+ onClick={click}
144
+ onChange={change}
145
+ />
146
+ </ThemeProvider>
147
+ );
148
+
149
+ const radio = screen.getByTitle(/radio/);
150
+ fireEvent.click(radio);
151
+
152
+ expect(click).toHaveBeenCalledTimes(1);
153
+ expect(change).toHaveBeenCalledTimes(1);
43
154
  });
@@ -1,74 +1,44 @@
1
1
  import React from 'react';
2
- import { CircleUnchecked, CircleChecked, Required } from '@marigold/icons';
3
- import { useStyles } from '@marigold/system';
4
2
  import { ComponentProps } from '@marigold/types';
3
+ import { Exclamation } from '@marigold/icons';
4
+ import { useFocusRing } from '@react-aria/focus';
5
+ import { VisuallyHidden } from '@react-aria/visually-hidden';
6
+
7
+ import { RadioIcon, RadioIconProps } from './RadioIcon';
5
8
  import { Box } from '../Box';
6
9
  import { Label } from '../Label';
10
+ import { ValidationMessage } from '../ValidationMessage';
7
11
 
8
- // Radio Icon
12
+ // Theme Extension
9
13
  // ---------------
10
- type RadioIconProps = {
11
- className?: string;
12
- variant?: string;
13
- checked?: boolean;
14
- children?: never;
15
- };
16
-
17
- const RadioIcon: React.FC<RadioIconProps> = ({
18
- className,
19
- variant,
20
- checked,
21
- }) => {
22
- const radioIconStyle = useStyles({
23
- variant: `form.${variant}`,
24
- css: {
25
- ariaHidden: 'true',
26
- mr: 2,
27
- verticalAlign: 'middle',
28
- ':hover': { cursor: 'pointer' },
29
- 'input:disabled ~ &': {
30
- color: 'muted',
31
- cursor: 'not-allowed',
32
- },
33
- },
34
- className,
35
- });
36
-
37
- if (checked) {
38
- return <CircleChecked className={radioIconStyle} />;
39
- }
40
- return <CircleUnchecked className={radioIconStyle} />;
41
- };
14
+ export interface RadioThemeExtension<Value> {
15
+ radio?: {
16
+ [key: string]: Value;
17
+ };
18
+ }
42
19
 
43
20
  // Radio Input
44
21
  // ---------------
45
- type RadioInputProps = {
46
- variant?: string;
47
- } & ComponentProps<'input'>;
22
+ type RadioInputProps = RadioIconProps & ComponentProps<'input'>;
48
23
 
49
- const RadioInput: React.FC<RadioInputProps> = ({
50
- className,
51
- variant = 'radio',
52
- ...props
53
- }) => {
54
- const radioStyle = useStyles({
55
- css: {
56
- position: 'absolute',
57
- opacity: 0,
58
- zIndex: -1,
59
- width: 1,
60
- height: 1,
61
- overflow: 'hidden',
62
- },
63
- });
24
+ const RadioInput: React.FC<RadioInputProps> = ({ error, ...props }) => {
25
+ const { focusProps } = useFocusRing();
64
26
 
65
27
  return (
66
- <Box display="inline-block">
67
- <input type="radio" className={radioStyle} {...props} />
28
+ <Box pr="xsmall">
29
+ <VisuallyHidden>
30
+ <input
31
+ type="radio"
32
+ disabled={props.disabled}
33
+ {...focusProps}
34
+ {...props}
35
+ />
36
+ </VisuallyHidden>
68
37
  <RadioIcon
38
+ variant={props.variant}
39
+ disabled={props.disabled}
69
40
  checked={props.checked}
70
- className={className}
71
- variant={variant}
41
+ error={error}
72
42
  />
73
43
  </Box>
74
44
  );
@@ -78,20 +48,41 @@ const RadioInput: React.FC<RadioInputProps> = ({
78
48
  // ---------------
79
49
  export type RadioProps = {
80
50
  id: string;
81
- label?: string;
51
+ label: string;
82
52
  required?: boolean;
53
+ labelVariant?: string;
54
+ error?: boolean;
55
+ errorMessage?: string;
83
56
  } & RadioInputProps;
84
57
 
85
- export const Radio: React.FC<RadioProps> = ({ label, required, ...props }) => {
86
- if (label) {
87
- return (
88
- <Label htmlFor={props.id}>
89
- <RadioInput {...props} />
90
- {label}
91
- {required && <Required size={16} />}
92
- </Label>
93
- );
94
- }
95
-
96
- return <RadioInput {...props} />;
97
- };
58
+ export const Radio: React.FC<RadioProps> = ({
59
+ label,
60
+ required,
61
+ labelVariant = 'inline',
62
+ error,
63
+ errorMessage,
64
+ ...props
65
+ }) => (
66
+ <>
67
+ <Box
68
+ as={Label}
69
+ htmlFor={props.id}
70
+ required={required}
71
+ variant={`label.${labelVariant}`}
72
+ css={
73
+ props.disabled
74
+ ? { color: 'disabled', ':hover': { cursor: 'not-allowed' } }
75
+ : { color: 'text', ':hover': { cursor: 'pointer' } }
76
+ }
77
+ >
78
+ <Box as={RadioInput} error={error} {...props} />
79
+ {label}
80
+ </Box>
81
+ {error && errorMessage && (
82
+ <ValidationMessage>
83
+ <Exclamation size={16} />
84
+ {errorMessage}
85
+ </ValidationMessage>
86
+ )}
87
+ </>
88
+ );
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { conditional, State, SVG } from '@marigold/system';
3
+
4
+ import { Box } from '../Box';
5
+
6
+ // Radio Icon
7
+ // ---------------
8
+ export type RadioIconProps = {
9
+ variant?: string;
10
+ checked?: boolean;
11
+ disabled?: boolean;
12
+ error?: boolean;
13
+ children?: never;
14
+ };
15
+
16
+ export const RadioIcon: React.FC<RadioIconProps> = ({
17
+ variant = '',
18
+ checked = false,
19
+ disabled = false,
20
+ error = false,
21
+ }) => {
22
+ const conditionalStates: State = disabled
23
+ ? {
24
+ disabled: disabled,
25
+ }
26
+ : {
27
+ checked: checked,
28
+ error: error,
29
+ };
30
+
31
+ return (
32
+ <SVG
33
+ width="16"
34
+ height="32"
35
+ viewBox="0 0 16 32"
36
+ fill="none"
37
+ aria-hidden="true"
38
+ >
39
+ <Box
40
+ variant={conditional(`radio.${variant}`, conditionalStates)}
41
+ as="circle"
42
+ cx="8"
43
+ cy="16"
44
+ r="7.5"
45
+ />
46
+ {checked && <circle fill="white" cx="8" cy="16" r="3" />}
47
+ </SVG>
48
+ );
49
+ };
@@ -0,0 +1,40 @@
1
+ import React, { useRef } from 'react';
2
+ import { useListBox } from '@react-aria/listbox';
3
+ import type { AriaListBoxOptions } from '@react-aria/listbox';
4
+ import type { ListState } from '@react-stately/list';
5
+
6
+ import { Box } from '../Box';
7
+ import { Option } from './Option';
8
+ import { ListBoxSection } from './ListBoxSection';
9
+
10
+ interface ListBoxProps extends AriaListBoxOptions<unknown> {
11
+ state: ListState<unknown>;
12
+ error?: boolean;
13
+ }
14
+
15
+ export const ListBox = (props: ListBoxProps) => {
16
+ const ref = useRef<HTMLUListElement>(null);
17
+ const { state, error } = props;
18
+ const { listBoxProps } = useListBox(props, state, ref);
19
+
20
+ return (
21
+ <Box
22
+ as="ul"
23
+ p="none"
24
+ css={{
25
+ listStyle: 'none',
26
+ }}
27
+ {...listBoxProps}
28
+ variant={error ? 'select.listbox.error' : 'select.listbox'}
29
+ ref={ref}
30
+ >
31
+ {[...state.collection].map(item =>
32
+ item.type === 'section' ? (
33
+ <ListBoxSection key={item.key} section={item} state={state} />
34
+ ) : (
35
+ <Option key={item.key} item={item} state={state} />
36
+ )
37
+ )}
38
+ </Box>
39
+ );
40
+ };
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { useListBoxSection } from '@react-aria/listbox';
3
+ import type { ListState } from '@react-stately/list';
4
+ import type { Node } from '@react-types/shared';
5
+
6
+ import { Box } from '../Box';
7
+ import { Option } from './Option';
8
+
9
+ interface SectionProps {
10
+ section: Node<unknown>;
11
+ state: ListState<unknown>;
12
+ }
13
+
14
+ export const ListBoxSection = ({ section, state }: SectionProps) => {
15
+ const { itemProps, headingProps, groupProps } = useListBoxSection({
16
+ heading: section.rendered,
17
+ 'aria-label': section['aria-label'],
18
+ });
19
+
20
+ return (
21
+ <Box
22
+ as="li"
23
+ {...itemProps}
24
+ css={{
25
+ cursor: 'not-allowed',
26
+ }}
27
+ >
28
+ {section.rendered && (
29
+ <Box as="span" {...headingProps} variant={'select.section'}>
30
+ {section.rendered}
31
+ </Box>
32
+ )}
33
+ <Box as="ul" {...groupProps}>
34
+ {[...section.childNodes].map(node => (
35
+ <Option key={node.key} item={node} state={state} />
36
+ ))}
37
+ </Box>
38
+ </Box>
39
+ );
40
+ };
@@ -0,0 +1,48 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import type { ListState } from '@react-stately/list';
3
+ import type { Node } from '@react-types/shared';
4
+ import { useOption } from '@react-aria/listbox';
5
+
6
+ import { Box } from '../Box';
7
+
8
+ interface OptionProps {
9
+ item: Node<unknown>;
10
+ state: ListState<unknown>;
11
+ }
12
+
13
+ export const Option = ({ item, state }: OptionProps) => {
14
+ const ref = useRef<HTMLLIElement>(null);
15
+ const [disabled, setDisabled] = useState(false);
16
+ const { optionProps, isSelected } = useOption(
17
+ {
18
+ key: item.key,
19
+ },
20
+ state,
21
+ ref
22
+ );
23
+
24
+ useEffect(() => {
25
+ for (const key of state.disabledKeys.values()) {
26
+ if (key === item.key) {
27
+ setDisabled(true);
28
+ }
29
+ }
30
+ }, [state.disabledKeys, item.key]);
31
+
32
+ return (
33
+ <Box
34
+ as="li"
35
+ {...optionProps}
36
+ ref={ref}
37
+ variant={
38
+ isSelected
39
+ ? 'select.option.selected'
40
+ : disabled
41
+ ? 'select.option.disabled'
42
+ : 'select.option'
43
+ }
44
+ >
45
+ {item.rendered}
46
+ </Box>
47
+ );
48
+ };
@@ -0,0 +1,50 @@
1
+ import React, { forwardRef, RefObject } from 'react';
2
+ import { FocusScope } from '@react-aria/focus';
3
+ import {
4
+ DismissButton,
5
+ OverlayContainer,
6
+ useModal,
7
+ useOverlay,
8
+ } from '@react-aria/overlays';
9
+ import { mergeProps } from '@react-aria/utils';
10
+
11
+ import { Box } from '../Box';
12
+
13
+ interface PopoverProps {
14
+ isOpen?: boolean;
15
+ onClose?: () => void;
16
+ ref?: React.Ref<HTMLDivElement>;
17
+ className?: string;
18
+ }
19
+
20
+ export const Popover: React.FC<PopoverProps> = forwardRef(
21
+ ({ children, className, isOpen, onClose, ...otherProps }, ref) => {
22
+ // Handle events that should cause the popup to close,
23
+ const { overlayProps } = useOverlay(
24
+ {
25
+ isOpen,
26
+ onClose,
27
+ shouldCloseOnBlur: true,
28
+ isDismissable: true,
29
+ },
30
+ ref as RefObject<HTMLElement>
31
+ );
32
+ // Hide content outside the modal from screen readers.
33
+ const { modalProps } = useModal();
34
+
35
+ return (
36
+ <OverlayContainer>
37
+ <FocusScope restoreFocus>
38
+ <Box
39
+ {...mergeProps(overlayProps, otherProps, modalProps)}
40
+ className={className}
41
+ ref={ref}
42
+ >
43
+ {children}
44
+ <DismissButton onDismiss={onClose} />
45
+ </Box>
46
+ </FocusScope>
47
+ </OverlayContainer>
48
+ );
49
+ }
50
+ );
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import type { Meta, ComponentStory } from '@storybook/react';
3
+ import { Select } from './Select';
4
+ import { Item } from '@marigold/components';
5
+
6
+ export default {
7
+ title: 'Components/Select',
8
+ argTypes: {
9
+ label: {
10
+ control: {
11
+ type: 'text',
12
+ },
13
+ table: {
14
+ defaultValue: {
15
+ summary: 'Select label',
16
+ },
17
+ },
18
+ defaultValue: 'Favorite Color',
19
+ },
20
+ placeholder: {
21
+ control: {
22
+ type: 'text',
23
+ },
24
+ table: {
25
+ defaultValue: {
26
+ summary: 'Select an option',
27
+ },
28
+ },
29
+ },
30
+ disabled: {
31
+ control: {
32
+ type: 'boolean',
33
+ },
34
+ options: [true, false],
35
+ table: {
36
+ defaultValue: {
37
+ summary: false,
38
+ },
39
+ },
40
+ },
41
+ required: {
42
+ control: {
43
+ type: 'boolean',
44
+ },
45
+ options: [true, false],
46
+ table: {
47
+ defaultValue: {
48
+ summary: false,
49
+ },
50
+ },
51
+ },
52
+ error: {
53
+ control: {
54
+ type: 'boolean',
55
+ },
56
+ description: 'Error',
57
+ table: {
58
+ defaultValue: {
59
+ summary: false,
60
+ },
61
+ },
62
+ },
63
+ errorMessage: {
64
+ control: {
65
+ type: 'text',
66
+ },
67
+ description: 'Error Message',
68
+ },
69
+ width: {
70
+ control: 'number',
71
+ },
72
+ },
73
+ } as Meta;
74
+
75
+ export const Basic: ComponentStory<typeof Select> = args => (
76
+ <Select {...args}>
77
+ <Item>Red</Item>
78
+ <Item>Orange</Item>
79
+ <Item>Yellow</Item>
80
+ </Select>
81
+ );