Skip to content

Commit

Permalink
moving buttongroup to context
Browse files Browse the repository at this point in the history
  • Loading branch information
dleroux committed Nov 20, 2019
1 parent 35d61d2 commit 7ff1a54
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 80 deletions.
2 changes: 2 additions & 0 deletions UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Added `external` prop to `ResourceList` ([#2408](https://github.com/Shopify/polaris-react/pull/2408))
- Added `onMouseEnter` and `onTouchStart` props to `Button` ([#2409](https://github.com/Shopify/polaris-react/pull/2409))
- Added `ariaHaspopup` prop to `Popover` ([#2248](https://github.com/Shopify/polaris-react/pull/2248))
- Made use of the `ButtonGroup` context to apply necessary styles to `Button` ([#2441](https://github.com/Shopify/polaris-react/pull/2441))

### Bug fixes

Expand All @@ -33,5 +34,6 @@
### Code quality

- Changed `aria-labelledby` to always exist on `TextField` ([#2401](https://github.com/Shopify/polaris-react/pull/2401))
- Converted `ButtonGroup > Item` into a functional component ([#2441](https://github.com/Shopify/polaris-react/pull/2441))

### Deprecations
36 changes: 34 additions & 2 deletions src/components/Button/Button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ $partial-button-filled-pressed-box-shadow: inset 0 0 0 0 transparent,
transition-duration: duration(fast);
background: linear-gradient(to bottom, $color-light, $color-light);
border-color: $color-dark;
box-shadow: $partial-button-filled-pressed-box-shadow$color-dark;
box-shadow: $partial-button-filled-pressed-box-shadow $color-dark;
}

&:active {
background: linear-gradient(to bottom, $color-lightest, $color-lightest);
border-color: $color-dark;
box-shadow: $partial-button-filled-pressed-box-shadow$color-darkest;
box-shadow: $partial-button-filled-pressed-box-shadow $color-darkest;
}
}

Expand All @@ -71,6 +71,38 @@ $partial-button-filled-pressed-box-shadow: inset 0 0 0 0 transparent,
}
}

.globalTheming.inSegmentedGroup,
.inSegmentedGroup {
border-radius: 0;
}

.globalTheming.firstInSegmentedGroup,
.firstInSegmentedGroup {
border-top-left-radius: var(--p-border-radius-base, border-radius());
border-bottom-left-radius: var(--p-border-radius-base, border-radius());
}

.globalTheming.lastInSegmentedGroup,
.lastInSegmentedGroup {
border-top-right-radius: var(--p-border-radius-base, border-radius());
border-bottom-right-radius: var(--p-border-radius-base, border-radius());
}

.globalTheming.firstInConnectedTopGroup,
.firstInConnectedTopGroup {
border-top-left-radius: 0;
}

.globalTheming.lastInConnectedTopGroup,
.lastInConnectedTopGroup {
border-top-right-radius: 0;
}

.globalTheming.inFullWidthGroup,
.inFullWidthGroup {
@include button-full-width;
}

.Content {
@include text-style-button;
position: relative;
Expand Down
27 changes: 27 additions & 0 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import React, {useRef} from 'react';
import {CaretDownMinor} from '@shopify/polaris-icons';
import {classNames, variationName} from '../../utilities/css';
import {handleMouseUpByBlurring} from '../../utilities/focus';
import {useUniqueId} from '../../utilities/unique-id';
import {useFeatures} from '../../utilities/features';
import {useI18n} from '../../utilities/i18n';
import {UnstyledLink} from '../UnstyledLink';
import {useButtonGroup} from '../../utilities/button-group';
import {Icon} from '../Icon';
import {IconProps} from '../../types';
import {Spinner} from '../Spinner';
Expand Down Expand Up @@ -124,6 +126,7 @@ export function Button({
}: ButtonProps) {
const {unstableGlobalTheming = false} = useFeatures();
const hasGivenDeprecationWarning = useRef(false);
const idRef = useUniqueId('Button');

if (ariaPressed && !hasGivenDeprecationWarning.current) {
// eslint-disable-next-line no-console
Expand All @@ -135,6 +138,29 @@ export function Button({

const i18n = useI18n();

const buttonGroupContext = useButtonGroup();
let buttonGroupClassName;

if (buttonGroupContext) {
!buttonGroupContext.buttons.includes(idRef) &&
buttonGroupContext.setButtons([...buttonGroupContext.buttons, idRef]);

const {segmented, fullWidth, connectedTop} = buttonGroupContext;
const lastInGroup = buttonGroupContext.buttons[0] === idRef;
const firstInGroup =
buttonGroupContext.buttons[buttonGroupContext.buttons.length - 1] ===
idRef;

buttonGroupClassName = classNames(
segmented && styles.inSegmentedGroup,
segmented && lastInGroup && styles.lastInSegmentedGroup,
segmented && firstInGroup && styles.firstInSegmentedGroup,
fullWidth && styles.inFullWidthGroup,
connectedTop && firstInGroup && styles.firstInConnectedTopGroup,
connectedTop && lastInGroup && styles.lastInConnectedTopGroup,
);
}

const isDisabled = disabled || loading;

const className = classNames(
Expand All @@ -152,6 +178,7 @@ export function Button({
textAlign && styles[variationName('textAlign', textAlign)],
fullWidth && styles.fullWidth,
icon && children == null && styles.iconOnly,
buttonGroupClassName,
);

const disclosureIcon = (
Expand Down
45 changes: 8 additions & 37 deletions src/components/ButtonGroup/ButtonGroup.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ $item-spacing: spacing(tight);
margin-top: 0;
margin-left: 0;

// This is a violation of our component model, but it’s the cleanest
// way to remove the border radii on connected elements.
.Item {
position: relative;
z-index: z-index(item, $stacking-order);
Expand All @@ -48,51 +46,24 @@ $item-spacing: spacing(tight);
&:not(:first-child) {
margin-left: -(border-width());
}

// stylelint-disable-next-line selector-max-combinators
> * {
border-radius: 0;
}

// stylelint-disable-next-line selector-max-combinators
&:first-child > * {
border-top-left-radius: border-radius();
border-bottom-left-radius: border-radius();
}

// stylelint-disable-next-line selector-max-combinators
&:last-child > * {
border-top-right-radius: border-radius();
border-bottom-right-radius: border-radius();
}
}

.Item-focused {
z-index: z-index(focused, $stacking-order);
}
}

.fullWidth {
.Item {
flex: 1 1 auto;

// stylelint-disable-next-line selector-max-combinators
> * {
@include button-full-width;
&.globalTheming {
.Item {
// stylelint-disable-next-line selector-max-class, selector-max-specificity
&:not(:first-child) {
margin-left: spacing(extra-tight);
}
}
}
}

.connectedTop {
.fullWidth {
.Item {
// stylelint-disable-next-line selector-max-combinators
&:first-child > * {
border-top-left-radius: 0;
}

// stylelint-disable-next-line selector-max-combinators
&:last-child > * {
border-top-right-radius: 0;
}
flex: 1 1 auto;
}
}
26 changes: 24 additions & 2 deletions src/components/ButtonGroup/ButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';
import React, {useState} from 'react';
import {ButtonGroupContext} from '../../utilities/button-group';
import {classNames} from '../../utilities/css';
import {useFeatures} from '../../utilities/features';
import {elementChildren} from '../../utilities/components';
import {Item} from './components';
import styles from './ButtonGroup.scss';
Expand All @@ -21,16 +23,36 @@ export function ButtonGroup({
fullWidth,
connectedTop,
}: ButtonGroupProps) {
const [buttons, setButtons] = useState([] as string[]);
const {unstableGlobalTheming = false} = useFeatures();

const className = classNames(
styles.ButtonGroup,
unstableGlobalTheming && styles.globalTheming,
segmented && styles.segmented,
fullWidth && styles.fullWidth,
connectedTop && styles.connectedTop,
);

const context = {
buttons,
segmented,
setButtons,
fullWidth,
connectedTop,
};

const contents = elementChildren(children).map((child, index) => (
<Item button={child} key={index} />
));

return <div className={className}>{contents}</div>;
const buttonGroup = <div className={className}>{contents}</div>;

return context ? (
<ButtonGroupContext.Provider value={context}>
{buttonGroup}
</ButtonGroupContext.Provider>
) : (
buttonGroup
);
}
60 changes: 23 additions & 37 deletions src/components/ButtonGroup/components/Item/Item.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';

import {useForcibleToggle} from '../../../../utilities/use-toggle';
import {classNames} from '../../../../utilities/css';
import {ButtonProps} from '../../../Button';

Expand All @@ -9,39 +9,25 @@ export interface ItemProps {
button: React.ReactElement<ButtonProps>;
}

interface State {
focused: boolean;
}

export class Item extends React.PureComponent<ItemProps, State> {
state: State = {focused: false};

render() {
const {button} = this.props;
const {focused} = this.state;

const className = classNames(
styles.Item,
focused && styles['Item-focused'],
button.props.plain && styles['Item-plain'],
);

return (
<div
className={className}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
>
{button}
</div>
);
}

private handleFocus = () => {
this.setState({focused: true});
};

private handleBlur = () => {
this.setState({focused: false});
};
}
export const Item = React.memo(function Item({button}: ItemProps) {
const [
focused,
{forceTrue: forceTrueFocused, forceFalse: forceFalseFocused},
] = useForcibleToggle(false);

const className = classNames(
styles.Item,
focused && styles['Item-focused'],
button.props.plain && styles['Item-plain'],
);

return (
<div
className={className}
onFocus={forceTrueFocused}
onBlur={forceFalseFocused}
>
{button}
</div>
);
});
31 changes: 31 additions & 0 deletions src/components/ButtonGroup/tests/ButtonGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import {mountWithAppProvider} from 'test-utilities/legacy';
import {Button} from 'components';
import {Item} from '../components';
import {ButtonGroupContext} from '../../../utilities/button-group';
import {ButtonGroup} from '../ButtonGroup';

describe('<ButtonGroup />', () => {
Expand All @@ -25,5 +26,35 @@ describe('<ButtonGroup />', () => {
);
expect(buttonGroup.find(Item).prop('button').key).toContain(key);
});

it('provides the ButtonGroupContext with the values from props', () => {
function TestComponent(_: {value: any}) {
return null;
}

const expectedContext = {
buttons: [expect.any(String), expect.any(String)],
connectedTop: true,
fullWidth: true,
segmented: true,
setButtons: expect.any(Function),
};

const buttonGroup = mountWithAppProvider(
<ButtonGroup connectedTop fullWidth segmented>
<Button />
<Button />
<ButtonGroupContext.Consumer>
{(buttonGroupContext) => {
return <TestComponent value={buttonGroupContext} />;
}}
</ButtonGroupContext.Consumer>
</ButtonGroup>,
);

expect(buttonGroup.find(TestComponent).prop('value')).toStrictEqual(
expectedContext,
);
});
});
});
11 changes: 10 additions & 1 deletion src/components/Portal/Portal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {createPortal} from 'react-dom';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
import {ButtonGroupContext} from '../../utilities/button-group';
import {ThemeContext} from '../../utilities/theme';
import {portal} from '../shared';

Expand Down Expand Up @@ -67,8 +68,16 @@ export class Portal extends React.PureComponent<PortalProps, State> {
}

render() {
const childrenMarkup = (
// Portals can be rendered inside a ButtonGroup e.g. in a Popover
// This ensures that buttons inside the Portals don't get registered as part of the group
<ButtonGroupContext.Provider value={undefined}>
{this.props.children}
</ButtonGroupContext.Provider>
);

return this.state.isMounted
? createPortal(this.props.children, this.portalNode)
? createPortal(childrenMarkup, this.portalNode)
: null;
}
}
Expand Down
Loading

0 comments on commit 7ff1a54

Please sign in to comment.