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

Add prop to InputReadonly to toggle hidden values #626

Merged
merged 1 commit into from
Apr 22, 2024
Merged
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
3 changes: 2 additions & 1 deletion packages/app-elements/src/styles/vendor.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ div[role='textbox'] {

input:focus-visible,
textarea:focus-visible,
div[role='textbox']:focus {
div[role='textbox']:focus,
.group:focus-within input, .group:focus-within div[role='textbox'] {
ring: none;
outline: none !important;
box-shadow: inset 0 0 0 2px theme(colors.primary.DEFAULT) !important;
Expand Down
1 change: 1 addition & 0 deletions packages/app-elements/src/ui/atoms/Icon/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const iconMapping = {
dotsThree: phosphor.DotsThree,
download: phosphor.Download,
eye: phosphor.Eye,
eyeSlash: phosphor.EyeSlash,
flag: phosphor.Flag,
folderOpen: phosphor.FolderOpen,
funnelSimple: phosphor.FunnelSimple,
Expand Down
48 changes: 18 additions & 30 deletions packages/app-elements/src/ui/forms/InputReadonly.test.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,27 @@
import { render, type RenderResult } from '@testing-library/react'
import { fireEvent, render } from '@testing-library/react'
import { InputReadonly } from './InputReadonly'

interface SetupProps {
id: string
value: string
}

type SetupResult = RenderResult & {
element: HTMLInputElement
}

const setup = ({ id, value }: SetupProps): SetupResult => {
const utils = render(<InputReadonly data-testid={id} value={value} />)
const element = utils.getByTestId(id) as HTMLInputElement
return {
element,
...utils
}
}

describe('InputReadonly', () => {
test('Should be rendered', () => {
const { element } = setup({
id: 'my-input-readonly',
value: ''
})
expect(element).toBeInTheDocument()
const { container } = render(<InputReadonly value='' />)
expect(container.querySelector('input')).toBeInTheDocument()
})

test('Should has value', () => {
const { element } = setup({
id: 'my-input-readonly',
value: 'NAx1zYM55_B3Eq2wiFg'
})
expect(element.getElementsByTagName('input')[0]?.value).toBe(
'NAx1zYM55_B3Eq2wiFg'
const { container } = render(<InputReadonly value='NAx1zYM55_B3Eq2wiFg' />)
expect(container.querySelector('input')?.value).toBe('NAx1zYM55_B3Eq2wiFg')
})

test('Should handle secret value', () => {
const { container, getByTestId } = render(
<InputReadonly value='abc-123' secret />
)
expect(container.querySelector('input')?.value).includes('****')

fireEvent.click(getByTestId('toggle-secret'))
expect(container.querySelector('input')?.value).toBe('abc-123')

fireEvent.click(getByTestId('toggle-secret'))
expect(container.querySelector('input')?.value).includes('****')
})
})
59 changes: 47 additions & 12 deletions packages/app-elements/src/ui/forms/InputReadonly.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CopyToClipboard } from '#ui/atoms/CopyToClipboard'
import { Icon } from '#ui/atoms/Icon'
import { withSkeletonTemplate } from '#ui/atoms/SkeletonTemplate'
import {
InputWrapper,
type InputWrapperBaseProps
} from '#ui/internals/InputWrapper'
import cn from 'classnames'
import { useState } from 'react'

export type InputReadonlyProps = InputWrapperBaseProps & {
/**
Expand All @@ -28,6 +30,10 @@ export type InputReadonlyProps = InputWrapperBaseProps & {
* It only accepts a string and will respect new lines when passing a template literal (backticks).
*/
children?: string
/**
* Show an icon to hide/show content. Content starts hidden if `secret` is true.
*/
secret?: boolean
}

export const InputReadonly = withSkeletonTemplate<InputReadonlyProps>(
Expand All @@ -42,11 +48,14 @@ export const InputReadonly = withSkeletonTemplate<InputReadonlyProps>(
isLoading,
delayMs,
children,
secret = false,
...rest
}) => {
const cssBase =
'block w-full rounded bg-gray-50 text-teal text-sm font-mono font-medium marker:font-bold border-none'

const [hideValue, setHideValue] = useState(secret)

return (
<InputWrapper
{...rest}
Expand All @@ -60,10 +69,16 @@ export const InputReadonly = withSkeletonTemplate<InputReadonlyProps>(
<input
className={cn(
cssBase,
'px-4 h-[44px] outline-0 !ring-0',
inputClassName
inputClassName,
'px-4 h-[44px] outline-0 !ring-0'
)}
value={isLoading === true ? '' : value}
value={
isLoading === true
? ''
: hideValue
? randomHiddenValue()
: value
}
readOnly
/>
) : (
Expand All @@ -78,27 +93,47 @@ export const InputReadonly = withSkeletonTemplate<InputReadonlyProps>(
)}
>
{children.split('\n').map((line, idx) => (
<span key={idx}>{line}</span>
<span key={idx}>{hideValue ? randomHiddenValue() : line}</span>
))}
</div>
)}
{showCopyAction && (
{showCopyAction || secret ? (
<div
className={cn('absolute right-4 flex', {
'top-[2px] items-center': children == null,
className={cn('absolute right-4 flex gap-4 items-center', {
'top-[2px] bottom-[2px] items-center': children == null,
'top-2': children != null
})}
>
<CopyToClipboard
value={value ?? children?.trim()}
showValue={false}
/>
{secret && (
<button
onClick={() => {
setHideValue(!hideValue)
}}
data-testid='toggle-secret'
>
<Icon
name={hideValue ? 'eyeSlash' : 'eye'}
className='text-gray-500 hover:text-gray-300'
size={20}
/>
</button>
)}
{showCopyAction && (
<CopyToClipboard
value={value ?? children?.trim()}
showValue={false}
/>
)}
</div>
)}
) : null}
</div>
</InputWrapper>
)
}
)

InputReadonly.displayName = 'InputReadonly'

function randomHiddenValue(): string {
return '*'.repeat(Math.floor(Math.random() * 7) + 10)
}
29 changes: 28 additions & 1 deletion packages/docs/src/stories/forms/ui/InputReadonly.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,20 @@ const Template: StoryFn<typeof InputReadonly> = (args) => {

export const Default = Template.bind({})
Default.args = {
label: 'Domain',
value: 'https://demo-store.commercelayer.io',
showCopyAction: true
}

/**
* This component can be used to hide (from the screen) sensitive information like API keys or secrets.
*/
export const Secret = Template.bind({})
Secret.args = {
label: 'Secret',
value: 'elyFpGvqXsOSsvEko6ues2Ua4No1_HxaKH_0rUaFuYiX9',
showCopyAction: true
showCopyAction: true,
secret: true
}

/**
Expand All @@ -43,6 +54,22 @@ export const MultiLine: StoryFn = () => {
)
}

export const MultiLineSecret: StoryFn = () => {
return (
<InputReadonly
label='Login with your admin credentials'
showCopyAction
secret
>
{`commercelayer app:login \\
-i asdGvqXsOSsdko6ueiX9 \\
-s elyFpGvqXsOSss2Ua4No1_HxaKH_0rUsFuYiX9 \\
-o demo-store \\
-a admin`}
</InputReadonly>
)
}

export const WithHint = Template.bind({})
WithHint.args = {
label: 'Secret',
Expand Down
Loading