Skip to content

Commit

Permalink
refactor(SegmentedButton): Remove visible prop and rename enabled (#125)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `SegmentedButtonItem` removed `visible` prop, please use conditional rendering instead
BREAKING CHANGE: `SegmentedButton` and `SegmentedButtonItem` prop enabled is now renamed to disabled and logic is inverted
[ci skip]
  • Loading branch information
MarcusNotheis authored Sep 17, 2019
1 parent 8f962bf commit 174447a
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 166 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { getEventFromCallback, mountThemedComponent } from '@shared/tests/utils';
import { mount } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import { SegmentedButton } from '@ui5/webcomponents-react/lib/SegmentedButton';
import { SegmentedButtonItem } from '@ui5/webcomponents-react/lib/SegmentedButtonItem';
import React, { cloneElement } from 'react';
import sinon from 'sinon';

describe('SegmentedButton', () => {
test('Selection', () => {
Expand All @@ -25,21 +24,19 @@ describe('SegmentedButton', () => {

test('Update Selection via API', () => {
const callback = sinon.spy();
const SegmentedBtn = (SegmentedButton as any).InnerComponent;
const SegmentedBtnItem = (SegmentedButtonItem as any).InnerComponent;
const wrapper = mount(
<SegmentedBtn onItemSelected={callback} classes={{}}>
<SegmentedBtnItem id="btn-1" classes={{}}>
Test
</SegmentedBtnItem>
<SegmentedBtnItem id="btn-2" classes={{}}>
Test
</SegmentedBtnItem>
</SegmentedBtn>

const wrapper = mountThemedComponent(
<SegmentedButton onItemSelected={callback}>
<SegmentedButtonItem id="btn-1">Test</SegmentedButtonItem>
<SegmentedButtonItem id="btn-2">Test</SegmentedButtonItem>
</SegmentedButton>
);
wrapper.setProps({ selectedKey: 'btn-2' });
expect(wrapper.render()).toMatchSnapshot();
wrapper.setProps({
children: cloneElement(wrapper.prop('children'), { selectedKey: 'btn-2' })
});
wrapper.update();
expect(wrapper.state('selectedKey')).toEqual('btn-2');
expect(wrapper.render()).toMatchSnapshot();
expect(callback.called).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SegmentedButton Update Selection via API 1`] = `
<ul
class="SegmentedButton--segmentedButton-"
role="radiogroup"
tabindex="0"
>
<li
class="SegmentedButtonItem--segmentedButtonItem- SegmentedButtonItem--focusableItem- SegmentedButtonItem--selected-"
data-has-own-width="false"
>
Test
</li>
<li
class="SegmentedButtonItem--segmentedButtonItem- SegmentedButtonItem--focusableItem-"
data-has-own-width="false"
>
Test
</li>
</ul>
`;

exports[`SegmentedButton Update Selection via API 2`] = `
<ul
class="SegmentedButton--segmentedButton-"
role="radiogroup"
tabindex="0"
>
<li
class="SegmentedButtonItem--segmentedButtonItem- SegmentedButtonItem--focusableItem-"
data-has-own-width="false"
>
Test
</li>
<li
class="SegmentedButtonItem--segmentedButtonItem- SegmentedButtonItem--focusableItem- SegmentedButtonItem--selected-"
data-has-own-width="false"
>
Test
</li>
</ul>
`;
6 changes: 4 additions & 2 deletions packages/main/src/components/SegmentedButton/demo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import { SegmentedButtonItem } from '@ui5/webcomponents-react/lib/SegmentedButto
export const renderStory = () => {
return (
<SegmentedButton
enabled={boolean('enabled', true)}
disabled={boolean('disabled', false)}
onItemSelected={action('onItemSelected')}
selectedKey={number('SelectedKey', 1)}
>
<SegmentedButtonItem id={1} icon={<Icon src="world" />} />
<SegmentedButtonItem id={2} icon={<Icon src="world" />}>
Button 2
</SegmentedButtonItem>
<SegmentedButtonItem id={3}>Button 3</SegmentedButtonItem>
<SegmentedButtonItem id={3} width="300px">
Button 3
</SegmentedButtonItem>
</SegmentedButton>
);
};
Expand Down
185 changes: 91 additions & 94 deletions packages/main/src/components/SegmentedButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
import { Event, StyleClassHelper, withStyles } from '@ui5/webcomponents-react-base';
import React, { Children, cloneElement, Component, CSSProperties, ReactElement, RefObject } from 'react';
import { ClassProps } from '../../interfaces/ClassProps';
import { CommonProps } from '../../interfaces/CommonProps';
import { Event, StyleClassHelper, useConsolidatedRef } from '@ui5/webcomponents-react-base';
import { ContentDensity } from '@ui5/webcomponents-react/lib/ContentDensity';
import { SegmentedButtonItemPropTypes } from '../SegmentedButtonItem';
import React, {
Children,
cloneElement,
FC,
forwardRef,
ReactNode,
Ref,
RefObject,
useCallback,
useEffect,
useState
} from 'react';
import { createUseStyles } from 'react-jss';
import { CommonProps } from '../../interfaces/CommonProps';
import { JSSTheme } from '../../interfaces/JSSTheme';

export type SelectedKey = string | number;

export interface SegmentedButtonPropTypes extends CommonProps {
enabled?: boolean;
disabled?: boolean;
selectedKey?: SelectedKey;
children: ReactElement<SegmentedButtonItemPropTypes> | Array<ReactElement<SegmentedButtonItemPropTypes>>;
children: ReactNode | ReactNode[];
onItemSelected?: (event: Event) => void;
}

interface SegmentedButtonInternalProps extends SegmentedButtonPropTypes, ClassProps {}

interface SegmentedButtonState {
selectedKey: SelectedKey;
prevPropSelectedKey: SelectedKey;
itemWidth: CSSProperties['width'];
}

const styles = ({ contentDensity }) => ({
segmentedButton: {
verticalAlign: 'top',
position: 'relative',
margin: '0',
padding: contentDensity === ContentDensity.Compact ? '0.1875rem 0' : '0.250rem 0',
WebkitTapHighlightColor: 'rgba(255, 255, 255, 0)',
border: 'none',
whiteSpace: 'nowrap',
display: 'inline-block',
Expand All @@ -41,105 +43,100 @@ const styles = ({ contentDensity }) => ({
}
});

@withStyles(styles)
export class SegmentedButton extends Component<SegmentedButtonPropTypes, SegmentedButtonState> {
static defaultProps = {
enabled: true,
selectedKey: '',
onItemSelected: null,
width: null
};

state = {
selectedKey: null,
prevPropSelectedKey: null,
itemWidth: 'auto'
};

items: RefObject<HTMLUListElement> = (this.props as SegmentedButtonInternalProps).innerRef;

static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.prevPropSelectedKey !== nextProps.selectedKey) {
const newKey = nextProps.selectedKey ? nextProps.selectedKey : nextProps.children[0].props.id;
return {
selectedKey: newKey,
prevPropSelectedKey: newKey
};
}
return null;
}
const useStyles = createUseStyles<JSSTheme, keyof ReturnType<typeof styles>>(styles, { name: 'SegmentedButton' });

private handleSegmentedButtonItemSelected = (e) => {
const selectedKey = e.getParameter('selectedKey');
if (selectedKey !== this.state.selectedKey) {
this.setState({
selectedKey
});
if (this.props.onItemSelected) {
this.props.onItemSelected(Event.of(this, e.getOriginalEvent(), e.getParameters()));
}
}
};

private updateChildElementSize() {
let maxWidth = 0;
requestAnimationFrame(() => {
for (let i = 0; i < this.items.current.childElementCount; i++) {
const item = this.items.current.children.item(i) as HTMLUListElement;
if (item.offsetWidth && item.offsetWidth > maxWidth) {
maxWidth = item.offsetWidth;
}
}
const SegmentedButton: FC<SegmentedButtonPropTypes> = forwardRef(
(props: SegmentedButtonPropTypes, ref: Ref<HTMLUListElement>) => {
const { children, disabled, className, style, tooltip, slot, onItemSelected, selectedKey } = props;

if (maxWidth > this.items.current.offsetWidth) {
this.setState({
itemWidth: 'auto'
});
} else if (this.state.itemWidth !== `${maxWidth}px`) {
this.setState({
itemWidth: `${maxWidth}px`
});
const listRef: RefObject<HTMLUListElement> = useConsolidatedRef(ref);

const [internalSelectedKey, setSelectedKey] = useState(() => {
if (selectedKey) return selectedKey;
const firstChild: any = Children.toArray(children)[0];
if (firstChild && firstChild.props) {
return firstChild.props.id;
}
return null;
});
}

componentDidMount() {
this.updateChildElementSize();
}

componentDidUpdate() {
this.updateChildElementSize();
}
useEffect(() => {
if (selectedKey) {
setSelectedKey(selectedKey);
}
}, [selectedKey, setSelectedKey]);

render() {
const { children, enabled, classes, className, style, tooltip, slot } = this.props as SegmentedButtonInternalProps;
const { selectedKey } = this.state;
const classes = useStyles();

const segmentedBtnClasses = StyleClassHelper.of(classes.segmentedButton);
if (className) {
segmentedBtnClasses.put(className);
}

const handleSegmentedButtonItemSelected = useCallback(
(e) => {
const newSelectedKey = e.getParameter('selectedKey');
if (newSelectedKey !== internalSelectedKey) {
setSelectedKey(newSelectedKey);
if (typeof onItemSelected === 'function') {
onItemSelected(Event.of(null, e.getOriginalEvent(), e.getParameters()));
}
}
},
[internalSelectedKey, setSelectedKey, onItemSelected]
);

useEffect(() => {
requestAnimationFrame(() => {
let maxWidth = 0;
for (let i = 0; i < listRef.current.childElementCount; i++) {
const item = listRef.current.children.item(i) as HTMLLIElement;
if (item.offsetWidth && item.offsetWidth > maxWidth) {
maxWidth = item.offsetWidth;
}
}
if (maxWidth < listRef.current.offsetWidth) {
for (let i = 0; i < listRef.current.childElementCount; i++) {
const item = listRef.current.children.item(i) as HTMLLIElement;
if (item.getAttribute('data-has-own-width') === 'false') {
item.style.width = `${maxWidth}px`;
}
}
}
});
}, [children, listRef]);

return (
<ul
tabIndex={0}
role="radiogroup"
className={segmentedBtnClasses.toString()}
style={style}
ref={this.items}
ref={listRef}
title={tooltip}
slot={slot}
>
{Children.map(children, (item: any) =>
cloneElement(item, {
key: item.props.id,
selected: selectedKey === item.props.id,
enabled: enabled === false ? enabled : item.props.enabled,
width: item.props.width ? item.props.width : this.state.itemWidth,
onClick: this.handleSegmentedButtonItemSelected
})
)}
{Children.toArray(children)
.filter(Boolean)
.map((item: any) =>
cloneElement(item, {
key: item.props.id,
selected: internalSelectedKey === item.props.id,
disabled: disabled === true ? disabled : item.props.disabled,
onClick: handleSegmentedButtonItemSelected
})
)}
</ul>
);
}
}
);

SegmentedButton.displayName = 'SegmentedButton';

SegmentedButton.defaultProps = {
disabled: false,
selectedKey: '',
onItemSelected: null
};

export { SegmentedButton };
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fonts } from '@ui5/webcomponents-react-base';
import { JSSTheme } from '../../interfaces/JSSTheme';
import { ContentDensity } from '@ui5/webcomponents-react/lib/ContentDensity';

const styles = ({ theme, contentDensity, parameters }: JSSTheme) => ({
const styles = ({ contentDensity, parameters }: JSSTheme) => ({
segmentedButtonItem: {
fontFamily: fonts.sapUiFontFamily,
listStyle: 'none',
Expand Down Expand Up @@ -41,6 +41,7 @@ const styles = ({ theme, contentDensity, parameters }: JSSTheme) => ({
background: parameters.sapUiSegmentedButtonSelectedBackground,
color: parameters.sapUiSegmentedButtonSelectedTextColor,
borderColor: parameters.sapUiSegmentedButtonSelectedHoverBorderColor,
'--sapUiContentNonInteractiveIconColor': parameters.sapUiContentContrastIconColor,
'$:active': {
background: parameters.sapUiButtonActiveBackground,
color: parameters.sapUiButtonActiveTextColor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('SegmentedButtonItem', () => {
test('SegmentedButtonItem Disabled', () => {
const callback = sinon.spy();
const wrapper = mountThemedComponent(
<SegmentedButtonItem id={1} icon={<Icon src="add" />} enabled={false} onClick={callback} />
<SegmentedButtonItem id={1} icon={<Icon src="add" />} disabled onClick={callback} />
);
wrapper.simulate('click');
expect(wrapper.render()).toMatchSnapshot();
Expand Down
Loading

0 comments on commit 174447a

Please sign in to comment.