@kanso-labs/kanso-ui 0.11.21 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -8
- package/dist/components/app-bar/index.d.ts +2 -2
- package/dist/components/app-bar/index.js +1 -1
- package/dist/components/app-bar/index.js.map +1 -1
- package/dist/components/avatar/index.d.ts +4 -4
- package/dist/components/avatar/index.js +21 -15
- package/dist/components/avatar/index.js.map +1 -1
- package/dist/components/badge/index.d.ts +2 -2
- package/dist/components/badge/index.js +1 -1
- package/dist/components/badge/index.js.map +1 -1
- package/dist/components/button/index.d.ts +32 -21
- package/dist/components/button/index.js +75 -19
- package/dist/components/button/index.js.map +1 -1
- package/dist/components/card/index.d.ts +2 -2
- package/dist/components/card/index.js +1 -1
- package/dist/components/card/index.js.map +1 -1
- package/dist/components/chip/index.d.ts +5 -5
- package/dist/components/chip/index.js +5 -5
- package/dist/components/chip/index.js.map +1 -1
- package/dist/components/code/index.d.ts +2 -2
- package/dist/components/code/index.js +1 -1
- package/dist/components/code/index.js.map +1 -1
- package/dist/components/container/index.d.ts +2 -2
- package/dist/components/container/index.js +1 -1
- package/dist/components/container/index.js.map +1 -1
- package/dist/components/copy-field/index.js +1 -1
- package/dist/components/currency/index.d.ts +2 -2
- package/dist/components/currency/index.js +1 -1
- package/dist/components/currency/index.js.map +1 -1
- package/dist/components/feed/index.d.ts +2 -2
- package/dist/components/feed/index.js +1 -1
- package/dist/components/feed/index.js.map +1 -1
- package/dist/components/icon-button/index.d.ts +24 -18
- package/dist/components/icon-button/index.js +62 -15
- package/dist/components/icon-button/index.js.map +1 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/keycap/index.d.ts +2 -2
- package/dist/components/keycap/index.js +1 -1
- package/dist/components/keycap/index.js.map +1 -1
- package/dist/components/link/index.d.ts +2 -2
- package/dist/components/link/index.js +1 -1
- package/dist/components/link/index.js.map +1 -1
- package/dist/components/list-detail/index.d.ts +2 -2
- package/dist/components/list-detail/index.js +1 -1
- package/dist/components/list-detail/index.js.map +1 -1
- package/dist/components/popover/index.d.ts +105 -0
- package/dist/components/popover/index.js +246 -0
- package/dist/components/popover/index.js.map +1 -0
- package/dist/components/product-icon/index.d.ts +3 -3
- package/dist/components/product-icon/index.js +23 -17
- package/dist/components/product-icon/index.js.map +1 -1
- package/dist/components/separator/index.d.ts +14 -4
- package/dist/components/separator/index.js +23 -9
- package/dist/components/separator/index.js.map +1 -1
- package/dist/components/sheet/index.d.ts +26 -28
- package/dist/components/sheet/index.js +47 -82
- package/dist/components/sheet/index.js.map +1 -1
- package/dist/components/stack/index.d.ts +2 -2
- package/dist/components/stack/index.js +1 -1
- package/dist/components/stack/index.js.map +1 -1
- package/dist/components/supporting-pane/index.d.ts +2 -2
- package/dist/components/supporting-pane/index.js +1 -1
- package/dist/components/supporting-pane/index.js.map +1 -1
- package/dist/components/tabs/index.d.ts +15 -8
- package/dist/components/tabs/index.js +133 -45
- package/dist/components/tabs/index.js.map +1 -1
- package/dist/components/text/index.d.ts +2 -2
- package/dist/components/text/index.js +1 -1
- package/dist/components/text/index.js.map +1 -1
- package/dist/components/text-field/index.d.ts +12 -5
- package/dist/components/text-field/index.js +48 -33
- package/dist/components/text-field/index.js.map +1 -1
- package/dist/hooks/useImageLoadingStatus.js +53 -0
- package/dist/hooks/useImageLoadingStatus.js.map +1 -0
- package/dist/hooks/useRipple.js +1 -1
- package/dist/hooks/useRipple.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/render/aria.js +66 -0
- package/dist/render/aria.js.map +1 -0
- package/dist/render/useRender.d.ts +28 -0
- package/dist/render/useRender.js +132 -0
- package/dist/render/useRender.js.map +1 -0
- package/dist/styles/merge.js +14 -8
- package/dist/styles/merge.js.map +1 -1
- package/dist/styles.css +88 -36
- package/package.json +4 -4
- package/dist/render/nativeButton.js +0 -26
- package/dist/render/nativeButton.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRipple.js","names":["rect","width","size","height","positionEvent","startPoint","endPoint","event","target","rippleRef","resolve"],"sources":["../../src/hooks/useRipple.tsx"],"sourcesContent":["import type { MouseEvent, MouseEventHandler, PointerEvent, PointerEventHandler, ReactNode } from 'react';\nimport * as stylex from '@stylexjs/stylex';\nimport { useCallback, useMemo, useRef, useState } from 'react';\nimport { rippleStyles } from '../styles/ripple';\nimport { motionDurationMs, motionEasing, spacingPx } from '../tokens/values';\n\n// Every timing this file drives is a motion token, spent at the point of use\n// below. They come from tokens/values rather than the StyleX theme because\n// element.animate() and setTimeout both reject a token's var() form, and the\n// same is true of the one geometry token: `PADDING` is added to a radius, and\n// a var() would concatenate rather than add. See src/styles/ripple.ts for the\n// opacity half of the mechanism, whose three timings are deliberately not\n// tokens.\n\n/** How far the ripple grows past the host's corners. `spacing.sm`. */\nconst PADDING = spacingPx.sm;\n\n// The rest are ratios and a pixel floor that no token group covers — spacing\n// is layout rhythm, and nothing in the set holds bare geometric ratios.\nconst INITIAL_ORIGIN_SCALE = 0.2;\nconst SOFT_EDGE_MINIMUM_SIZE = 75;\nconst SOFT_EDGE_CONTAINER_RATIO = 0.35;\n\n// Static input, so this is computed once for every consumer of the module\n// rather than once per component instance.\nconst surfaceProps = stylex.props(rippleStyles.surface);\n\n/**\n * On touch: `Inactive -> TouchDelay -> WaitingForClick -> Inactive`, or\n * `Inactive -> TouchDelay -> Holding -> WaitingForClick -> Inactive` for a\n * held press.\n *\n * On mouse or pen: `Inactive -> WaitingForClick -> Inactive`.\n */\nconst RippleState = {\n /** A touch has been confirmed as a press (held past the touch delay). */\n Holding: 2,\n /** No press in progress. Touch down moves to TouchDelay; mouse/pen down moves to WaitingForClick. */\n Inactive: 0,\n /** Touch down received; waiting to see if it's a tap or a scroll/swipe. */\n TouchDelay: 1,\n /** The press has finished growing; waiting for the click that ends it. */\n WaitingForClick: 3\n} as const;\ninterface RippleEventHandlers<HostElement extends HTMLElement> {\n onClick?: MouseEventHandler<HostElement>;\n onContextMenu?: MouseEventHandler<HostElement>;\n onPointerCancel?: PointerEventHandler<HostElement>;\n onPointerDown?: PointerEventHandler<HostElement>;\n onPointerLeave?: PointerEventHandler<HostElement>;\n onPointerUp?: PointerEventHandler<HostElement>;\n}\ntype RippleState = (typeof RippleState)[keyof typeof RippleState];\nfunction isTouch(event: PointerEvent) {\n return event.pointerType === 'touch';\n}\n\n/**\n * Renders a press ripple that grows from the pointer's position, or from\n * the element's center for a keyboard or other non-pointer activation.\n *\n * Growth runs via the Web Animations API on a single reused element (no\n * per-click DOM nodes to clean up), over a fixed duration independent of how\n * long the element is held — a real press-and-hold keeps the ripple\n * growing until release, while a quick tap is still held visible for a\n * minimum duration so it plays a complete animation rather than a flicker.\n *\n * Merges its own handlers with `externalHandlers` (e.g. a component's own\n * consumer-supplied props — ripple's own run first, then the external\n * ones) and returns `handlers` to spread onto the interactive element and\n * `surface`, the node to render as a child. The host element must\n * establish a positioning context (`position: relative` or similar) so\n * `surface`, which fills it via `inset: 0`, clips to its bounds and\n * inherits its shape.\n *\n * Pass `enabled: false` to opt out — no ripple listeners are attached and\n * no surface is rendered, leaving `externalHandlers` and a component's own\n * hover/press state layer (if any) untouched.\n *\n * `handlers` and `surface` are hand-memoized (not left to build-tool\n * optimization) so a consumer rendering many instances — a list of icon\n * buttons, say — can memoize around them too, and so stability doesn't\n * depend on any particular compiler being configured.\n */\nfunction useRipple<HostElement extends HTMLElement = HTMLElement>(enabled = true, externalHandlers: RippleEventHandlers<HostElement> = {}): {\n handlers: RippleEventHandlers<HostElement>;\n surface: ReactNode;\n} {\n const [pressed, setPressed] = useState(false);\n const rippleRef = useRef<HTMLSpanElement>(null);\n const state = useRef<RippleState>(RippleState.Inactive);\n const growAnimation = useRef<Animation | undefined>(undefined);\n const rippleStartEvent = useRef<PointerEvent<HostElement> | undefined>(undefined);\n const initialSize = useRef(0);\n const rippleScale = useRef('1');\n const shouldReactToEvent = useCallback((event: PointerEvent<HostElement>) => {\n if (!event.isPrimary) {\n return false;\n }\n const startEvent = rippleStartEvent.current;\n if (startEvent && startEvent.pointerId !== event.pointerId) {\n return false;\n }\n return isTouch(event) || event.buttons === 1;\n }, []);\n const determineRippleSize = useCallback((rect: DOMRect) => {\n const {\n height,\n width\n } = rect;\n const maxDimension = Math.max(height, width);\n const softEdgeSize = Math.max(SOFT_EDGE_CONTAINER_RATIO * maxDimension, SOFT_EDGE_MINIMUM_SIZE);\n const size = Math.floor(maxDimension * INITIAL_ORIGIN_SCALE);\n const hypotenuse = Math.hypot(width, height);\n const maxRadius = hypotenuse + PADDING;\n initialSize.current = size;\n rippleScale.current = `${(maxRadius + softEdgeSize) / size}`;\n return `${size}px`;\n }, []);\n const getTranslationCoordinates = useCallback((rect: DOMRect, positionEvent?: PointerEvent<HostElement>) => {\n const {\n height,\n left,\n top,\n width\n } = rect;\n const size = initialSize.current;\n const endPoint = {\n x: (width - size) / 2,\n y: (height - size) / 2\n };\n const origin = positionEvent ? {\n x: positionEvent.clientX - left,\n y: positionEvent.clientY - top\n } : {\n x: width / 2,\n y: height / 2\n };\n const startPoint = {\n x: origin.x - size / 2,\n y: origin.y - size / 2\n };\n return {\n endPoint,\n startPoint\n };\n }, []);\n const startPressAnimation = useCallback((target: HostElement, positionEvent?: PointerEvent<HostElement>) => {\n const ripple = rippleRef.current;\n if (!ripple) {\n return;\n }\n setPressed(true);\n growAnimation.current?.cancel();\n\n // Read once and share: the two helpers below both only need the\n // target's rect, and a second `getBoundingClientRect()` call back to\n // back with the first would be a redundant layout read.\n const rect = target.getBoundingClientRect();\n const size = determineRippleSize(rect);\n const {\n endPoint,\n startPoint\n } = getTranslationCoordinates(rect, positionEvent);\n ripple.style.width = size;\n ripple.style.height = size;\n growAnimation.current = ripple.animate({\n transform: [`translate(${startPoint.x}px, ${startPoint.y}px) scale(1)`, `translate(${endPoint.x}px, ${endPoint.y}px) scale(${rippleScale.current})`]\n }, {\n // Growth runs over a fixed duration regardless of how long the\n // press is held, so the token is spent here rather than named as a\n // constant — element.animate() is one of the two places a StyleX\n // token could not have gone.\n duration: motionDurationMs.long1,\n easing: motionEasing.standard,\n fill: 'forwards'\n });\n }, [determineRippleSize, getTranslationCoordinates]);\n const endPressAnimation = useCallback(async () => {\n rippleStartEvent.current = undefined;\n state.current = RippleState.Inactive;\n const animation = growAnimation.current;\n const elapsed = typeof animation?.currentTime === 'number' ? animation.currentTime : Infinity;\n\n // The floor on how briefly a ripple may be visible, so a quick tap reads\n // as a deliberate ripple rather than a flicker.\n if (elapsed >= motionDurationMs.medium1) {\n setPressed(false);\n return;\n }\n await new Promise(resolve => {\n setTimeout(resolve, motionDurationMs.medium1 - elapsed);\n });\n if (growAnimation.current !== animation) {\n // A newer press started; let it own the pressed state instead.\n return;\n }\n setPressed(false);\n }, []);\n\n // Reads through a function so a stale narrowing of `state.current` from\n // before the `await` below isn't (incorrectly) carried across it — the\n // ref can change via a different handler while this one is suspended.\n const getState = useCallback(() => state.current, []);\n const handlePointerDownAsync = useCallback(async (event: PointerEvent<HostElement>) => {\n if (!shouldReactToEvent(event)) {\n return;\n }\n rippleStartEvent.current = event;\n const target = event.currentTarget;\n if (!isTouch(event)) {\n state.current = RippleState.WaitingForClick;\n startPressAnimation(target, event);\n return;\n }\n\n // Delay so a scroll/swipe starting on the element doesn't flash a\n // ripple — only a held (or quickly released) touch counts as a press.\n state.current = RippleState.TouchDelay;\n await new Promise(resolve => {\n setTimeout(resolve, motionDurationMs.short2);\n });\n if (getState() !== RippleState.TouchDelay) {\n return;\n }\n state.current = RippleState.Holding;\n startPressAnimation(target, event);\n }, [getState, shouldReactToEvent, startPressAnimation]);\n const handlePointerDown = useCallback((event: PointerEvent<HostElement>) => {\n void handlePointerDownAsync(event);\n }, [handlePointerDownAsync]);\n const handlePointerUp = useCallback((event: PointerEvent<HostElement>) => {\n if (!shouldReactToEvent(event)) {\n return;\n }\n if (state.current === RippleState.Holding) {\n state.current = RippleState.WaitingForClick;\n return;\n }\n if (state.current === RippleState.TouchDelay) {\n // Released before the delay elapsed: treat as a completed tap.\n state.current = RippleState.WaitingForClick;\n startPressAnimation(event.currentTarget, rippleStartEvent.current);\n }\n }, [shouldReactToEvent, startPressAnimation]);\n const handlePointerLeave = useCallback((event: PointerEvent<HostElement>) => {\n if (isTouch(event) || !shouldReactToEvent(event)) {\n return;\n }\n if (state.current !== RippleState.Inactive) {\n void endPressAnimation();\n }\n }, [endPressAnimation, shouldReactToEvent]);\n const handlePointerCancel = useCallback((event: PointerEvent<HostElement>) => {\n if (!shouldReactToEvent(event)) {\n return;\n }\n void endPressAnimation();\n }, [endPressAnimation, shouldReactToEvent]);\n const handleClick = useCallback((event: MouseEvent<HostElement>) => {\n if (state.current === RippleState.WaitingForClick) {\n void endPressAnimation();\n return;\n }\n if (state.current === RippleState.Inactive) {\n // No pointerdown preceded this click — keyboard activation, a\n // programmatic `.click()`, or assistive-tech-synthesized activation.\n // Play a complete, centered press immediately.\n startPressAnimation(event.currentTarget);\n void endPressAnimation();\n }\n }, [endPressAnimation, startPressAnimation]);\n const handleContextMenu = useCallback(() => {\n void endPressAnimation();\n }, [endPressAnimation]);\n const {\n onClick: onClickProp,\n onContextMenu: onContextMenuProp,\n onPointerCancel: onPointerCancelProp,\n onPointerDown: onPointerDownProp,\n onPointerLeave: onPointerLeaveProp,\n onPointerUp: onPointerUpProp\n } = externalHandlers;\n const onClick = useCallback((event: MouseEvent<HostElement>) => {\n handleClick(event);\n onClickProp?.(event);\n }, [handleClick, onClickProp]);\n const onContextMenu = useCallback((event: MouseEvent<HostElement>) => {\n handleContextMenu();\n onContextMenuProp?.(event);\n }, [handleContextMenu, onContextMenuProp]);\n const onPointerCancel = useCallback((event: PointerEvent<HostElement>) => {\n handlePointerCancel(event);\n onPointerCancelProp?.(event);\n }, [handlePointerCancel, onPointerCancelProp]);\n const onPointerDown = useCallback((event: PointerEvent<HostElement>) => {\n handlePointerDown(event);\n onPointerDownProp?.(event);\n }, [handlePointerDown, onPointerDownProp]);\n const onPointerLeave = useCallback((event: PointerEvent<HostElement>) => {\n handlePointerLeave(event);\n onPointerLeaveProp?.(event);\n }, [handlePointerLeave, onPointerLeaveProp]);\n const onPointerUp = useCallback((event: PointerEvent<HostElement>) => {\n handlePointerUp(event);\n onPointerUpProp?.(event);\n }, [handlePointerUp, onPointerUpProp]);\n const handlers = useMemo(() => ({\n onClick,\n onContextMenu,\n onPointerCancel,\n onPointerDown,\n onPointerLeave,\n onPointerUp\n }), [onClick, onContextMenu, onPointerCancel, onPointerDown, onPointerLeave, onPointerUp]);\n const surface = useMemo(() => <span aria-hidden=\"true\" {...surfaceProps}>\n <span ref={rippleRef} {...stylex.props(rippleStyles.press, pressed && rippleStyles.pressed)} />\n </span>, [pressed]);\n\n // Two separate memos rather than one branching on `enabled`, so that\n // whichever branch is actually active stays keyed only on its own\n // inputs — the enabled result doesn't get a new identity every time\n // `externalHandlers` does (a component typically passes a fresh object\n // literal each render), and vice versa.\n const enabledResult = useMemo(() => ({\n handlers,\n surface\n }), [handlers, surface]);\n const disabledResult = useMemo(() => ({\n handlers: externalHandlers,\n surface: null\n }), [externalHandlers]);\n return enabled ? enabledResult : disabledResult;\n}\nexport { useRipple };"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useRipple.js","names":["rect","width","size","height","positionEvent","startPoint","endPoint","event","target","rippleRef","resolve"],"sources":["../../src/hooks/useRipple.tsx"],"sourcesContent":["import type { MouseEvent, MouseEventHandler, PointerEvent, PointerEventHandler, ReactNode } from 'react';\nimport * as stylex from '@stylexjs/stylex';\nimport { useCallback, useMemo, useRef, useState } from 'react';\nimport { rippleStyles } from '../styles/ripple';\nimport { motionDurationMs, motionEasing, spacingPx } from '../tokens/values';\n\n// Every timing this file drives is a motion token, spent at the point of use\n// below. They come from tokens/values rather than the StyleX theme because\n// element.animate() and setTimeout both reject a token's var() form, and the\n// same is true of the one geometry token: `PADDING` is added to a radius, and\n// a var() would concatenate rather than add. See src/styles/ripple.ts for the\n// opacity half of the mechanism, whose three timings are deliberately not\n// tokens.\n\n/** How far the ripple grows past the host's corners. `spacing.sm`. */\nconst PADDING = spacingPx.sm;\n\n// The rest are ratios and a pixel floor that no token group covers — spacing\n// is layout rhythm, and nothing in the set holds bare geometric ratios.\nconst INITIAL_ORIGIN_SCALE = 0.2;\nconst SOFT_EDGE_MINIMUM_SIZE = 75;\nconst SOFT_EDGE_CONTAINER_RATIO = 0.35;\n\n// Static input, so this is computed once for every consumer of the module\n// rather than once per component instance.\nconst surfaceProps = stylex.props(rippleStyles.surface);\n\n/**\n * On touch: `Inactive -> TouchDelay -> WaitingForClick -> Inactive`, or\n * `Inactive -> TouchDelay -> Holding -> WaitingForClick -> Inactive` for a\n * held press.\n *\n * On mouse or pen: `Inactive -> WaitingForClick -> Inactive`.\n */\nconst RippleState = {\n /** A touch has been confirmed as a press (held past the touch delay). */\n Holding: 2,\n /** No press in progress. Touch down moves to TouchDelay; mouse/pen down moves to WaitingForClick. */\n Inactive: 0,\n /** Touch down received; waiting to see if it's a tap or a scroll/swipe. */\n TouchDelay: 1,\n /** The press has finished growing; waiting for the click that ends it. */\n WaitingForClick: 3\n} as const;\ninterface RippleEventHandlers<HostElement extends Element> {\n onClick?: MouseEventHandler<HostElement>;\n onContextMenu?: MouseEventHandler<HostElement>;\n onPointerCancel?: PointerEventHandler<HostElement>;\n onPointerDown?: PointerEventHandler<HostElement>;\n onPointerLeave?: PointerEventHandler<HostElement>;\n onPointerUp?: PointerEventHandler<HostElement>;\n}\ntype RippleState = (typeof RippleState)[keyof typeof RippleState];\nfunction isTouch(event: PointerEvent) {\n return event.pointerType === 'touch';\n}\n\n/**\n * Renders a press ripple that grows from the pointer's position, or from\n * the element's center for a keyboard or other non-pointer activation.\n *\n * Growth runs via the Web Animations API on a single reused element (no\n * per-click DOM nodes to clean up), over a fixed duration independent of how\n * long the element is held — a real press-and-hold keeps the ripple\n * growing until release, while a quick tap is still held visible for a\n * minimum duration so it plays a complete animation rather than a flicker.\n *\n * Merges its own handlers with `externalHandlers` (e.g. a component's own\n * consumer-supplied props — ripple's own run first, then the external\n * ones) and returns `handlers` to spread onto the interactive element and\n * `surface`, the node to render as a child. The host element must\n * establish a positioning context (`position: relative` or similar) so\n * `surface`, which fills it via `inset: 0`, clips to its bounds and\n * inherits its shape.\n *\n * Pass `enabled: false` to opt out — no ripple listeners are attached and\n * no surface is rendered, leaving `externalHandlers` and a component's own\n * hover/press state layer (if any) untouched.\n *\n * `handlers` and `surface` are hand-memoized (not left to build-tool\n * optimization) so a consumer rendering many instances — a list of icon\n * buttons, say — can memoize around them too, and so stability doesn't\n * depend on any particular compiler being configured.\n */\nfunction useRipple<HostElement extends Element = Element>(enabled = true, externalHandlers: RippleEventHandlers<HostElement> = {}): {\n handlers: RippleEventHandlers<HostElement>;\n surface: ReactNode;\n} {\n const [pressed, setPressed] = useState(false);\n const rippleRef = useRef<HTMLSpanElement>(null);\n const state = useRef<RippleState>(RippleState.Inactive);\n const growAnimation = useRef<Animation | undefined>(undefined);\n const rippleStartEvent = useRef<PointerEvent<HostElement> | undefined>(undefined);\n const initialSize = useRef(0);\n const rippleScale = useRef('1');\n const shouldReactToEvent = useCallback((event: PointerEvent<HostElement>) => {\n if (!event.isPrimary) {\n return false;\n }\n const startEvent = rippleStartEvent.current;\n if (startEvent && startEvent.pointerId !== event.pointerId) {\n return false;\n }\n return isTouch(event) || event.buttons === 1;\n }, []);\n const determineRippleSize = useCallback((rect: DOMRect) => {\n const {\n height,\n width\n } = rect;\n const maxDimension = Math.max(height, width);\n const softEdgeSize = Math.max(SOFT_EDGE_CONTAINER_RATIO * maxDimension, SOFT_EDGE_MINIMUM_SIZE);\n const size = Math.floor(maxDimension * INITIAL_ORIGIN_SCALE);\n const hypotenuse = Math.hypot(width, height);\n const maxRadius = hypotenuse + PADDING;\n initialSize.current = size;\n rippleScale.current = `${(maxRadius + softEdgeSize) / size}`;\n return `${size}px`;\n }, []);\n const getTranslationCoordinates = useCallback((rect: DOMRect, positionEvent?: PointerEvent<HostElement>) => {\n const {\n height,\n left,\n top,\n width\n } = rect;\n const size = initialSize.current;\n const endPoint = {\n x: (width - size) / 2,\n y: (height - size) / 2\n };\n const origin = positionEvent ? {\n x: positionEvent.clientX - left,\n y: positionEvent.clientY - top\n } : {\n x: width / 2,\n y: height / 2\n };\n const startPoint = {\n x: origin.x - size / 2,\n y: origin.y - size / 2\n };\n return {\n endPoint,\n startPoint\n };\n }, []);\n const startPressAnimation = useCallback((target: HostElement, positionEvent?: PointerEvent<HostElement>) => {\n const ripple = rippleRef.current;\n if (!ripple) {\n return;\n }\n setPressed(true);\n growAnimation.current?.cancel();\n\n // Read once and share: the two helpers below both only need the\n // target's rect, and a second `getBoundingClientRect()` call back to\n // back with the first would be a redundant layout read.\n const rect = target.getBoundingClientRect();\n const size = determineRippleSize(rect);\n const {\n endPoint,\n startPoint\n } = getTranslationCoordinates(rect, positionEvent);\n ripple.style.width = size;\n ripple.style.height = size;\n growAnimation.current = ripple.animate({\n transform: [`translate(${startPoint.x}px, ${startPoint.y}px) scale(1)`, `translate(${endPoint.x}px, ${endPoint.y}px) scale(${rippleScale.current})`]\n }, {\n // Growth runs over a fixed duration regardless of how long the\n // press is held, so the token is spent here rather than named as a\n // constant — element.animate() is one of the two places a StyleX\n // token could not have gone.\n duration: motionDurationMs.long1,\n easing: motionEasing.standard,\n fill: 'forwards'\n });\n }, [determineRippleSize, getTranslationCoordinates]);\n const endPressAnimation = useCallback(async () => {\n rippleStartEvent.current = undefined;\n state.current = RippleState.Inactive;\n const animation = growAnimation.current;\n const elapsed = typeof animation?.currentTime === 'number' ? animation.currentTime : Infinity;\n\n // The floor on how briefly a ripple may be visible, so a quick tap reads\n // as a deliberate ripple rather than a flicker.\n if (elapsed >= motionDurationMs.medium1) {\n setPressed(false);\n return;\n }\n await new Promise(resolve => {\n setTimeout(resolve, motionDurationMs.medium1 - elapsed);\n });\n if (growAnimation.current !== animation) {\n // A newer press started; let it own the pressed state instead.\n return;\n }\n setPressed(false);\n }, []);\n\n // Reads through a function so a stale narrowing of `state.current` from\n // before the `await` below isn't (incorrectly) carried across it — the\n // ref can change via a different handler while this one is suspended.\n const getState = useCallback(() => state.current, []);\n const handlePointerDownAsync = useCallback(async (event: PointerEvent<HostElement>) => {\n if (!shouldReactToEvent(event)) {\n return;\n }\n rippleStartEvent.current = event;\n const target = event.currentTarget;\n if (!isTouch(event)) {\n state.current = RippleState.WaitingForClick;\n startPressAnimation(target, event);\n return;\n }\n\n // Delay so a scroll/swipe starting on the element doesn't flash a\n // ripple — only a held (or quickly released) touch counts as a press.\n state.current = RippleState.TouchDelay;\n await new Promise(resolve => {\n setTimeout(resolve, motionDurationMs.short2);\n });\n if (getState() !== RippleState.TouchDelay) {\n return;\n }\n state.current = RippleState.Holding;\n startPressAnimation(target, event);\n }, [getState, shouldReactToEvent, startPressAnimation]);\n const handlePointerDown = useCallback((event: PointerEvent<HostElement>) => {\n void handlePointerDownAsync(event);\n }, [handlePointerDownAsync]);\n const handlePointerUp = useCallback((event: PointerEvent<HostElement>) => {\n if (!shouldReactToEvent(event)) {\n return;\n }\n if (state.current === RippleState.Holding) {\n state.current = RippleState.WaitingForClick;\n return;\n }\n if (state.current === RippleState.TouchDelay) {\n // Released before the delay elapsed: treat as a completed tap.\n state.current = RippleState.WaitingForClick;\n startPressAnimation(event.currentTarget, rippleStartEvent.current);\n }\n }, [shouldReactToEvent, startPressAnimation]);\n const handlePointerLeave = useCallback((event: PointerEvent<HostElement>) => {\n if (isTouch(event) || !shouldReactToEvent(event)) {\n return;\n }\n if (state.current !== RippleState.Inactive) {\n void endPressAnimation();\n }\n }, [endPressAnimation, shouldReactToEvent]);\n const handlePointerCancel = useCallback((event: PointerEvent<HostElement>) => {\n if (!shouldReactToEvent(event)) {\n return;\n }\n void endPressAnimation();\n }, [endPressAnimation, shouldReactToEvent]);\n const handleClick = useCallback((event: MouseEvent<HostElement>) => {\n if (state.current === RippleState.WaitingForClick) {\n void endPressAnimation();\n return;\n }\n if (state.current === RippleState.Inactive) {\n // No pointerdown preceded this click — keyboard activation, a\n // programmatic `.click()`, or assistive-tech-synthesized activation.\n // Play a complete, centered press immediately.\n startPressAnimation(event.currentTarget);\n void endPressAnimation();\n }\n }, [endPressAnimation, startPressAnimation]);\n const handleContextMenu = useCallback(() => {\n void endPressAnimation();\n }, [endPressAnimation]);\n const {\n onClick: onClickProp,\n onContextMenu: onContextMenuProp,\n onPointerCancel: onPointerCancelProp,\n onPointerDown: onPointerDownProp,\n onPointerLeave: onPointerLeaveProp,\n onPointerUp: onPointerUpProp\n } = externalHandlers;\n const onClick = useCallback((event: MouseEvent<HostElement>) => {\n handleClick(event);\n onClickProp?.(event);\n }, [handleClick, onClickProp]);\n const onContextMenu = useCallback((event: MouseEvent<HostElement>) => {\n handleContextMenu();\n onContextMenuProp?.(event);\n }, [handleContextMenu, onContextMenuProp]);\n const onPointerCancel = useCallback((event: PointerEvent<HostElement>) => {\n handlePointerCancel(event);\n onPointerCancelProp?.(event);\n }, [handlePointerCancel, onPointerCancelProp]);\n const onPointerDown = useCallback((event: PointerEvent<HostElement>) => {\n handlePointerDown(event);\n onPointerDownProp?.(event);\n }, [handlePointerDown, onPointerDownProp]);\n const onPointerLeave = useCallback((event: PointerEvent<HostElement>) => {\n handlePointerLeave(event);\n onPointerLeaveProp?.(event);\n }, [handlePointerLeave, onPointerLeaveProp]);\n const onPointerUp = useCallback((event: PointerEvent<HostElement>) => {\n handlePointerUp(event);\n onPointerUpProp?.(event);\n }, [handlePointerUp, onPointerUpProp]);\n const handlers = useMemo(() => ({\n onClick,\n onContextMenu,\n onPointerCancel,\n onPointerDown,\n onPointerLeave,\n onPointerUp\n }), [onClick, onContextMenu, onPointerCancel, onPointerDown, onPointerLeave, onPointerUp]);\n const surface = useMemo(() => <span aria-hidden=\"true\" {...surfaceProps}>\n <span ref={rippleRef} {...stylex.props(rippleStyles.press, pressed && rippleStyles.pressed)} />\n </span>, [pressed]);\n\n // Two separate memos rather than one branching on `enabled`, so that\n // whichever branch is actually active stays keyed only on its own\n // inputs — the enabled result doesn't get a new identity every time\n // `externalHandlers` does (a component typically passes a fresh object\n // literal each render), and vice versa.\n const enabledResult = useMemo(() => ({\n handlers,\n surface\n }), [handlers, surface]);\n const disabledResult = useMemo(() => ({\n handlers: externalHandlers,\n surface: null\n }), [externalHandlers]);\n return enabled ? enabledResult : disabledResult;\n}\nexport { useRipple };"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import Keycap, { KeycapProps } from "./components/keycap/index.js";
|
|
|
14
14
|
import Link, { LinkProps } from "./components/link/index.js";
|
|
15
15
|
import ListDetail, { ListDetailProps } from "./components/list-detail/index.js";
|
|
16
16
|
import ListItem, { ListItemProps } from "./components/list-item/index.js";
|
|
17
|
+
import Popover, { PopoverProps } from "./components/popover/index.js";
|
|
17
18
|
import ProductIcon, { ProductIconProps } from "./components/product-icon/index.js";
|
|
18
19
|
import Separator, { SeparatorProps } from "./components/separator/index.js";
|
|
19
20
|
import Sheet, { SheetProps } from "./components/sheet/index.js";
|
|
@@ -24,4 +25,4 @@ import Text, { TextProps } from "./components/text/index.js";
|
|
|
24
25
|
import TextField, { TextFieldProps } from "./components/text-field/index.js";
|
|
25
26
|
import "./components/index.js";
|
|
26
27
|
import "./styles.css";
|
|
27
|
-
export { AppBar, type AppBarProps, Avatar, type AvatarProps, Badge, type BadgeProps, Button, type ButtonProps, Card, type CardProps, Chip, type ChipProps, Code, type CodeProps, Container, type ContainerProps, CopyField, type CopyFieldProps, Currency, type CurrencyProps, Feed, type FeedProps, IconButton, type IconButtonProps, Keycap, type KeycapProps, Link, type LinkProps, ListDetail, type ListDetailProps, ListItem, type ListItemProps, ProductIcon, type ProductIconProps, Separator, type SeparatorProps, Sheet, type SheetProps, Stack, type StackAlign, type StackGap, type StackJustify, type StackProps, SupportingPane, type SupportingPaneProps, Tabs, type TabsProps, Text, TextField, type TextFieldProps, type TextProps };
|
|
28
|
+
export { AppBar, type AppBarProps, Avatar, type AvatarProps, Badge, type BadgeProps, Button, type ButtonProps, Card, type CardProps, Chip, type ChipProps, Code, type CodeProps, Container, type ContainerProps, CopyField, type CopyFieldProps, Currency, type CurrencyProps, Feed, type FeedProps, IconButton, type IconButtonProps, Keycap, type KeycapProps, Link, type LinkProps, ListDetail, type ListDetailProps, ListItem, type ListItemProps, Popover, type PopoverProps, ProductIcon, type ProductIconProps, Separator, type SeparatorProps, Sheet, type SheetProps, Stack, type StackAlign, type StackGap, type StackJustify, type StackProps, SupportingPane, type SupportingPaneProps, Tabs, type TabsProps, Text, TextField, type TextFieldProps, type TextProps };
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import Keycap from "./components/keycap/index.js";
|
|
|
15
15
|
import Link from "./components/link/index.js";
|
|
16
16
|
import ListDetail from "./components/list-detail/index.js";
|
|
17
17
|
import ListItem from "./components/list-item/index.js";
|
|
18
|
+
import Popover from "./components/popover/index.js";
|
|
18
19
|
import ProductIcon from "./components/product-icon/index.js";
|
|
19
20
|
import Separator from "./components/separator/index.js";
|
|
20
21
|
import Sheet from "./components/sheet/index.js";
|
|
@@ -23,4 +24,4 @@ import SupportingPane from "./components/supporting-pane/index.js";
|
|
|
23
24
|
import Tabs from "./components/tabs/index.js";
|
|
24
25
|
import TextField from "./components/text-field/index.js";
|
|
25
26
|
import "./styles.css";
|
|
26
|
-
export { AppBar, Avatar, Badge, Button, Card, Chip, Code, Container, CopyField, Currency, Feed, IconButton, Keycap, Link, ListDetail, ListItem, ProductIcon, Separator, Sheet, Stack, SupportingPane, Tabs, Text, TextField };
|
|
27
|
+
export { AppBar, Avatar, Badge, Button, Card, Chip, Code, Container, CopyField, Currency, Feed, IconButton, Keycap, Link, ListDetail, ListItem, Popover, ProductIcon, Separator, Sheet, Stack, SupportingPane, Tabs, Text, TextField };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
//#region src/render/aria.tsx
|
|
3
|
+
const LABELLING = /* @__PURE__ */ new Set([
|
|
4
|
+
"aria-describedby",
|
|
5
|
+
"aria-details",
|
|
6
|
+
"aria-label",
|
|
7
|
+
"aria-labelledby"
|
|
8
|
+
]);
|
|
9
|
+
/** The `aria-*` props React Aria would drop, collected for {@link ElementProps}. */
|
|
10
|
+
function ariaAttributesOf(props) {
|
|
11
|
+
const aria = {};
|
|
12
|
+
for (const [key, value] of Object.entries(props)) if (key.startsWith("aria-") && !LABELLING.has(key)) aria[key] = value;
|
|
13
|
+
return aria;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The `render` function for a React Aria `Button`: the element it would have
|
|
17
|
+
* rendered, plus {@link ElementProps}, handed on to the call site's own
|
|
18
|
+
* `render` when there is one. Built by a call rather than written inline at
|
|
19
|
+
* the prop, which is what react-perf's no-new-function-as-prop is after.
|
|
20
|
+
*
|
|
21
|
+
* `createElement` rather than JSX, here and below: React Aria types the
|
|
22
|
+
* props it hands over as a union of the elements it might have rendered, and
|
|
23
|
+
* a spread of that union onto one tag does not type-check, where the plain
|
|
24
|
+
* call takes any props object.
|
|
25
|
+
*/
|
|
26
|
+
function buttonRenderer(element, render) {
|
|
27
|
+
return (domProps, state) => {
|
|
28
|
+
const merged = withElementProps(domProps, element);
|
|
29
|
+
if (render !== void 0) return render(merged, state);
|
|
30
|
+
return createElement("button", merged);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function chain(first, second) {
|
|
34
|
+
if (second === void 0) return first;
|
|
35
|
+
return (event) => {
|
|
36
|
+
first?.(event);
|
|
37
|
+
second(event);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The same for a React Aria `Link`, which renders an anchor — or, while
|
|
42
|
+
* disabled, a span in its place, since a disabled anchor is no link at all.
|
|
43
|
+
*/
|
|
44
|
+
function linkRenderer(element) {
|
|
45
|
+
return (domProps, state) => {
|
|
46
|
+
const merged = withElementProps(domProps, element);
|
|
47
|
+
if (state.isDisabled) return createElement("span", {
|
|
48
|
+
...merged,
|
|
49
|
+
href: void 0
|
|
50
|
+
});
|
|
51
|
+
return createElement("a", merged);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function withElementProps(domProps, element) {
|
|
55
|
+
const dom = domProps;
|
|
56
|
+
return {
|
|
57
|
+
...domProps,
|
|
58
|
+
...element.aria,
|
|
59
|
+
onKeyDown: chain(dom.onKeyDown, element.onKeyDown),
|
|
60
|
+
onKeyUp: chain(dom.onKeyUp, element.onKeyUp)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { ariaAttributesOf, buttonRenderer, linkRenderer };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=aria.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aria.js","names":[],"sources":["../../src/render/aria.tsx"],"sourcesContent":["import type { AriaAttributes, KeyboardEventHandler } from 'react'\nimport type { ButtonProps, LinkProps } from 'react-aria-components'\n\nimport { createElement } from 'react'\n\n// The four React Aria forwards on its own, and may combine with what a parent\n// provides through context — a dialog's trigger names its dialog this way.\n// Everything else it drops, so those stay out of the spread below rather\n// than overriding a value React Aria computed.\nconst LABELLING = new Set([\n 'aria-describedby',\n 'aria-details',\n 'aria-label',\n 'aria-labelledby',\n])\n\n/**\n * What a component puts on the element itself, past React Aria: the `aria-*`\n * props it would drop, and the keyboard handlers it would wrap.\n *\n * React Aria forwards only the labelling attributes, which is right for the\n * states it manages but leaves a call site no way to mark a button as\n * expanded, pressed, or in control of something React Aria does not know\n * about. Its keyboard handlers stop propagation unless the handler asks\n * otherwise, which is its own convention rather than the DOM's; attached to\n * the element directly they bubble as a call site expects, and an Escape\n * pressed on a button inside a dialog still reaches the dialog.\n */\ninterface ElementProps {\n aria: AriaAttributes\n onKeyDown?: KeyboardEventHandler<HTMLElement> | undefined\n onKeyUp?: KeyboardEventHandler<HTMLElement> | undefined\n}\n\n/** The `aria-*` props React Aria would drop, collected for {@link ElementProps}. */\nfunction ariaAttributesOf(props: object): AriaAttributes {\n const aria: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(props)) {\n if (key.startsWith('aria-') && !LABELLING.has(key)) {\n aria[key] = value\n }\n }\n return aria\n}\n\n/**\n * The `render` function for a React Aria `Button`: the element it would have\n * rendered, plus {@link ElementProps}, handed on to the call site's own\n * `render` when there is one. Built by a call rather than written inline at\n * the prop, which is what react-perf's no-new-function-as-prop is after.\n *\n * `createElement` rather than JSX, here and below: React Aria types the\n * props it hands over as a union of the elements it might have rendered, and\n * a spread of that union onto one tag does not type-check, where the plain\n * call takes any props object.\n */\nfunction buttonRenderer(\n element: ElementProps,\n render: ButtonProps['render'],\n): NonNullable<ButtonProps['render']> {\n return (domProps, state) => {\n const merged = withElementProps(domProps, element)\n if (render !== undefined) {\n return render(merged, state)\n }\n return createElement('button', merged)\n }\n}\n\n// React Aria's own keyboard handling comes first, then the call site's.\nfunction chain<Event>(\n first: ((event: Event) => void) | undefined,\n second: ((event: Event) => void) | undefined,\n) {\n if (second === undefined) {\n return first\n }\n return (event: Event) => {\n first?.(event)\n second(event)\n }\n}\n\n/**\n * The same for a React Aria `Link`, which renders an anchor — or, while\n * disabled, a span in its place, since a disabled anchor is no link at all.\n */\nfunction linkRenderer(element: ElementProps): NonNullable<LinkProps['render']> {\n return (domProps, state) => {\n const merged = withElementProps(domProps, element)\n if (state.isDisabled) {\n return createElement('span', { ...merged, href: undefined })\n }\n return createElement('a', merged)\n }\n}\n\nfunction withElementProps<Props extends object>(\n domProps: Props,\n element: ElementProps,\n) {\n const dom = domProps as Props & {\n onKeyDown?: KeyboardEventHandler<HTMLElement>\n onKeyUp?: KeyboardEventHandler<HTMLElement>\n }\n return {\n ...domProps,\n ...element.aria,\n onKeyDown: chain(dom.onKeyDown, element.onKeyDown),\n onKeyUp: chain(dom.onKeyUp, element.onKeyUp),\n }\n}\n\nexport type { ElementProps }\n\nexport { ariaAttributesOf, buttonRenderer, linkRenderer }\n"],"mappings":";;AASA,MAAM,4BAAY,IAAI,IAAI;CACxB;CACA;CACA;CACA;AAAiB,CAClB;;AAqBD,SAAS,iBAAiB,OAA+B;CACvD,MAAM,OAAgC,CAAC;CACvC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,IAAI,IAAI,WAAW,OAAO,KAAK,CAAC,UAAU,IAAI,GAAG,GAC/C,KAAK,OAAO;CAGhB,OAAO;AACT;;;;;;;;;;;;AAaA,SAAS,eACP,SACA,QACoC;CACpC,QAAQ,UAAU,UAAU;EAC1B,MAAM,SAAS,iBAAiB,UAAU,OAAO;EACjD,IAAI,WAAW,KAAA,GACb,OAAO,OAAO,QAAQ,KAAK;EAE7B,OAAO,cAAc,UAAU,MAAM;CACvC;AACF;AAGA,SAAS,MACP,OACA,QACA;CACA,IAAI,WAAW,KAAA,GACb,OAAO;CAET,QAAQ,UAAiB;EACvB,QAAQ,KAAK;EACb,OAAO,KAAK;CACd;AACF;;;;;AAMA,SAAS,aAAa,SAAyD;CAC7E,QAAQ,UAAU,UAAU;EAC1B,MAAM,SAAS,iBAAiB,UAAU,OAAO;EACjD,IAAI,MAAM,YACR,OAAO,cAAc,QAAQ;GAAE,GAAG;GAAQ,MAAM,KAAA;EAAU,CAAC;EAE7D,OAAO,cAAc,KAAK,MAAM;CAClC;AACF;AAEA,SAAS,iBACP,UACA,SACA;CACA,MAAM,MAAM;CAIZ,OAAO;EACL,GAAG;EACH,GAAG,QAAQ;EACX,WAAW,MAAM,IAAI,WAAW,QAAQ,SAAS;EACjD,SAAS,MAAM,IAAI,SAAS,QAAQ,OAAO;CAC7C;AACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, HTMLAttributes, JSX, ReactElement, Ref } from "react";
|
|
2
|
+
//#region src/render/useRender.d.ts
|
|
3
|
+
type IntrinsicTag = keyof JSX.IntrinsicElements;
|
|
4
|
+
/**
|
|
5
|
+
* The props of a component rendered through {@link useRender}: everything the
|
|
6
|
+
* element accepts, plus `render`.
|
|
7
|
+
*/
|
|
8
|
+
type RenderComponentProps<Tag extends IntrinsicTag> = ComponentPropsWithRef<Tag> & {
|
|
9
|
+
/**
|
|
10
|
+
* Replaces the element the component renders by default: another tag, as
|
|
11
|
+
* `render={<h2 />}`, or a function returning the element to render, given
|
|
12
|
+
* the props the component would have put on its own.
|
|
13
|
+
*/
|
|
14
|
+
render?: RenderProp | undefined;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* What a `render` function is handed: the element's props, merged and ready
|
|
18
|
+
* to spread onto whatever element it returns. `any` on purpose, in the one
|
|
19
|
+
* place it appears — the function may return any element, and a ref typed for
|
|
20
|
+
* one of them would refuse every other.
|
|
21
|
+
*/
|
|
22
|
+
type RenderFunctionProps = HTMLAttributes<any> & {
|
|
23
|
+
ref?: Ref<any> | undefined;
|
|
24
|
+
};
|
|
25
|
+
type RenderProp = ((props: RenderFunctionProps) => ReactElement) | ReactElement;
|
|
26
|
+
//#endregion
|
|
27
|
+
export type { RenderComponentProps, RenderFunctionProps };
|
|
28
|
+
//# sourceMappingURL=useRender.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { cloneElement, createElement, isValidElement, useMemo } from "react";
|
|
2
|
+
//#region src/render/useRender.tsx
|
|
3
|
+
function attach(ref, node) {
|
|
4
|
+
if (ref === null) return () => {};
|
|
5
|
+
if (typeof ref === "function") {
|
|
6
|
+
const cleanup = ref(node);
|
|
7
|
+
return typeof cleanup === "function" ? cleanup : () => {
|
|
8
|
+
ref(null);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
ref.current = node;
|
|
12
|
+
return () => {
|
|
13
|
+
ref.current = null;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function isHandler(value) {
|
|
17
|
+
return typeof value === "function";
|
|
18
|
+
}
|
|
19
|
+
function isHandlerKey(key) {
|
|
20
|
+
return /^on[A-Z]/u.test(key);
|
|
21
|
+
}
|
|
22
|
+
function isRefCallback(value) {
|
|
23
|
+
return typeof value === "function";
|
|
24
|
+
}
|
|
25
|
+
function isRefObject(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && "current" in value;
|
|
27
|
+
}
|
|
28
|
+
function joinClassNames(...names) {
|
|
29
|
+
return names.filter(Boolean).join(" ") || void 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The component's props over the element's own. The element wins a plain
|
|
33
|
+
* conflict — it is the more specific of the two — while class names, styles
|
|
34
|
+
* and handlers are combined rather than chosen between. `ref` is left out:
|
|
35
|
+
* the hook merges the two refs itself.
|
|
36
|
+
*/
|
|
37
|
+
function mergeElementProps(ours, theirs) {
|
|
38
|
+
const merged = {
|
|
39
|
+
...ours,
|
|
40
|
+
...theirs
|
|
41
|
+
};
|
|
42
|
+
merged.className = joinClassNames(stringOf(ours.className), stringOf(theirs.className));
|
|
43
|
+
merged.style = mergeStyleObjects(styleOf(ours.style), styleOf(theirs.style));
|
|
44
|
+
for (const key of Object.keys(theirs)) {
|
|
45
|
+
const own = ours[key];
|
|
46
|
+
const their = theirs[key];
|
|
47
|
+
if (isHandlerKey(key) && isHandler(own) && isHandler(their)) merged[key] = (...args) => {
|
|
48
|
+
their(...args);
|
|
49
|
+
own(...args);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
delete merged.ref;
|
|
53
|
+
return merged;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One ref that feeds both, or whichever one there is. Attaching through a
|
|
57
|
+
* callback lets each side keep its own cleanup: React 19 calls a callback
|
|
58
|
+
* ref's returned function on detach in place of calling it with `null`, so
|
|
59
|
+
* the merged callback returns one that does the same for each of the two.
|
|
60
|
+
*/
|
|
61
|
+
function mergeRefs(a, b) {
|
|
62
|
+
if (a === void 0) return b;
|
|
63
|
+
if (b === void 0) return a;
|
|
64
|
+
return (node) => {
|
|
65
|
+
const detachA = attach(a, node);
|
|
66
|
+
const detachB = attach(b, node);
|
|
67
|
+
return () => {
|
|
68
|
+
detachA();
|
|
69
|
+
detachB();
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function mergeStyleObjects(ours, theirs) {
|
|
74
|
+
if (ours === void 0 && theirs === void 0) return;
|
|
75
|
+
return {
|
|
76
|
+
...ours,
|
|
77
|
+
...theirs
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function refOf(value) {
|
|
81
|
+
if (isRefCallback(value) || isRefObject(value)) return value;
|
|
82
|
+
}
|
|
83
|
+
function stringOf(value) {
|
|
84
|
+
return typeof value === "string" ? value : void 0;
|
|
85
|
+
}
|
|
86
|
+
function styleOf(value) {
|
|
87
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Renders a component's element, or whatever its `render` prop asks for
|
|
91
|
+
* instead.
|
|
92
|
+
*
|
|
93
|
+
* `render` is either a React element, cloned with the component's props
|
|
94
|
+
* merged over its own — class names joined, styles merged, event handlers
|
|
95
|
+
* chained, the element's own value winning any other conflict — or a function
|
|
96
|
+
* handed those merged props to return an element of its choosing. Without one,
|
|
97
|
+
* `defaultTagName` is rendered. A default `button` is given `type="button"`,
|
|
98
|
+
* since a bare one submits any form it happens to sit in.
|
|
99
|
+
*
|
|
100
|
+
* The element form is the point of owning this. React Aria's own `render`
|
|
101
|
+
* is a function that must return the element the component would have
|
|
102
|
+
* rendered itself, which suits its interactive components and nothing else;
|
|
103
|
+
* the non-interactive ones here take `render={<h2 />}`, and keeping that
|
|
104
|
+
* contract in a hook of the library's own is what lets them stay put
|
|
105
|
+
* whichever behaviour library sits underneath the rest.
|
|
106
|
+
*/
|
|
107
|
+
function useRender({ defaultTagName = "div", props = {}, render }) {
|
|
108
|
+
const element = isValidElement(render) ? render : void 0;
|
|
109
|
+
const ownRef = refOf(props.ref);
|
|
110
|
+
const elementRef = refOf(element?.props.ref);
|
|
111
|
+
const ref = useMemo(() => mergeRefs(ownRef, elementRef), [ownRef, elementRef]);
|
|
112
|
+
if (element !== void 0) return cloneElement(element, {
|
|
113
|
+
...mergeElementProps(props, element.props),
|
|
114
|
+
ref
|
|
115
|
+
});
|
|
116
|
+
if (typeof render === "function") return render({
|
|
117
|
+
...props,
|
|
118
|
+
ref
|
|
119
|
+
});
|
|
120
|
+
return createElement(defaultTagName, defaultTagName === "button" ? {
|
|
121
|
+
type: "button",
|
|
122
|
+
...props,
|
|
123
|
+
ref
|
|
124
|
+
} : {
|
|
125
|
+
...props,
|
|
126
|
+
ref
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { useRender };
|
|
131
|
+
|
|
132
|
+
//# sourceMappingURL=useRender.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRender.js","names":[],"sources":["../../src/render/useRender.tsx"],"sourcesContent":["import type {\n ComponentPropsWithRef,\n CSSProperties,\n HTMLAttributes,\n JSX,\n ReactElement,\n Ref,\n RefCallback,\n RefObject,\n} from 'react'\n\nimport { cloneElement, createElement, isValidElement, useMemo } from 'react'\n\ntype Handler = (...args: ReadonlyArray<unknown>) => unknown\n\ntype IntrinsicTag = keyof JSX.IntrinsicElements\n\n/**\n * The props of a component rendered through {@link useRender}: everything the\n * element accepts, plus `render`.\n */\ntype RenderComponentProps<Tag extends IntrinsicTag> =\n ComponentPropsWithRef<Tag> & {\n /**\n * Replaces the element the component renders by default: another tag, as\n * `render={<h2 />}`, or a function returning the element to render, given\n * the props the component would have put on its own.\n */\n render?: RenderProp | undefined\n }\n\n/**\n * What a `render` function is handed: the element's props, merged and ready\n * to spread onto whatever element it returns. `any` on purpose, in the one\n * place it appears — the function may return any element, and a ref typed for\n * one of them would refuse every other.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- see above\ntype RenderFunctionProps = HTMLAttributes<any> & { ref?: Ref<any> | undefined }\n\ntype RenderProp = ((props: RenderFunctionProps) => ReactElement) | ReactElement\n\ninterface UseRenderParameters {\n /**\n * The tag rendered when `render` is not given.\n * @default 'div'\n */\n defaultTagName?: IntrinsicTag | undefined\n /** The props to put on whichever element ends up rendered. */\n props?: Record<string, unknown> | undefined\n render?: RenderProp | undefined\n}\n\nfunction attach(ref: Ref<Element>, node: Element | null): () => void {\n if (ref === null) {\n return () => {}\n }\n if (typeof ref === 'function') {\n const cleanup = ref(node)\n return typeof cleanup === 'function'\n ? cleanup\n : () => {\n ref(null)\n }\n }\n ref.current = node\n return () => {\n ref.current = null\n }\n}\n\nfunction isHandler(value: unknown): value is Handler {\n return typeof value === 'function'\n}\n\nfunction isHandlerKey(key: string) {\n return /^on[A-Z]/u.test(key)\n}\n\nfunction isRefCallback(value: unknown): value is RefCallback<Element> {\n return typeof value === 'function'\n}\n\nfunction isRefObject(value: unknown): value is RefObject<Element | null> {\n return typeof value === 'object' && value !== null && 'current' in value\n}\n\n// undefined rather than '' when there is nothing to say, so an element with\n// no classes on either side renders without a class attribute at all.\nfunction joinClassNames(...names: ReadonlyArray<string | undefined>) {\n return names.filter(Boolean).join(' ') || undefined\n}\n\n/**\n * The component's props over the element's own. The element wins a plain\n * conflict — it is the more specific of the two — while class names, styles\n * and handlers are combined rather than chosen between. `ref` is left out:\n * the hook merges the two refs itself.\n */\nfunction mergeElementProps(\n ours: Record<string, unknown>,\n theirs: Record<string, unknown>,\n) {\n const merged: Record<string, unknown> = { ...ours, ...theirs }\n\n merged.className = joinClassNames(\n stringOf(ours.className),\n stringOf(theirs.className),\n )\n merged.style = mergeStyleObjects(styleOf(ours.style), styleOf(theirs.style))\n\n for (const key of Object.keys(theirs)) {\n const own = ours[key]\n const their = theirs[key]\n if (isHandlerKey(key) && isHandler(own) && isHandler(their)) {\n // The element's own handler runs first, as the more specific one.\n merged[key] = (...args: ReadonlyArray<unknown>) => {\n their(...args)\n own(...args)\n }\n }\n }\n\n delete merged.ref\n return merged\n}\n\n/**\n * One ref that feeds both, or whichever one there is. Attaching through a\n * callback lets each side keep its own cleanup: React 19 calls a callback\n * ref's returned function on detach in place of calling it with `null`, so\n * the merged callback returns one that does the same for each of the two.\n */\nfunction mergeRefs(\n a: Ref<Element> | undefined,\n b: Ref<Element> | undefined,\n): Ref<Element> | undefined {\n if (a === undefined) {\n return b\n }\n if (b === undefined) {\n return a\n }\n return (node: Element | null) => {\n const detachA = attach(a, node)\n const detachB = attach(b, node)\n return () => {\n detachA()\n detachB()\n }\n }\n}\n\nfunction mergeStyleObjects(\n ours: CSSProperties | undefined,\n theirs: CSSProperties | undefined,\n) {\n if (ours === undefined && theirs === undefined) {\n return undefined\n }\n return { ...ours, ...theirs }\n}\n\nfunction refOf(value: unknown): Ref<Element> | undefined {\n if (isRefCallback(value) || isRefObject(value)) {\n return value\n }\n return undefined\n}\n\nfunction stringOf(value: unknown) {\n return typeof value === 'string' ? value : undefined\n}\n\nfunction styleOf(value: unknown) {\n return typeof value === 'object' && value !== null\n ? (value as CSSProperties)\n : undefined\n}\n\n/**\n * Renders a component's element, or whatever its `render` prop asks for\n * instead.\n *\n * `render` is either a React element, cloned with the component's props\n * merged over its own — class names joined, styles merged, event handlers\n * chained, the element's own value winning any other conflict — or a function\n * handed those merged props to return an element of its choosing. Without one,\n * `defaultTagName` is rendered. A default `button` is given `type=\"button\"`,\n * since a bare one submits any form it happens to sit in.\n *\n * The element form is the point of owning this. React Aria's own `render`\n * is a function that must return the element the component would have\n * rendered itself, which suits its interactive components and nothing else;\n * the non-interactive ones here take `render={<h2 />}`, and keeping that\n * contract in a hook of the library's own is what lets them stay put\n * whichever behaviour library sits underneath the rest.\n */\nfunction useRender({\n defaultTagName = 'div',\n props = {},\n render,\n}: UseRenderParameters): ReactElement {\n const element = isValidElement<Record<string, unknown>>(render)\n ? render\n : undefined\n\n // Both refs, when there are two, have to land on one node. The merged\n // callback is memoised so React does not detach and re-attach it on every\n // render — a new identity each time would.\n const ownRef = refOf(props.ref)\n const elementRef = refOf(element?.props.ref)\n const ref = useMemo(() => mergeRefs(ownRef, elementRef), [ownRef, elementRef])\n\n if (element !== undefined) {\n return cloneElement(element, {\n ...mergeElementProps(props, element.props),\n ref,\n })\n }\n\n if (typeof render === 'function') {\n return render({ ...props, ref } as RenderFunctionProps)\n }\n\n return createElement(\n defaultTagName,\n defaultTagName === 'button'\n ? { type: 'button', ...props, ref }\n : { ...props, ref },\n )\n}\n\nexport type { RenderComponentProps, RenderFunctionProps, UseRenderParameters }\n\nexport { useRender }\n"],"mappings":";;AAqDA,SAAS,OAAO,KAAmB,MAAkC;CACnE,IAAI,QAAQ,MACV,aAAa,CAAC;CAEhB,IAAI,OAAO,QAAQ,YAAY;EAC7B,MAAM,UAAU,IAAI,IAAI;EACxB,OAAO,OAAO,YAAY,aACtB,gBACM;GACJ,IAAI,IAAI;EACV;CACN;CACA,IAAI,UAAU;CACd,aAAa;EACX,IAAI,UAAU;CAChB;AACF;AAEA,SAAS,UAAU,OAAkC;CACnD,OAAO,OAAO,UAAU;AAC1B;AAEA,SAAS,aAAa,KAAa;CACjC,OAAO,YAAY,KAAK,GAAG;AAC7B;AAEA,SAAS,cAAc,OAA+C;CACpE,OAAO,OAAO,UAAU;AAC1B;AAEA,SAAS,YAAY,OAAoD;CACvE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,aAAa;AACrE;AAIA,SAAS,eAAe,GAAG,OAA0C;CACnE,OAAO,MAAM,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,KAAK,KAAA;AAC5C;;;;;;;AAQA,SAAS,kBACP,MACA,QACA;CACA,MAAM,SAAkC;EAAE,GAAG;EAAM,GAAG;CAAO;CAE7D,OAAO,YAAY,eACjB,SAAS,KAAK,SAAS,GACvB,SAAS,OAAO,SAAS,CAC3B;CACA,OAAO,QAAQ,kBAAkB,QAAQ,KAAK,KAAK,GAAG,QAAQ,OAAO,KAAK,CAAC;CAE3E,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;EACrC,MAAM,MAAM,KAAK;EACjB,MAAM,QAAQ,OAAO;EACrB,IAAI,aAAa,GAAG,KAAK,UAAU,GAAG,KAAK,UAAU,KAAK,GAExD,OAAO,QAAQ,GAAG,SAAiC;GACjD,MAAM,GAAG,IAAI;GACb,IAAI,GAAG,IAAI;EACb;CAEJ;CAEA,OAAO,OAAO;CACd,OAAO;AACT;;;;;;;AAQA,SAAS,UACP,GACA,GAC0B;CAC1B,IAAI,MAAM,KAAA,GACR,OAAO;CAET,IAAI,MAAM,KAAA,GACR,OAAO;CAET,QAAQ,SAAyB;EAC/B,MAAM,UAAU,OAAO,GAAG,IAAI;EAC9B,MAAM,UAAU,OAAO,GAAG,IAAI;EAC9B,aAAa;GACX,QAAQ;GACR,QAAQ;EACV;CACF;AACF;AAEA,SAAS,kBACP,MACA,QACA;CACA,IAAI,SAAS,KAAA,KAAa,WAAW,KAAA,GACnC;CAEF,OAAO;EAAE,GAAG;EAAM,GAAG;CAAO;AAC9B;AAEA,SAAS,MAAM,OAA0C;CACvD,IAAI,cAAc,KAAK,KAAK,YAAY,KAAK,GAC3C,OAAO;AAGX;AAEA,SAAS,SAAS,OAAgB;CAChC,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;AAEA,SAAS,QAAQ,OAAgB;CAC/B,OAAO,OAAO,UAAU,YAAY,UAAU,OACzC,QACD,KAAA;AACN;;;;;;;;;;;;;;;;;;;AAoBA,SAAS,UAAU,EACjB,iBAAiB,OACjB,QAAQ,CAAC,GACT,UACoC;CACpC,MAAM,UAAU,eAAwC,MAAM,IAC1D,SACA,KAAA;CAKJ,MAAM,SAAS,MAAM,MAAM,GAAG;CAC9B,MAAM,aAAa,MAAM,SAAS,MAAM,GAAG;CAC3C,MAAM,MAAM,cAAc,UAAU,QAAQ,UAAU,GAAG,CAAC,QAAQ,UAAU,CAAC;CAE7E,IAAI,YAAY,KAAA,GACd,OAAO,aAAa,SAAS;EAC3B,GAAG,kBAAkB,OAAO,QAAQ,KAAK;EACzC;CACF,CAAC;CAGH,IAAI,OAAO,WAAW,YACpB,OAAO,OAAO;EAAE,GAAG;EAAO;CAAI,CAAwB;CAGxD,OAAO,cACL,gBACA,mBAAmB,WACf;EAAE,MAAM;EAAU,GAAG;EAAO;CAAI,IAChC;EAAE,GAAG;EAAO;CAAI,CACtB;AACF"}
|
package/dist/styles/merge.js
CHANGED
|
@@ -3,19 +3,25 @@ function joinClassNames(...names) {
|
|
|
3
3
|
return names.filter(Boolean).join(" ") || void 0;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* {@link mergeStyles} for an element rendered by a
|
|
7
|
-
* `className` and `style` may each be a function of the
|
|
8
|
-
* state rather than a plain value.
|
|
6
|
+
* {@link mergeStyles} for an element rendered by a behaviour-library
|
|
7
|
+
* component, where `className` and `style` may each be a function of the
|
|
8
|
+
* component's render state rather than a plain value.
|
|
9
9
|
*
|
|
10
|
-
* Returns functions unconditionally, since
|
|
11
|
-
*
|
|
12
|
-
* changes with the arguments. `compiled` is itself a
|
|
13
|
-
* component's own styles depend on that state.
|
|
10
|
+
* Returns functions unconditionally, since the libraries accept them
|
|
11
|
+
* everywhere they accept the plain form and one shape is easier to follow
|
|
12
|
+
* than a union that changes with the arguments. `compiled` is itself a
|
|
13
|
+
* function where the component's own styles depend on that state.
|
|
14
|
+
*
|
|
15
|
+
* The two consumer functions carry their own state types rather than sharing
|
|
16
|
+
* `State`: React Aria hands a `className` function the render state plus
|
|
17
|
+
* `defaultClassName`, and a `style` function the same state plus
|
|
18
|
+
* `defaultStyle`, and one parameter could not name both. Each extends
|
|
19
|
+
* `State`, so `compiled` can be handed either.
|
|
14
20
|
*/
|
|
15
21
|
function mergeStatefulStyles(compiled, consumer) {
|
|
16
22
|
const compiledFor = (state) => typeof compiled === "function" ? compiled(state) : compiled;
|
|
17
23
|
return {
|
|
18
|
-
className: (state) => joinClassNames(compiledFor(state).className, typeof consumer.className === "function" ? consumer.className(state) : consumer.className),
|
|
24
|
+
className: (state) => joinClassNames(compiledFor(state).className, typeof consumer.className === "function" ? consumer.className(state) : consumer.className) ?? "",
|
|
19
25
|
style: (state) => mergeStyleObjects(compiledFor(state).style, typeof consumer.style === "function" ? consumer.style(state) : consumer.style)
|
|
20
26
|
};
|
|
21
27
|
}
|
package/dist/styles/merge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.js","names":[],"sources":["../../src/styles/merge.ts"],"sourcesContent":["import type * as stylex from '@stylexjs/stylex';\nimport type { CSSProperties } from 'react';\n\n// What `stylex.props()` hands back: a className, and a `style` object only\n// when a dynamic style contributed one — Feed's cell minimum, for instance,\n// arrives as a custom property there rather than as a class.\ntype CompiledStyles = ReturnType<typeof stylex.props>;\n\n//
|
|
1
|
+
{"version":3,"file":"merge.js","names":[],"sources":["../../src/styles/merge.ts"],"sourcesContent":["import type * as stylex from '@stylexjs/stylex';\nimport type { CSSProperties } from 'react';\n\n// What `stylex.props()` hands back: a className, and a `style` object only\n// when a dynamic style contributed one — Feed's cell minimum, for instance,\n// arrives as a custom property there rather than as a class.\ntype CompiledStyles = ReturnType<typeof stylex.props>;\n\n// React Aria lets `className` and `style` be functions of a component's own\n// render state, which is how a style can depend on something CSS cannot\n// select — Chip's selected state, a tab's active one. A consumer may pass\n// either form, so anything merging with one has to accept both.\ntype StatefulClassName<State> = ((state: State) => string | undefined) | string | undefined;\ntype StatefulStyle<State> = ((state: State) => CSSProperties | undefined) | CSSProperties | undefined;\nfunction joinClassNames(...names: ReadonlyArray<string | undefined>) {\n // undefined rather than '' when there is nothing to say, so an element with\n // no classes on either side renders without a class attribute at all.\n return names.filter(Boolean).join(' ') || undefined;\n}\n\n/**\n * {@link mergeStyles} for an element rendered by a behaviour-library\n * component, where `className` and `style` may each be a function of the\n * component's render state rather than a plain value.\n *\n * Returns functions unconditionally, since the libraries accept them\n * everywhere they accept the plain form and one shape is easier to follow\n * than a union that changes with the arguments. `compiled` is itself a\n * function where the component's own styles depend on that state.\n *\n * The two consumer functions carry their own state types rather than sharing\n * `State`: React Aria hands a `className` function the render state plus\n * `defaultClassName`, and a `style` function the same state plus\n * `defaultStyle`, and one parameter could not name both. Each extends\n * `State`, so `compiled` can be handed either.\n */\nfunction mergeStatefulStyles<State, ClassNameState extends State, StyleState extends State>(compiled: ((state: State) => CompiledStyles) | CompiledStyles, consumer: {\n className?: StatefulClassName<ClassNameState>;\n style?: StatefulStyle<StyleState>;\n}) {\n const compiledFor = (state: State) => typeof compiled === 'function' ? compiled(state) : compiled;\n\n // Resolved inline rather than through one shared helper: a generic one\n // cannot narrow `typeof value === 'function'` away from its own type\n // parameter, so it needs an assertion where these two need none.\n //\n // '' rather than undefined when there is nothing to say: a className\n // function has to return a string for React Aria, and every component that\n // reaches here has compiled classes of its own, so the case never renders.\n return {\n className: (state: ClassNameState) => joinClassNames(compiledFor(state).className, typeof consumer.className === 'function' ? consumer.className(state) : consumer.className) ?? '',\n style: (state: StyleState) => mergeStyleObjects(compiledFor(state).style, typeof consumer.style === 'function' ? consumer.style(state) : consumer.style)\n };\n}\nfunction mergeStyleObjects(compiled: CompiledStyles['style'], consumer: CSSProperties | undefined) {\n if (compiled === undefined && consumer === undefined) {\n return undefined;\n }\n return {\n ...compiled,\n ...consumer\n };\n}\n\n/**\n * Combines the styles StyleX compiled for a component with the `className`\n * and `style` its call site passed, so both reach the element.\n *\n * Spreading `stylex.props()` after the consumer's own props is what this\n * replaces, and that spread silently dropped both: the returned object always\n * carries `className`, so it overwrote rather than merged, leaving no way to\n * position a component from outside it and no warning that the prop had gone.\n *\n * Order is presentational rather than load-bearing. StyleX compiles into\n * `@layer` (see `useCSSLayers` in tsdown.config.ts) and a consumer's own\n * stylesheet is unlayered, so the cascade already gives the call site the\n * last word whichever way round the two class strings are written.\n *\n * The call site's `style` is applied over the compiled one, which is the\n * order that matters: both are inline, so nothing but source order separates\n * them.\n */\nfunction mergeStyles(compiled: CompiledStyles, consumer: {\n className?: string | undefined;\n style?: CSSProperties | undefined;\n}) {\n return {\n ...compiled,\n className: joinClassNames(compiled.className, consumer.className),\n style: mergeStyleObjects(compiled.style, consumer.style)\n };\n}\nexport { mergeStatefulStyles, mergeStyles };"],"mappings":";AAcA,SAAS,eAAe,GAAG,OAA0C;CAGnE,OAAO,MAAM,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,KAAK,KAAA;AAC5C;;;;;;;;;;;;;;;;;AAkBA,SAAS,oBAAmF,UAA+D,UAGxJ;CACD,MAAM,eAAe,UAAiB,OAAO,aAAa,aAAa,SAAS,KAAK,IAAI;CASzF,OAAO;EACL,YAAY,UAA0B,eAAe,YAAY,KAAK,CAAC,CAAC,WAAW,OAAO,SAAS,cAAc,aAAa,SAAS,UAAU,KAAK,IAAI,SAAS,SAAS,KAAK;EACjL,QAAQ,UAAsB,kBAAkB,YAAY,KAAK,CAAC,CAAC,OAAO,OAAO,SAAS,UAAU,aAAa,SAAS,MAAM,KAAK,IAAI,SAAS,KAAK;CACzJ;AACF;AACA,SAAS,kBAAkB,UAAmC,UAAqC;CACjG,IAAI,aAAa,KAAA,KAAa,aAAa,KAAA,GACzC;CAEF,OAAO;EACL,GAAG;EACH,GAAG;CACL;AACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAS,YAAY,UAA0B,UAG5C;CACD,OAAO;EACL,GAAG;EACH,WAAW,eAAe,SAAS,WAAW,SAAS,SAAS;EAChE,OAAO,kBAAkB,SAAS,OAAO,SAAS,KAAK;CACzD;AACF"}
|