Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.4.8",
"version": "0.4.11",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { buttonCSS } from './styles';

export interface ButtonProps extends BaseButtonProps {
children?: ReactNode | string;
variant: 'primary' | 'default' | 'danger';
variant: 'primary' | 'default' | 'danger' | 'quiet';
disabled?: boolean;
className?: string;
onClick?: (e: SyntheticEvent<HTMLButtonElement>) => void;
Expand Down
12 changes: 10 additions & 2 deletions src/button/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@ export const buttonCSS = css`
border: 1px solid ${theme.colors.dark1};
font-size: ${theme.typography.sizes.medium};
font-weight: 600;

margin: 0;
display: flex;
justify-content: center;
align-items: center;
box-sizing: content-box;
border-radius: 4px;
color: ${theme.textColors.white90};
cursor: pointer;
/* Disable outline since there are other mechanisms to show focus */
outline: none;
&:not([disabled]) {
transition: all 0.2s ease-in-out;
}
&[disabled] {
color: ${theme.textColors.white70};
cursor: not-allowed;
cursor: default;
}
&[data-size='normal'][data-childless='false'] {
padding: ${theme.spacing.padding8}px ${theme.spacing.padding16}px;
Expand Down Expand Up @@ -46,6 +48,12 @@ export const buttonCSS = css`
background-color: ${theme.components.button.defaultHoverBackgroundColor};
}
}
&[data-variant='quiet'] {
background-color: ${theme.colors.gray500};
&:hover:not([disabled]) {
background-color: ${theme.components.button.defaultHoverBackgroundColor};
}
}
&[data-variant='danger'] {
background-color: ${theme.colors.statusDanger};
border-color: ${theme.components.button.dangerBorderColor};
Expand Down
59 changes: 59 additions & 0 deletions src/icon/Icons.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React from 'react';
import { css, keyframes } from '@emotion/core';

const loadingCircleKeyframes = keyframes`
100% {
transform: rotate(360deg);
}
`;
/**
* Raw svg icons from eva. Easily stylizable via CSS
* @src https://akveo.github.io/eva-icons/#/
Expand Down Expand Up @@ -405,3 +411,56 @@ export const SearchOutline = () => (
</g>
</svg>
);

export const LoadingOutline = () => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
css={css`
animation: ${loadingCircleKeyframes} 1s infinite linear;
`}
>
<g id="loading">
<mask
id="mask0_804_24"
style={{ maskType: 'alpha' }}
maskUnits="userSpaceOnUse"
x="2"
y="2"
width="20"
height="20"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.486 6.486 2 12 2C17.514 2 22 6.486 22 12C22 17.514 17.514 22 12 22C6.486 22 2 17.514 2 12ZM4 12C4 16.411 7.589 20 12 20C16.411 20 20 16.411 20 12C20 7.589 16.411 4 12 4C7.589 4 4 7.589 4 12Z"
fill="inherit"
/>
</mask>
<g mask="url(#mask0_804_24)">
<path
id="Union"
fillRule="evenodd"
clipRule="evenodd"
d="M15.8268 2.7612C14.6136 2.25866 13.3132 2 12 2C11.4477 2 11 2.44772 11 3C11 3.55228 11.4477 4 12 4V12H20C20 12.5523 20.4477 13 21 13C21.5523 13 22 12.5523 22 12C22 10.6868 21.7413 9.38642 21.2388 8.17317C20.7362 6.95991 19.9997 5.85752 19.0711 4.92893C18.1425 4.00035 17.0401 3.26375 15.8268 2.7612Z"
fill="inherit"
/>
</g>
</g>
</svg>
);

export const CloseCircleOutline = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g data-name="Layer 2">
<g data-name="close-circle">
<rect width="24" height="24" opacity="0" />
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z" />
<path d="M14.71 9.29a1 1 0 0 0-1.42 0L12 10.59l-1.29-1.3a1 1 0 0 0-1.42 1.42l1.3 1.29-1.3 1.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l1.29-1.3 1.29 1.3a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42L13.41 12l1.3-1.29a1 1 0 0 0 0-1.42z" />
</g>
</g>
</svg>
);
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from './notification';
export * from './field';
export * from './textfield';
export * from './form';
export * from './search';
// export interface Props extends HTMLAttributes<HTMLDivElement> {
// /** custom content, defaults to 'the snozzberries taste like snozzberries' */
// children?: ReactChild;
Expand Down
99 changes: 99 additions & 0 deletions src/search/CompactSearchField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, { useState, useEffect } from 'react';
import { css } from '@emotion/core';
import { Icon, SearchOutline, LoadingOutline } from '../icon';
import { Button } from '../button';
import { TextField, TextFieldRef, TextFieldProps } from '../textfield';
import { classNames } from '../utils';
import theme from '../theme';

export interface CompactSearchFieldProps
extends Omit<TextFieldProps, 'variant'> {
/**
* Whether or not there is a search in-flight
* @default false
*/
isSearching?: boolean;
}

export function CompactSearchField(props: CompactSearchFieldProps) {
const { isSearching = false, onFocus, onBlur } = props;
const inputRef = React.useRef<TextFieldRef>(null);
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [isActive, setIsActive] = useState<boolean>(false);

// Focus the input when the search button is clicked
useEffect(() => {
if (isExpanded) {
inputRef.current?.focus();
}
}, [isExpanded]);

return (
<div
css={css`
display: inline-flex;
flex-direction: row;
border: 1px solid ${theme.components.textField.borderColor};
border-radius: ${theme.rounding.rounding4}px;
overflow: hidden;
transition: all 0.2s ease-in-out;
& > .ac-button {
flex: none;
border: none;
border-radius: 0;
}
& .ac-textfield {
border-radius: 0 ${theme.rounding.rounding4}px
${theme.rounding.rounding4}px 0;
// The button padding provides enough space
& > input {
padding-left: 0;
}
min-width: 0;
transition: width 0.2s ease-in-out;
}
&:not(.is-expanded) {
.ac-textfield {
width: 0;
visibility: none;
}
}
&.is-expanded {
.ac-textfield {
width: 200px;
}
}
&.is-active {
border-color: ${theme.components.textField.activeBorderColor};
}
`}
className={classNames('ac-compact-search-field', {
'is-expanded': isExpanded,
'is-active': isActive,
})}
>
<Button
variant="quiet"
icon={
<Icon svg={isSearching ? <LoadingOutline /> : <SearchOutline />} />
}
onClick={() => setIsExpanded(true)}
disabled={isExpanded}
></Button>
<TextField
ref={inputRef}
{...props}
type="search"
variant="quiet"
onFocus={e => {
setIsActive(true);
onFocus && onFocus(e);
}}
onBlur={e => {
setIsActive(false);
onBlur && onBlur(e);
}}
/>
</div>
);
}
1 change: 1 addition & 0 deletions src/search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CompactSearchField';
2 changes: 2 additions & 0 deletions src/textfield/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { useProviderProps } from '../provider';

export interface TextFieldProps extends AriaTextFieldProps, AddonableProps {
className?: string;
variant?: 'default' | 'quiet';
}

function TextField(props: TextFieldProps, ref: RefObject<TextFieldRef>) {
// Call use provider props so the textfield can inherit from the provider
// E.x. disabled, readOnly, etc.
Expand Down
23 changes: 18 additions & 5 deletions src/textfield/TextFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const appearKeyframes = keyframes`
100% { opacity: 1; }
`;

export interface TextFieldProps
interface TextFieldProps
extends InputBase,
Validation,
HelpTextProps,
Expand Down Expand Up @@ -100,6 +100,7 @@ interface TextFieldBaseProps
loadingIndicator?: ReactElement;
isLoading?: boolean;
className?: string;
variant?: 'default' | 'quiet';
}

export interface TextFieldRef
Expand Down Expand Up @@ -130,6 +131,7 @@ function TextFieldBase(props: TextFieldBaseProps, ref: Ref<TextFieldRef>) {
loadingIndicator,
addonBefore,
className,
variant = 'default',
} = props;
let { hoverProps, isHovered } = useHover({ isDisabled });
let [isFocused, setIsFocused] = React.useState(false);
Expand Down Expand Up @@ -173,22 +175,25 @@ function TextFieldBase(props: TextFieldBaseProps, ref: Ref<TextFieldRef>) {
'is-disabled': isDisabled,
'is-readonly': isReadOnly,
})}
data-variant={variant}
css={css`
display: flex;
flex-direction: row;
align-items: center;
min-width: 270px;
border: 1px solid ${theme.colors.lightGrayBorder};
border-radius: ${theme.borderRadius.medium}px;
background-color: ${theme.components.textField.backgroundColor};
transition: all 0.2s ease-in-out;
overflow: hidden;
font-size: ${theme.typography.sizes.medium.fontSize}px;
&.is-hovered[:not(.is-disabled)] {
&[data-variant='default'] {
border: 1px solid ${theme.colors.lightGrayBorder};
border-radius: ${theme.borderRadius.medium}px;
}
&.is-hovered:not(.is-disabled)[data-variant='default'] {
border: 1px solid ${theme.components.textField.hoverBorderColor};
background-color: ${theme.components.textField.activeBackgroundColor};
}
&.is-focused {
&.is-focused[data-variant='default'] {
border: 1px solid ${theme.components.textField.activeBorderColor};
background-color: ${theme.components.textField.activeBackgroundColor};
}
Expand Down Expand Up @@ -229,6 +234,14 @@ function TextFieldBase(props: TextFieldBaseProps, ref: Ref<TextFieldRef>) {
color: ${theme.colors.statusDanger};
}
}
/* Style for type=search */
input[type='search']::-webkit-search-cancel-button {
-webkit-appearance: none;
width: 16px;
height: 16px;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='rgba(255,255,255, 0.7)'><path d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/></svg>");
cursor: pointer;
}
`}
>
{addonBefore != null ? <AddonBefore>{addonBefore}</AddonBefore> : null}
Expand Down
1 change: 1 addition & 0 deletions src/textfield/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './TextField';
export * from './TextFieldBase';
4 changes: 3 additions & 1 deletion src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ const theme = {
thin: 2,
},
},

rounding: {
rounding4: 4,
},
animation: {
global: {
/**
Expand Down
58 changes: 58 additions & 0 deletions stories/CompactSearchField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import { Meta, Story } from '@storybook/react';
import { css } from '@emotion/core';
import {
Provider,
CompactSearchField,
CompactSearchFieldProps,
Button,
Card,
} from '../src';
import { Icon, SearchOutline, Settings } from '../src/icon';

const meta: Meta = {
title: 'CompactSearchField',
component: CompactSearchField,
parameters: {
controls: { expanded: true },
},
};

export default meta;

const Template: Story<CompactSearchFieldProps> = args => {
const [search, setSearch] = useState('');
return (
<Provider>
<Card
title="hello"
extra={
<div
css={css`
display: flex;
& > * + * {
margin-left: 8px;
}
`}
>
<Button
variant="default"
icon={<Icon svg={<Settings />} />}
></Button>
<CompactSearchField
{...args}
placeholder="Search..."
onChange={e => setSearch(e)}
/>
</div>
}
>
{`Search for: ${search}`}
</Card>
</Provider>
);
};

// By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
// https://storybook.js.org/docs/react/workflows/unit-testing
export const Default = Template.bind({});