0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat(ui): add button components (#400)

* feat(ui): add button components

add button components

* fix(ui): add conncetor name i18n

add connector name i18n

* fix(ui): remove useless noop click function

remove useless noop click function
This commit is contained in:
simeng-li 2022-03-17 16:42:03 +08:00 committed by GitHub
parent 6f3c762632
commit 0feeee263a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 181 additions and 38 deletions

View file

@ -65,14 +65,16 @@ $font-family: 'PingFang SC', 'SF Pro Text', sans-serif;
/* Color */
--color-primary: #{$color-primary};
--color-background: #{$color-neutral-0};
--color-secondary-background-active: #{$color-neutral-10};
--color-secondary-background-disabled: #{$color-neutral-10};
--color-border: #{$color-neutral-100};
--color-border-active: #{$color-neutral-70};
--color-border-disabled: #{$color-neutral-30};
--color-control-background: #{$color-neutral-10};
--color-control-focus: #{$color-primary-tint-60};
--color-control-action: #{$color-neutral-70};
--color-control-action-focus: #{$color-primary-tint-70};
/* Font Color */
--color-font-primary: #{$color-neutral-100};
--color-font-secondary: #444;
@ -80,6 +82,7 @@ $font-family: 'PingFang SC', 'SF Pro Text', sans-serif;
--color-font-button-text: #{$color-neutral-0};
--color-font-button-text-active: #{rgba($color-neutral-0, 0.4)};
--color-font-secondary-active: #{$color-neutral-70};
--color-font-secondary-disabled: #{rgba($color-neutral-100, 0.4)};
/* ===== Legacy Styling ===== */
--color-heading: #333;
@ -97,7 +100,7 @@ $font-family: 'PingFang SC', 'SF Pro Text', sans-serif;
--font-title: 600 32px/40px #{$font-family};
--font-heading-2: 400 18px/22px #{$font-family};
--font-heading-2-bold: 600 18px/22px #{$font-family};
--font-control: 500 18px/22px #{$font-family};
--font-control: 500 18px/20px #{$font-family};
--font-button-text: 600 20px/24px #{$font-family};
/* ===== Legacy Styling ===== */

View file

@ -0,0 +1,32 @@
@use '@/scss/underscore' as _;
.socialButton {
font: var(--font-control);
border: _.border(var(--color-border));
background: var(--color-background);
color: var(--color-font-primary);
justify-content: flex-start;
.icon {
width: _.unit(5);
height: _.unit(5);
@include _.image-align-center;
margin-right: _.unit(4);
}
&:active {
background: var(--color-secondary-background-active);
border: _.border(var(--color-border-active));
color: var(--color-font-primary);
}
&:disabled {
background: var(--color-secondary-background-disabled);
border: _.border(var(--color-border-disabled));
color: var(--color-font-secondary-disabled);
.icon {
filter: grayscale(100%);
}
}
}

View file

@ -0,0 +1,39 @@
import { ConnectorMetadata } from '@logto/schemas';
import classNames from 'classnames';
import React from 'react';
import { useTranslation } from 'react-i18next';
import * as SocialLinkButtonStyles from './SocialLinkButton.module.scss';
import * as styles from './index.module.scss';
export type Props = {
isDisabled?: boolean;
className?: string;
connector: Pick<ConnectorMetadata, 'id' | 'name' | 'logo'>;
onClick?: (id: string) => void;
};
const SocialLinkButton = ({ isDisabled, className, connector, onClick }: Props) => {
const { id, name, logo } = connector;
const {
i18n: { language },
} = useTranslation();
const localName = name[language] ?? name.en;
return (
<button
disabled={isDisabled}
className={classNames(styles.button, SocialLinkButtonStyles.socialButton, className)}
type="button"
onClick={() => {
onClick?.(id);
}}
>
{logo && <img src={logo} alt={localName} className={SocialLinkButtonStyles.icon} />}
{localName}
</button>
);
};
export default SocialLinkButton;

View file

@ -5,22 +5,34 @@
flex-direction: row;
justify-content: center;
align-items: center;
padding: _.unit(3) _.unit(8);
border-radius: 12px;
background: var(--color-button-background);
color: var(--color-button-text);
font: var(--font-control);
box-shadow: var(--shadow-button);
padding: _.unit(3) _.unit(10);
border-radius: _.unit(2);
font: var(--font-button-text);
transition: var(--transition-default-control);
cursor: pointer;
-webkit-appearance: none;
width: 100%;
-webkit-tap-highlight-color: transparent;
}
&:disabled {
background: var(--color-button-background-disabled);
color: var(--color-button-text-disabled);
}
.primary {
border: none;
background: var(--color-primary);
color: var(--color-font-button-text);
&:not(:disabled):hover {
background: var(--color-button-background-hover);
&:active {
background: linear-gradient(0deg, rgba(0, 0, 0, 16%), rgba(0, 0, 0, 16%)), var(--color-primary);
color: var(--color-font-button-text-active);
}
}
.secondary {
border: _.border(var(--color-border));
background: var(--color-background);
color: var(--color-font-primary);
&:active {
border: _.border(var(--color-border-active));
color: var(--color-font-secondary-active);
}
}

View file

@ -0,0 +1,52 @@
import { render, fireEvent } from '@testing-library/react';
import React from 'react';
import SocialLinkButton from './SocialLinkButton';
import Button from './index';
describe('Button Component', () => {
const onClick = jest.fn();
beforeEach(() => {
onClick.mockClear();
});
it('render Button', () => {
const { queryByText, container } = render(<Button onClick={onClick}>foo</Button>);
expect(queryByText('foo')).not.toBeNull();
const button = container.querySelector('button');
if (button) {
fireEvent.click(button);
expect(onClick).toBeCalled();
}
});
it('render SocialLinkButton', () => {
const connector = {
id: 'foo',
name: {
en: 'Sign in with Logto',
},
logo: 'http://logto.dev/logto.png',
};
const { queryByText, container } = render(
<SocialLinkButton connector={connector} onClick={onClick} />
);
expect(queryByText(connector.name.en)).not.toBeNull();
const button = container.querySelector('button');
const icon = container.querySelector('img');
if (icon) {
expect(icon.src).toEqual(connector.logo);
}
if (button) {
fireEvent.click(button);
expect(onClick).toBeCalled();
}
});
});

View file

@ -4,22 +4,30 @@ import React from 'react';
import * as styles from './index.module.scss';
export type Props = {
htmlType?: 'button' | 'submit' | 'reset';
isDisabled?: boolean;
className?: string;
value?: string;
children: string; // TODO: make it i18nKey with optional params
type?: 'primary' | 'secondary';
onClick?: React.MouseEventHandler;
};
const Button = ({ isDisabled, className, value, onClick }: Props) => {
return (
<input
disabled={isDisabled}
className={classNames(styles.button, className)}
type="button"
value={value}
onClick={onClick}
/>
);
};
const Button = ({
htmlType = 'button',
type = 'primary',
isDisabled,
className,
children,
onClick,
}: Props) => (
<button
disabled={isDisabled}
className={classNames(styles.button, styles[type], className)}
type={htmlType}
onClick={onClick}
>
{children}
</button>
);
export default Button;

View file

@ -57,11 +57,9 @@ const Register: FC = () => {
{i18n.t<string, LogtoErrorI18nKey>(`errors:${error.code}`)}
</MessageBox>
)}
<Button
isDisabled={loading}
value={loading ? t('register.loading') : t('register.action')}
onClick={signUp}
/>
<Button isDisabled={loading} onClick={signUp}>
{loading ? t('register.loading') : t('register.action')}
</Button>
<div className={styles.haveAccount}>
<span className={styles.prefix}>{t('register.have_account')}</span>

View file

@ -25,14 +25,15 @@
margin-bottom: _.unit(-6);
}
.inputField {
> .inputField {
width: 100%;
margin-top: _.unit(3);
max-width: 320px;
}
> input[type='button'] {
> button {
margin-top: _.unit(12);
max-width: 320px;
}
.createAccount {

View file

@ -62,11 +62,9 @@ const SignIn: FC = () => {
{i18n.t<string, LogtoErrorI18nKey>(`errors:${error.code}`)}
</MessageBox>
)}
<Button
isDisabled={loading}
value={loading ? t('sign_in.loading') : t('sign_in.action')}
onClick={signInHandler}
/>
<Button isDisabled={loading} type="primary" onClick={signInHandler}>
{loading ? t('sign_in.loading') : t('sign_in.action')}
</Button>
<TextLink className={styles.createAccount} href="/register">
{t('register.create_account')}
</TextLink>