Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Components] Add SegmentedControl #405

Merged
merged 39 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b3443d3
add SegmentedControl element
heyamykate Sep 25, 2020
073eac8
add docs
heyamykate Sep 25, 2020
1b2ef12
add handling for active index
heyamykate Sep 28, 2020
555fa60
add type for example element
heyamykate Sep 28, 2020
3cb4c78
add variable items to example story
heyamykate Sep 28, 2020
f6b66ae
update comments
heyamykate Sep 28, 2020
2fdf3ca
add tests
heyamykate Sep 28, 2020
0cf1851
fix styling
heyamykate Sep 29, 2020
a09919b
update snapshot
heyamykate Sep 29, 2020
762bc2f
add focus box-shadow
heyamykate Oct 5, 2020
54aff38
update snapshot
heyamykate Oct 5, 2020
8bfbb8a
remove Knobs
heyamykate Oct 7, 2020
ef5439a
update
heyamykate Oct 8, 2020
af00a01
focus styling
heyamykate Oct 8, 2020
cc99778
update snapshot
heyamykate Oct 8, 2020
cdbd2ac
return null if length is less than 1
heyamykate Oct 8, 2020
59c38d7
Merge branch 'master' into add-new-tab-component
heyamykate Oct 8, 2020
edd2717
Merge branch 'master' into add-new-tab-component
heyamykate Oct 20, 2020
c342101
update
heyamykate Oct 20, 2020
959d467
Merge branch 'master' into add-new-tab-component
michaeljaltamirano Oct 20, 2020
6f55c81
Update to new Component Story Format
michaeljaltamirano Oct 21, 2020
dbf0ac7
Remove markdown file
michaeljaltamirano Oct 21, 2020
efd5c99
Merge branch 'master' into add-new-tab-component
michaeljaltamirano Nov 9, 2020
aaab47f
Pull COLORS from theme object, use theme test utils, update onClick test
michaeljaltamirano Nov 9, 2020
9ce66a1
Merge branch 'master' into add-new-tab-component
michaeljaltamirano Dec 22, 2020
2add20e
use React Testing Library instead of Enzyme
michaeljaltamirano Dec 22, 2020
b4fbec0
Merge branch 'master' into add-new-tab-component
michaeljaltamirano Dec 31, 2020
4f75e46
Merge branch 'master' into add-new-tab-component
michaeljaltamirano Apr 29, 2021
49de0c3
Add secondary stories
michaeljaltamirano Apr 29, 2021
b6005d7
Merge branch 'master' into add-new-tab-component
michaeljaltamirano Sep 22, 2021
57f6de2
Update snapshots
michaeljaltamirano Sep 22, 2021
41d7b89
Add sstyling for secondary theme
winstonkim Sep 23, 2021
f611f53
Merge branch 'master' of github.com:curology/radiance-ui into add-new…
winstonkim Sep 23, 2021
c23a2d9
Run ESLint fix
michaeljaltamirano Sep 23, 2021
9809faf
Use BORDER_RADIUS.large (32px equivalent to 80px)
michaeljaltamirano Sep 24, 2021
87e96b7
[SegmentedControl] Refinements (#1177)
michaeljaltamirano Sep 28, 2021
23b393f
Merge branch 'add-new-tab-component' of github.com:curology/radiance-…
michaeljaltamirano Sep 28, 2021
d5eb34b
use Z_SCALE constant
michaeljaltamirano Sep 28, 2021
1164df8
Update snapshot for 40px height change
michaeljaltamirano Sep 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/segmentedControl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Segmented Control

## Usage

```jsx
import { SegmentedControl } from 'radiance-ui';

class SegmentedControlExample extends React.Component {
segmentItems = [
{ id: 1, text: 'Option 1' },
{ id: 2, text: 'Option 2' },
{ id: 3, text: 'Option 3' },
];

const onClick = segment => {
console.log(segment);
}

render() {
return <SegmentedControl initialActiveId={1} segmentItems={this.segmentItems} onClick={onClick} />;
}
}
```

<!-- STORY -->

### Proptypes

| prop | propType | required | default | description |
| --------------- | ---------------- | -------- | ------- | -------------------------------- |
| initialActiveId | number | no | 1 | initial segment id to display |
| onClick | number | no | - | function to call on tab click |
| segmentItems | array of objects | yes | - | see segmentItem properties below |

#### `segmentItem` Proptypes

| prop | propType | description |
| ---- | -------- | ------------------------ |
| id | number | the segment indentifier |
| text | string | the title of the segment |

### Notes

The width of the top-level container is set to 100%, so it will expand to fill its parent container. Set a non-percentage `width` on the parent element during implementation to avoid stretched-out layout or animation effects.
1 change: 1 addition & 0 deletions src/shared-components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export { default as OffClickWrapper } from './offClickWrapper';
export { default as OptionButton } from './optionButton';
export { default as ProgressBar } from './progressBar';
export { default as RadioButton } from './radioButton';
export { default as SegmentedControl } from './segmentedControl';
export { default as Tabs } from './tabs';
export { default as Toggle } from './toggle';
export { default as Tooltip } from './tooltip';
Expand Down
123 changes: 123 additions & 0 deletions src/shared-components/segmentedControl/__snapshots__/test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<SegmentedControl /> renders a regular segmented control 1`] = `
.emotion-8 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
position: relative;
border-radius: 80px;
background-color: #ededf0;
padding: 0.25rem;
border: 4px solid #ededf0;
}

.emotion-0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
cursor: pointer;
margin: 0;
position: absolute;
border-radius: 80px;
width: 33.333333333333336%;
top: 0;
left: 0;
bottom: 0;
-webkit-transition: -webkit-transform 0.22s cubic-bezier(0.22,1,0.36,1);
-webkit-transition: transform 0.22s cubic-bezier(0.22,1,0.36,1);
transition: transform 0.22s cubic-bezier(0.22,1,0.36,1);
background-color: white;
color: #706D87;
font-size: 0.875rem;
line-height: 1.71;
color: #332e54;
font-weight: bold;
-webkit-transform: initial;
-ms-transform: initial;
transform: initial;
}

.emotion-2 {
background: transparent;
border: none;
color: #706D87;
font-size: 0.875rem;
line-height: 1.71;
color: #706D87;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
cursor: pointer;
margin: 0;
border-radius: 80px;
background-color: #ededf0;
width: 33.333333333333336%;
}

.emotion-2:focus {
outline: none;
box-shadow: 0px 0px 0px 2px #ffffff,0px 0px 0px 4px #332e54;
}

<div
className="emotion-8 emotion-9"
>
<div
className="emotion-0 emotion-1"
transform="initial"
width={33.333333333333336}
>
Tab 1
</div>
<button
className="emotion-2 emotion-3"
onClick={[Function]}
width={33.333333333333336}
>
Tab 1
</button>
<button
className="emotion-2 emotion-3"
onClick={[Function]}
width={33.333333333333336}
>
Tab 2
</button>
<button
className="emotion-2 emotion-3"
onClick={[Function]}
width={33.333333333333336}
>
Tab 3
</button>
</div>
`;
69 changes: 69 additions & 0 deletions src/shared-components/segmentedControl/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useState, useRef, useEffect } from 'react';

import { SegmentsContainer, SegmentItem, Indicator } from './style';
import { SegmentedControlProps, SegmentItemType } from './types';

const SegmentedControl: React.FC<SegmentedControlProps> = ({
segmentItems,
initialActiveId = 1,
onClick,
}) => {
const initialActiveItem = segmentItems.find(
(item: SegmentItemType) => item.id === initialActiveId,
);
const initialActiveIndex = initialActiveItem
heyamykate marked this conversation as resolved.
Show resolved Hide resolved
? segmentItems.indexOf(initialActiveItem)
: 0;

const targetRef = useRef<HTMLDivElement>(null);
const [activeSegmentId, setActiveSegmentId] = useState(initialActiveId);
const [activeSegmentText, setActiveSegmentText] = useState(
heyamykate marked this conversation as resolved.
Show resolved Hide resolved
initialActiveItem?.text,
);
const [activeSegmentIndex, setActiveSegmentIndex] = useState(
initialActiveIndex,
);
const [transform, setTransform] = useState('initial');
const [targetWidth, setTargetWidth] = useState(0);
const segmentWidth = 100 / segmentItems.length;

useEffect(() => {
if (targetRef.current) {
setTargetWidth(targetRef.current.offsetWidth);
setTransform(
`translate3d(${
targetRef.current.offsetWidth * activeSegmentIndex
}px, 0, 0)`,
);
}
}, [targetRef]);

const onSegmentClick = (segment: SegmentItemType) => {
const index = segmentItems.indexOf(segment);
setActiveSegmentId(segment.id);
setActiveSegmentText(segment.text);
setActiveSegmentIndex(index);
setTransform(`translate3d(${targetWidth * index}px, 0, 0)`);
return onClick ? onClick(segment) : null;
};

return (
<SegmentsContainer>
<Indicator width={segmentWidth} transform={transform} ref={targetRef}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to consolidate Indicator within the segmentItems map function to prevent adding an additional button to the DOM? It looks like there's always one more than there exists segmentItems:

Screen Shot 2020-10-20 at 7 53 45 PM

Also, since Indicator precedes all the segment items, the current keyboard navigation behave is to immediately tab to the currently active segment. Is this intended? E.g., if Option 2 of 3 options is active, tabbing will go to 2, then to 1, then to 3. My expectation is that it would go in order.

{activeSegmentText}
</Indicator>
{segmentItems.map((segment) => (
<SegmentItem
width={segmentWidth}
active={segment.id === activeSegmentId}
key={segment.id}
onClick={() => onSegmentClick(segment)}
>
{segment.text}
</SegmentItem>
))}
</SegmentsContainer>
);
};

export default SegmentedControl;
67 changes: 67 additions & 0 deletions src/shared-components/segmentedControl/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import styled from '@emotion/styled';
import { buttonReset } from 'src/utils/styles/buttonReset';

import { COLORS, SPACER, BOX_SHADOWS } from '../../constants';
import { style as TYPOGRAPHY_STYLE } from '../typography';

type SegmentItemProps = {
active: boolean;
key: number;
onClick: () => void;
width: number;
};

type IndicatorProps = {
width: number;
transform: string;
};

export const SegmentsContainer = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
position: relative;
border-radius: 80px;
background-color: ${COLORS.purple10};
padding: ${SPACER.xsmall};
border: 4px solid ${COLORS.purple10};
`;

export const SegmentItem = styled.button<SegmentItemProps>`
${buttonReset};
${TYPOGRAPHY_STYLE.caption};
color: ${COLORS.purple70};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 0;
border-radius: 80px;
background-color: ${COLORS.purple10};
width: ${({ width }) => `${width}%`};
&:focus {
outline: none;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ben will probably be able to provide more guidance re: styling for different states, but there's a new focus variable you can use here to start playing around with how the component would work with only keyboard controls:

&:focus {
  outline: none;
  box-shadow: ${BOX_SHADOWS.focus};
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yeah, thanks for calling that out @michaeljaltamirano but we should apply the new focus styling to this: unsure on how it will react between the two options focus and focusInner but would it be possible to get a screenshot of both with the two scenarios below. These are how it looks with designs but unsure how it will with live code

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@heyamykate I ended up getting out of being underwater this cycle--can you re-tag me for review once the keyboard controls are in place?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@heyamykate It looks like there's some CSS finessing to match the above:

Active item has focus state:
Screen Shot 2020-10-07 at 10 20 55 AM

Inactive item has focus state:
Screen Shot 2020-10-07 at 10 20 48 AM

Copy link
Member

@LilCare LilCare Oct 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@heyamykate The focused state looks perfect for the active item.
Screen Shot 2020-10-20 at 8 58 27 AM

However, if an inactive item adjacent still has a small piece cutoff:
Screen Shot 2020-10-20 at 8 58 35 AM

If you add a z-index, it should look like below:
Screen Shot 2020-10-20 at 9 01 36 AM

box-shadow: ${BOX_SHADOWS.focus};
}
`;

export const Indicator = styled.div<IndicatorProps>`
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 0;
position: absolute;
border-radius: 80px;
width: ${({ width }) => `${width}%`};
top: 0;
left: 0;
bottom: 0;
transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The animation looks sooo good.

background-color: white;
${TYPOGRAPHY_STYLE.caption};
color: ${COLORS.primary};
font-weight: bold;
transform: ${({ transform }) => transform};
`;
36 changes: 36 additions & 0 deletions src/shared-components/segmentedControl/test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';

import SegmentedControl from './index';

const testSegmentedControl = {
segmentItems: [
{ id: 1, text: 'Tab 1' },
{ id: 2, text: 'Tab 2' },
{ id: 3, text: 'Tab 3' },
],
};
/* eslint-disable react/jsx-props-no-spreading */
describe('<SegmentedControl />', () => {
test('renders a regular segmented control', () => {
const component = renderer.create(
<SegmentedControl {...testSegmentedControl} />,
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test('calls onClick when button is clicked', () => {
const spy = jest.fn();
const buttonIndex = 2;

const component = shallow(
<SegmentedControl {...testSegmentedControl} onClick={spy} />,
);
const button = component.childAt(buttonIndex);

button.simulate('click');
expect(spy).toHaveBeenCalled();
});
});
10 changes: 10 additions & 0 deletions src/shared-components/segmentedControl/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type SegmentedControlProps = {
initialActiveId?: number;
segmentItems: SegmentItemType[];
onClick?: (segment: SegmentItemType) => void;
};

export type SegmentItemType = {
id: number;
text: string;
};
27 changes: 27 additions & 0 deletions stories/segmentedControl/defaultControl/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { css } from '@emotion/core';

import { SegmentedControl } from 'src/shared-components';
import { SegmentItemType } from 'src/shared-components/segmentedControl/types';

type DefaultControlProps = {
width?: string;
segmentItems: SegmentItemType[];
};

const DefaultControl: React.FC<DefaultControlProps> = ({
width = '500px',
segmentItems,
}) => {
return (
<div
css={css`
width: ${width};
`}
>
<SegmentedControl segmentItems={segmentItems} initialActiveId={1} />
</div>
);
};

export default DefaultControl;
Loading