Skip to content

Commit d210124

Browse files
authoredMar 18, 2023
[PAY-1070] Update TabSlider/SegmentedControl slider size on resize (#3044)
1 parent 0b65663 commit d210124

File tree

18 files changed

+93
-206
lines changed

18 files changed

+93
-206
lines changed
 

‎packages/stems/package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/stems/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
],
9999
"dependencies": {
100100
"@juggle/resize-observer": "3.3.1",
101+
"react-merge-refs": "2.0.1",
101102
"react-perfect-scrollbar": "1.5.8",
102103
"react-use": "15.3.8",
103104
"react-use-measure": "2.1.1"

‎packages/stems/src/components/SegmentedControl/SegmentedControl.module.css

+12
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,23 @@
2020
font-family: var(--font-family);
2121
font-size: 15px;
2222
font-weight: var(--font-demi-bold);
23+
position: relative;
2324
}
2425

2526
.tab.isMobile {
2627
padding: var(--unit-1) var(--unit-3);
2728
}
2829

30+
.tab > input {
31+
opacity: 0;
32+
position: absolute;
33+
cursor: pointer;
34+
left: 0;
35+
right: 0;
36+
top: 0;
37+
bottom: 0;
38+
}
39+
2940
.separator {
3041
opacity: 1;
3142
width: 1px;
@@ -50,6 +61,7 @@
5061

5162
.containerFullWidth {
5263
width: 100%;
64+
min-width: fit-content;
5365
}
5466

5567
.tabFullWidth {

‎packages/stems/src/components/SegmentedControl/SegmentedControl.stories.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ export default {
99
argTypes: {}
1010
}
1111

12-
const Template: Story<SegmentedControlProps> = (args) => (
12+
const Template: Story<SegmentedControlProps<string>> = (args) => (
1313
<SegmentedControl {...args} />
1414
)
1515

16-
const options: Option[] = [
16+
const options: Option<string>[] = [
1717
{
1818
key: 'a',
1919
text: 'Long Option A'
@@ -36,7 +36,7 @@ let selectedOption = ''
3636

3737
const handleOptionSelect = (key: string) => (selectedOption = key)
3838

39-
const baseProps: SegmentedControlProps = {
39+
const baseProps: SegmentedControlProps<string> = {
4040
options,
4141
selected: selectedOption,
4242
onSelectOption: handleOptionSelect

‎packages/stems/src/components/SegmentedControl/SegmentedControl.tsx

+42-11
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import { createRef, Fragment, useState, useEffect, useRef } from 'react'
22

33
import cn from 'classnames'
4+
import { mergeRefs } from 'react-merge-refs'
45
import { useSpring, animated } from 'react-spring'
6+
import useMeasure, { RectReadOnly } from 'react-use-measure'
57

68
import styles from './SegmentedControl.module.css'
79
import { SegmentedControlProps } from './types'
810

9-
export const SegmentedControl = (props: SegmentedControlProps) => {
10-
const optionRefs = useRef<Array<React.RefObject<HTMLDivElement>>>(
11-
props.options.map(() => createRef())
11+
export const SegmentedControl = <T extends string>(
12+
props: SegmentedControlProps<T>
13+
) => {
14+
const optionRefs = useRef(
15+
props.options.map((_) => createRef<HTMLLabelElement>())
1216
)
1317
const [selected, setSelected] = useState(props.options[0].key)
1418

19+
const lastBounds = useRef<RectReadOnly>()
1520
const selectedOption = props.selected || selected
1621

17-
const onSetSelected = (option: string) => {
22+
const onSetSelected = (option: T) => {
1823
// Call props function if controlled
1924
if (props.onSelectOption) props.onSelectOption(option)
2025
setSelected(option)
@@ -24,6 +29,12 @@ export const SegmentedControl = (props: SegmentedControlProps) => {
2429
to: { left: '0px', width: '0px' }
2530
}))
2631

32+
// Watch for resizes and repositions so that we move and resize the slider appropriately
33+
const [selectedRef, bounds] = useMeasure({
34+
offsetSize: true,
35+
polyfill: ResizeObserver
36+
})
37+
2738
useEffect(() => {
2839
let selectedRefIdx = props.options.findIndex(
2940
(option) => option.key === selectedOption
@@ -34,14 +45,19 @@ export const SegmentedControl = (props: SegmentedControlProps) => {
3445
selectedRefIdx
3546
]?.current ?? { clientWidth: 0, offsetLeft: 0 }
3647

37-
setAnimatedProps({ to: { left: `${left}px`, width: `${width}px` } })
48+
setAnimatedProps({
49+
to: { left: `${left}px`, width: `${width}px` },
50+
immediate: bounds !== lastBounds.current // Don't animate on moves/resizes
51+
})
52+
lastBounds.current = bounds
3853
}, [
3954
props.options,
4055
selectedOption,
4156
props.selected,
4257
setAnimatedProps,
4358
selected,
44-
optionRefs
59+
optionRefs,
60+
bounds
4561
])
4662

4763
return (
@@ -51,20 +67,35 @@ export const SegmentedControl = (props: SegmentedControlProps) => {
5167
[styles.isMobile]: props.isMobile
5268
})}
5369
>
54-
<animated.div className={styles.tabBackground} style={animatedProps} />
70+
<animated.div
71+
className={styles.tabBackground}
72+
style={animatedProps}
73+
role='radiogroup'
74+
aria-label={props.label}
75+
/>
5576
{props.options.map((option, idx) => {
5677
return (
5778
<Fragment key={option.key}>
58-
<div
59-
ref={optionRefs.current[idx]}
79+
<label
80+
ref={
81+
option.key === selectedOption
82+
? mergeRefs([optionRefs.current[idx], selectedRef])
83+
: optionRefs.current[idx]
84+
}
6085
className={cn(styles.tab, {
6186
[styles.tabFullWidth]: !!props.fullWidth,
6287
[styles.isMobile]: props.isMobile
6388
})}
64-
onClick={() => onSetSelected(option.key)}
6589
>
90+
<input
91+
type='radio'
92+
checked={option.key === selectedOption}
93+
onChange={() => {
94+
onSetSelected(option.key)
95+
}}
96+
/>
6697
{option.text}
67-
</div>
98+
</label>
6899
<div
69100
className={cn(styles.separator, {
70101
[styles.invisible]:

‎packages/stems/src/components/SegmentedControl/types.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
export type Option = {
2-
key: string
1+
export type Option<T> = {
2+
key: T
33
text: string
44
}
55

6-
export type SegmentedControlProps = {
6+
export type SegmentedControlProps<T extends string> = {
77
// The options to display for the tab slider
8-
options: Array<Option>
8+
options: Array<Option<T>>
99

1010
// References the key of an available option that is selected
1111
selected: string
1212

1313
// Called on select option
14-
onSelectOption: (key: string) => void
14+
onSelectOption: (key: T) => void
1515

1616
fullWidth?: boolean
1717

@@ -31,4 +31,9 @@ export type SegmentedControlProps = {
3131
* Styles applied only to active cell text
3232
*/
3333
activeTextClassName?: string
34+
35+
/**
36+
* The label for the radio group
37+
*/
38+
label?: string
3439
}

‎packages/web/src/components/data-entry/TabSlider.js

-100
This file was deleted.

‎packages/web/src/components/data-entry/TabSlider.module.css

-59
This file was deleted.

‎packages/web/src/components/embed-modal/EmbedModal.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { useState, useMemo, useEffect, useCallback } from 'react'
22

33
import { PlayableType, ID, Name, Track, encodeHashId } from '@audius/common'
4-
import { Modal, Button, ButtonType } from '@audius/stems'
4+
import { Modal, Button, ButtonType, SegmentedControl } from '@audius/stems'
55
import cn from 'classnames'
66
import { connect } from 'react-redux'
77
import { Dispatch } from 'redux'
88

99
import { useRecord, make } from 'common/store/analytics/actions'
10-
import TabSlider from 'components/data-entry/TabSlider'
1110
import { AppState } from 'store/types'
1211
import { BASE_GA_URL } from 'utils/route'
1312

@@ -186,7 +185,7 @@ const EmbedModal = ({ isOpen, kind, id, metadata, close }: EmbedModalProps) => {
186185
{metadata && (metadata as Track).track_id && (
187186
<div className={styles.panel}>
188187
<div className={styles.title}>{messages.playerSize}</div>
189-
<TabSlider
188+
<SegmentedControl
190189
options={tabOptions}
191190
selected={size}
192191
onSelectOption={(size) => setSize(size)}

0 commit comments

Comments
 (0)