@indico-data/design-system 2.11.0 → 2.12.1

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.
@@ -0,0 +1 @@
1
+ export { Form } from './Form';
@@ -0,0 +1,6 @@
1
+ import { Props as ReactSelectProps } from 'react-select';
2
+ import { SelectOption } from './types';
3
+ export interface SelectProps<OptionType extends SelectOption> extends ReactSelectProps<OptionType> {
4
+ options: OptionType[];
5
+ }
6
+ export declare const Select: <OptionType extends SelectOption>({ classNamePrefix, className, components: customComponents, ...props }: SelectProps<OptionType>) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { SelectProps } from './Select';
3
+ import { SelectOption } from './types';
4
+ declare const meta: Meta<SelectProps<SelectOption>>;
5
+ export default meta;
6
+ type Story = StoryObj<SelectProps<SelectOption>>;
7
+ export declare const Default: Story;
@@ -0,0 +1 @@
1
+ export { Select } from './Select';
@@ -0,0 +1,6 @@
1
+ export type SelectOption = {
2
+ label: string;
3
+ value: string;
4
+ detail?: string;
5
+ [key: string]: any;
6
+ };
@@ -8,3 +8,5 @@ export { Checkbox } from './forms/checkbox';
8
8
  export { Toggle } from './forms/toggle';
9
9
  export { Textarea } from './forms/textarea';
10
10
  export { PasswordInput } from './forms/passwordInput';
11
+ export { Select } from './forms/select';
12
+ export { Form } from './forms/form';
@@ -13,3 +13,5 @@ export { Checkbox } from './components/forms/checkbox';
13
13
  export { Toggle as ToggleInput } from './components/forms/toggle';
14
14
  export { Textarea } from './components/forms/textarea';
15
15
  export { PasswordInput } from './components/forms/passwordInput';
16
+ export { Select as SelectInput } from './components/forms/select';
17
+ export { Form } from './components/forms/form';
@@ -10,3 +10,5 @@ import { IconName } from '../build/generated/iconTypes';
10
10
  export type { IconName };
11
11
  import { IconSizes } from './components/icons/types';
12
12
  export type { IconSizes };
13
+ import { SelectOption } from './components/forms/select/types';
14
+ export type { SelectOption };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indico-data/design-system",
3
- "version": "2.11.0",
3
+ "version": "2.12.1",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "main": "lib/index.js",
@@ -103,7 +103,7 @@ const {
103
103
  <div className="radio-group mb-5">
104
104
  <h2 className="mb-4">Select a class</h2>
105
105
  <Radio label="Rogue" value="Rogue" id="rogue" {...register('rpgClass')} />
106
- <Radio label="Warrior" value="Warruir" id="warrior" {...register('rpgClass')} />
106
+ <Radio label="Warrior" value="Warrior" id="warrior" {...register('rpgClass')} />
107
107
  <Radio label="Mage" value="Mage" id="mage" {...register('rpgClass')} />
108
108
  <Radio label="Monk" value="Monk" id="monk" {...register('rpgClass')} />
109
109
  </div>
@@ -257,7 +257,7 @@ export const Uncontrolled: Story = {
257
257
  <div className="radio-group mb-5">
258
258
  <h2 className="mb-4">Select a class</h2>
259
259
  <Radio label="Rogue" value="Rogue" id="rogue" {...register('rpgClass')} />
260
- <Radio label="Warrior" value="Warruir" id="warrior" {...register('rpgClass')} />
260
+ <Radio label="Warrior" value="Warrior" id="warrior" {...register('rpgClass')} />
261
261
  <Radio label="Mage" value="Mage" id="mage" {...register('rpgClass')} />
262
262
  <Radio label="Monk" value="Monk" id="monk" {...register('rpgClass')} />
263
263
  </div>
@@ -383,7 +383,7 @@ export const UncontrolledPreFilled: Story = {
383
383
  <Radio label="Rogue" value="Rogue" id="rogue" {...register('rpgClass')} />
384
384
  <Radio
385
385
  label="Warrior"
386
- value="Warruir"
386
+ value="Warrior"
387
387
  id="warrior"
388
388
  {...register('rpgClass')}
389
389
  defaultChecked
@@ -0,0 +1 @@
1
+ export { Form } from './Form';
@@ -60,7 +60,7 @@ const meta: Meta = {
60
60
  },
61
61
  isRequired: {
62
62
  control: 'boolean',
63
- description: 'Toggles the required astherisc on the label',
63
+ description: 'Toggles the required asterisk on the label',
64
64
  table: {
65
65
  category: 'Props',
66
66
  type: {
@@ -80,16 +80,16 @@ const meta: Meta = {
80
80
  },
81
81
  defaultValue: { summary: 'false' },
82
82
  },
83
- errorList: {
83
+ errorMessage: {
84
84
  control: false,
85
- description: 'An array of error messages',
85
+ description: 'Error message',
86
86
  table: {
87
87
  category: 'Props',
88
88
  type: {
89
- summary: 'string[]',
89
+ summary: 'string',
90
90
  },
91
91
  },
92
- defaultValue: { summary: '[]' },
92
+ defaultValue: { summary: '' },
93
93
  },
94
94
  helpText: {
95
95
  control: 'text',
@@ -164,14 +164,14 @@ export const Default: Story = {
164
164
  placeholder: 'Please enter a value',
165
165
  hasHiddenLabel: false,
166
166
  isDisabled: false,
167
- errorList: [],
167
+ errorMessage: '',
168
168
  value: '',
169
169
  },
170
170
  render: (args) => {
171
171
  const [value, setValue] = useState('');
172
172
 
173
173
  useEffect(() => {
174
- setValue(args.value);
174
+ setValue(args.value || '');
175
175
  }, [args.value]);
176
176
 
177
177
  const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
@@ -184,13 +184,13 @@ export const Default: Story = {
184
184
  export const Errors: Story = {
185
185
  args: {
186
186
  ...defaultArgs,
187
- errorList: ['You require a username value.'],
187
+ errorMessage: 'You require a username value.',
188
188
  },
189
189
  render: (args) => {
190
190
  const [value, setValue] = useState('');
191
191
 
192
192
  useEffect(() => {
193
- setValue(args.value);
193
+ setValue(args.value || '');
194
194
  }, [args.value]);
195
195
 
196
196
  const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
@@ -210,7 +210,7 @@ export const HiddenLabel: Story = {
210
210
  const [value, setValue] = useState('');
211
211
 
212
212
  useEffect(() => {
213
- setValue(args.value);
213
+ setValue(args.value || '');
214
214
  }, [args.value]);
215
215
 
216
216
  const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
@@ -230,7 +230,7 @@ export const HelpText: Story = {
230
230
  const [value, setValue] = useState('');
231
231
 
232
232
  useEffect(() => {
233
- setValue(args.value);
233
+ setValue(args.value || '');
234
234
  }, [args.value]);
235
235
 
236
236
  const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
@@ -250,7 +250,7 @@ export const Clearable: Story = {
250
250
  const [value, setValue] = useState('');
251
251
 
252
252
  useEffect(() => {
253
- setValue(args.value);
253
+ setValue(args.value || '');
254
254
  }, [args.value]);
255
255
 
256
256
  const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
@@ -270,7 +270,7 @@ export const Icon: Story = {
270
270
  const [value, setValue] = useState('');
271
271
 
272
272
  useEffect(() => {
273
- setValue(args.value);
273
+ setValue(args.value || '');
274
274
  }, [args.value]);
275
275
 
276
276
  const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
@@ -290,7 +290,7 @@ export const Required: Story = {
290
290
  const [value, setValue] = useState('');
291
291
 
292
292
  useEffect(() => {
293
- setValue(args.value);
293
+ setValue(args.value || '');
294
294
  }, [args.value]);
295
295
 
296
296
  const handleChange = (e: { target: { value: SetStateAction<string> } }) => {
@@ -0,0 +1,118 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { Select, SelectProps } from './Select';
3
+ import { SelectOption } from './types';
4
+
5
+ const meta: Meta<SelectProps<SelectOption>> = {
6
+ title: 'Forms/Select',
7
+ component: Select,
8
+ argTypes: {
9
+ options: {
10
+ control: 'object',
11
+ description: 'Options for the select component',
12
+ table: {
13
+ category: 'Props',
14
+ type: {
15
+ summary: '{ value: string, label: string, detail?: string }[]',
16
+ },
17
+ },
18
+ defaultValue: { summary: '[]' },
19
+ },
20
+ isDisabled: {
21
+ control: 'boolean',
22
+ description: 'Toggles the disabled state of the select component',
23
+ table: {
24
+ category: 'Props',
25
+ type: {
26
+ summary: 'boolean',
27
+ },
28
+ },
29
+ defaultValue: { summary: 'false' },
30
+ },
31
+ isLoading: {
32
+ control: 'boolean',
33
+ description: 'Toggles the loading state of the select component',
34
+ table: {
35
+ category: 'Props',
36
+ type: {
37
+ summary: 'boolean',
38
+ },
39
+ },
40
+ defaultValue: { summary: 'false' },
41
+ },
42
+ isClearable: {
43
+ control: 'boolean',
44
+ description: 'Enables the clearable feature of the select component',
45
+ table: {
46
+ category: 'Props',
47
+ type: {
48
+ summary: 'boolean',
49
+ },
50
+ },
51
+ defaultValue: { summary: 'false' },
52
+ },
53
+ isSearchable: {
54
+ control: 'boolean',
55
+ description: 'Enables the searchable feature of the select component',
56
+ table: {
57
+ category: 'Props',
58
+ type: {
59
+ summary: 'boolean',
60
+ },
61
+ },
62
+ defaultValue: { summary: 'true' },
63
+ },
64
+ placeholder: {
65
+ control: 'text',
66
+ description: 'The placeholder text for the select component',
67
+ table: {
68
+ category: 'Props',
69
+ type: {
70
+ summary: 'string',
71
+ },
72
+ },
73
+ defaultValue: { summary: 'Select...' },
74
+ },
75
+ className: {
76
+ control: 'text',
77
+ description: 'Additional CSS class for the select component',
78
+ table: {
79
+ category: 'Props',
80
+ type: {
81
+ summary: 'string',
82
+ },
83
+ },
84
+ defaultValue: { summary: '' },
85
+ },
86
+ onChange: {
87
+ control: false,
88
+ description: 'Event handler for when the selected value changes',
89
+ table: {
90
+ category: 'Callbacks',
91
+ type: {
92
+ summary:
93
+ '(newValue: SingleValue<SelectOption> | MultiValue<SelectOption>, actionMeta: ActionMeta<SelectOption>) => void',
94
+ },
95
+ },
96
+ action: 'onChange',
97
+ },
98
+ },
99
+ };
100
+
101
+ export default meta;
102
+
103
+ type Story = StoryObj<SelectProps<SelectOption>>;
104
+
105
+ export const Default: Story = {
106
+ args: {
107
+ options: [
108
+ { value: 'option1', label: 'Option 1', detail: '123 Count' },
109
+ { value: 'option2', label: 'Option 2', detail: '456 Count' },
110
+ { value: 'option3', label: 'Option 3', detail: '789 Count' },
111
+ ],
112
+ placeholder: 'Select an option...',
113
+ isClearable: false,
114
+ isSearchable: true,
115
+ isDisabled: false,
116
+ isLoading: false,
117
+ },
118
+ };
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import classNames from 'classnames';
3
+ import ReactSelect, { Props as ReactSelectProps, components, OptionProps } from 'react-select';
4
+ import { SelectOption } from './types';
5
+
6
+ export interface SelectProps<OptionType extends SelectOption> extends ReactSelectProps<OptionType> {
7
+ options: OptionType[];
8
+ }
9
+
10
+ const OptionComponent = <OptionType extends SelectOption>({
11
+ ...props
12
+ }: OptionProps<OptionType>) => {
13
+ return (
14
+ <components.Option {...props}>
15
+ <div className="select__items">
16
+ <div className="select__item-value">{props?.data?.label}</div>
17
+ {props?.data?.detail && <div className="select__item-detail">{props?.data?.detail}</div>}
18
+ </div>
19
+ </components.Option>
20
+ );
21
+ };
22
+
23
+ export const Select = <OptionType extends SelectOption>({
24
+ classNamePrefix = 'select',
25
+ className,
26
+ components: customComponents,
27
+ ...props
28
+ }: SelectProps<OptionType>) => {
29
+ const defaultComponents = {
30
+ Option: OptionComponent as React.ComponentType<OptionProps<OptionType>>,
31
+ };
32
+
33
+ const mergedComponents = { ...defaultComponents, ...customComponents };
34
+
35
+ return (
36
+ <ReactSelect
37
+ classNamePrefix={classNamePrefix}
38
+ className={classNames('select-wrapper', className)}
39
+ components={mergedComponents}
40
+ {...props}
41
+ />
42
+ );
43
+ };
@@ -0,0 +1,67 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+
3
+ import { Select } from '../Select';
4
+ import { SelectOption } from '../types';
5
+ import { OptionProps, components } from 'react-select';
6
+
7
+ const options: SelectOption[] = [
8
+ { value: 'option1', label: 'Option 1' },
9
+ { value: 'option2', label: 'Option 2' },
10
+ { value: 'option3', label: 'Option 3' },
11
+ ];
12
+
13
+ describe('Select Component', () => {
14
+ it('displays options correctly', () => {
15
+ render(<Select options={options} menuIsOpen />);
16
+ options.forEach((option) => {
17
+ expect(screen.getByText(option.label)).toBeInTheDocument();
18
+ });
19
+ });
20
+
21
+ it('handles onChange event', () => {
22
+ const handleChange = jest.fn();
23
+ render(<Select options={options} menuIsOpen onChange={handleChange} />);
24
+ const option = screen.getByText('Option 1');
25
+ fireEvent.click(option);
26
+ expect(handleChange).toHaveBeenCalledWith(options[0], expect.any(Object));
27
+ });
28
+
29
+ it('applies custom class names', () => {
30
+ const customClassName = 'custom-select-class';
31
+ render(<Select options={options} className={customClassName} />);
32
+ const selectWrapper = screen.getByRole('combobox').closest('.select-wrapper');
33
+ expect(selectWrapper).toHaveClass(customClassName);
34
+ });
35
+
36
+ it('applies custom class name prefix', () => {
37
+ const customClassNamePrefix = 'custom-prefix';
38
+ render(<Select options={options} classNamePrefix={customClassNamePrefix} />);
39
+ const selectWrapper = screen.getByRole('combobox');
40
+ expect(selectWrapper).toHaveClass(`${customClassNamePrefix}__input`);
41
+ });
42
+
43
+ it('displays detail when present', () => {
44
+ const optionsWithDetail = options.map((option, index) => ({
45
+ ...option,
46
+ detail: `${index + 1}23 Count`,
47
+ }));
48
+
49
+ render(<Select options={optionsWithDetail} menuIsOpen />);
50
+ optionsWithDetail.forEach((option) => {
51
+ expect(screen.getByText(option.detail)).toBeInTheDocument();
52
+ });
53
+ });
54
+
55
+ it('uses custom components', () => {
56
+ const oneOption = options.slice(0, 1);
57
+ const CustomOptionComponent = (props: OptionProps<SelectOption>) => (
58
+ <div data-testid="custom-option">{props.data.label}</div>
59
+ );
60
+
61
+ render(
62
+ <Select options={oneOption} components={{ Option: CustomOptionComponent }} menuIsOpen />,
63
+ );
64
+
65
+ expect(screen.getByTestId('custom-option')).toHaveTextContent(oneOption[0].label);
66
+ });
67
+ });
@@ -0,0 +1 @@
1
+ export { Select } from './Select';
@@ -0,0 +1,120 @@
1
+ // Common Variables
2
+ :root,
3
+ :root [data-theme='light'] {
4
+ --pf-select-background-color: var(--pf-white-color);
5
+ --pf-select-border-color: var(--pf-gray-color);
6
+ --pf-select-text-color: var(--pf-gray-color);
7
+ --pf-select-indicator-color: var(--pf-gray-color);
8
+ --pf-select-placeholder-text-color: var(--pf-gray-color);
9
+ --pf-select-hover-color: var(--pf-gray-color);
10
+ --pf-select-option-selected-color: var(--pf-primary-color-200);
11
+ --pf-select-option-text-color: var(--pf-gray-color);
12
+ --pf-select-option-hover-color: var(--pf-primary-color-100);
13
+
14
+ // Disabled
15
+ --pf-select-disabled-background-color: var(--pf-gray-color-100);
16
+ --pf-select-disabled-color: var(--pf-gray-color-400);
17
+ }
18
+
19
+ // Dark Theme Specific Variables
20
+ :root [data-theme='dark'] {
21
+ --pf-select-background-color: var(--pf-primary-color);
22
+ --pf-select-border-color: var(--pf-gray-color-100);
23
+ --pf-select-text-color: var(--pf-gray-color-100);
24
+ --pf-select-indicator-color: var(--pf-gray-color-100);
25
+ --pf-select-placeholder-text-color: var(--pf-gray-color-100);
26
+ --pf-select-hover-color: var(--pf-gray-color-100);
27
+ --pf-select-option-selected-color: var(--pf-primary-color-200);
28
+ --pf-select-option-text-color: var(--pf-gray-color-100);
29
+ --pf-select-option-hover-color: var(--pf-primary-color-300);
30
+
31
+ // Disabled
32
+ --pf-select-disabled-background-color: var(--pf-primary-color-200);
33
+ --pf-select-disabled-color: var(--pf-gray-color-400);
34
+ }
35
+
36
+ .select-wrapper {
37
+ .select__ {
38
+ &control {
39
+ background-color: var(--pf-select-background-color);
40
+ border: 1px solid var(--pf-select-border-color);
41
+ color: var(--pf-select-text-color);
42
+ box-shadow: none;
43
+
44
+ &:hover,
45
+ &:focus,
46
+ &--is-focused {
47
+ border: 1px solid var(--pf-select-hover-color);
48
+ }
49
+
50
+ &--is-disabled {
51
+ background-color: var(--pf-select-disabled-background-color);
52
+ color: var(--pf-select-disabled-color);
53
+ }
54
+ }
55
+
56
+ &placeholder {
57
+ color: var(--pf-select-placeholder-text-color);
58
+ }
59
+
60
+ &value-container {
61
+ cursor: text;
62
+ }
63
+
64
+ &input-container {
65
+ color: var(--pf-select-text-color);
66
+ }
67
+
68
+ &menu {
69
+ border: 1px solid var(--pf-select-border-color);
70
+ box-shadow: 0px 1px 8px 0px rgba(0, 0, 0, 0.4);
71
+ margin: 4px 0px;
72
+ background-color: var(--pf-select-background-color);
73
+ }
74
+
75
+ &menu-notice {
76
+ color: var(--pf-select-text-color);
77
+ }
78
+
79
+ &indicator-separator {
80
+ background-color: var(--pf-select-indicator-color);
81
+ }
82
+
83
+ &indicator {
84
+ cursor: pointer;
85
+ color: var(--pf-select-indicator-color);
86
+
87
+ &:hover,
88
+ &:focus {
89
+ color: var(--pf-select-hover-color);
90
+ }
91
+ }
92
+
93
+ &items {
94
+ display: flex;
95
+ align-items: center;
96
+ }
97
+
98
+ &item-detail {
99
+ margin-left: auto;
100
+ }
101
+
102
+ &option {
103
+ cursor: pointer;
104
+ overflow-wrap: break-word;
105
+ color: var(--pf-select-option-text-color);
106
+ &--is-focused,
107
+ &:active {
108
+ background: var(--pf-select-option-hover-color);
109
+ }
110
+
111
+ &--is-selected {
112
+ background: var(--pf-select-option-selected-color);
113
+ }
114
+ }
115
+
116
+ &single-value {
117
+ color: var(--pf-select-option-text-color);
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,6 @@
1
+ export type SelectOption = {
2
+ label: string;
3
+ value: string;
4
+ detail?: string;
5
+ [key: string]: any; // Allow for additional properties
6
+ };
@@ -8,3 +8,5 @@ export { Checkbox } from './forms/checkbox';
8
8
  export { Toggle } from './forms/toggle';
9
9
  export { Textarea } from './forms/textarea';
10
10
  export { PasswordInput } from './forms/passwordInput';
11
+ export { Select } from './forms/select';
12
+ export { Form } from './forms/form';
package/src/index.ts CHANGED
@@ -67,3 +67,5 @@ export { Checkbox } from './components/forms/checkbox';
67
67
  export { Toggle as ToggleInput } from './components/forms/toggle';
68
68
  export { Textarea } from './components/forms/textarea';
69
69
  export { PasswordInput } from './components/forms/passwordInput';
70
+ export { Select as SelectInput } from './components/forms/select';
71
+ export { Form } from './components/forms/form';
@@ -12,8 +12,9 @@
12
12
  @import '../components/forms/textarea/styles/Textarea.scss';
13
13
  @import '../components/forms/passwordInput/styles/PasswordInput.scss';
14
14
  @import '../components/forms/form/styles/Form.scss';
15
-
15
+ @import '../components/forms/select/styles/Select.scss';
16
16
  @import '../components/forms/toggle/styles/Toggle.scss';
17
+
17
18
  @import 'typography';
18
19
  @import 'colors';
19
20
  @import 'borders';
package/src/types.ts CHANGED
@@ -13,3 +13,6 @@ export type { IconName };
13
13
 
14
14
  import { IconSizes } from './components/icons/types';
15
15
  export type { IconSizes };
16
+
17
+ import { SelectOption } from './components/forms/select/types';
18
+ export type { SelectOption };