forked from geist-org/geist-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
554 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import Select from './select' | ||
import SelectOption from './select-option' | ||
|
||
Select.Option = SelectOption | ||
|
||
export default Select |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import React from 'react' | ||
import { NormalSizes } from '../utils/prop-types' | ||
|
||
export interface SelectConfig { | ||
value?: string | ||
updateValue?: Function | ||
visible?: boolean | ||
updateVisible?: Function | ||
size?: NormalSizes | ||
disableAll?: boolean | ||
} | ||
|
||
const defaultContext = { | ||
visible: false, | ||
size: 'medium' as NormalSizes, | ||
disableAll: false, | ||
} | ||
|
||
export const SelectContext = React.createContext<SelectConfig>(defaultContext) | ||
|
||
export const useSelectContext = (): SelectConfig => React.useContext<SelectConfig>(SelectContext) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import React from 'react' | ||
import withDefaults from '../utils/with-defaults' | ||
|
||
interface Props { | ||
width?: string | ||
} | ||
|
||
const defaultProps = { | ||
width: '1.25em' | ||
} | ||
|
||
export type SelectIconProps = Props & typeof defaultProps | ||
|
||
const SelectIcon: React.FC<SelectIconProps> = React.memo(({ | ||
width, | ||
}) => { | ||
|
||
return ( | ||
<svg viewBox="0 0 24 24" width={width} height={width} strokeWidth="1" strokeLinecap="round" | ||
strokeLinejoin="round" fill="none" shapeRendering="geometricPrecision"> | ||
<path d="M6 9l6 6 6-6" /> | ||
<style jsx>{` | ||
svg { | ||
color: inherit; | ||
stroke: currentColor; | ||
transition: all 200ms ease; | ||
} | ||
`}</style> | ||
</svg> | ||
) | ||
}) | ||
|
||
export default withDefaults(SelectIcon, defaultProps) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import React, { useMemo } from 'react' | ||
import withDefaults from '../utils/with-defaults' | ||
import useTheme from '../styles/use-theme' | ||
import { useSelectContext } from './select-context' | ||
|
||
interface Props { | ||
value: string | ||
disabled?: boolean | ||
className?: string | ||
preventAllEvents?: boolean | ||
} | ||
|
||
const defaultProps = { | ||
disabled: false, | ||
className: '', | ||
preventAllEvents: false, | ||
} | ||
|
||
export type SelectOptionProps = Props & typeof defaultProps & React.HTMLAttributes<any> | ||
|
||
const SelectOption: React.FC<React.PropsWithChildren<SelectOptionProps>> = ({ | ||
value: identValue, className, children, disabled, preventAllEvents, ...props | ||
}) => { | ||
const theme = useTheme() | ||
const { updateValue, value, disableAll } = useSelectContext() | ||
const isDisabled = useMemo(() => disabled || disableAll, [disabled, disableAll]) | ||
if (identValue === undefined) { | ||
console.error('[Select Option]: the props "value" is required.') | ||
} | ||
|
||
const selected = useMemo(() => value ? identValue === value : false, [identValue, value]) | ||
|
||
const bgColor = useMemo(() => { | ||
if (isDisabled) return theme.palette.accents_1 | ||
return selected ? theme.palette.accents_1 : theme.palette.background | ||
}, [selected, isDisabled, theme.palette]) | ||
|
||
const color = useMemo(() => { | ||
if (isDisabled) return theme.palette.accents_4 | ||
return selected ? theme.palette.foreground : theme.palette.accents_5 | ||
}, [selected, isDisabled, theme.palette]) | ||
|
||
const clickHandler = (event: React.MouseEvent<HTMLDivElement>) => { | ||
if (preventAllEvents) return | ||
event.stopPropagation() | ||
event.nativeEvent.stopImmediatePropagation() | ||
event.preventDefault() | ||
if (isDisabled) return | ||
updateValue && updateValue(identValue) | ||
} | ||
|
||
return ( | ||
<> | ||
<div className={className} onClick={clickHandler} {...props}>{children}</div> | ||
|
||
<style jsx>{` | ||
div { | ||
display: flex; | ||
justify-content: flex-start; | ||
align-items: center; | ||
font-weight: normal; | ||
white-space: pre; | ||
font-size: .75rem; | ||
height: calc(1.688 * ${theme.layout.gap}); | ||
padding: 0 ${theme.layout.gapHalf}; | ||
background-color: ${bgColor}; | ||
color: ${color}; | ||
user-select: none; | ||
border: 0; | ||
cursor: ${isDisabled ? 'not-allowed' : 'pointer'}; | ||
transition: background 0.2s ease 0s, border-color 0.2s ease 0s; | ||
} | ||
div:first-of-type { | ||
border-top-left-radius: ${theme.layout.radius}; | ||
border-top-right-radius: ${theme.layout.radius}; | ||
} | ||
div:last-of-type { | ||
border-bottom-left-radius: ${theme.layout.radius}; | ||
border-bottom-right-radius: ${theme.layout.radius}; | ||
} | ||
div:hover { | ||
background-color: ${theme.palette.accents_1}; | ||
} | ||
`}</style> | ||
</> | ||
) | ||
} | ||
|
||
export default withDefaults(SelectOption, defaultProps) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react' | ||
import useTheme from '../styles/use-theme' | ||
import SelectOption from './select-option' | ||
import SelectIcon from './select-icon' | ||
import Dropdown from '../shared/dropdown' | ||
import { ZeitUIThemes } from '../styles/themes' | ||
import { SelectContext, SelectConfig } from './select-context' | ||
import { NormalSizes } from '../utils/prop-types' | ||
import { getSizes } from './styles' | ||
import { pickChildByProps, pickChildrenFirst } from '../utils/collections' | ||
|
||
interface Props { | ||
disabled?: boolean | ||
size?: NormalSizes | ||
initialValue?: string | ||
placeholder?: React.ReactNode | string | ||
icon?: React.ReactNode | ||
onChange?: (value: string) => void | ||
pure?: boolean | ||
className?: string | ||
} | ||
|
||
const defaultProps = { | ||
disabled: false, | ||
size: 'medium' as NormalSizes, | ||
icon: SelectIcon, | ||
pure: false, | ||
className: '', | ||
} | ||
|
||
export type SelectProps = Props & typeof defaultProps & React.HTMLAttributes<any> | ||
|
||
const getDropdown = ( | ||
ref: MutableRefObject<HTMLDivElement | null>, | ||
children: React.ReactNode | null, | ||
theme: ZeitUIThemes, | ||
visible: boolean, | ||
) => ( | ||
<Dropdown parent={ref} visible={visible}> | ||
<div className="select-dropdown"> | ||
{children} | ||
<style jsx>{` | ||
.select-dropdown { | ||
border-radius: ${theme.layout.radius}; | ||
box-shadow: ${theme.expressiveness.shadowLarge}; | ||
} | ||
`}</style> | ||
</div> | ||
</Dropdown> | ||
) | ||
|
||
const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({ | ||
children, size, disabled, initialValue: init, placeholder, | ||
icon: Icon, onChange, className, pure, ...props | ||
}) => { | ||
const theme = useTheme() | ||
const ref = useRef<HTMLDivElement>(null) | ||
const [visible, setVisible] = useState<boolean>(false) | ||
const [value, setValue] = useState<string | undefined>(init) | ||
const sizes = useMemo(() => getSizes(theme, size), [theme, size]) | ||
|
||
const updateVisible = (next: boolean) => setVisible(next) | ||
const updateValue = (next: string) => { | ||
setValue(next) | ||
onChange && onChange(next) | ||
setVisible(false) | ||
} | ||
|
||
const initialValue: SelectConfig = useMemo(() => ({ | ||
value, visible, updateValue, updateVisible, | ||
size, disableAll: disabled, | ||
}), [visible, size, disabled]) | ||
|
||
const clickHandler = (event: React.MouseEvent<HTMLDivElement>) => { | ||
event.stopPropagation() | ||
event.nativeEvent.stopImmediatePropagation() | ||
event.preventDefault() | ||
if (disabled) return | ||
setVisible(!visible) | ||
} | ||
|
||
useEffect(() => { | ||
const closeHandler = () => setVisible(false) | ||
document.addEventListener('click', closeHandler) | ||
return () => document.removeEventListener('click', closeHandler) | ||
}, []) | ||
|
||
const selectedChild = useMemo(() => { | ||
const [, optionChildren] = pickChildByProps(children, 'value', value) | ||
const child = pickChildrenFirst(optionChildren) | ||
if (!React.isValidElement(child)) return optionChildren | ||
return React.cloneElement(child, { preventAllEvents: true }) | ||
}, [value, children]) | ||
|
||
return ( | ||
<SelectContext.Provider value={initialValue}> | ||
<div className={`select ${className}`} ref={ref} onClick={clickHandler} {...props}> | ||
{!value && <span className="value placeholder">{placeholder}</span>} | ||
{value && <span className="value">{selectedChild}</span>} | ||
{getDropdown(ref, children, theme, visible)} | ||
{!pure && <div className="icon"><Icon /></div>} | ||
<style jsx>{` | ||
.select { | ||
display: inline-flex; | ||
align-items: center; | ||
user-select: none; | ||
white-space: nowrap; | ||
position: relative; | ||
cursor: ${disabled ? 'not-allowed' : 'pointer'}; | ||
max-width: 80vw; | ||
width: initial; | ||
overflow: hidden; | ||
transition: border 0.2s ease 0s, color 0.2s ease-out 0s, box-shadow 0.2s ease 0s; | ||
border: 1px solid ${theme.palette.border}; | ||
border-radius: ${theme.layout.radius}; | ||
padding: 0 ${theme.layout.gapQuarter} 0 ${theme.layout.gapHalf}; | ||
height: ${sizes.height}; | ||
min-width: ${sizes.minWidth}; | ||
background-color: ${disabled ? theme.palette.accents_1 : theme.palette.background}; | ||
} | ||
.select:hover { | ||
border-color: ${disabled ? theme.palette.border : theme.palette.foreground}; | ||
} | ||
.select:hover .icon { | ||
color: ${disabled ? theme.palette.accents_5 : theme.palette.foreground} | ||
} | ||
.value { | ||
display: inline-flex; | ||
flex: 1; | ||
height: 100%; | ||
align-items: center; | ||
line-height: 1; | ||
padding: 0; | ||
margin-right: 1.25rem; | ||
font-size: ${sizes.fontSize}; | ||
color: ${disabled ? theme.palette.accents_4 : theme.palette.foreground}; | ||
width: calc(100% - 1.25rem); | ||
} | ||
.value > :global(div), .value > :global(div:hover) { | ||
border-radius: 0; | ||
background-color: transparent; | ||
padding: 0; | ||
margin: 0; | ||
color: inherit; | ||
} | ||
.placeholder { | ||
color: ${theme.palette.accents_3}; | ||
} | ||
.icon { | ||
position: absolute; | ||
right: ${theme.layout.gapQuarter}; | ||
font-size: ${sizes.fontSize}; | ||
top: 50%; | ||
bottom: 0; | ||
transform: translateY(-50%) rotate(${visible ? '180' : '0'}deg); | ||
pointer-events: none; | ||
transition: transform 200ms ease; | ||
display: flex; | ||
align-items: center; | ||
color: ${theme.palette.accents_5}; | ||
} | ||
`}</style> | ||
</div> | ||
</SelectContext.Provider> | ||
) | ||
} | ||
|
||
type SelectComponent<P = {}> = React.FC<P> & { | ||
Option: typeof SelectOption | ||
} | ||
|
||
type ComponentProps = Partial<typeof defaultProps> & Omit<Props, keyof typeof defaultProps> | ||
|
||
(Select as SelectComponent<ComponentProps>).defaultProps = defaultProps | ||
|
||
export default Select as SelectComponent<ComponentProps> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { NormalSizes } from 'components/utils/prop-types' | ||
import { ZeitUIThemes } from 'components/styles/themes' | ||
|
||
export interface SelectSize { | ||
height: string | ||
fontSize: string | ||
minWidth: string | ||
} | ||
|
||
export const getSizes = (theme: ZeitUIThemes, size?: NormalSizes) => { | ||
const sizes: { [key in NormalSizes]: SelectSize } = { | ||
medium: { | ||
height: `calc(1.688 * ${theme.layout.gap})`, | ||
fontSize: '.875rem', | ||
minWidth: '10rem', | ||
}, | ||
small: { | ||
height: `calc(1.344 * ${theme.layout.gap})`, | ||
fontSize: '.75rem', | ||
minWidth: '8rem', | ||
}, | ||
mini: { | ||
height: `calc(1 * ${theme.layout.gap})`, | ||
fontSize: '.75rem', | ||
minWidth: '6.5rem', | ||
}, | ||
large: { | ||
height: `calc(2 * ${theme.layout.gap})`, | ||
fontSize: '1.225rem', | ||
minWidth: '12.5rem', | ||
}, | ||
} | ||
|
||
return size ? sizes[size] : sizes.medium | ||
} | ||
|
Oops, something went wrong.