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 all 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
1 change: 1 addition & 0 deletions src/shared-components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './offClickWrapper';
export * from './optionButton';
export * from './progressBar';
export * from './radioButton';
export * from './segmentedControl';
export * from './tabs';
export * from './toggle';
export * from './tooltip';
Expand Down
128 changes: 128 additions & 0 deletions src/shared-components/segmentedControl/__snapshots__/test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<SegmentedControl /> renders a regular segmented control 1`] = `
.emotion-0 {
width: 100%;
height: 2.5rem;
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: justify;
-webkit-justify-content: space-between;
justify-content: space-between;
position: relative;
border-radius: 32px;
background-color: #EDEDF0;
padding: 0.25rem 0;
}

.emotion-2 {
background: transparent;
border: none;
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;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
cursor: pointer;
margin: 0;
position: absolute;
border-radius: 32px;
width: 33.333333333333336%;
top: 0;
left: 0;
bottom: 0;
background-color: white;
-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
color: #706D87;
font-size: 0.875rem;
line-height: 1.71;
color: #332E54;
font-weight: bold;
-webkit-transform: translate3d(0px, 0, 0);
-moz-transform: translate3d(0px, 0, 0);
-ms-transform: translate3d(0px, 0, 0);
transform: translate3d(0px, 0, 0);
border: 4px solid #EDEDF0;
}

.emotion-2:focus {
box-shadow: inset 0px 0px 0px 2px #332E54;
outline: none;
}

.emotion-4 {
background: transparent;
border: none;
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;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
margin: 0 0.125rem;
top: 0;
left: 0;
bottom: 0;
padding: 0.25rem;
color: #706D87;
font-size: 0.875rem;
line-height: 1.71;
color: #706D87;
border-radius: 32px;
background-color: #EDEDF0;
width: 33.333333333333336%;
}

.emotion-4:focus {
outline: none;
box-shadow: 0px 0px 0px 2px #332E54;
z-index: 3;
}

<div
class="emotion-0 emotion-1"
>
<button
class="emotion-2 emotion-3"
transform="translate3d(0px, 0, 0)"
>
Tab 1
</button>
<button
class="emotion-4 emotion-5"
disabled=""
>
Tab 1
</button>
<button
class="emotion-4 emotion-5"
>
Tab 2
</button>
<button
class="emotion-4 emotion-5"
>
Tab 3
</button>
</div>
`;
85 changes: 85 additions & 0 deletions src/shared-components/segmentedControl/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { useState, useRef, useEffect } from 'react';

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

/**
* 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.
*/
export const SegmentedControl: React.FC<SegmentedControlProps> = ({
segmentItems,
initialActiveId = 1,
onClick,
}) => {
const itemsCount = segmentItems.length;

if (itemsCount === 0) {
return null;
}

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 [activeSegmentText, setActiveSegmentText] = useState(() =>
initialActiveItem ? initialActiveItem.text : segmentItems[0].text,
);
const [activeSegmentIndex, setActiveSegmentIndex] =
useState(initialActiveIndex);
const targetRef = useRef<HTMLButtonElement>(null);
const [transform, setTransform] = useState('');
const [targetWidth, setTargetWidth] = useState(0);
const segmentWidth = 100 / itemsCount;

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

const onSegmentClick = (segment: SegmentItemType, index: number) => {
setActiveSegmentText(segment.text);
setActiveSegmentIndex(index);
setTransform(`translate3d(${targetWidth * index}px, 0, 0)`);

if (onClick) {
onClick(segment);
}
};

return (
<SegmentsContainer>
<Indicator
segmentWidth={segmentWidth}
transform={transform}
ref={targetRef}
>
{activeSegmentText}
</Indicator>
{segmentItems.map((segment, index) => (
<SegmentItem
segmentWidth={segmentWidth}
active={index === activeSegmentIndex}
key={segment.id}
onClick={() => {
onSegmentClick(segment, index);
}}
disabled={index === activeSegmentIndex}
>
{segment.text}
</SegmentItem>
))}
</SegmentsContainer>
);
};
78 changes: 78 additions & 0 deletions src/shared-components/segmentedControl/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import styled from '@emotion/styled';
import { buttonReset } from 'src/utils/styles/buttonReset';

import { SPACER, Z_SCALE } from '../../constants';
import { TYPOGRAPHY_STYLE } from '../typography';

interface SegmentItemProps {
active: boolean;
onClick: () => void;
segmentWidth: number;
}

interface IndicatorProps {
segmentWidth: number;
transform: string;
}

export const SegmentsContainer = styled.div`
width: 100%;
height: ${SPACER.x2large};
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
border-radius: ${({ theme }) => theme.BORDER_RADIUS.large};
background-color: ${({ theme }) => theme.COLORS.border};
padding: ${SPACER.xsmall} 0;
`;

export const SegmentItem = styled.button<SegmentItemProps>`
${buttonReset};
display: flex;
align-items: center;
justify-content: center;
margin: 0 ${SPACER.x2small};
top: 0;
left: 0;
bottom: 0;
padding: ${SPACER.xsmall};
${({ theme }) => TYPOGRAPHY_STYLE.caption(theme)};
color: ${({ theme }) => theme.COLORS.primaryTint2};
border-radius: ${({ theme }) => theme.BORDER_RADIUS.large};
background-color: ${({ theme }) => theme.COLORS.border};
width: ${({ segmentWidth }) => `${segmentWidth}%;`};

&: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: 0px 0px 0px 2px ${({ theme }) => theme.COLORS.primary};
z-index: ${Z_SCALE.e3};
}
`;

export const Indicator = styled.button<IndicatorProps>`
${buttonReset};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 0;
position: absolute;
border-radius: ${({ theme }) => theme.BORDER_RADIUS.large};
width: ${({ segmentWidth }) => `${segmentWidth}%`};
top: 0;
left: 0;
bottom: 0;
background-color: white;
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
${({ theme }) => TYPOGRAPHY_STYLE.caption(theme)};
color: ${({ theme }) => theme.COLORS.primary};
font-weight: bold;
transform: ${({ transform }) => transform};
border: 4px solid ${({ theme }) => theme.COLORS.border};

&:focus {
box-shadow: ${({ theme }) => theme.BOX_SHADOWS.focusInner};
outline: none;
}
`;
35 changes: 35 additions & 0 deletions src/shared-components/segmentedControl/test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { render, userEvent } from 'src/tests/testingLibraryHelpers';

import { SegmentedControl } from './index';

const testSegmentedControl = {
segmentItems: [
{ id: 1, text: 'Tab 1' },
{ id: 2, text: 'Tab 2' },
{ id: 3, text: 'Tab 3' },
],
};

describe('<SegmentedControl />', () => {
it('renders a regular segmented control', () => {
const { container } = render(
<SegmentedControl {...testSegmentedControl} />,
);

expect(container.firstElementChild).toMatchSnapshot();
});

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

const { getByRole } = render(
<SegmentedControl {...testSegmentedControl} onClick={spy} />,
);

const button = getByRole('button', { name: 'Tab 3' });

userEvent.click(button);
expect(spy).toHaveBeenCalled();
});
});
16 changes: 16 additions & 0 deletions src/shared-components/segmentedControl/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface SegmentedControlProps {
/**
* Initial segment id to display as active
*/
initialActiveId?: number;
/**
* Callback invoked on `segmentItem` click
*/
onClick?: (segment: SegmentItemType) => void;
segmentItems: SegmentItemType[];
}

export interface SegmentItemType {
id: number;
text: string;
}
1 change: 0 additions & 1 deletion stories/field/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ export const WithControls = () => (
<FieldsContainer>
<Field
disabled={boolean('disabled', false)}
// @ts-expect-error: select + Field['message'] type compat issue
messages={select('messages', messagesOptions, {})}
messagesType={
select('messagesType', messagesTypeOptions, 'error') as
Expand Down
Loading