Skip to content

Commit

Permalink
[C-3749] Migrate stem components to harmony round 1 (#7459)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanjeffers authored Feb 6, 2024
1 parent 39e4575 commit 9e0131e
Show file tree
Hide file tree
Showing 92 changed files with 1,096 additions and 453 deletions.
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions packages/common/src/models/Collection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { FunctionComponent, SVGProps } from 'react'

import { CID, ID, UID } from '../models/Identifiers'
import { CoverArtSizes } from '../models/ImageSizes'
import { Repost } from '../models/Repost'
Expand Down Expand Up @@ -97,7 +95,6 @@ export type SmartCollection = {
gradient?: string
imageOverride?: string
shadow?: string
icon?: FunctionComponent<SVGProps<SVGSVGElement> & { title?: string }>
link: string
playlist_contents?: PlaylistContents
has_current_user_saved?: boolean
Expand Down
3 changes: 3 additions & 0 deletions packages/harmony/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,8 @@
"react-spring": "^8.0.27",
"react-use": "^15.3.8",
"react-use-measure": "^2.1.1"
},
"dependencies": {
"@juggle/resize-observer": "3.4.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
minWidth,
fullWidth,
styles,
style,
children,
'aria-label': ariaLabelProp,
asChild,
Expand Down Expand Up @@ -92,7 +91,8 @@ export const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
}),
...(_isPressed && {
transform: fullWidth ? 'scale(1.00)' : 'scale(0.98)'
})
}),
minWidth: minWidth && !isTextHidden ? `${minWidth}px` : 'unset'
}

const iconCss = !isStaticIcon && {
Expand All @@ -107,10 +107,6 @@ export const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
disabled={disabled || isLoading}
ref={ref}
type={asChild ? undefined : 'button'}
style={{
minWidth: minWidth && !isTextHidden ? `${minWidth}px` : 'unset',
...style
}}
aria-label={getAriaLabel()}
{...other}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type IconButtonProps = {
icon: IconComponent
ripple?: boolean
'aria-label': string
} & Pick<IconProps, 'color' | 'size' | 'shadow'> &
} & Pick<IconProps, 'color' | 'size' | 'shadow' | 'height' | 'width'> &
Pick<BaseButtonProps, 'onClick' | 'disabled' | 'className'>

/**
Expand All @@ -23,6 +23,8 @@ export const IconButton = (props: IconButtonProps) => {
size = 'l',
shadow,
ripple,
height,
width,
...other
} = props
const { disabled } = other
Expand Down Expand Up @@ -56,6 +58,8 @@ export const IconButton = (props: IconButtonProps) => {
color={disabled ? 'disabled' : iconColor}
size={size}
shadow={shadow}
height={height}
width={width}
/>
</BaseButton>
)
Expand Down
14 changes: 14 additions & 0 deletions packages/harmony/src/components/common/HiddenInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from '@emotion/styled'

export const HiddenInput = styled.input({
cursor: 'inherit',
position: 'absolute',
opacity: 0,
width: '100%',
height: '100%',
top: 0,
left: 0,
margin: 0,
padding: 0,
zIndex: 1
})
2 changes: 2 additions & 0 deletions packages/harmony/src/components/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type IconProps = {
size?: IconSize
sizeW?: IconSize
sizeH?: IconSize
height?: number
width?: number
shadow?: ShadowOptions
}

Expand Down
4 changes: 4 additions & 0 deletions packages/harmony/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export * from './completion-check'
export * from './icon'
export * from './text-link'
export * from './hint'
export * from './tag'
export * from './switch'
export * from './segmented-control'
export * from './scrollbar'
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
import { CSSObject, useTheme } from '@emotion/react'
import styled from '@emotion/styled'

import { HiddenInput } from 'components/common/HiddenInput'
import { Text } from 'components/text'

import type { SelectablePillProps } from './types'

const InputRoot = styled.input({
cursor: 'inherit',
position: 'absolute',
opacity: 0,
width: '100%',
height: '100%',
top: 0,
left: 0,
margin: 0,
padding: 0,
zIndex: 1
})

export const SelectablePill = (props: SelectablePillProps) => {
const { isSelected, size = 'small', _isHovered, icon: Icon, ...other } = props

Expand Down Expand Up @@ -108,7 +95,10 @@ export const SelectablePill = (props: SelectablePillProps) => {
return (
<label css={rootCss}>
{pillContent}
<InputRoot {...rest} checked={checked ?? isSelected} />
<HiddenInput
{...rest}
checked={type === 'checkbox' ? checked ?? isSelected : undefined}
/>
</label>
)
}
Expand Down
5 changes: 3 additions & 2 deletions packages/harmony/src/components/layout/Flex/Flex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ export const Flex = styled(Box, {
justifyContent,
gap,
rowGap,
columnGap
columnGap,
inline
} = props
const { spacing } = theme

return {
display: 'flex',
display: inline ? 'inline-flex' : 'flex',
alignItems,
justifyContent,
flexDirection: direction,
Expand Down
1 change: 1 addition & 0 deletions packages/harmony/src/components/layout/Flex/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type BaseFlexProps = {
columnGap?: SpacingOptions
justifyContent?: CSSProperties['justifyContent']
wrap?: CSSProperties['flexWrap']
inline?: boolean
}

export type FlexProps = BaseFlexProps & BoxProps
41 changes: 41 additions & 0 deletions packages/harmony/src/components/scrollbar/Scrollbar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.scrollbar {
height: 100%;
min-height: 0;
}

.scrollbar :global(.ps__thumb-y) {
opacity: 0.5;
background-color: var(--neutral-dark-3);
}

.scrollbar :global(.ps__rail-y) {
margin-top: var(--unit);
margin-bottom: var(--unit);
transition: background-color 0.2s ease-in-out, opacity 0.2s ease-in-out;
}

/* Cancel the default of perfect scroll, which always shows the scrollbar when hovering over container. Our desired behavior is to
only show the scrollbar for ~1s, then fade away if not scrolling. */
:global(.ps:hover).scrollbar > :global(.ps__rail-y),
:global(.ps:hover).scrollbar > :global(.ps__rail-x) {
opacity: 0;
}

:global(.scrollbar--hovered-visible).scrollbar:not(:global(.ps--scrolling-y))
> :global(.ps__rail-y),
:global(.scrollbar--hovered-visible).scrollbar:not(:global(.ps--scrolling-x))
> :global(.ps__rail-x) {
opacity: 0.6;
}

:global(.ps--focus).scrollbar > :global(.ps__rail-x),
:global(.ps--focus).scrollbar > :global(.ps__rail-y),
:global(.ps--scrolling-x).scrollbar > :global(.ps__rail-x),
:global(.ps--scrolling-y).scrollbar > :global(.ps__rail-y) {
opacity: 0.6 !important;
}

.scrollbar > :global(.ps__rail-y):hover,
.scrollbar > :global(.ps__rail-x):hover {
opacity: 0.9 !important;
}
63 changes: 63 additions & 0 deletions packages/harmony/src/components/scrollbar/Scrollbar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { StoryObj } from '@storybook/react'

import { TextLink } from 'components/text-link'

import { Text } from '../text'

import { Scrollbar } from '.'

export default {
component: Scrollbar,
title: 'Components/Scrollbar'
}

type Story = StoryObj<typeof Scrollbar>

export const Primary: Story = {
render: () => (
<div css={{ height: 200 }}>
<Scrollbar>
<Text variant='body'>
<Text>
This is some content in a scrollable container. The surrounding
`Scrollbar` component gives this a custom scrollbar.
</Text>
<Text>
`Scrollbar` is meant to be used for small scrolling areas within a
page/view (e.g. a scrolling navigation bar), not the entire page
itself.
</Text>
<Text>
`Scrollbar` uses{' '}
<TextLink
href='https://www.npmjs.com/package/react-perfect-scrollbar'
isExternal
>
react-perfect-scrollbar
</TextLink>{' '}
under the hood. For advanced use cases, refer to the documentation.
</Text>
<Text style={{ paddingTop: 200 }}>
This is some content in a scrollable container. The surrounding
`Scrollbar` component gives this a custom scrollbar.
</Text>
<Text>
`Scrollbar` is meant to be used for small scrolling areas within a
page/view (e.g. a scrolling navigation bar), not the entire page
itself.
</Text>
<Text>
`Scrollbar` uses{' '}
<TextLink
href='https://www.npmjs.com/package/react-perfect-scrollbar'
isExternal
>
react-perfect-scrollbar
</TextLink>{' '}
under the hood. For advanced use cases, refer to the documentation.
</Text>
</Text>
</Scrollbar>
</div>
)
}
85 changes: 85 additions & 0 deletions packages/harmony/src/components/scrollbar/Scrollbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useEffect, useRef, forwardRef, Ref, useCallback, useId } from 'react'

import { ResizeObserver } from '@juggle/resize-observer'
import cn from 'classnames'
import PerfectScrollbar from 'react-perfect-scrollbar'
import useMeasure from 'react-use-measure'

import styles from './Scrollbar.module.css'
import { ScrollbarProps } from './types'

/**
* A container with a custom scrollbar, meant to be used for small scrolling areas within a
* page/view (e.g. a scrolling navigation bar), not the entire page itself.
* `Scrollbar` uses react-perfect-scrollbar (https://www.npmjs.com/package/react-perfect-scrollbar)
* under the hood. For advanced use cases, refer to the documentation.
*/
export const Scrollbar = forwardRef(
(
{ children, className, id, forward, isHidden, ...props }: ScrollbarProps,
forwardedRef: Ref<PerfectScrollbar>
) => {
// Do not remove:
// useMeasure ref is required for infinite scrolling to work
const [ref] = useMeasure({ polyfill: ResizeObserver })
const timerRef = useRef<NodeJS.Timeout | null>(null)
const reactId = useId()
const elementId = id || reactId

useEffect(() => {
return () => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current)
}
}
}, [])

const hideScrollbar = useCallback(() => {
const element = document.getElementById(elementId)
if (element) {
element.classList.remove('scrollbar--hovered-visible')
}
if (timerRef.current !== null) {
clearTimeout(timerRef.current)
}
}, [elementId, timerRef])

const showScrollbar = useCallback(() => {
if (isHidden) return
const element = document.getElementById(elementId)
if (element) {
element.classList.add('scrollbar--hovered-visible')
}
if (timerRef.current !== null) {
clearTimeout(timerRef.current)
}
timerRef.current = setTimeout(() => {
const element = document.getElementById(elementId)
if (element) {
element.classList.remove('scrollbar--hovered-visible')
}
}, 1400)
}, [elementId, timerRef, isHidden])

useEffect(() => {
if (isHidden) {
hideScrollbar()
}
}, [isHidden, hideScrollbar])

const content = forward ? children : <div ref={ref}>{children}</div>

return (
<PerfectScrollbar
{...props}
ref={forwardedRef}
id={elementId}
className={cn(styles.scrollbar, className)}
onMouseEnter={showScrollbar}
onMouseLeave={hideScrollbar}
>
{content}
</PerfectScrollbar>
)
}
)
2 changes: 2 additions & 0 deletions packages/harmony/src/components/scrollbar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Scrollbar } from './Scrollbar'
export { ScrollbarProps } from './types'
Loading

0 comments on commit 9e0131e

Please sign in to comment.