0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-17 23:44:39 -05:00

Admin X Tiers Fields (#17332)

refs. https://github.com/TryGhost/Product/issues/3580

- The currency dropdown needed a small version of the regular select
- The input field needed a version with a right-side placeholder text
This commit is contained in:
Peter Zimon 2023-07-12 18:23:51 +02:00 committed by GitHub
parent 42d87d1437
commit 497d1be2ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 80 additions and 18 deletions

View file

@ -4,7 +4,7 @@ import type {Meta, StoryObj} from '@storybook/react';
import Select, {SelectOption} from './Select'; import Select, {SelectOption} from './Select';
const meta = { const meta = {
title: 'Global / Simple select', title: 'Global / Form / Select',
component: Select, component: Select,
tags: ['autodocs'], tags: ['autodocs'],
decorators: [(_story: any) => (<div style={{maxWidth: '400px'}}>{_story()}</div>)], decorators: [(_story: any) => (<div style={{maxWidth: '400px'}}>{_story()}</div>)],
@ -61,6 +61,13 @@ export const WithHint: Story = {
} }
}; };
export const ExtraSmall: Story = {
args: {
options: selectOptions,
size: 'xs'
}
};
export const WithSelectedOption: Story = { export const WithSelectedOption: Story = {
render: function Component(args) { render: function Component(args) {
const [, updateArgs] = useArgs(); const [, updateArgs] = useArgs();

View file

@ -11,6 +11,7 @@ export interface SelectOption {
export interface SelectProps { export interface SelectProps {
title?: string; title?: string;
size?: 'xs' | 'md';
prompt?: string; prompt?: string;
options: SelectOption[]; options: SelectOption[];
selectedOption?: string selectedOption?: string
@ -18,6 +19,7 @@ export interface SelectProps {
error?:boolean; error?:boolean;
hint?: React.ReactNode; hint?: React.ReactNode;
clearBg?: boolean; clearBg?: boolean;
border?: boolean;
containerClassName?: string; containerClassName?: string;
selectClassName?: string; selectClassName?: string;
optionClassName?: string; optionClassName?: string;
@ -26,6 +28,7 @@ export interface SelectProps {
const Select: React.FC<SelectProps> = ({ const Select: React.FC<SelectProps> = ({
title, title,
size = 'md',
prompt, prompt,
options, options,
selectedOption, selectedOption,
@ -33,6 +36,7 @@ const Select: React.FC<SelectProps> = ({
error, error,
hint, hint,
clearBg = true, clearBg = true,
border = true,
containerClassName, containerClassName,
selectClassName, selectClassName,
optionClassName, optionClassName,
@ -49,7 +53,7 @@ const Select: React.FC<SelectProps> = ({
containerClasses = clsx( containerClasses = clsx(
'relative w-full after:pointer-events-none', 'relative w-full after:pointer-events-none',
`after:absolute after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-['']`, `after:absolute after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-['']`,
title ? 'after:top-[14px]' : 'after:top-[14px]', size === 'xs' ? 'after:top-[6px]' : 'after:top-[14px]',
clearBg ? 'after:right-0' : 'after:right-4' clearBg ? 'after:right-0' : 'after:right-4'
); );
} }
@ -61,7 +65,9 @@ const Select: React.FC<SelectProps> = ({
let selectClasses = ''; let selectClasses = '';
if (!unstyled) { if (!unstyled) {
selectClasses = clsx( selectClasses = clsx(
'h-10 w-full cursor-pointer appearance-none border-b py-2 pr-5 outline-none', size === 'xs' ? 'h-6 py-0 pr-3 text-xs' : 'h-10 py-2 pr-5',
'w-full cursor-pointer appearance-none outline-none',
border && 'border-b',
!clearBg && 'bg-grey-75 px-[10px]', !clearBg && 'bg-grey-75 px-[10px]',
error ? 'border-red' : 'border-grey-500 hover:border-grey-700 focus:border-black', error ? 'border-red' : 'border-grey-500 hover:border-grey-700 focus:border-black',
(title && !clearBg) && 'mt-2' (title && !clearBg) && 'mt-2'

View file

@ -58,6 +58,14 @@ export const WithHint: Story = {
} }
}; };
export const WithRightPlaceholder: Story = {
args: {
title: 'Monthly price',
placeholder: '0',
rightPlaceholder: 'USD/month'
}
};
export const PasswordType: Story = { export const PasswordType: Story = {
args: { args: {
title: 'Password', title: 'Password',

View file

@ -12,6 +12,7 @@ export type TextFieldProps = React.InputHTMLAttributes<HTMLInputElement> & {
value?: string; value?: string;
error?: boolean; error?: boolean;
placeholder?: string; placeholder?: string;
rightPlaceholder?: string;
hint?: React.ReactNode; hint?: React.ReactNode;
clearBg?: boolean; clearBg?: boolean;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void; onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
@ -33,6 +34,7 @@ const TextField: React.FC<TextFieldProps> = ({
value, value,
error, error,
placeholder, placeholder,
rightPlaceholder,
hint, hint,
clearBg = true, clearBg = true,
onChange, onChange,
@ -53,10 +55,13 @@ const TextField: React.FC<TextFieldProps> = ({
error ? `border-red` : `${disabled ? 'border-grey-300' : 'border-grey-500 hover:border-grey-700 focus:border-black'}`, error ? `border-red` : `${disabled ? 'border-grey-300' : 'border-grey-500 hover:border-grey-700 focus:border-black'}`,
(title && !hideTitle && !clearBg) && `mt-2`, (title && !hideTitle && !clearBg) && `mt-2`,
(disabled ? 'text-grey-700' : ''), (disabled ? 'text-grey-700' : ''),
rightPlaceholder && 'peer w-0 grow',
className className
); );
const field = <input let field = <></>;
const inputField = <input
ref={inputRef} ref={inputRef}
className={textFieldClasses || className} className={textFieldClasses || className}
disabled={disabled} disabled={disabled}
@ -69,6 +74,22 @@ const TextField: React.FC<TextFieldProps> = ({
onChange={onChange} onChange={onChange}
{...props} />; {...props} />;
if (rightPlaceholder) {
const rightPHClasses = !unstyled && clsx(
'h-10 border-b py-2 text-right text-grey-500',
error ? `border-red` : `${disabled ? 'border-grey-300' : 'border-grey-500 peer-hover:border-grey-700 peer-focus:border-black'}`
);
field = (
<div className='flex w-full items-center'>
{inputField}
<span className={rightPHClasses || ''}>{rightPlaceholder}</span>
</div>
);
} else {
field = inputField;
}
if (title || hint) { if (title || hint) {
let titleGrey = false; let titleGrey = false;
if (titleColor === 'auto') { if (titleColor === 'auto') {

View file

@ -5,6 +5,7 @@ import Icon from '../../../../admin-x-ds/global/Icon';
import Modal from '../../../../admin-x-ds/global/modal/Modal'; import Modal from '../../../../admin-x-ds/global/modal/Modal';
import NiceModal, {useModal} from '@ebay/nice-modal-react'; import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react'; import React from 'react';
import Select from '../../../../admin-x-ds/global/form/Select';
import SortableList from '../../../../admin-x-ds/global/SortableList'; import SortableList from '../../../../admin-x-ds/global/SortableList';
import TextField from '../../../../admin-x-ds/global/form/TextField'; import TextField from '../../../../admin-x-ds/global/form/TextField';
import TierDetailPreview from './TierDetailPreview'; import TierDetailPreview from './TierDetailPreview';
@ -88,22 +89,40 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
onChange={e => updateForm(state => ({...state, description: e.target.value}))} onChange={e => updateForm(state => ({...state, description: e.target.value}))}
/> />
<div className='flex gap-10'> <div className='flex gap-10'>
<div className='flex basis-1/2 flex-col gap-2'> <div className='basis-1/2'>
<TextField <div className='mb-1 flex h-6 items-center justify-between'>
placeholder='1' <Heading level={6}>Prices</Heading>
title='Prices' <div className='w-10'>
value={formState.monthly_price} <Select
onChange={e => updateForm(state => ({...state, monthly_price: e.target.value.replace(/[^\d.]/, '')}))} border={false}
/> options={[
<TextField {label: 'USD', value: 'US Dollaz'},
placeholder='10' {label: 'HUF', value: 'Hungarian Dollaz'}
value={formState.yearly_price} ]}
onChange={e => updateForm(state => ({...state, yearly_price: e.target.value.replace(/[^\d.]/, '')}))} selectClassName='font-medium'
/> size='xs'
onSelect={() => {}}
/>
</div>
</div>
<div className='flex flex-col gap-2'>
<TextField
placeholder='1'
rightPlaceholder='USD/month'
value={formState.monthly_price}
onChange={e => updateForm(state => ({...state, monthly_price: e.target.value.replace(/[^\d.]/, '')}))}
/>
<TextField
placeholder='10'
rightPlaceholder='USD/year'
value={formState.yearly_price}
onChange={e => updateForm(state => ({...state, yearly_price: e.target.value.replace(/[^\d.]/, '')}))}
/>
</div>
</div> </div>
<div className='basis-1/2'> <div className='basis-1/2'>
<div className='flex justify-between'> <div className='mb-1 flex h-6 items-center justify-between'>
<Heading level={6} grey>Add a free trial</Heading> <Heading level={6}>Add a free trial</Heading>
<Toggle onChange={() => {}} /> <Toggle onChange={() => {}} />
</div> </div>
<TextField <TextField
@ -111,6 +130,7 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
Members will be subscribed at full price once the trial ends. <a href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a> Members will be subscribed at full price once the trial ends. <a href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a>
</>} </>}
placeholder='0' placeholder='0'
rightPlaceholder='days'
value={formState.trial_days} value={formState.trial_days}
disabled disabled
onChange={e => updateForm(state => ({...state, trial_days: e.target.value.replace(/^[\d.]/, '')}))} onChange={e => updateForm(state => ({...state, trial_days: e.target.value.replace(/^[\d.]/, '')}))}