Skip to content

Commit 910d43f

Browse files
authored
Merge pull request #99 from toggle-corp/feature/dropdown-actions
add(select-inputs): add action container to options
2 parents 5e1dd92 + edad47e commit 910d43f

9 files changed

Lines changed: 77 additions & 86 deletions

File tree

src/components/GenericOption/index.tsx

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import React, { useCallback, useRef, useEffect } from 'react';
2-
import { _cs } from '@togglecorp/fujs';
2+
import {
3+
_cs,
4+
isNotDefined,
5+
} from '@togglecorp/fujs';
36

47
import RawButton from '../RawButton';
58

@@ -15,6 +18,7 @@ export interface GenericOptionParams<P extends ContentBaseProps, OK extends Opti
1518
optionContainerClassName?: string;
1619
contentRenderer: (props: Pick<P, Exclude<keyof P, 'containerClassName' | 'title'>>) => React.ReactNode;
1720
contentRendererParam: (key: OK, opt: O) => P;
21+
actionsSelector?: (props: O) => React.ReactNode;
1822
option: O;
1923
optionKey: OK;
2024
onClick: (optionKey: OK, option: O) => void;
@@ -25,6 +29,7 @@ function GenericOption<P extends ContentBaseProps, OK extends OptionKey, O>({
2529
optionContainerClassName,
2630
contentRenderer,
2731
contentRendererParam,
32+
actionsSelector,
2833
option,
2934
onClick,
3035
onFocus,
@@ -81,23 +86,41 @@ function GenericOption<P extends ContentBaseProps, OK extends OptionKey, O>({
8186
[],
8287
);
8388

89+
if (isNotDefined(actionsSelector)) {
90+
return (
91+
<RawButton
92+
elementRef={divRef}
93+
className={_cs(styles.optionRenderer, containerClassName, optionContainerClassName)}
94+
onClick={handleClick}
95+
onMouseMove={handleMouseMove}
96+
onMouseLeave={handleMouseLeave}
97+
title={title}
98+
name={optionKey}
99+
focused={isFocused}
100+
>
101+
{contentRenderer(props)}
102+
</RawButton>
103+
);
104+
}
105+
84106
return (
85-
<RawButton
86-
elementRef={divRef}
87-
className={_cs(
88-
styles.optionRenderer,
89-
optionContainerClassName,
90-
containerClassName,
91-
)}
92-
onClick={handleClick}
93-
onMouseMove={handleMouseMove}
94-
onMouseLeave={handleMouseLeave}
95-
title={title}
96-
name={optionKey}
97-
focused={isFocused}
107+
<div
108+
className={styles.optionContainer}
98109
>
99-
{contentRenderer(props)}
100-
</RawButton>
110+
<RawButton
111+
elementRef={divRef}
112+
className={_cs(styles.optionRenderer, containerClassName, optionContainerClassName)}
113+
onClick={handleClick}
114+
onMouseMove={handleMouseMove}
115+
onMouseLeave={handleMouseLeave}
116+
title={title}
117+
name={optionKey}
118+
focused={isFocused}
119+
>
120+
{contentRenderer(props)}
121+
</RawButton>
122+
{actionsSelector(option)}
123+
</div>
101124
);
102125
}
103126
export default GenericOption;

src/components/GenericOption/styles.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,12 @@
22
padding: calc(var(--tui-spacing-large) - var(--tui-spacing-small));
33
text-align: left;
44
}
5+
.option-container {
6+
display: flex;
7+
align-items: center;
8+
padding-right: var(--tui-spacing-small);
9+
10+
.option-renderer {
11+
flex-grow: 1;
12+
}
13+
}

src/components/MultiSelectInput/SearchMultiSelectInput.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ import { rankedSearchOnList } from '../../utils';
1212
import styles from './styles.css';
1313

1414
interface OptionProps {
15-
actions?: React.ReactNode;
1615
children: React.ReactNode;
1716
isActive: boolean;
1817
}
18+
1919
function Option(props: OptionProps) {
2020
const {
21-
actions,
2221
children,
2322
isActive,
2423
} = props;
@@ -31,9 +30,6 @@ function Option(props: OptionProps) {
3130
<div className={styles.label}>
3231
{ children }
3332
</div>
34-
<div className={styles.actions}>
35-
{actions}
36-
</div>
3733
</>
3834
);
3935
}
@@ -244,11 +240,10 @@ function SearchMultiSelectInput<
244240
children: labelSelector(option),
245241
containerClassName: _cs(styles.option, isActive && styles.active),
246242
title: labelSelector(option),
247-
actions: actionsSelector?.(option),
248243
isActive,
249244
};
250245
},
251-
[labelSelector, value, actionsSelector],
246+
[labelSelector, value],
252247
);
253248

254249
// FIXME: value should not be on dependency list, also try to pass options like in SelectInput
@@ -296,6 +291,7 @@ function SearchMultiSelectInput<
296291
optionKeySelector={keySelector}
297292
optionRenderer={Option}
298293
optionRendererParams={optionRendererParams}
294+
actionsSelector={actionsSelector}
299295
optionContainerClassName={styles.optionContainer}
300296
onOptionClick={handleOptionClick}
301297
valueDisplay={valueDisplay}

src/components/SelectInput/SearchSelectInput.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ import { rankedSearchOnList } from '../../utils';
1212
import styles from './styles.css';
1313

1414
interface OptionProps {
15-
actions?: React.ReactNode;
1615
children: React.ReactNode;
1716
}
1817
function Option(props: OptionProps) {
1918
const {
20-
actions,
2119
children,
2220
} = props;
2321

@@ -29,9 +27,6 @@ function Option(props: OptionProps) {
2927
<div className={styles.label}>
3028
{ children }
3129
</div>
32-
<div className={styles.actions}>
33-
{actions}
34-
</div>
3530
</>
3631
);
3732
}
@@ -237,12 +232,11 @@ function SearchSelectInput<
237232

238233
return {
239234
children: labelSelector(option),
240-
actions: actionsSelector?.(option),
241235
containerClassName: _cs(styles.option, isActive && styles.active),
242236
title: labelSelector(option),
243237
};
244238
},
245-
[value, labelSelector, actionsSelector],
239+
[value, labelSelector],
246240
);
247241

248242
const handleOptionClick = useCallback(
@@ -283,6 +277,7 @@ function SearchSelectInput<
283277
optionKeySelector={keySelector}
284278
optionRenderer={Option}
285279
optionRendererParams={optionRendererParams}
280+
actionsSelector={actionsSelector}
286281
optionContainerClassName={styles.optionContainer}
287282
onOptionClick={handleOptionClick}
288283
valueDisplay={valueDisplay}

src/components/SelectInputContainer/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export type SelectInputContainerProps<
6161
optionKeySelector: (datum: O, index: number) => OK;
6262
optionRenderer: (props: Pick<P, Exclude<keyof P, 'containerClassName' | 'title'>>) => React.ReactNode;
6363
optionRendererParams: (optionKey: OK, option: O) => P;
64+
actionsSelector?: (option: O) => React.ReactNode;
6465
totalOptionsCount?: number;
6566
optionsPopupContentClassName?: string;
6667
options: O[] | undefined | null;
@@ -112,6 +113,7 @@ function SelectInputContainer<OK extends OptionKey, N extends string, O extends
112113
optionKeySelector,
113114
optionRenderer,
114115
optionRendererParams,
116+
actionsSelector,
115117
options: optionsFromProps,
116118
optionsPopupClassName,
117119
optionsPopupContentClassName,
@@ -221,6 +223,7 @@ function SelectInputContainer<OK extends OptionKey, N extends string, O extends
221223
optionKey: key,
222224
focusedKey,
223225
contentRenderer: optionRenderer,
226+
actionsSelector,
224227
onClick: handleOptionClick,
225228
onFocus: onFocusedKeyChange,
226229
optionContainerClassName: _cs(optionContainerClassName, styles.listItem),
@@ -232,6 +235,7 @@ function SelectInputContainer<OK extends OptionKey, N extends string, O extends
232235
optionContainerClassName,
233236
optionRenderer,
234237
optionRendererParams,
238+
actionsSelector,
235239
],
236240
);
237241

src/stories/MultiSelectInput.stories.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Story } from '@storybook/react/types-6-0';
33
import { useArgs } from '@storybook/client-api';
44
import { IoOpenOutline } from 'react-icons/io5';
55
import MultiSelectInput, { MultiSelectInputProps } from '#components/MultiSelectInput';
6-
import QuickActionButton from '#components/QuickActionButton';
76

87
export default {
98
title: 'Input/MultiSelectInput',
@@ -48,24 +47,16 @@ const Template: Story<MultiSelectInputProps<string, string, Option, { containerC
4847
);
4948
};
5049

51-
function handleClick(_:string | undefined, e: React.MouseEvent<HTMLButtonElement>) {
52-
// NOTE: This intentionally breaks HTML semantics (link inside a button).
53-
// This workaround is to allow clickable links inside SelectInput options
54-
// where a button wrapper is required.
55-
e.stopPropagation();
56-
window.open('https://www.google.com/search?q=story', '_blank');
57-
}
58-
5950
export const WithActions = Template.bind({});
6051
WithActions.args = {
6152
actionsSelector: () => (
62-
<QuickActionButton
63-
name={undefined}
64-
onClick={handleClick}
65-
transparent
53+
<a
54+
href="https://www.google.com/search?q=story"
55+
target="_blank"
56+
rel="noopener noreferrer"
6657
>
6758
<IoOpenOutline />
68-
</QuickActionButton>
59+
</a>
6960
),
7061
};
7162

src/stories/SearchMultiSelectInput.stories.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Story } from '@storybook/react/types-6-0';
33
import { useArgs } from '@storybook/client-api';
44
import { IoOpenOutline } from 'react-icons/io5';
55
import SearchMultiSelectInput, { SearchMultiSelectInputProps } from '#components/MultiSelectInput/SearchMultiSelectInput';
6-
import QuickActionButton from '#components/QuickActionButton';
76
import useQuery, { entityListTransformer } from '../utils/useQuery';
87

98
export default {
@@ -190,24 +189,16 @@ const Template: Story<SearchMultiSelectInputProps<string, string, Option, { cont
190189
);
191190
};
192191

193-
function handleClick(_:string | undefined, e: React.MouseEvent<HTMLButtonElement>) {
194-
// NOTE: This intentionally breaks HTML semantics (link inside a button).
195-
// This workaround is to allow clickable links inside SelectInput options
196-
// where a button wrapper is required.
197-
e.stopPropagation();
198-
window.open('https://www.google.com/search?q=story', '_blank');
199-
}
200-
201192
export const WithActions = Template.bind({});
202193
WithActions.args = {
203194
actionsSelector: () => (
204-
<QuickActionButton
205-
name={undefined}
206-
onClick={handleClick}
207-
transparent
195+
<a
196+
href="https://www.google.com/search?q=story"
197+
target="_blank"
198+
rel="noopener noreferrer"
208199
>
209200
<IoOpenOutline />
210-
</QuickActionButton>
201+
</a>
211202
),
212203
};
213204

src/stories/SearchSelectInput.stories.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Story } from '@storybook/react/types-6-0';
33
import { useArgs } from '@storybook/client-api';
44
import { IoOpenOutline } from 'react-icons/io5';
55
import SearchSelectInput, { SearchSelectInputProps } from '#components/SelectInput/SearchSelectInput';
6-
import QuickActionButton from '#components/QuickActionButton';
76
import useQuery, { entityListTransformer } from '../utils/useQuery';
87

98
export default {
@@ -195,24 +194,16 @@ Default.args = {
195194
value: '1',
196195
};
197196

198-
function handleClick(_:string | undefined, e: React.MouseEvent<HTMLButtonElement>) {
199-
// NOTE: This intentionally breaks HTML semantics (link inside a button).
200-
// This workaround is to allow clickable links inside SelectInput options
201-
// where a button wrapper is required.
202-
e.stopPropagation();
203-
window.open('https://www.google.com/search?q=story', '_blank');
204-
}
205-
206197
export const WithActions = Template.bind({});
207198
WithActions.args = {
208199
actionsSelector: () => (
209-
<QuickActionButton
210-
name={undefined}
211-
onClick={handleClick}
212-
transparent
200+
<a
201+
href="https://www.google.com/search?q=story"
202+
target="_blank"
203+
rel="noopener noreferrer"
213204
>
214205
<IoOpenOutline />
215-
</QuickActionButton>
206+
</a>
216207
),
217208
};
218209

src/stories/SelectInput.stories.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Story } from '@storybook/react/types-6-0';
33
import { useArgs } from '@storybook/client-api';
44
import { IoOpenOutline } from 'react-icons/io5';
55
import SelectInput, { SelectInputProps } from '#components/SelectInput';
6-
import QuickActionButton from '#components/QuickActionButton';
76

87
export default {
98
title: 'Input/SelectInput',
@@ -47,24 +46,16 @@ const Template: Story<SelectInputProps<string, string, Option, { containerClassN
4746
);
4847
};
4948

50-
function handleClick(_:string | undefined, e: React.MouseEvent<HTMLButtonElement>) {
51-
// NOTE: This intentionally breaks HTML semantics (link inside a button).
52-
// This workaround is to allow clickable links inside SelectInput options
53-
// where a button wrapper is required.
54-
e.stopPropagation();
55-
window.open('https://www.google.com/search?q=story', '_blank');
56-
}
57-
5849
export const WithActions = Template.bind({});
5950
WithActions.args = {
6051
actionsSelector: () => (
61-
<QuickActionButton
62-
name={undefined}
63-
onClick={handleClick}
64-
transparent
52+
<a
53+
href="https://www.google.com/search?q=story"
54+
target="_blank"
55+
rel="noopener noreferrer"
6556
>
6657
<IoOpenOutline />
67-
</QuickActionButton>
58+
</a>
6859
),
6960
};
7061

0 commit comments

Comments
 (0)