Skip to content

Commit 6ff2f3f

Browse files
adids1221M-i-k-e-l
andauthored
Add support for rendering FloatingButton secondary button standalone (#3862)
* feat: add support for rendering FloatingButton secondary button standalone * fix: restore backward compatibility in FloatingButton component * feat(FloatingButton): add support for secondary-only button rendering - Add support for rendering FloatingButton with only secondary button - Secondary-only mode automatically uses horizontal layout with outline styling - Add isSecondaryOnly and shouldSecondaryButtonFlex getters - Fix fullWidth margin consistency for secondary-only button - Refactor rendering logic to use positive checks for better readability - Fix fullWidth support in horizontal layout with only primary button Breaking: FloatingButton now requires button prop to render primary button (previously rendered empty wrapper with only visible: true) * refactor: simplify FloatingButton styles and logic * refactor: removed shouldUseHorizontalStyle const, use isHorizontalLayout directly * revert comment --------- Co-authored-by: Miki Leib <[email protected]>
1 parent e0f8fa0 commit 6ff2f3f

File tree

3 files changed

+116
-66
lines changed

3 files changed

+116
-66
lines changed

demo/src/screens/componentScreens/FloatingButtonScreen.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import {renderBooleanOption} from '../ExampleScreenPresenter';
55

66
interface State {
77
showButton: boolean;
8+
showPrimary: boolean;
89
showSecondary: boolean;
910
showVertical: boolean;
1011
}
1112

1213
export default class FloatingButtonScreen extends Component<{}, State> {
1314
state = {
1415
showButton: true,
16+
showPrimary: true,
1517
showSecondary: true,
1618
showVertical: true,
1719
fullWidth: false
@@ -36,6 +38,7 @@ export default class FloatingButtonScreen extends Component<{}, State> {
3638
</Text>
3739
{renderBooleanOption.call(this, 'Show Floating Button', 'showButton')}
3840
{renderBooleanOption.call(this, 'Full Width Button', 'fullWidth')}
41+
{renderBooleanOption.call(this, 'Show Primary Button', 'showPrimary')}
3942
{renderBooleanOption.call(this, 'Show Secondary Button', 'showSecondary')}
4043
{renderBooleanOption.call(this, 'Button Layout Vertical', 'showVertical')}
4144

@@ -67,10 +70,14 @@ export default class FloatingButtonScreen extends Component<{}, State> {
6770
<FloatingButton
6871
visible={this.state.showButton}
6972
fullWidth={this.state.fullWidth}
70-
button={{
71-
label: 'Approve',
72-
onPress: this.close
73-
}}
73+
button={
74+
this.state.showPrimary
75+
? {
76+
label: 'Approve',
77+
onPress: this.close
78+
}
79+
: undefined
80+
}
7481
secondaryButton={
7582
showSecondary
7683
? {

packages/react-native-ui-lib/src/components/floatingButton/__tests__/index.spec.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ describe('FloatingButton', () => {
3131
const buttonDriver = ButtonDriver({renderTree, testID: `${TEST_ID}.button`});
3232
expect(await buttonDriver.exists()).not.toBeTruthy();
3333

34-
renderTree.rerender(<TestCase visible/>);
34+
renderTree.rerender(<TestCase visible button={button}/>);
3535
expect(await buttonDriver.exists()).toBeTruthy();
3636
});
3737
});
3838

3939
describe('buttons', () => {
4040
it('should render a button', async () => {
41-
const props = {visible: true};
41+
const props = {visible: true, button};
4242
const renderTree = render(<TestCase {...props}/>);
4343
const buttonDriver = ButtonDriver({renderTree, testID: `${TEST_ID}.button`});
4444
expect(await buttonDriver.exists()).toBeTruthy();
@@ -68,15 +68,15 @@ describe('FloatingButton', () => {
6868

6969
describe('bottomMargin', () => {
7070
it('should have default bottom margin', () => {
71-
const props = {visible: true};
71+
const props = {visible: true, button};
7272
const renderTree = render(<TestCase {...props}/>);
7373
const buttonDriver = ButtonDriver({renderTree, testID: `${TEST_ID}.button`});
7474

7575
expect(buttonDriver.getElement().props.style?.marginBottom).toBe(Spacings.s8);
7676
});
7777

7878
it('should have default bottom margin for both buttons', () => {
79-
const props = {visible: true, secondaryButton};
79+
const props = {visible: true, button, secondaryButton};
8080
const renderTree = render(<TestCase {...props}/>);
8181
const buttonDriver = ButtonDriver({renderTree, testID: `${TEST_ID}.button`});
8282
const buttonDriver2 = ButtonDriver({renderTree, testID: `${TEST_ID}.secondaryButton`});
@@ -86,15 +86,15 @@ describe('FloatingButton', () => {
8686
});
8787

8888
it('should have bottom margin that match bottomMargin prop', () => {
89-
const props = {visible: true, bottomMargin: 10};
89+
const props = {visible: true, button, bottomMargin: 10};
9090
const renderTree = render(<TestCase {...props}/>);
9191
const buttonDriver = ButtonDriver({renderTree, testID: `${TEST_ID}.button`});
9292

9393
expect(buttonDriver.getElement().props.style?.marginBottom).toBe(10);
9494
});
9595

9696
it('should have bottom margin for secondary button that match bottomMarginProp', () => {
97-
const props = {visible: true, secondaryButton, bottomMargin: 10};
97+
const props = {visible: true, button, secondaryButton, bottomMargin: 10};
9898
const renderTree = render(<TestCase {...props}/>);
9999
const buttonDriver = ButtonDriver({renderTree, testID: `${TEST_ID}.button`});
100100
const buttonDriver2 = ButtonDriver({renderTree, testID: `${TEST_ID}.secondaryButton`});
@@ -106,7 +106,7 @@ describe('FloatingButton', () => {
106106

107107
describe('buttonLayout', () => {
108108
it('should style vertical layout (default)', () => {
109-
const props = {visible: true, secondaryButton};
109+
const props = {visible: true, button, secondaryButton};
110110
const renderTree = render(<TestCase {...props}/>);
111111
const driver = FloatingButtonDriver({renderTree, testID: TEST_ID});
112112

@@ -117,7 +117,7 @@ describe('FloatingButton', () => {
117117
});
118118

119119
it('should style horizontal layout', () => {
120-
const props = {visible: true, secondaryButton, buttonLayout: FloatingButtonLayouts.HORIZONTAL};
120+
const props = {visible: true, button, secondaryButton, buttonLayout: FloatingButtonLayouts.HORIZONTAL};
121121
const renderTree = render(<TestCase {...props}/>);
122122
const driver = FloatingButtonDriver({renderTree, testID: TEST_ID});
123123

@@ -130,7 +130,7 @@ describe('FloatingButton', () => {
130130

131131
describe('fullWidth', () => {
132132
it('should style vertical layout (default) when fullWidth is true', () => {
133-
const props = {visible: true, secondaryButton, fullWidth: true};
133+
const props = {visible: true, button, secondaryButton, fullWidth: true};
134134
const renderTree = render(<TestCase {...props}/>);
135135
const driver = FloatingButtonDriver({renderTree, testID: TEST_ID});
136136

@@ -143,7 +143,7 @@ describe('FloatingButton', () => {
143143
});
144144

145145
it('should style horizontal layout when fullWidth is true', () => {
146-
const props = {visible: true, secondaryButton, buttonLayout: FloatingButtonLayouts.HORIZONTAL, fullWidth: true};
146+
const props = {visible: true, button, secondaryButton, buttonLayout: FloatingButtonLayouts.HORIZONTAL, fullWidth: true};
147147
const renderTree = render(<TestCase {...props}/>);
148148
const driver = FloatingButtonDriver({renderTree, testID: TEST_ID});
149149

packages/react-native-ui-lib/src/components/floatingButton/index.tsx

Lines changed: 95 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -111,34 +111,50 @@ class FloatingButton extends PureComponent<FloatingButtonProps> {
111111
};
112112
};
113113

114+
get isSecondaryOnly() {
115+
const {secondaryButton, button} = this.props;
116+
return !!secondaryButton && !button;
117+
}
118+
119+
get isHorizontalLayout() {
120+
const {buttonLayout} = this.props;
121+
return buttonLayout === FloatingButtonLayouts.HORIZONTAL || this.isSecondaryOnly;
122+
}
123+
114124
get isSecondaryHorizontal() {
115-
const {secondaryButton, buttonLayout} = this.props;
116-
return secondaryButton && buttonLayout === FloatingButtonLayouts.HORIZONTAL;
125+
const {secondaryButton} = this.props;
126+
return secondaryButton && this.isHorizontalLayout;
117127
}
118128

119129
get isSecondaryVertical() {
120-
const {secondaryButton, buttonLayout} = this.props;
121-
return secondaryButton && buttonLayout === FloatingButtonLayouts.VERTICAL;
130+
const {secondaryButton} = this.props;
131+
return secondaryButton && !this.isHorizontalLayout;
122132
}
123133

124134
renderButton() {
125135
const {bottomMargin, button, fullWidth, testID} = this.props;
126136

127-
const bottom = this.isSecondaryVertical ? Spacings.s4 : bottomMargin || Spacings.s8;
128-
const left = this.isSecondaryHorizontal || fullWidth ? Spacings.s4 : undefined;
129-
const right = this.isSecondaryHorizontal ? 20 : fullWidth ? Spacings.s4 : undefined;
130-
const shadowStyle = !button?.outline && !button?.link && styles.shadow;
131-
const marginStyle = {marginTop: 16, marginBottom: bottom, marginLeft: left, marginRight: right};
137+
if (button) {
138+
const shadowStyle = button && !button.outline && !button.link ? styles.shadow : undefined;
139+
const marginStyle = {
140+
marginTop: Spacings.s4,
141+
marginBottom: this.isSecondaryVertical ? Spacings.s4 : bottomMargin || Spacings.s8,
142+
marginLeft: this.isSecondaryHorizontal || fullWidth ? Spacings.s4 : undefined,
143+
marginRight: this.isSecondaryHorizontal ? Spacings.s5 : fullWidth ? Spacings.s4 : undefined
144+
};
132145

133-
return (
134-
<Button
135-
size={Button.sizes.large}
136-
flex={!!this.isSecondaryHorizontal}
137-
style={[shadowStyle, marginStyle]}
138-
testID={`${testID}.button`}
139-
{...button}
140-
/>
141-
);
146+
const shouldFlex = this.isSecondaryHorizontal || (fullWidth && this.isHorizontalLayout);
147+
148+
return (
149+
<Button
150+
size={Button.sizes.large}
151+
flex={!!shouldFlex}
152+
style={[shadowStyle, marginStyle]}
153+
testID={`${testID}.button`}
154+
{...button}
155+
/>
156+
);
157+
}
142158
}
143159

144160
renderOverlay = () => {
@@ -157,30 +173,55 @@ class FloatingButton extends PureComponent<FloatingButtonProps> {
157173
};
158174

159175
renderSecondaryButton() {
160-
const {secondaryButton, bottomMargin, testID, buttonLayout} = this.props;
176+
const {secondaryButton, bottomMargin, testID, fullWidth, button} = this.props;
177+
178+
if (secondaryButton) {
179+
const bgColor = secondaryButton.backgroundColor || Colors.$backgroundDefault;
180+
const shouldFlex = (this.isHorizontalLayout && !!button) || (fullWidth && this.isSecondaryOnly);
181+
182+
const buttonStyle = this.isHorizontalLayout
183+
? [styles.shadow, styles.horizontalSecondaryMargin, {backgroundColor: bgColor}]
184+
: {marginBottom: bottomMargin || Spacings.s7};
161185

162-
const bgColor = secondaryButton?.backgroundColor || Colors.$backgroundDefault;
163-
const isHorizontal = buttonLayout === FloatingButtonLayouts.HORIZONTAL;
164-
const buttonStyle = isHorizontal ?
165-
[styles.shadow, styles.secondaryMargin, {backgroundColor: bgColor}] : {marginBottom: bottomMargin || Spacings.s7};
166-
186+
return (
187+
<Button
188+
outline={this.isHorizontalLayout}
189+
flex={shouldFlex}
190+
link={!this.isHorizontalLayout}
191+
size={Button.sizes.large}
192+
testID={`${testID}.secondaryButton`}
193+
{...secondaryButton}
194+
style={buttonStyle}
195+
enableShadow={false}
196+
/>
197+
);
198+
}
199+
}
200+
201+
renderHorizontalLayout() {
167202
return (
168-
<Button
169-
outline={isHorizontal}
170-
flex={isHorizontal}
171-
link={!isHorizontal}
172-
size={Button.sizes.large}
173-
testID={`${testID}.secondaryButton`}
174-
{...secondaryButton}
175-
style={buttonStyle}
176-
enableShadow={false}
177-
/>
203+
<>
204+
{this.renderOverlay()}
205+
{this.renderSecondaryButton()}
206+
{this.renderButton()}
207+
</>
208+
);
209+
}
210+
211+
renderVerticalLayout() {
212+
return (
213+
<>
214+
{this.renderOverlay()}
215+
{this.renderButton()}
216+
{this.renderSecondaryButton()}
217+
</>
178218
);
179219
}
180220

181221
render() {
182-
const {withoutAnimation, visible, fullWidth, testID} = this.props;
183222
// NOTE: keep this.firstLoad as true as long as the visibility changed to true
223+
const {withoutAnimation, visible, fullWidth, testID, button, secondaryButton} = this.props;
224+
184225
this.firstLoad && !visible ? (this.firstLoad = true) : (this.firstLoad = false);
185226

186227
// NOTE: On first load, don't show if it should not be visible
@@ -191,21 +232,23 @@ class FloatingButton extends PureComponent<FloatingButtonProps> {
191232
return false;
192233
}
193234

194-
return (
195-
<View
196-
row={this.isSecondaryHorizontal}
197-
center={this.isSecondaryHorizontal || !fullWidth}
198-
pointerEvents="box-none"
199-
animated
200-
style={[styles.container, this.getAnimatedStyle()]}
201-
testID={testID}
202-
>
203-
{this.renderOverlay()}
204-
{this.isSecondaryHorizontal && this.renderSecondaryButton()}
205-
{this.renderButton()}
206-
{this.isSecondaryVertical && this.renderSecondaryButton()}
207-
</View>
208-
);
235+
if (button || secondaryButton) {
236+
const hasBothButtons = !!(button && secondaryButton);
237+
const shouldCenter = !fullWidth || (this.isHorizontalLayout && hasBothButtons);
238+
239+
return (
240+
<View
241+
row={this.isHorizontalLayout}
242+
center={shouldCenter}
243+
pointerEvents="box-none"
244+
animated
245+
style={[styles.container, this.getAnimatedStyle()]}
246+
testID={testID}
247+
>
248+
{this.isHorizontalLayout ? this.renderHorizontalLayout() : this.renderVerticalLayout()}
249+
</View>
250+
);
251+
}
209252
}
210253
}
211254

@@ -222,10 +265,10 @@ const styles = StyleSheet.create({
222265
shadow: {
223266
...Shadows.sh20.bottom
224267
},
225-
secondaryMargin: {
268+
horizontalSecondaryMargin: {
226269
marginTop: Spacings.s4,
227270
marginBottom: Spacings.s7,
228-
marginLeft: 20
271+
marginLeft: Spacings.s5
229272
}
230273
});
231274

0 commit comments

Comments
 (0)