Skip to content

Commit

Permalink
Merge pull request #626 from commercelayer/input-readonly-hide
Browse files Browse the repository at this point in the history
Add prop to `InputReadonly` to toggle hidden values
  • Loading branch information
gciotola committed Apr 22, 2024
2 parents 1dd85a7 + a3d82f1 commit f681072
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 44 deletions.
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

0 comments on commit f681072

Please sign in to comment.