Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize what is going to be copied / Copy everything together #193

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions docs/advanced/controlled-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const data = useControls({
See an [example in Storybook](https://leva.pmnd.rs/?path=/story/misc-input-options--on-change).

### Transient

If you need the `onChange` callback while still wanting to retrieve the input value, you can set `transient: false`.

```jsx
Expand All @@ -62,16 +63,10 @@ const [, set] = useControls(() => ({
}))

const targetRef = useRef()
useDrag(
({ offset: [x, y] }) => set({ position: { x, y } }),
{ domTarget: targetRef }
)
useDrag(({ offset: [x, y] }) => set({ position: { x, y } }), { domTarget: targetRef })

const targetRef = useRef()
useDrag(
({ offset: [x, y] }) => set({ position: { x, y } }),
{ domTarget: targetRef }
)
useDrag(({ offset: [x, y] }) => set({ position: { x, y } }), { domTarget: targetRef })
```

[codesandbox-drag]: (https://codesandbox.io/s/leva-controlled-input-71dkb?file=/src/App.tsx)
Expand Down
18 changes: 8 additions & 10 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,20 @@ You can configure Leva by using the `<Leva>` component anywhere in your App:
import { Leva } from 'leva'

export default function MyApp() {

return (
<>
<Leva
theme={myTheme} // you can pass a custom theme (see the styling section)
fill // default = false, true makes the pane fill the parent dom node it's rendered in
flat // default = false, true removes border radius and shadow
oneLineLabels // default = false, alternative layout for labels, with labels and fields on separate rows
hideTitleBar // default = false, hides the GUI header
collapsed // default = false, when true the GUI is collpased
hidden // default = false, when true the GUI is hidden
theme={myTheme} // you can pass a custom theme (see the styling section)
fill // default = false, true makes the pane fill the parent dom node it's rendered in
flat // default = false, true removes border radius and shadow
oneLineLabels // default = false, alternative layout for labels, with labels and fields on separate rows
hideTitleBar // default = false, hides the GUI header
collapsed // default = false, when true the GUI is collpased
hidden // default = false, when true the GUI is hidden
/>
</>
)

}
```

- TODO // Add default config for LevaPanel as well
- TODO // Add default config for LevaPanel as well
56 changes: 52 additions & 4 deletions packages/leva/src/components/Leva/Filter.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useMemo, useState, useEffect, useRef } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useDrag } from 'react-use-gesture'
import { debounce } from '../../utils'
import { debounce, LevaErrors, warn } from '../../utils'
import { FolderTitleProps } from '../Folder'
import { Chevron } from '../UI'
import { StyledFilterInput, StyledTitleWithFilter, TitleContainer, Icon, FilterWrapper } from './StyledFilter'
import { FilterWrapper, Icon, StyledFilterInput, StyledTitleWithFilter, TitleContainer } from './StyledFilter'
import { useStoreContext } from '../../context'
import { DataInput } from '../../types'

type FilterProps = { setFilter: (value: string) => void; toggle: (flag?: boolean) => void }

Expand Down Expand Up @@ -53,6 +55,8 @@ export type TitleWithFilterProps = FilterProps &
title: React.ReactNode
drag: boolean
filterEnabled: boolean
hideCopyButton: boolean
copy?: (values: unknown) => string
}

export function TitleWithFilter({
Expand All @@ -63,9 +67,12 @@ export function TitleWithFilter({
title,
drag,
filterEnabled,
hideCopyButton,
copy,
}: TitleWithFilterProps) {
const [filterShown, setShowFilter] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
const store = useStoreContext()

useEffect(() => {
if (filterShown) inputRef.current?.focus()
Expand All @@ -84,13 +91,35 @@ export function TitleWithFilter({
return () => window.removeEventListener('keydown', handleShortcut)
}, [])

const [copied, setCopied] = useState(false)
const handleCopyClick = async () => {
const data = { ...store.getData() } as any
try {
for (let key in data) {
if (data.hasOwnProperty(key)) {
const keyData = data[key] as DataInput
if (!keyData.disabled) {
data[key] = keyData.value
} else {
delete data[key]
}
}
}
await navigator.clipboard.writeText(copy ? copy(data) : JSON.stringify(data))
setCopied(true)
} catch {
warn(LevaErrors.CLIPBOARD_ERROR, data)
}
}

return (
<>
<StyledTitleWithFilter mode={drag ? 'drag' : undefined}>
<StyledTitleWithFilter mode={drag ? 'drag' : undefined} onPointerLeave={() => setCopied(false)}>
<Icon active={!toggled} onClick={() => toggle()}>
<Chevron toggled={toggled} width={12} height={8} />
</Icon>
<TitleContainer {...(drag ? bind() : {})} drag={drag} filterEnabled={filterEnabled}>
{!hideCopyButton && <div style={{ width: 40 }} />}
{title === undefined && drag ? (
<svg width="20" height="10" viewBox="0 0 28 14" xmlns="http://www.w3.org/2000/svg">
<circle cx="2" cy="2" r="2" />
Expand All @@ -104,6 +133,25 @@ export function TitleWithFilter({
title
)}
</TitleContainer>
{!hideCopyButton && (
<Icon onClick={!copied ? handleCopyClick : undefined} title={`Click to copy all values`}>
{!copied ? (
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
<path
fillRule="evenodd"
d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm9.707 5.707a1 1 0 00-1.414-1.414L9 12.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
)}
</Icon>
)}
{filterEnabled && (
<Icon active={filterShown} onClick={() => setShowFilter((f) => !f)}>
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 20 20">
Expand Down
55 changes: 30 additions & 25 deletions packages/leva/src/components/Leva/LevaRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export type LevaRootProps = {
* If true, the copy button will be hidden
*/
hideCopyButton?: boolean
/**
* Change what will be copied when clicking the global copy button
*/
copy?: (values: unknown) => string
Comment on lines 65 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether it might sense to group those options similar to the titleBar options?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of that, but hideCopyButton works for both Input and Title Bar copy button.
But perhaps I could move copy into titleBar? What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on this? I was planning on finishing the changes for this PR today.

}

export function LevaRoot({ store, hidden = false, theme, collapsed = false, ...props }: LevaRootProps) {
Expand Down Expand Up @@ -98,6 +102,7 @@ const LevaCore = React.memo(
filter: true,
},
hideCopyButton = false,
copy,
toggled,
setToggle,
}: LevaCoreProps) => {
Expand All @@ -116,31 +121,31 @@ const LevaCore = React.memo(

return (
<PanelSettingsContext.Provider value={{ hideCopyButton }}>
<StyledRoot
ref={rootRef}
className={rootClass}
fill={fill}
flat={flat}
oneLineLabels={oneLineLabels}
hideTitleBar={!titleBar}
style={{ display: shouldShow ? 'block' : 'none' }}>
{titleBar && (
<TitleWithFilter
onDrag={set}
setFilter={setFilter}
toggle={(flag?: boolean) => setToggle((t) => flag ?? !t)}
toggled={toggled}
title={title}
drag={drag}
filterEnabled={filterEnabled}
/>
)}
{shouldShow && (
<StoreContext.Provider value={store}>
<TreeWrapper isRoot fill={fill} flat={flat} tree={tree} toggled={toggled} />
</StoreContext.Provider>
)}
</StyledRoot>
<StoreContext.Provider value={store}>
<StyledRoot
ref={rootRef}
className={rootClass}
fill={fill}
flat={flat}
oneLineLabels={oneLineLabels}
hideTitleBar={!titleBar}
style={{ display: shouldShow ? 'block' : 'none' }}>
{titleBar && (
<TitleWithFilter
onDrag={set}
setFilter={setFilter}
toggle={(flag?: boolean) => setToggle((t) => flag ?? !t)}
toggled={toggled}
title={title}
drag={drag}
filterEnabled={filterEnabled}
hideCopyButton={hideCopyButton}
copy={copy}
/>
)}
{shouldShow && <TreeWrapper isRoot fill={fill} flat={flat} tree={tree} toggled={toggled} />}
</StyledRoot>
</StoreContext.Provider>
</PanelSettingsContext.Provider>
)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/leva/src/components/UI/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function RawLabel(props: LabelProps) {
}

export function Label({ align, ...props }: LabelProps & { align?: 'top' }) {
const { value, label, key } = useInputContext()
const { value, label, key, copy = (key, value) => JSON.stringify({ [key]: value ?? '' }) } = useInputContext()
const { hideCopyButton } = usePanelSettingsContext()

const copyEnabled = !hideCopyButton && key !== undefined
Expand All @@ -54,7 +54,7 @@ export function Label({ align, ...props }: LabelProps & { align?: 'top' }) {

const handleClick = async () => {
try {
await navigator.clipboard.writeText(JSON.stringify({ [key]: value ?? '' }))
await navigator.clipboard.writeText(copy(key, value))
setCopied(true)
} catch {
warn(LevaErrors.CLIPBOARD_ERROR, { [key]: value })
Expand Down
2 changes: 2 additions & 0 deletions packages/leva/src/types/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ type GenericSchemaItemOptions = {
render?: RenderFn
label?: string | JSX.Element
hint?: string
copy?: (key: string, value: unknown) => string
}

type OnHandlerContext = DataInput & { get(path: string): any }
Expand Down Expand Up @@ -310,6 +311,7 @@ export type InputContextProps = {
id: string
label: string | JSX.Element
hint?: string
copy: (key: string, value: unknown) => string
path: string
key: string
optional: boolean
Expand Down
2 changes: 2 additions & 0 deletions packages/leva/src/utils/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function parseOptions(_input: any, key: string, mergedOptions = {}, custo
optional,
disabled,
hint,
copy,
onChange,
onEditStart,
onEditEnd,
Expand All @@ -55,6 +56,7 @@ export function parseOptions(_input: any, key: string, mergedOptions = {}, custo
key,
label: label ?? key,
hint,
copy,
transient: transient ?? !!onChange,
onEditStart,
onEditEnd,
Expand Down
38 changes: 34 additions & 4 deletions packages/leva/stories/input-options.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Meta } from '@storybook/react'
import Reset from './components/decorator-reset'
import { Half2Icon, OpacityIcon, DimensionsIcon } from '@radix-ui/react-icons'

import { folder, useControls, LevaInputs } from '../src'
import { folder, useControls, Leva, LevaInputs } from '../src'

export default {
title: 'Misc/Input options',
Expand Down Expand Up @@ -76,14 +76,14 @@ export const Optional = () => {

function A() {
const renderRef = React.useRef(0)
const divRef = React.useRef(null)
const divRef = React.useRef<HTMLDivElement>(null)
renderRef.current++
const data = useControls({
color: {
value: '#f00',
onChange: (v) => {
divRef.current.style.color = v
divRef.current.innerText = `Transient color is ${v}`
divRef.current!.style.color = v
divRef.current!.innerText = `Transient color is ${v}`
},
},
})
Expand Down Expand Up @@ -121,6 +121,36 @@ export const OnChange = () => {

OnChange.storyName = 'onChange'

export const CustomCopy = () => {
const { id, label } = useControls({
id: {
value: 'button-id',
copy(key, value): string {
return `<button ${key}="${value}">${label}</button>`
},
},
label: {
value: 'Leva is awesome',
copy(key, value) {
return `<button>{${value}}</button>`
},
},
})

const handleLevaCopy = (values: any) => {
return `<button
id="${id}"
>${label}</button>`
}

return (
<div>
<Leva copy={handleLevaCopy} />
<button id={id}>{label}</button>
</div>
)
}

export const OnChangeWithRender = ({ transient }) => {
const ref = React.useRef<HTMLPreElement | null>(null)
const data = useControls({
Expand Down