From 174447a323192ef29e6668393e20800e68df920e Mon Sep 17 00:00:00 2001 From: Marcus Notheis Date: Tue, 17 Sep 2019 16:39:10 +0200 Subject: [PATCH] refactor(SegmentedButton): Remove visible prop and rename enabled (#125) 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] --- .../SegmentedButton/SegmentedButton.test.tsx | 29 ++- .../SegmentedButton.test.tsx.snap | 43 ++++ .../SegmentedButton/demo.stories.tsx | 6 +- .../src/components/SegmentedButton/index.tsx | 185 +++++++++--------- .../SegmentedButtonItem.jss.ts | 3 +- .../SegmentedButtonItem.test.tsx | 2 +- .../SegmentedButtonItem.test.tsx.snap | 25 ++- .../components/SegmentedButtonItem/index.tsx | 90 +++++---- 8 files changed, 217 insertions(+), 166 deletions(-) create mode 100644 packages/main/src/components/SegmentedButton/__snapshots__/SegmentedButton.test.tsx.snap diff --git a/packages/main/src/components/SegmentedButton/SegmentedButton.test.tsx b/packages/main/src/components/SegmentedButton/SegmentedButton.test.tsx index 1115629e8c8..2bff98dd473 100644 --- a/packages/main/src/components/SegmentedButton/SegmentedButton.test.tsx +++ b/packages/main/src/components/SegmentedButton/SegmentedButton.test.tsx @@ -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', () => { @@ -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( - - - Test - - - Test - - + + const wrapper = mountThemedComponent( + + Test + Test + ); - 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); }); }); diff --git a/packages/main/src/components/SegmentedButton/__snapshots__/SegmentedButton.test.tsx.snap b/packages/main/src/components/SegmentedButton/__snapshots__/SegmentedButton.test.tsx.snap new file mode 100644 index 00000000000..55d744c189d --- /dev/null +++ b/packages/main/src/components/SegmentedButton/__snapshots__/SegmentedButton.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SegmentedButton Update Selection via API 1`] = ` + +`; + +exports[`SegmentedButton Update Selection via API 2`] = ` + +`; diff --git a/packages/main/src/components/SegmentedButton/demo.stories.tsx b/packages/main/src/components/SegmentedButton/demo.stories.tsx index 59d5adb560a..d5155056561 100644 --- a/packages/main/src/components/SegmentedButton/demo.stories.tsx +++ b/packages/main/src/components/SegmentedButton/demo.stories.tsx @@ -8,7 +8,7 @@ import { SegmentedButtonItem } from '@ui5/webcomponents-react/lib/SegmentedButto export const renderStory = () => { return ( @@ -16,7 +16,9 @@ export const renderStory = () => { }> Button 2 - Button 3 + + Button 3 + ); }; diff --git a/packages/main/src/components/SegmentedButton/index.tsx b/packages/main/src/components/SegmentedButton/index.tsx index 339b6771bd7..5ac16decada 100644 --- a/packages/main/src/components/SegmentedButton/index.tsx +++ b/packages/main/src/components/SegmentedButton/index.tsx @@ -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 | Array>; + 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', @@ -41,105 +43,100 @@ const styles = ({ contentDensity }) => ({ } }); -@withStyles(styles) -export class SegmentedButton extends Component { - static defaultProps = { - enabled: true, - selectedKey: '', - onItemSelected: null, - width: null - }; - - state = { - selectedKey: null, - prevPropSelectedKey: null, - itemWidth: 'auto' - }; - - items: RefObject = (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>(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 = forwardRef( + (props: SegmentedButtonPropTypes, ref: Ref) => { + 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 = 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 (
    - {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 + }) + )}
); } -} +); + +SegmentedButton.displayName = 'SegmentedButton'; + +SegmentedButton.defaultProps = { + disabled: false, + selectedKey: '', + onItemSelected: null +}; + +export { SegmentedButton }; diff --git a/packages/main/src/components/SegmentedButtonItem/SegmentedButtonItem.jss.ts b/packages/main/src/components/SegmentedButtonItem/SegmentedButtonItem.jss.ts index 796456c02a7..5dd689576ba 100644 --- a/packages/main/src/components/SegmentedButtonItem/SegmentedButtonItem.jss.ts +++ b/packages/main/src/components/SegmentedButtonItem/SegmentedButtonItem.jss.ts @@ -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', @@ -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 diff --git a/packages/main/src/components/SegmentedButtonItem/SegmentedButtonItem.test.tsx b/packages/main/src/components/SegmentedButtonItem/SegmentedButtonItem.test.tsx index bcd1c6af3fa..faf97c6b0e9 100644 --- a/packages/main/src/components/SegmentedButtonItem/SegmentedButtonItem.test.tsx +++ b/packages/main/src/components/SegmentedButtonItem/SegmentedButtonItem.test.tsx @@ -32,7 +32,7 @@ describe('SegmentedButtonItem', () => { test('SegmentedButtonItem Disabled', () => { const callback = sinon.spy(); const wrapper = mountThemedComponent( - } enabled={false} onClick={callback} /> + } disabled onClick={callback} /> ); wrapper.simulate('click'); expect(wrapper.render()).toMatchSnapshot(); diff --git a/packages/main/src/components/SegmentedButtonItem/__snapshots__/SegmentedButtonItem.test.tsx.snap b/packages/main/src/components/SegmentedButtonItem/__snapshots__/SegmentedButtonItem.test.tsx.snap index 35754ee8251..713cffd0762 100644 --- a/packages/main/src/components/SegmentedButtonItem/__snapshots__/SegmentedButtonItem.test.tsx.snap +++ b/packages/main/src/components/SegmentedButtonItem/__snapshots__/SegmentedButtonItem.test.tsx.snap @@ -2,10 +2,11 @@ exports[`SegmentedButtonItem Basic SegmentedButtonItem 1`] = `
  • void; } -export interface SegmentedButtonItemInternalProps extends SegmentedButtonItemPropTypes, ClassProps { - selected?: boolean; -} +const useStyles = createUseStyles>(styles, { name: 'SegmentedButtonItem' }); -@withStyles(styles) -export class SegmentedButtonItem extends PureComponent { - static defaultProps = { - icon: null, - visible: true, - enabled: true, - children: null, - onClick: null, - selected: false - }; - - private handleOnClick = (e) => { - if (this.props.enabled && this.props.onClick && typeof this.props.onClick === 'function') { - this.props.onClick(Event.of(this, e, { selectedKey: this.props.id })); - } - }; +const SegmentedButtonItem: FC = forwardRef( + (props: SegmentedButtonItemPropTypes, ref: Ref) => { + const { disabled, children, icon, className, style, tooltip, onClick, id, width } = props; - render() { - const { enabled, children, selected, icon, classes, width, className, style, tooltip, innerRef } = this - .props as SegmentedButtonItemInternalProps; + const classes = useStyles(); const iconClasses = StyleClassHelper.of(classes.icon); const segmentedButtonItemClasses = StyleClassHelper.of(classes.segmentedButtonItem); @@ -49,36 +31,60 @@ export class SegmentedButtonItem extends PureComponent { + if (!disabled && typeof onClick === 'function') { + onClick(Event.of(null, e, { selectedKey: id })); + } + }, + [onClick, disabled, id] + ); + + const inlineStyles = useMemo(() => { + if (width === undefined || width === null) { + return style; + } + + return { + ...style, + width + }; + }, [style, width]); + return (
  • {icon &&
    {icon}
    } {children}
  • ); } -} +); + +SegmentedButtonItem.displayName = 'SegmentedButtonItem'; + +SegmentedButtonItem.defaultProps = { + disabled: false +}; + +export { SegmentedButtonItem };