Skip to content

Commit

Permalink
Hyperspeed POC
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan committed Jul 11, 2024
1 parent 7412f4a commit 70c8f35
Show file tree
Hide file tree
Showing 30 changed files with 236 additions and 351 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sanity/ui",
"version": "2.6.4",
"version": "2.6.4-canary.2",
"keywords": [
"sanity",
"ui",
Expand Down
21 changes: 11 additions & 10 deletions pnpm-lock.yaml

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

21 changes: 15 additions & 6 deletions src/core/components/breadcrumbs/breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import {Children, forwardRef, Fragment, isValidElement, useCallback, useMemo, useState} from 'react'
import {
Children,
forwardRef,
Fragment,
isValidElement,
useCallback,
useMemo,
useRef,
useState,
} from 'react'
import {useArrayProp, useClickOutside} from '../../hooks'
import {Box, Popover, Stack, Text} from '../../primitives'
import {ExpandButton, Root} from './breadcrumbs.styles'
Expand All @@ -22,13 +31,13 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
const {children, maxLength, separator, space: spaceRaw = 2, ...restProps} = props
const space = useArrayProp(spaceRaw)
const [open, setOpen] = useState(false)
const [expandElement, setExpandElement] = useState<HTMLButtonElement | null>(null)
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
const expandElementRef = useRef<HTMLButtonElement | null>(null)
const popoverElementRef = useRef<HTMLDivElement | null>(null)

const collapse = useCallback(() => setOpen(false), [])
const expand = useCallback(() => setOpen(true), [])

useClickOutside(collapse, [expandElement, popoverElement])
useClickOutside(collapse, () => [expandElementRef.current, popoverElementRef.current])

const rawItems = useMemo(() => Children.toArray(children).filter(isValidElement), [children])

Expand All @@ -52,14 +61,14 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
open={open}
placement="top"
portal
ref={setPopoverElement}
ref={popoverElementRef}
>
<ExpandButton
fontSize={1}
mode="bleed"
onClick={open ? collapse : expand}
padding={1}
ref={setExpandElement}
ref={expandElementRef}
selected={open}
text="…"
/>
Expand Down
33 changes: 12 additions & 21 deletions src/core/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {CloseIcon} from '@sanity/icons'
import {ThemeColorSchemeKey} from '@sanity/ui/theme'
import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'
import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef} from 'react'
import {styled} from 'styled-components'
import {
containsOrEqualsElement,
Expand Down Expand Up @@ -170,7 +170,6 @@ const DialogCard = forwardRef(function DialogCard(
const shadow = useArrayProp(shadowProp)
const width = useArrayProp(widthProp)
const ref = useRef<HTMLDivElement | null>(null)
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
const contentRef = useRef<HTMLDivElement | null>(null)
const layer = useLayer()
const {isTopLayer} = layer
Expand Down Expand Up @@ -216,32 +215,24 @@ const DialogCard = forwardRef(function DialogCard(
)

useClickOutside(
useCallback(
(event: MouseEvent) => {
if (!isTopLayer || !onClickOutside) return
(event: MouseEvent | TouchEvent) => {
if (!isTopLayer || !onClickOutside) return

const target = event.target as Node | null
const target = event.target as Node | null

if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
// Ignore clicks outside of the scope
return
}
if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
// Ignore clicks outside of the scope
return
}

onClickOutside()
},
[boundaryElement, isTopLayer, onClickOutside, portalElement],
),
[rootElement],
onClickOutside()
},
() => [ref.current],
)

const setRef = useCallback((el: HTMLDivElement | null) => {
setRootElement(el)
ref.current = el
}, [])

return (
<DialogContainer data-ui="DialogCard" width={width}>
<DialogCardRoot radius={radius} ref={setRef} scheme={scheme} shadow={shadow}>
<DialogCardRoot radius={radius} ref={ref} scheme={scheme} shadow={shadow}>
<DialogLayout direction="column">
{showHeader && (
<DialogHeader>
Expand Down
29 changes: 11 additions & 18 deletions src/core/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface MenuProps extends ResponsivePaddingProps {
* @deprecated Use `shouldFocus="last"` instead.
*/
focusLast?: boolean
onClickOutside?: (event: MouseEvent) => void
onClickOutside?: (event: MouseEvent | TouchEvent) => void
onEscape?: () => void
onItemClick?: () => void
onItemSelect?: (index: number) => void
Expand Down Expand Up @@ -75,16 +75,18 @@ export const Menu = forwardRef(function Menu(
handleItemMouseLeave,
handleKeyDown,
mount,
rootElement,
setRootElement,
} = useMenuController({onKeyDown, originElement, shouldFocus})
} = useMenuController({onKeyDown, originElement, shouldFocus, rootElementRef: ref})

const handleRefChange = useCallback(
(el: HTMLDivElement | null) => {
setRootElement(el)
ref.current = el

// Register root element (for nested menus)
if (ref.current && registerElement) {
registerElement(ref.current)
}
},
[setRootElement],
[registerElement],
)

// Trigger `onItemSelect` when active index changes
Expand All @@ -94,11 +96,8 @@ export const Menu = forwardRef(function Menu(

// Close menu when clicking outside
useClickOutside(
useCallback(
(event) => isTopLayer && onClickOutside && onClickOutside(event),
[isTopLayer, onClickOutside],
),
[rootElement],
(event) => isTopLayer && onClickOutside && onClickOutside(event),
() => [ref.current],
)

// Close menu when pressing Escape
Expand All @@ -116,13 +115,7 @@ export const Menu = forwardRef(function Menu(
),
)

// Register root element (for nested menus)
useEffect(() => {
if (!rootElement || !registerElement) return

return registerElement(rootElement)
}, [registerElement, rootElement])

// @TODO split out into separate contexts
const value: MenuContextValue = useMemo(
() => ({
version: 0.0,
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/menu/menuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const MenuButton = forwardRef(function MenuButton(
}, [])

const handleMenuClickOutside = useCallback(
(event: MouseEvent) => {
(event: MouseEvent | TouchEvent) => {
const target = event.target

if (!(target instanceof Node)) {
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/menu/menuContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface MenuContextValue {
activeElement: HTMLElement | null
activeIndex: number
mount: (element: HTMLElement | null, selected?: boolean) => () => void
onClickOutside?: (event: MouseEvent) => void
onClickOutside?: (event: MouseEvent | TouchEvent) => void
onEscape?: () => void
onItemClick?: () => void
onItemMouseEnter?: (event: React.MouseEvent<HTMLElement>) => void
Expand Down
28 changes: 11 additions & 17 deletions src/core/components/menu/useMenuController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useCallback, useEffect, useRef, useState} from 'react'
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {_getFocusableElements, _sortElements} from './helpers'

/**
Expand All @@ -11,8 +11,6 @@ export interface MenuController {
handleItemMouseLeave: () => void
handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void
mount: (element: HTMLElement | null, selected?: boolean) => () => void
rootElement: HTMLDivElement | null
setRootElement: (el: HTMLDivElement | null) => void
}

/**
Expand All @@ -24,14 +22,14 @@ export function useMenuController(props: {
onKeyDown?: React.KeyboardEventHandler
originElement?: HTMLElement | null
shouldFocus: 'first' | 'last' | null
rootElementRef: React.MutableRefObject<HTMLDivElement | null>
}): MenuController {
const {onKeyDown, originElement, shouldFocus} = props
const {onKeyDown, originElement, shouldFocus, rootElementRef} = props
const elementsRef = useRef<HTMLElement[]>([])
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
const [activeIndex, _setActiveIndex] = useState(-1)
const activeIndexRef = useRef(activeIndex)
const activeElement = elementsRef.current[activeIndex] || null
const mounted = Boolean(rootElement)
const activeElement = useMemo(() => elementsRef.current[activeIndex] || null, [activeIndex])
const mounted = Boolean(rootElementRef.current)

const setActiveIndex = useCallback((nextActiveIndex: number) => {
_setActiveIndex(nextActiveIndex)
Expand All @@ -44,7 +42,7 @@ export function useMenuController(props: {

if (elementsRef.current.indexOf(element) === -1) {
elementsRef.current.push(element)
_sortElements(rootElement, elementsRef.current)
_sortElements(rootElementRef.current, elementsRef.current)
}

if (selected) {
Expand All @@ -61,7 +59,7 @@ export function useMenuController(props: {
}
}
},
[rootElement, setActiveIndex],
[rootElementRef, setActiveIndex],
)

const handleKeyDown = useCallback(
Expand Down Expand Up @@ -179,17 +177,15 @@ export function useMenuController(props: {
// which would be incorrect when the user hovers over a gap
// between two menu items or a menu divider.
setActiveIndex(-2)
rootElement?.focus()
}, [setActiveIndex, rootElement])
rootElementRef.current?.focus()
}, [rootElementRef, setActiveIndex])

// Set focus on the currently active element
useEffect(() => {
if (!mounted) return

const rafId = window.requestAnimationFrame(() => {
const _activeIndex = activeIndexRef.current

if (_activeIndex === -1) {
if (activeIndex === -1) {
if (shouldFocus === 'first') {
const focusableElements = _getFocusableElements(elementsRef.current)
const el = focusableElements[0]
Expand Down Expand Up @@ -217,7 +213,7 @@ export function useMenuController(props: {
return
}

const element = elementsRef.current[_activeIndex] || null
const element = elementsRef.current[activeIndex] || null

element?.focus()
})
Expand All @@ -234,7 +230,5 @@ export function useMenuController(props: {
handleItemMouseLeave,
handleKeyDown,
mount,
rootElement,
setRootElement,
}
}
3 changes: 2 additions & 1 deletion src/core/components/toast/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ const loadingAnimation = keyframes`
}
100% {
width: 100%;
}
}
`

const LOADING_BAR_HEIGHT = 2

// @TODO get rid of $duration modifier, set data attribute instead and use stable selector
export function rootStyles(
props: {$duration?: number; tone: ThemeColorStateToneKey} & ThemeProps,
): ReturnType<typeof css> {
Expand Down
1 change: 1 addition & 0 deletions src/core/components/toast/useToast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function useToast(): ToastContextValue {
throw new Error('useToast(): missing context value')
}

// @TODO context and hooks doesn't really work like this, there will never be a mismatch between the provider and the consumer, we can remove these version specifiers
// NOTE: This check is for future-compatiblity
// - If the value is not an object, it’s not compatible with the current version
// - If the value is an object, but doesn’t have `version: 0.0`, it’s not compatible with the current version
Expand Down
1 change: 1 addition & 0 deletions src/core/components/tree/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const Tree = memo(
})
}, [])

// @TODO split out into separate contexts
const contextValue: TreeContextValue = useMemo(
() => ({
version: 0.0,
Expand Down
1 change: 1 addition & 0 deletions src/core/components/tree/treeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const TreeItem = memo(function TreeItem(
const focused = tree.focusedElement === rootRef.current
const expanded = itemState?.expanded === undefined ? expandedProp : itemState?.expanded || false
const tabIndex = tree.focusedElement && tree.focusedElement === rootRef.current ? 0 : -1
// @TODO split out into separate contexts
const contextValue = useMemo(
() => ({...tree, level: tree.level + 1, path: itemPath}),
[itemPath, tree],
Expand Down
1 change: 1 addition & 0 deletions src/core/helpers/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
* @internal
*/
export function _hasFocus(element: HTMLElement): boolean {
// @TODO verify this is not called during render
return Boolean(document.activeElement) && element.contains(document.activeElement)
}

Expand Down
Loading

0 comments on commit 70c8f35

Please sign in to comment.