|
1 |
| -import React, { useCallback, useContext, useMemo, useState } from 'react'; |
2 |
| -import styled from 'styled-components'; |
3 |
| - |
4 |
| -import log from '../../shared/logging'; |
5 |
| -import { Colors } from '../lib/foundations'; |
6 |
| -import { useMounted } from '../lib/utility-hooks'; |
7 |
| -import { |
8 |
| - StyledButtonContent, |
9 |
| - StyledHiddenSide, |
10 |
| - StyledLabel, |
11 |
| - StyledLeft, |
12 |
| - StyledRight, |
13 |
| - StyledVisibleSide, |
14 |
| - transparentButton, |
15 |
| -} from './AppButtonStyles'; |
16 |
| -import { measurements } from './common-styles'; |
17 |
| - |
18 |
| -interface ILabelProps { |
19 |
| - textOffset?: number; |
20 |
| - children?: React.ReactNode; |
21 |
| -} |
22 |
| - |
23 |
| -export function Label(props: ILabelProps) { |
24 |
| - return <StyledLabel $textOffset={props.textOffset ?? 0}>{props.children}</StyledLabel>; |
25 |
| -} |
26 |
| - |
27 |
| -export interface IProps extends React.HTMLAttributes<HTMLButtonElement> { |
28 |
| - children?: React.ReactNode; |
29 |
| - className?: string; |
30 |
| - disabled?: boolean; |
31 |
| - onClick?: () => void; |
32 |
| - textOffset?: number; |
33 |
| -} |
34 |
| - |
35 |
| -type ChildrenGroups = { left: React.ReactNode[]; label: React.ReactNode; right: React.ReactNode[] }; |
36 |
| - |
37 |
| -const BaseButton = React.memo(function BaseButtonT(props: IProps) { |
38 |
| - const { children, textOffset, ...otherProps } = props; |
39 |
| - |
40 |
| - const groupedChildren = useMemo(() => { |
41 |
| - return React.Children.toArray(children).reduce( |
42 |
| - (groups: ChildrenGroups, child) => { |
43 |
| - if (groups.label === undefined && typeof child === 'string') { |
44 |
| - return { ...groups, label: <Label textOffset={textOffset}>{child}</Label> }; |
45 |
| - } else if (React.isValidElement(child) && child.type === Label) { |
46 |
| - return { |
47 |
| - ...groups, |
48 |
| - label: React.cloneElement(child as React.ReactElement<ILabelProps>, { textOffset }), |
49 |
| - }; |
50 |
| - } else if (groups.label === undefined) { |
51 |
| - return { ...groups, left: [...groups.left, child] }; |
52 |
| - } else { |
53 |
| - return { ...groups, right: [...groups.right, child] }; |
54 |
| - } |
55 |
| - }, |
56 |
| - { left: [], label: undefined, right: [] }, |
57 |
| - ); |
58 |
| - }, [children, textOffset]); |
59 |
| - |
60 |
| - return ( |
61 |
| - <StyledSimpleButton {...otherProps}> |
62 |
| - <StyledButtonContent> |
63 |
| - <StyledLeft> |
64 |
| - <StyledVisibleSide>{groupedChildren.left}</StyledVisibleSide> |
65 |
| - <StyledHiddenSide>{groupedChildren.right}</StyledHiddenSide> |
66 |
| - </StyledLeft> |
67 |
| - |
68 |
| - {groupedChildren.label ?? <Label />} |
69 |
| - |
70 |
| - <StyledRight> |
71 |
| - <StyledVisibleSide>{groupedChildren.right}</StyledVisibleSide> |
72 |
| - <StyledHiddenSide>{groupedChildren.left}</StyledHiddenSide> |
73 |
| - </StyledRight> |
74 |
| - </StyledButtonContent> |
75 |
| - </StyledSimpleButton> |
76 |
| - ); |
77 |
| -}); |
78 |
| - |
79 |
| -function SimpleButtonT(props: React.ButtonHTMLAttributes<HTMLButtonElement>) { |
80 |
| - const blockingContext = useContext(BlockingContext); |
81 |
| - |
82 |
| - return ( |
83 |
| - <button |
84 |
| - {...props} |
85 |
| - disabled={props.disabled || blockingContext.disabled} |
86 |
| - onClick={blockingContext.onClick ?? props.onClick}> |
87 |
| - {props.children} |
88 |
| - </button> |
89 |
| - ); |
90 |
| -} |
91 |
| - |
92 |
| -export const SimpleButton = React.memo(SimpleButtonT); |
93 |
| - |
94 |
| -const StyledSimpleButton = styled(SimpleButton)({ |
95 |
| - display: 'flex', |
96 |
| - cursor: 'default', |
97 |
| - borderRadius: 4, |
98 |
| - border: 'none', |
99 |
| - padding: 0, |
100 |
| - '&&:disabled': { |
101 |
| - opacity: 0.5, |
102 |
| - }, |
103 |
| -}); |
104 |
| - |
105 |
| -interface IBlockingContext { |
106 |
| - disabled?: boolean; |
107 |
| - onClick?: () => Promise<void>; |
108 |
| -} |
109 |
| - |
110 |
| -const BlockingContext = React.createContext<IBlockingContext>({}); |
111 |
| - |
112 |
| -interface IBlockingProps { |
113 |
| - children?: React.ReactNode; |
114 |
| - onClick: () => Promise<void>; |
115 |
| - disabled?: boolean; |
116 |
| -} |
117 |
| - |
118 |
| -export function BlockingButton(props: IBlockingProps) { |
119 |
| - const { onClick: propsOnClick } = props; |
120 |
| - |
121 |
| - const isMounted = useMounted(); |
122 |
| - const [isBlocked, setIsBlocked] = useState(false); |
123 |
| - |
124 |
| - const onClick = useCallback(async () => { |
125 |
| - setIsBlocked(true); |
126 |
| - try { |
127 |
| - await propsOnClick(); |
128 |
| - } catch (error) { |
129 |
| - log.error(`onClick() failed - ${error}`); |
130 |
| - } |
131 |
| - |
132 |
| - if (isMounted()) { |
133 |
| - setIsBlocked(false); |
134 |
| - } |
135 |
| - }, [isMounted, propsOnClick]); |
136 |
| - |
137 |
| - const contextValue = useMemo( |
138 |
| - () => ({ |
139 |
| - disabled: isBlocked || props.disabled, |
140 |
| - onClick, |
141 |
| - }), |
142 |
| - [isBlocked, props.disabled, onClick], |
143 |
| - ); |
144 |
| - |
145 |
| - return <BlockingContext.Provider value={contextValue}>{props.children}</BlockingContext.Provider>; |
146 |
| -} |
147 |
| - |
148 |
| -export const RedButton = styled(BaseButton)({ |
149 |
| - backgroundColor: Colors.red, |
150 |
| - '&&:not(:disabled):hover': { |
151 |
| - backgroundColor: Colors.red95, |
152 |
| - }, |
153 |
| -}); |
154 |
| - |
155 |
| -export const GreenButton = styled(BaseButton)({ |
156 |
| - backgroundColor: Colors.green, |
157 |
| - '&&:not(:disabled):hover': { |
158 |
| - backgroundColor: Colors.green90, |
159 |
| - }, |
160 |
| -}); |
161 |
| - |
162 |
| -export const BlueButton = styled(BaseButton)({ |
163 |
| - backgroundColor: Colors.blue80, |
164 |
| - '&&:not(:disabled):hover': { |
165 |
| - backgroundColor: Colors.blue60, |
166 |
| - }, |
167 |
| -}); |
168 |
| - |
169 |
| -export const TransparentButton = styled(BaseButton)(transparentButton, { |
170 |
| - backgroundColor: Colors.white20, |
171 |
| - '&&:not(:disabled):hover': { |
172 |
| - backgroundColor: Colors.white40, |
173 |
| - }, |
174 |
| -}); |
175 |
| - |
176 |
| -export const RedTransparentButton = styled(BaseButton)(transparentButton, { |
177 |
| - backgroundColor: Colors.red60, |
178 |
| - '&&:not(:disabled):hover': { |
179 |
| - backgroundColor: Colors.red80, |
180 |
| - }, |
181 |
| -}); |
182 |
| - |
183 |
| -const StyledButtonWrapper = styled.div({ |
184 |
| - display: 'flex', |
185 |
| - flexDirection: 'column', |
186 |
| - flex: 0, |
187 |
| - '&&:not(:last-child)': { |
188 |
| - marginBottom: measurements.buttonVerticalMargin, |
189 |
| - }, |
190 |
| -}); |
191 |
| - |
192 |
| -interface IButtonGroupProps { |
193 |
| - children: React.ReactNode | React.ReactNode[]; |
194 |
| -} |
195 |
| - |
196 |
| -export function ButtonGroup(props: IButtonGroupProps) { |
197 |
| - return ( |
198 |
| - <> |
199 |
| - {React.Children.map(props.children, (button, index) => ( |
200 |
| - <StyledButtonWrapper key={index}>{button}</StyledButtonWrapper> |
201 |
| - ))} |
202 |
| - </> |
203 |
| - ); |
204 |
| -} |
0 commit comments