= React.ForwardRefExoticComponent<
+ PropsWithoutRef & RefAttributes
+>
-export default withDefaults(ModalAction, defaultProps)
+type ComponentProps = Partial &
+ Omit &
+ Partial>
+
+ModalAction.defaultProps = defaultProps
+
+export default ModalAction as ModalActionComponent
diff --git a/components/modal/modal-actions.tsx b/components/modal/modal-actions.tsx
index 0c2d088..443c3ac 100644
--- a/components/modal/modal-actions.tsx
+++ b/components/modal/modal-actions.tsx
@@ -22,7 +22,7 @@ const ModalActions: React.FC> = ({ children, ...prop
border-bottom-right-radius: ${theme.layout.radius};
}
- footer > :global(button + button) {
+ footer > :global(button.btn + button.btn) {
border-left: 1px solid ${theme.palette.border};
}
diff --git a/components/modal/modal-content.tsx b/components/modal/modal-content.tsx
index ac7c45b..43561bf 100644
--- a/components/modal/modal-content.tsx
+++ b/components/modal/modal-content.tsx
@@ -26,6 +26,7 @@ const ModalContent: React.FC = ({ className, children, ...pro
margin: 0 -${theme.layout.gap};
padding: ${theme.layout.gap} ${theme.layout.gap} ${theme.layout.gapHalf};
overflow-y: auto;
+ position: relative;
}
.content > :global(*:first-child) {
diff --git a/components/modal/modal-wrapper.tsx b/components/modal/modal-wrapper.tsx
index 568f81e..35e79b3 100644
--- a/components/modal/modal-wrapper.tsx
+++ b/components/modal/modal-wrapper.tsx
@@ -1,7 +1,8 @@
-import React from 'react'
+import React, { useEffect, useRef } from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
import CSSTransition from '../shared/css-transition'
+import { isChildElement } from '../utils/collections'
interface Props {
className?: string
@@ -24,15 +25,49 @@ const ModalWrapper: React.FC> = ({
...props
}) => {
const theme = useTheme()
+ const modalContent = useRef(null)
+ const tabStart = useRef(null)
+ const tabEnd = useRef(null)
+
+ useEffect(() => {
+ if (!visible) return
+ const activeElement = document.activeElement
+ const isChild = isChildElement(modalContent.current, activeElement)
+ if (isChild) return
+ tabStart.current && tabStart.current.focus()
+ }, [visible])
+
+ const onKeyDown = (event: React.KeyboardEvent) => {
+ const isTabDown = event.keyCode === 9
+ if (!visible || !isTabDown) return
+ const activeElement = document.activeElement
+ if (event.shiftKey) {
+ if (activeElement === tabStart.current) {
+ tabEnd.current && tabEnd.current.focus()
+ }
+ } else {
+ if (activeElement === tabEnd.current) {
+ tabStart.current && tabStart.current.focus()
+ }
+ }
+ }
return (
-
+
diff --git a/components/popover/popover-item.tsx b/components/popover/popover-item.tsx
index 21c5c5d..ba13aac 100644
--- a/components/popover/popover-item.tsx
+++ b/components/popover/popover-item.tsx
@@ -39,7 +39,6 @@ const PopoverItem: React.FC
> = ({
line-height: 1.25rem;
text-align: left;
transition: color 0.1s ease 0s, background-color 0.1s ease 0s;
- width: max-content;
}
.item:hover {
@@ -50,6 +49,12 @@ const PopoverItem: React.FC> = ({
margin: 0;
}
+ .item > :global(.link) {
+ width: 100%;
+ padding: 0.5rem ${theme.layout.gap};
+ margin: -0.5rem -${theme.layout.gap};
+ }
+
.item.line {
line-height: 0;
height: 0;
diff --git a/components/select/select-dropdown.tsx b/components/select/select-dropdown.tsx
index 25358e0..356451c 100644
--- a/components/select/select-dropdown.tsx
+++ b/components/select/select-dropdown.tsx
@@ -9,6 +9,7 @@ interface Props {
className?: string
dropdownStyle?: object
disableMatchWidth?: boolean
+ getPopupContainer?: () => HTMLElement | null
}
const defaultProps = {
@@ -25,12 +26,17 @@ const SelectDropdown: React.FC> = (
className,
dropdownStyle,
disableMatchWidth,
+ getPopupContainer,
}) => {
const theme = useTheme()
const { ref } = useSelectContext()
return (
-
+
{children}
"
`;
diff --git a/components/shared/__tests__/dropdown.test.tsx b/components/shared/__tests__/dropdown.test.tsx
index 95f2507..49b39b5 100644
--- a/components/shared/__tests__/dropdown.test.tsx
+++ b/components/shared/__tests__/dropdown.test.tsx
@@ -164,4 +164,24 @@ describe('Dropdown', () => {
expect(() => wrapper.unmount()).not.toThrow()
})
+
+ it('should render to specified container', () => {
+ const Mock: React.FC<{}> = () => {
+ const ref = useRef(null)
+ const customContainer = useRef(null)
+ return (
+
+
+
+ customContainer.current}>
+ test-value
+
+
+
+ )
+ }
+ const wrapper = mount()
+ const customContainer = wrapper.find('#custom')
+ expect(customContainer.html()).toContain('dropdown')
+ })
})
diff --git a/components/shared/backdrop.tsx b/components/shared/backdrop.tsx
index eb2fc80..425f1b9 100644
--- a/components/shared/backdrop.tsx
+++ b/components/shared/backdrop.tsx
@@ -12,13 +12,13 @@ interface Props {
const defaultProps = {
onClick: () => {},
visible: false,
- offsetY: 0,
}
-export type BackdropProps = Props & typeof defaultProps
+type NativeAttrs = Omit, keyof Props>
+export type BackdropProps = Props & typeof defaultProps & NativeAttrs
const Backdrop: React.FC> = React.memo(
- ({ children, onClick, visible }) => {
+ ({ children, onClick, visible, ...props }) => {
const theme = useTheme()
const [, setIsContentMouseDown, IsContentMouseDownRef] = useCurrentState(false)
const clickHandler = (event: MouseEvent) => {
@@ -37,8 +37,8 @@ const Backdrop: React.FC> = React.memo(
}
return (
-
-
+
+
> = React.memo(
align-content: center;
align-items: center;
flex-direction: column;
- justify-content: space-around;
+ justify-content: center;
height: 100vh;
width: 100vw;
overflow: auto;
@@ -93,6 +93,22 @@ const Backdrop: React.FC> = React.memo(
pointer-events: none;
z-index: 1000;
}
+
+ .backdrop-wrapper-enter .layer {
+ opacity: 0;
+ }
+
+ .backdrop-wrapper-enter-active .layer {
+ opacity: ${theme.expressiveness.portalOpacity};
+ }
+
+ .backdrop-wrapper-leave .layer {
+ opacity: ${theme.expressiveness.portalOpacity};
+ }
+
+ .backdrop-wrapper-leave-active .layer {
+ opacity: 0;
+ }
`}
diff --git a/components/shared/dropdown.tsx b/components/shared/dropdown.tsx
index 6b6ecb0..50b60c5 100644
--- a/components/shared/dropdown.tsx
+++ b/components/shared/dropdown.tsx
@@ -10,6 +10,7 @@ interface Props {
parent?: MutableRefObject
| undefined
visible: boolean
disableMatchWidth?: boolean
+ getPopupContainer?: () => HTMLElement | null
}
interface ReactiveDomReact {
@@ -26,31 +27,48 @@ const defaultRect: ReactiveDomReact = {
width: 0,
}
-const getRect = (ref: MutableRefObject): ReactiveDomReact => {
+const getOffset = (el?: HTMLElement | null | undefined) => {
+ if (!el)
+ return {
+ top: 0,
+ left: 0,
+ }
+ const { top, left } = el.getBoundingClientRect()
+ return { top, left }
+}
+
+const getRect = (
+ ref: MutableRefObject,
+ getContainer?: () => HTMLElement | null,
+): ReactiveDomReact => {
if (!ref || !ref.current) return defaultRect
const rect = ref.current.getBoundingClientRect()
+ const container = getContainer ? getContainer() : null
+ const scrollElement = container || document.documentElement
+ const { top: offsetTop, left: offsetLeft } = getOffset(container)
+
return {
...rect,
width: rect.width || rect.right - rect.left,
- top: rect.bottom + document.documentElement.scrollTop,
- left: rect.left + document.documentElement.scrollLeft,
+ top: rect.bottom + scrollElement.scrollTop - offsetTop,
+ left: rect.left + scrollElement.scrollLeft - offsetLeft,
}
}
const Dropdown: React.FC> = React.memo(
- ({ children, parent, visible, disableMatchWidth }) => {
- const el = usePortal('dropdown')
+ ({ children, parent, visible, disableMatchWidth, getPopupContainer }) => {
+ const el = usePortal('dropdown', getPopupContainer)
const [rect, setRect] = useState(defaultRect)
if (!parent) return null
const updateRect = () => {
- const { top, left, right, width: nativeWidth } = getRect(parent)
+ const { top, left, right, width: nativeWidth } = getRect(parent, getPopupContainer)
setRect({ top, left, right, width: nativeWidth })
}
useResize(updateRect)
useClickAnyWhere(() => {
- const { top, left } = getRect(parent)
+ const { top, left } = getRect(parent, getPopupContainer)
const shouldUpdatePosition = top !== rect.top || left !== rect.left
if (!shouldUpdatePosition) return
updateRect()
diff --git a/components/snippet/__tests__/__snapshots__/index.test.tsx.snap b/components/snippet/__tests__/__snapshots__/index.test.tsx.snap
index 5a7ff70..5212bc3 100644
--- a/components/snippet/__tests__/__snapshots__/index.test.tsx.snap
+++ b/components/snippet/__tests__/__snapshots__/index.test.tsx.snap
@@ -58,6 +58,122 @@ exports[`Snippet should render correctly 1`] = `
"
`;
+exports[`Snippet should work with custom symbol 1`] = `
+""
+`;
+
+exports[`Snippet should work with custom symbol 2`] = `
+""
+`;
+
exports[`Snippet should work with different styles 1`] = `
""
+ textarea:-webkit-autofill,
+ textarea:-webkit-autofill:hover,
+ textarea:-webkit-autofill:active,
+ textarea:-webkit-autofill:focus {
+ -webkit-box-shadow: 0 0 0 30px #fff inset !important;
+ }
+
"
`;
exports[`Textarea should work with different styles 1`] = `
""
+ textarea:-webkit-autofill,
+ textarea:-webkit-autofill:hover,
+ textarea:-webkit-autofill:active,
+ textarea:-webkit-autofill:focus {
+ -webkit-box-shadow: 0 0 0 30px #fff inset !important;
+ }
+ "
`;
diff --git a/components/textarea/textarea.tsx b/components/textarea/textarea.tsx
index 1cffa1c..f563830 100644
--- a/components/textarea/textarea.tsx
+++ b/components/textarea/textarea.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useState } from 'react'
+import React, { useRef, useImperativeHandle, useEffect, useMemo, useState } from 'react'
import useTheme from '../styles/use-theme'
import withDefaults from '../utils/with-defaults'
import { NormalTypes } from '../utils/prop-types'
@@ -32,113 +32,130 @@ const defaultProps = {
type NativeAttrs = Omit, keyof Props>
export type TextareaProps = Props & typeof defaultProps & NativeAttrs
-const Textarea: React.FC> = ({
- width,
- status,
- minHeight,
- disabled,
- readOnly,
- onFocus,
- onBlur,
- className,
- initialValue,
- onChange,
- value,
- placeholder,
- ...props
-}) => {
- const theme = useTheme()
- const [selfValue, setSelfValue] = useState(initialValue)
- const [hover, setHover] = useState(false)
- const { color, borderColor, hoverBorder } = useMemo(() => getColors(theme.palette, status), [
- theme.palette,
- status,
- ])
+const Textarea = React.forwardRef>(
+ (
+ {
+ width,
+ status,
+ minHeight,
+ disabled,
+ readOnly,
+ onFocus,
+ onBlur,
+ className,
+ initialValue,
+ onChange,
+ value,
+ placeholder,
+ ...props
+ },
+ ref: React.Ref,
+ ) => {
+ const theme = useTheme()
+ const textareaRef = useRef(null)
+ useImperativeHandle(ref, () => textareaRef.current)
+ const isControlledComponent = useMemo(() => value !== undefined, [value])
+ const [selfValue, setSelfValue] = useState(initialValue)
+ const [hover, setHover] = useState(false)
+ const { color, borderColor, hoverBorder } = useMemo(() => getColors(theme.palette, status), [
+ theme.palette,
+ status,
+ ])
- const changeHandler = (event: React.ChangeEvent) => {
- if (disabled || readOnly) return
- setSelfValue(event.target.value)
- onChange && onChange(event)
- }
- const focusHandler = (e: React.FocusEvent) => {
- setHover(true)
- onFocus && onFocus(e)
- }
- const blurHandler = (e: React.FocusEvent) => {
- setHover(false)
- onBlur && onBlur(e)
- }
+ const changeHandler = (event: React.ChangeEvent) => {
+ if (disabled || readOnly) return
+ setSelfValue(event.target.value)
+ onChange && onChange(event)
+ }
+ const focusHandler = (e: React.FocusEvent) => {
+ setHover(true)
+ onFocus && onFocus(e)
+ }
+ const blurHandler = (e: React.FocusEvent) => {
+ setHover(false)
+ onBlur && onBlur(e)
+ }
- useEffect(() => {
- if (value === undefined) return
- setSelfValue(value)
- }, [value])
+ useEffect(() => {
+ if (isControlledComponent) {
+ setSelfValue(value as string)
+ }
+ })
- return (
-
-
-
-
- )
-}
+ .disabled > textarea {
+ cursor: not-allowed;
+ }
+
+ textarea:-webkit-autofill,
+ textarea:-webkit-autofill:hover,
+ textarea:-webkit-autofill:active,
+ textarea:-webkit-autofill:focus {
+ -webkit-box-shadow: 0 0 0 30px ${theme.palette.background} inset !important;
+ }
+ `}
+