radix select

This commit is contained in:
Sanster
2022-04-28 13:57:22 +08:00
parent bf1e990f00
commit a297a6d3d0
14 changed files with 499 additions and 303 deletions

View File

@@ -1,17 +1,30 @@
.modal-mask {
z-index: 9999;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
position: fixed;
z-index: 9998;
inset: 0;
background-color: var(--model-mask-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
animation: opacityReveal 150ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes contentShow {
0% {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
.modal {
background-color: var(--page-bg);
z-index: 9999;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: grid;
grid-auto-rows: max-content;
row-gap: 2rem;
@@ -19,6 +32,10 @@
padding: 2rem;
border-radius: 0.95rem;
&:focus {
outline: none;
}
.modal-header {
display: grid;
grid-template-columns: repeat(2, auto);
@@ -28,4 +45,6 @@
justify-self: end;
}
}
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

View File

@@ -1,6 +1,6 @@
import { XIcon } from '@heroicons/react/outline'
import React, { ReactNode, useRef } from 'react'
import { useClickAway, useKey, useKeyPress, useKeyPressEvent } from 'react-use'
import React, { ReactNode } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import Button from './Button'
export interface ModalProps {
@@ -11,34 +11,35 @@ export interface ModalProps {
className?: string
}
export default function Modal(props: ModalProps) {
const Modal = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Root>,
ModalProps
>((props, forwardedRef) => {
const { show, children, onClose, className, title } = props
const ref = useRef(null)
useClickAway(ref, () => {
if (show) {
const onOpenChange = (open: boolean) => {
if (!open) {
onClose?.()
}
})
useKeyPressEvent('Escape', e => {
if (show) {
onClose?.()
}
})
}
return (
<div
className="modal-mask"
style={{ visibility: show === true ? 'visible' : 'hidden' }}
>
<div ref={ref} className={`modal ${className}`}>
<div className="modal-header">
<h2>{title}</h2>
<Button icon={<XIcon />} onClick={onClose} />
</div>
{children}
</div>
</div>
<DialogPrimitive.Root open={show} onOpenChange={onOpenChange}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="modal-mask" />
<DialogPrimitive.Content
ref={forwardedRef}
className={`modal ${className}`}
>
<div className="modal-header">
<DialogPrimitive.Title>{title}</DialogPrimitive.Title>
<Button icon={<XIcon />} onClick={onClose} />
</div>
{children}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
)
}
})
export default Modal

View File

@@ -2,8 +2,7 @@
all: unset;
flex: 1 0 auto;
border-radius: 0.5rem;
padding: 0.2rem 0.8rem;
line-height: 1;
padding: 0.4rem 0.8rem;
outline: 1px solid var(--border-color);
&:focus-visible {

View File

@@ -1,26 +1,14 @@
@use '../../styles/Mixins' as *;
.selector {
position: relative;
display: flex;
flex-direction: column;
.select-trigger {
all: unset;
display: inline-flex;
align-items: center;
justify-content: space-between;
}
.selector-main {
@include accented-display(var(white));
width: 100%;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
outline: none;
border-radius: 0.5rem;
height: 38px;
gap: 8px;
padding: 0.2rem 0.8rem;
padding: 0 0.8rem;
border: 1px solid var(--border-color);
background-color: var(--page-bg);
color: var(--options-text-color);
svg {
@@ -28,47 +16,52 @@
height: 1rem;
margin-top: 0.25rem;
}
&:hover {
border-color: var(--yellow-accent);
}
// &:focus-visible {
// border-color: var(--yellow-accent);
// }
}
.selector-options {
@include accented-display(var(--btn-primary-bg));
width: 100%;
padding: 0;
display: grid;
justify-self: center;
position: absolute;
cursor: pointer;
top: 3rem;
color: var(--options-text-color);
.select-content {
overflow: hidden;
background-color: var(--page-bg);
border-radius: 0.5rem;
}
.select-viewport {
border: 1px solid var(--border-color);
border-radius: 0.5rem;
padding: 5px;
}
border-radius: 0.6rem;
.select-item {
all: unset;
background-color: var(--page-bg);
color: var(--options-text-color);
display: flex;
align-items: center;
border-radius: 0.5rem;
@include mobile {
bottom: 11.5rem;
}
padding: 6px 6px 6px 25px;
position: relative;
user-select: none;
.selector-option {
display: flex;
align-items: center;
user-select: none;
padding: 0.5rem 0.8rem;
&:first-of-type {
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
}
&:last-of-type {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
&:hover {
background-color: var(--yellow-accent);
color: var(--btn-text-hover-color);
}
&:focus {
color: var(--btn-text-hover-color);
background-color: var(--yellow-accent);
}
}
.select-item-indicator {
position: absolute;
left: 0;
width: 25px;
padding-right: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,86 +1,85 @@
import React, { MutableRefObject, useCallback, useRef, useState } from 'react'
import { useClickAway, useKeyPressEvent } from 'react-use'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/outline'
import React, { useRef } from 'react'
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@heroicons/react/outline'
import * as Select from '@radix-ui/react-select'
import { relative } from 'path'
type SelectorChevronDirection = 'up' | 'down'
type SelectorProps = {
minWidth?: number
chevronDirection?: SelectorChevronDirection
interface Props {
width?: number
value: string
options: string[]
chevronDirection?: SelectorChevronDirection
autoFocusAfterClose?: boolean
onChange: (value: string) => void
}
const selectorDefaultProps = {
minWidth: 128,
chevronDirection: 'down',
}
const Selector = (props: Props) => {
const {
width,
value,
chevronDirection,
options,
autoFocusAfterClose,
onChange,
} = props
function Selector(props: SelectorProps) {
const { minWidth, chevronDirection, value, options, onChange } = props
const [showOptions, setShowOptions] = useState<boolean>(false)
const selectorRef = useRef<HTMLDivElement | null>(null)
const contentRef = useRef<HTMLButtonElement>(null)
const showOptionsHandler = () => {
// console.log(selectorRef.current?.focus)
// selectorRef?.current?.focus()
setShowOptions(currentShowOptionsState => !currentShowOptionsState)
}
useClickAway(selectorRef, () => {
setShowOptions(false)
})
// TODO: how to prevent Modal close?
// useKeyPressEvent('Escape', (e: KeyboardEvent) => {
// if (showOptions === true) {
// console.log(`selector ${e}`)
// e.preventDefault()
// e.stopPropagation()
// setShowOptions(false)
// }
// })
const onOptionClick = (e: any, newIndex: number) => {
const currentRes = e.target.textContent.split('x')
onChange(currentRes[0])
setShowOptions(false)
const onOpenChange = (open: boolean) => {
if (!open) {
if (!autoFocusAfterClose) {
// 如果使用 Select.Content 的 onCloseAutoFocus 来取消 focus防止空格继续打开这个 select
// 会导致其它快捷键失效,原因未知
window.setTimeout(() => {
contentRef?.current?.blur()
}, 100)
}
}
}
return (
<div className="selector" ref={selectorRef} style={{ minWidth }}>
<div
className="selector-main"
role="button"
onClick={showOptionsHandler}
aria-hidden="true"
<Select.Root
value={value}
onValueChange={onChange}
onOpenChange={onOpenChange}
>
<Select.Trigger
className="select-trigger"
style={{ width }}
ref={contentRef}
>
<p>{value}</p>
<div className="selector-icon">
<Select.Value />
<Select.Icon>
{chevronDirection === 'up' ? <ChevronUpIcon /> : <ChevronDownIcon />}
</div>
</div>
</Select.Icon>
</Select.Trigger>
{showOptions && (
<div className="selector-options">
{options.map((val, _index) => (
<div
className="selector-option"
role="button"
tabIndex={0}
key={val}
onClick={e => onOptionClick(e, _index)}
aria-hidden="true"
>
{val}
</div>
<Select.Content className="select-content">
<Select.Viewport className="select-viewport">
{options.map(val => (
<Select.Item value={val} className="select-item" key={val}>
<Select.ItemText>{val}</Select.ItemText>
<Select.ItemIndicator className="select-item-indicator">
<CheckIcon />
</Select.ItemIndicator>
</Select.Item>
))}
</div>
)}
</div>
</Select.Viewport>
</Select.Content>
</Select.Root>
)
}
const selectorDefaultProps = {
chevronDirection: 'down',
autoFocusAfterClose: true,
}
Selector.defaultProps = selectorDefaultProps
export default Selector