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

feat: support as prop in uploader component #236

Merged
merged 59 commits into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
c824e44
feat!: integrate new ucanto and upload-client (#119)
Dec 2, 2022
012a565
fix: update vanilla sign-up example (#129)
yusefnapora Dec 2, 2022
0110ae8
fix: update vue examples (#130)
travis Dec 2, 2022
9ccbe67
fix: update solid examples (#131)
travis Dec 2, 2022
8699b40
docs: update docs for new ucanto integration (#135)
yusefnapora Dec 2, 2022
034ada0
chore: update access-client version
Dec 2, 2022
18e976a
feat: introduce react-ui package
travis Dec 1, 2022
e7737c4
fix: render Uploader children rather than markup
travis Dec 1, 2022
89050e8
chore: move to typescript
travis Dec 2, 2022
c4d74e6
wip: initial version of react ui example app
travis Dec 2, 2022
7ec7c33
chore: move Uploader into react-uploader package (#137)
travis Dec 5, 2022
ec98de1
chore: add docstrings to uploader state and component
travis Dec 5, 2022
0454d94
fix: use preferred CID link
travis Dec 5, 2022
bb0e2b2
fix: correct typo
travis Dec 6, 2022
6dec856
feat: UploadsList components (#141)
travis Dec 7, 2022
ba91721
feat: Add Authenticator and SimpleAuthenticator (#152)
travis Dec 9, 2022
b9802ec
Merge branch 'main' into feat/w3ui-ui
travis Dec 15, 2022
ff0b3d4
fix: unclutter PR
travis Dec 15, 2022
27c4de7
fix: one more extraneous change
travis Dec 15, 2022
4896a4a
fix: delete unecessary dev dependencies
travis Dec 15, 2022
0932d8c
fix: clear out package-log.json and reinstall to fix build issues
travis Dec 15, 2022
560676b
chore: appease linter
travis Dec 15, 2022
fdcce6e
fix: a few more linting and compilation errors
travis Dec 15, 2022
642b68c
fix: update react uploads list example
travis Dec 15, 2022
7072602
fix: update vue examples
travis Dec 19, 2022
110907f
feat: W3Upload "drop in" component (#155)
travis Dec 19, 2022
a0220fd
Merge branch 'fix/react-uploads-list' into feat/w3ui-ui
travis Dec 19, 2022
9c5374a
Merge branch 'fix/update-vue-examples' into feat/w3ui-ui
travis Dec 19, 2022
e6a3002
fix: typo
travis Dec 19, 2022
45a605e
fix: Fix docs link from react-uploads-list to keyring page (#196)
natevw Jan 4, 2023
0e5cf4c
Merge branch 'main' into feat/w3ui-ui
travis Jan 4, 2023
7829770
fix: remove react-ui package and react/ui example
travis Jan 4, 2023
93b7ee0
fix: remove react-ui typescript and rollup configs
travis Jan 4, 2023
9b278e2
chore: add basic docs to the react keyring and uploads list
travis Jan 4, 2023
2eaff3a
chore: a bit more documentation
travis Jan 4, 2023
09c362b
fix: remove circular dependency
travis Jan 5, 2023
4b85682
Merge branch 'main' into feat/w3ui-ui
travis Jan 5, 2023
9fc7505
Merge branch 'main' into feat/w3ui-ui
travis Jan 6, 2023
ba8b2f9
fix: don't let callers override important properties
travis Jan 9, 2023
cd140a7
fix: Cid -> CID and tweak storedDAGShards typing
travis Jan 9, 2023
9a02e03
fix: tweaks from review
travis Jan 9, 2023
cf6a576
Merge branch 'main' into feat/w3ui-ui
travis Jan 9, 2023
d7fae7c
fix: one more arrow function conversion
travis Jan 9, 2023
8a7e0ad
feat: use an enum for uploader status
travis Jan 9, 2023
5d74af8
feat: support `as` prop in uploader components
travis Jan 7, 2023
4839cdf
feat: use ariakit-react-utils to build all headless components
travis Jan 10, 2023
de61dd8
feat: get a version of UploadsListRoot working
travis Jan 10, 2023
713abcc
fix: don't smother error in authenticator
travis Jan 11, 2023
f1a46b4
fix: unfurl props first in components
travis Jan 11, 2023
0cea4b6
Merge branch 'feat/w3ui-ui' into feat/headless-as
travis Jan 11, 2023
8645fbb
Merge branch 'main' into feat/w3ui-ui
travis Jan 12, 2023
0d3d488
Merge branch 'main' into feat/headless-as
travis Jan 12, 2023
f1e19ea
Merge branch 'main' into feat/w3ui-ui
travis Jan 12, 2023
9534c29
Merge branch 'feat/w3ui-ui' into feat/headless-as
travis Jan 12, 2023
543e27c
fix: headless authenticator bugfixes
travis Jan 12, 2023
959aac3
Merge branch 'fix/headless-bugfixes' into feat/headless-as
travis Jan 12, 2023
945dc3f
fix: add uploadsList to the render prop props
travis Jan 12, 2023
2d1400b
fix: automatically load the first page of results
travis Jan 12, 2023
9ba973f
Merge branch 'fix/autoload-uploads' into feat/headless-as
travis Jan 12, 2023
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
3 changes: 2 additions & 1 deletion packages/react-keyring/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
},
"homepage": "https://github.com/web3-storage/w3ui/tree/main/packages/react-keyring",
"dependencies": {
"@w3ui/keyring-core": "workspace:^"
"@w3ui/keyring-core": "workspace:^",
"ariakit-react-utils": "0.17.0-next.27"
},
"devDependencies": {
"@ucanto/interface": "^4.0.3",
Expand Down
64 changes: 44 additions & 20 deletions packages/react-keyring/src/Authenticator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, {
useState, createContext, useContext, useCallback, useMemo
} from 'react'
import type { As, Component, Props, Options } from 'ariakit-react-utils'
import type { ChangeEvent } from 'react'

import React, { Fragment, useState, createContext, useContext, useCallback, useMemo, useEffect } from 'react'
import { createComponent, createElement } from 'ariakit-react-utils'
import { useKeyring, KeyringContextState, KeyringContextActions } from './providers/Keyring'

export type AuthenticatorContextState = KeyringContextState & {
Expand Down Expand Up @@ -49,6 +51,16 @@ export const AuthenticatorContext = createContext<AuthenticatorContextValue>([
}
])

export const AgentLoader = ({ children }: { children: JSX.Element }): JSX.Element => {
const [, { loadAgent }] = useKeyring()
// eslint-disable-next-line
useEffect(() => { loadAgent() }, []) // load agent - once.
return children
}

export type AuthenticatorRootOptions<T extends As = typeof Fragment> = Options<T>
export type AuthenticatorRootProps<T extends As = typeof Fragment> = Props<AuthenticatorRootOptions<T>>

/**
* Top level component of the headless Authenticator.
*
Expand All @@ -57,7 +69,7 @@ export const AuthenticatorContext = createContext<AuthenticatorContextValue>([
* Designed to be used by Authenticator.Form, Authenticator.EmailInput
* and others to make it easy to implement authentication UI.
*/
export function Authenticator (props: any): JSX.Element {
export const AuthenticatorRoot: Component<AuthenticatorRootProps> = createComponent((props) => {
const [state, actions] = useKeyring()
const { createSpace, registerSpace } = actions
const [email, setEmail] = useState('')
Expand All @@ -74,59 +86,71 @@ export function Authenticator (props: any): JSX.Element {
} finally {
setSubmitted(false)
}
}, [setSubmitted, createSpace, registerSpace])
}, [email, setSubmitted, createSpace, registerSpace])

const value = useMemo<AuthenticatorContextValue>(() => [
{ ...state, email, submitted, handleRegisterSubmit },
{ ...actions, setEmail }
], [state, actions, email, submitted, handleRegisterSubmit])
return (
<AuthenticatorContext.Provider {...props} value={value} />
<AgentLoader>
<AuthenticatorContext.Provider value={value}>
{createElement(Fragment, props)}
</AuthenticatorContext.Provider>
</AgentLoader>
)
}
})

export type FormOptions<T extends As = 'form'> = Options<T>
export type FormProps<T extends As = 'form'> = Props<FormOptions<T>>

/**
* Form component for the headless Authenticator.
*
* A `form` designed to work with `Authenticator`. Any passed props will
* be passed along to the `form` component.
*/
Authenticator.Form = function Form (props: any) {
export const Form: Component<FormProps> = createComponent((props) => {
const [{ handleRegisterSubmit }] = useAuthenticator()
return (
<form {...props} onSubmit={handleRegisterSubmit} />
createElement('form', { ...props, onSubmit: handleRegisterSubmit })
)
}
})

export type EmailInputOptions<T extends As = 'input'> = Options<T>
export type EmailInputProps<T extends As = 'input'> = Props<EmailInputOptions<T>>

/**
* Input component for the headless Uploader.
*
* An email `input` designed to work with `Authenticator.Form`. Any passed props will
* be passed along to the `input` component.
*/
Authenticator.EmailInput = function EmailInput (props: any) {
export const EmailInput: Component<EmailInputProps> = createComponent(props => {
const [{ email }, { setEmail }] = useAuthenticator()
return (
<input {...props} type='email' value={email} onChange={e => setEmail(e.target.value)} />
)
}
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value), [])
return createElement('input', { ...props, type: 'email', value: email, onChange })
})

export type CancelButtonOptions<T extends As = 'button'> = Options<T>
export type CancelButtonProps<T extends As = 'button'> = Props<CancelButtonOptions<T>>

/**
* A button that will cancel space registration.
*
* A `button` designed to work with `Authenticator.Form`. Any passed props will
* be passed along to the `button` component.
*/
Authenticator.CancelButton = function CancelButton (props: any) {
export const CancelButton: Component<CancelButtonProps> = createComponent((props) => {
const [, { cancelRegisterSpace }] = useAuthenticator()
return (
<button {...props} onClick={() => { cancelRegisterSpace() }} />
)
}
return createElement('button', { ...props, onClick: cancelRegisterSpace })
})

/**
* Use the scoped authenticator context state from a parent `Authenticator`.
*/
export function useAuthenticator (): AuthenticatorContextValue {
return useContext(AuthenticatorContext)
}

export const Authenticator = Object.assign(AuthenticatorRoot, { Form, EmailInput, CancelButton })
3 changes: 2 additions & 1 deletion packages/react-uploader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
},
"homepage": "https://github.com/web3-storage/w3ui/tree/main/packages/react-uploader",
"dependencies": {
"@w3ui/uploader-core": "workspace:^",
"@w3ui/react-keyring": "workspace:^",
"@w3ui/uploader-core": "workspace:^",
"@web3-storage/capabilities": "^2.0.0",
"ariakit-react-utils": "0.17.0-next.27",
"multiformats": "^10.0.2"
},
"peerDependencies": {
Expand Down
48 changes: 27 additions & 21 deletions packages/react-uploader/src/Uploader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import React, { useContext, useMemo, createContext, useState } from 'react'
import type { As, Component, Props, Options } from 'ariakit-react-utils'
import type { ChangeEvent } from 'react'

import React, { useContext, useMemo, useCallback, createContext, useState, Fragment } from 'react'
import { createComponent, createElement } from 'ariakit-react-utils'
import { Link, Version } from 'multiformats'
import { CARMetadata, UploaderContextState, UploaderContextActions } from '@w3ui/uploader-core'
import { useUploader } from './providers/Uploader'
Expand Down Expand Up @@ -63,9 +67,8 @@ const UploaderComponentContext = createContext<UploaderComponentContextValue>([
}
])

export interface UploaderComponentProps {
children?: JSX.Element
}
export type UploaderRootOptions<T extends As = typeof Fragment> = Options<T>
export type UploaderRootProps<T extends As = typeof Fragment> = Props<UploaderRootOptions<T>>

/**
* Top level component of the headless Uploader.
Expand All @@ -74,9 +77,7 @@ export interface UploaderComponentProps {
* to easily create a custom component for uploading files to
* web3.storage.
*/
export const Uploader = ({
children
}: UploaderComponentProps): JSX.Element => {
export const UploaderRoot: Component<UploaderRootProps> = createComponent((props) => {
const [uploaderState, uploaderActions] = useUploader()
const [file, setFile] = useState<File>()
const [dataCID, setDataCID] = useState<Link<unknown, number, number, Version>>()
Expand Down Expand Up @@ -106,42 +107,47 @@ export const Uploader = ({

return (
<UploaderComponentContext.Provider value={uploaderComponentContextValue}>
{children}
{createElement(Fragment, props)}
</UploaderComponentContext.Provider>
)
}
})

export type InputOptions<T extends As = 'input'> = Options<T>
export type InputProps<T extends As = 'input'> = Props<InputOptions<T>>

/**
* Input component for the headless Uploader.
*
* A file `input` designed to work with `Uploader`. Any passed props will
* be passed along to the `input` component.
*/
Uploader.Input = (props: any): JSX.Element => {
export const Input: Component<InputProps> = createComponent((props) => {
const [, { setFile }] = useContext(UploaderComponentContext)
return (
<input {...props} type='file' onChange={e => setFile(e.target.files?.[0])} />
)
}
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setFile(e?.target?.files?.[0])
}, [setFile])
return createElement('input', { ...props, type: 'file', onChange })
})

export type FormOptions<T extends As = 'form'> = Options<T>
export type FormProps<T extends As = 'form'> = Props<FormOptions<T>>

/**
* Form component for the headless Uploader.
*
* A `form` designed to work with `Uploader`. Any passed props will
* be passed along to the `form` component.
*/
Uploader.Form = ({ children, ...props }: { children: React.ReactNode } & any): JSX.Element => {
export const Form: Component<FormProps> = createComponent((props) => {
const [{ handleUploadSubmit }] = useContext(UploaderComponentContext)
return (
<form {...props} onSubmit={handleUploadSubmit}>
{children}
</form>
)
}
return createElement('form', { ...props, onSubmit: handleUploadSubmit })
})

/**
* Use the scoped uploader context state from a parent `Uploader`.
*/
export function useUploaderComponent (): UploaderComponentContextValue {
return useContext(UploaderComponentContext)
}

export const Uploader = Object.assign(UploaderRoot, { Input, Form })
5 changes: 3 additions & 2 deletions packages/react-uploads-list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
},
"homepage": "https://github.com/web3-storage/w3ui/tree/main/packages/react-uploads-list",
"dependencies": {
"@w3ui/uploads-list-core": "workspace:^",
"@w3ui/react-keyring": "workspace:^",
"@web3-storage/capabilities": "^2.0.0"
"@w3ui/uploads-list-core": "workspace:^",
"@web3-storage/capabilities": "^2.0.0",
"ariakit-react-utils": "0.17.0-next.27"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
Expand Down
68 changes: 43 additions & 25 deletions packages/react-uploads-list/src/UploadsList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React, { createContext, useContext, useMemo, useCallback } from 'react'
import { UploadsListContextState, UploadsListContextActions } from '@w3ui/uploads-list-core'
import type { As, Component, Props, Options, RenderProp } from 'ariakit-react-utils'
import type { UploadsListContextState, UploadsListContextActions } from '@w3ui/uploads-list-core'

import React, { Fragment, createContext, useContext, useMemo, useCallback, useEffect } from 'react'
import { createComponent, createElement } from 'ariakit-react-utils'
import { useUploadsList } from './providers/UploadsList'

export type UploadsListComponentContextState = UploadsListContextState & {
Expand All @@ -12,7 +15,7 @@ export type UploadsListComponentContextActions = UploadsListContextActions & {

export type UploadsListComponentContextValue = [
state: UploadsListComponentContextState,
actions: UploadsListContextActions
actions: UploadsListComponentContextActions
]

export const UploadsListComponentContext = createContext<UploadsListComponentContextValue>([
Expand All @@ -35,13 +38,13 @@ export const UploadsListComponentContext = createContext<UploadsListComponentCon
}
])

export type UploadsListComponentChildrenProps = [
state: UploadsListContextState,
actions: UploadsListComponentContextActions
]

export interface UploadsListComponentProps {
children?: (props: UploadsListComponentChildrenProps) => React.ReactNode
export type UploadsListRootOptions = Options<typeof Fragment>
export type UploadsListRenderProps = Omit<Props<UploadsListRootOptions>, 'children'> & {
uploadsList?: UploadsListComponentContextValue
}
export type UploadsListRootProps = Omit<Props<UploadsListRootOptions>, 'children'> & {
uploadsList?: UploadsListComponentContextValue
children?: React.ReactNode | RenderProp<UploadsListRenderProps>
}

/**
Expand All @@ -50,58 +53,73 @@ export interface UploadsListComponentProps {
* Designed to be used with UploadsList.NextButton,
* Uploader.ReloadButton, et al to easily create a
* custom component for listing uploads to a web3.storage space.
*
* Always renders as a Fragment and does not support the `as` property.
*/
export const UploadsList = ({ children }: UploadsListComponentProps): JSX.Element => {
export const UploadsListRoot = (props: UploadsListRootProps): JSX.Element => {
const [state, actions] = useUploadsList()
const contextValue = useMemo<UploadsListComponentChildrenProps>(
const contextValue = useMemo<UploadsListComponentContextValue>(
() => ([state, actions]),
[state, actions])
const { children, ...childlessProps } = props
let renderedChildren: React.ReactNode
if (Boolean(children) && (typeof children === 'function')) {
renderedChildren = children({ ...childlessProps, uploadsList: contextValue })
} else {
renderedChildren = children as React.ReactNode
}
useEffect(() => {
// load the first page of results asynchronously
void actions.next()
}, [])
return (
<UploadsListComponentContext.Provider value={contextValue}>
{(typeof children === 'function')
? (
children(contextValue)
)
: (
children
)}
{renderedChildren}
</UploadsListComponentContext.Provider>
)
}

export type NextButtonOptions<T extends As = 'button'> = Options<T>
export type NextButtonProps<T extends As = 'button'> = Props<NextButtonOptions<T>>

/**
* Button that loads the next page of results.
*
* A 'button' designed to work with `UploadsList`. Any passed props will
* be passed along to the `button` component.
*/
UploadsList.NextButton = (props: any) => {
export const NextButton: Component<NextButtonProps> = createComponent((props: any) => {
const [, { next }] = useContext(UploadsListComponentContext)
const onClick = useCallback((e: React.MouseEvent) => {
e.preventDefault()
void next()
}, [next])
return <button {...props} onClick={onClick} />
}
return createElement('button', { ...props, onClick })
})

export type ReloadButtonOptions<T extends As = 'button'> = Options<T>
export type ReloadButtonProps<T extends As = 'button'> = Props<ReloadButtonOptions<T>>

/**
* Button that reloads an `UploadsList`.
*
* A 'button' designed to work with `UploadsList`. Any passed props will
* be passed along to the `button` component.
*/
UploadsList.ReloadButton = (props: any) => {
export const ReloadButton: Component<ReloadButtonProps> = createComponent((props: any) => {
const [, { reload }] = useContext(UploadsListComponentContext)
const onClick = useCallback((e: React.MouseEvent) => {
e.preventDefault()
void reload()
}, [reload])
return <button onClick={onClick} {...props} />
}
return createElement('button', { ...props, onClick })
})

/**
* Use the scoped uploads list context state from a parent `UploadsList`.
*/
export function useUploadsListComponent (): UploadsListComponentContextValue {
return useContext(UploadsListComponentContext)
}

export const UploadsList = Object.assign(UploadsListRoot, { NextButton, ReloadButton })
Loading