4.6 KB172 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {ReactNode} from 'react';
9
10import * as stylex from '@stylexjs/stylex';
11import {Button, buttonStyles} from './Button';
12import {Icon} from './Icon';
13import {colors, spacing} from './theme/tokens.stylex';
14import {Tooltip, type TooltipProps} from './Tooltip';
15
16export const styles = stylex.create({
17 container: {
18 display: 'flex',
19 alignItems: 'stretch',
20 position: 'relative',
21 '::before': {
22 content: '',
23 position: 'absolute',
24 width: '100%',
25 height: '100%',
26 top: 0,
27 left: 0,
28 pointerEvents: 'none',
29 },
30 },
31 button: {
32 borderBottomRightRadius: 0,
33 borderTopRightRadius: 0,
34 },
35 chevron: {
36 color: 'var(--button-secondary-foreground)',
37 position: 'absolute',
38 top: 'calc(50% - 0.5em)',
39 right: 'calc(var(--halfpad) - 1px)',
40 pointerEvents: 'none',
41 },
42 select: {
43 backgroundColor: {
44 default: 'var(--button-secondary-background)',
45 ':hover': 'var(--button-secondary-hover-background)',
46 },
47 color: 'var(--button-secondary-foreground)',
48 opacity: {
49 default: 1,
50 ':disabled': 0.5,
51 },
52 cursor: {
53 default: 'pointer',
54 ':disabled': 'not-allowed',
55 },
56 width: '24px',
57 borderRadius: '0px 2px 2px 0px',
58 outline: {
59 default: 'none',
60 ':focus': '1px solid var(--focus-border)',
61 },
62 outlineOffset: '2px',
63 verticalAlign: 'bottom',
64 appearance: 'none',
65 lineHeight: '0',
66 border: '1px solid var(--button-border)',
67 borderLeft: '1px solid var(--button-secondary-foreground)',
68 },
69 builtinButtonBorder: {
70 borderLeft: 'unset',
71 },
72 iconButton: {
73 borderTopRightRadius: 0,
74 borderBottomRightRadius: 0,
75 paddingRight: spacing.half,
76 },
77 iconSelect: {
78 borderTopLeftRadius: 0,
79 borderBottomLeftRadius: 0,
80 borderLeftColor: colors.hoverDarken,
81 },
82 iconChevron: {
83 color: 'var(--button-icon-foreground)',
84 },
85 chevronDisabled: {
86 opacity: 0.5,
87 },
88});
89
90export function ButtonDropdown<T extends {label: ReactNode; id: string}>({
91 options,
92 kind,
93 onClick,
94 selected,
95 onChangeSelected,
96 buttonDisabled,
97 pickerDisabled,
98 icon,
99 customSelectComponent,
100 primaryTooltip,
101 ...rest
102}: {
103 options: ReadonlyArray<T>;
104 kind?: 'primary' | 'icon' | undefined;
105 onClick: (selected: T, event: React.MouseEvent<HTMLButtonElement>) => unknown;
106 selected: T;
107 onChangeSelected: (newSelected: T) => unknown;
108 buttonDisabled?: boolean;
109 pickerDisabled?: boolean;
110 /** Icon to place in the button */
111 icon?: React.ReactNode;
112 customSelectComponent?: React.ReactNode;
113 primaryTooltip?: TooltipProps;
114 'data-testId'?: string;
115}) {
116 const selectedOption = options.find(opt => opt.id === selected.id) ?? options[0];
117 // const themeName = useAtomValue(themeNameState); // TODO
118 // // Slightly hacky: in these themes, the border is too strong. Use the button border instead.
119 // const useBuiltinBorder = ['Default Light Modern', 'Default Dark Modern'].includes(
120 // themeName as string,
121 // );
122
123 const buttonComponent = (
124 <Button
125 kind={kind}
126 onClick={buttonDisabled ? undefined : e => onClick(selected, e)}
127 disabled={buttonDisabled}
128 xstyle={[styles.button, kind === 'icon' && styles.iconButton]}
129 {...rest}>
130 {icon ?? null} {selected.label}
131 </Button>
132 );
133
134 return (
135 <div {...stylex.props(styles.container)}>
136 {primaryTooltip ? <Tooltip {...primaryTooltip}>{buttonComponent}</Tooltip> : buttonComponent}
137 {customSelectComponent ?? (
138 <select
139 {...stylex.props(
140 styles.select,
141 kind === 'icon' && buttonStyles.icon,
142 kind === 'icon' && styles.iconSelect,
143 // useBuiltinBorder && styles.builtinButtonBorder,
144 )}
145 disabled={pickerDisabled}
146 value={selectedOption.id}
147 onClick={e => e.stopPropagation()}
148 onChange={event => {
149 const matching = options.find(opt => opt.id === (event.target.value as T['id']));
150 if (matching != null) {
151 onChangeSelected(matching);
152 }
153 }}>
154 {options.map(option => (
155 <option key={option.id} value={option.id}>
156 {option.label}
157 </option>
158 ))}
159 </select>
160 )}
161 <Icon
162 icon="chevron-down"
163 {...stylex.props(
164 styles.chevron,
165 kind === 'icon' && styles.iconChevron,
166 pickerDisabled && styles.chevronDisabled,
167 )}
168 />
169 </div>
170 );
171}
172