Skip to content

Commit

Permalink
Merge pull request #2 from LexSwed/alpha
Browse files Browse the repository at this point in the history
Alpha
  • Loading branch information
LexSwed authored Dec 8, 2020
2 parents 903f59d + cd4993d commit 09bc988
Show file tree
Hide file tree
Showing 60 changed files with 1,261 additions and 341 deletions.
9 changes: 8 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
{
"extends": "react-app"
"extends": "react-app",
"plugins": ["simple-import-sort"],
"rules": {
"sort-imports": "off",
"import/order": "off",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error"
}
}
18 changes: 10 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@
"@chakra-ui/core": "^0.8.0",
"@emotion/core": "^10.1.1",
"@emotion/styled": "^10.0.27",
"@types/jest": "^26.0.16",
"@types/node": "^14.14.10",
"@types/jest": "^26.0.17",
"@types/node": "^14.14.11",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.9.0",
"@typescript-eslint/parser": "^4.9.0",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.9.1",
"babel-eslint": "^10.1.0",
"babel-preset-react-app": "^10.0.0",
"dokz": "^1.0.79",
Expand All @@ -61,7 +61,8 @@
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^4.3.4",
"eslint-plugin-simple-import-sort": "^6.0.1",
"husky": "^4.3.5",
"jest": "^26.6.3",
"microbundle": "^0.12.4",
"next": "^10.0.3",
Expand All @@ -73,9 +74,6 @@
"react-icons": "^4.1.0",
"typescript": "^4.1.2"
},
"resolutions": {
"typescript": "^4.0.3"
},
"peerDependencies": {
"react": "^16.8.0",
"react-dom": "^16.8.0"
Expand All @@ -84,5 +82,9 @@
"hooks": {
"pre-commit": "pretty-quick --pattern \"lib/**/*.*\" --staged && run-p lint typecheck"
}
},
"resolutions": {
"typescript": "^4.1.2",
"rollup-plugin-typescript2": "^0.29.0"
}
}
20 changes: 11 additions & 9 deletions src/lib/Box/Box.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import React from 'react';
import { StitchesProps } from '@stitches/react';
import React from 'react';

import { styled } from '../stitches.config';
import type { CssProperties } from '../utils';

const Div = styled('div', {});

/** Not all properties supported */
type Props = Omit<StitchesProps<typeof Div>, 'as'> &
{
[prop in typeof acceptedProperties[number]]?: CssProperties[prop];
} & {
as?: JSX.IntrinsicElements | React.ElementType;
};

const Box: React.FC<Props> = ({ children, css, ...props }) => {
const [style, attrs] = Object.entries(props).reduce(
(res, [key, value]: [any, any]) => {
Expand Down Expand Up @@ -108,5 +101,14 @@ const acceptedProperties: readonly (keyof CssProperties)[] = [
'borderLeft',
'boxShadow',
] as const;
/** Not all properties supported */
export interface Props extends Omit<BoxProps, 'as' | 'translate' | 'color'>, CustomField {
as?: JSX.IntrinsicElements | React.ElementType;
}

type CustomField = {
[prop in typeof acceptedProperties[number]]?: CssProperties[prop];
};
type BoxProps = StitchesProps<typeof Div>;

const VALID_ITEMS = new Set<keyof CssProperties>(acceptedProperties);
11 changes: 9 additions & 2 deletions src/lib/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';

import { styled } from '../stitches.config';
import { FlexBox, FlexType } from '../Flex';
import { IconBox } from '../Icon/Icon';
import { styled } from '../stitches.config';

const ButtonRoot = styled(FlexBox as FlexType<'button'>, {
'transition': '0.2s ease-in-out',
Expand Down Expand Up @@ -126,11 +126,18 @@ const ButtonRoot = styled(FlexBox as FlexType<'button'>, {
color: '$textDisabled',
},
},
transparent: {
bc: 'transparent',
borderColor: 'transparent',
color: '$text',
},
},
},
});

const Button = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof ButtonRoot>>(
interface Props extends React.ComponentProps<typeof ButtonRoot> {}

const Button = React.forwardRef<HTMLButtonElement, Props>(
({ variant = 'primary', size = 'md', space = '$2', type = 'button', css, style, className, ...props }, ref) => {
return (
<ButtonRoot
Expand Down
15 changes: 9 additions & 6 deletions src/lib/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { StitchesProps } from '@stitches/react';
import React, { useMemo } from 'react';
import { HiCheck } from 'react-icons/hi';

import Box from '../Box';
import { attribute } from '../FocusRing/focus-visible';
import { FormField } from '../FormField/FormField';
import { FormField, FormFieldProps } from '../FormField/FormField';
import Icon from '../Icon';
import Label from '../Label';
import { styled } from '../stitches.config';
Expand Down Expand Up @@ -99,13 +101,14 @@ const Input = styled('input', {
},
});

type WrapperProps = React.ComponentProps<typeof CheckboxWrapper>;
type InputProps = React.ComponentProps<typeof Input>;
type FormFieldProps = React.ComponentProps<typeof FormField>;
type InputProps = StitchesProps<typeof Input>;

type Props = FormFieldProps & InputProps & WrapperProps & { label?: string; secondaryLabel?: string };
interface Props extends InputProps {
label?: string;
secondaryLabel?: string;
};

const Checkbox: React.FC<Props> = ({
const Checkbox: React.FC<FormFieldProps & Props> = ({
checked,
onChange,
css,
Expand Down
214 changes: 214 additions & 0 deletions src/lib/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React, { useCallback,useEffect, useMemo, useRef, useState } from 'react';
import { uid,useUIDSeed } from 'react-uid';

import { List as ListBox } from '../ListBox/ListBox';
import Popover from '../Popover';
import { useAllHandlers } from '../utils';
import { useOpenState, withOpenStateProvider } from '../utils/OpenStateProvider';
import Input from './Input';
import Item from './Item';
import { ComboBoxContext, ComboBoxProvider, FocusControls, RenderedItems } from './utils';

interface OptionType extends React.ReactElement<React.ComponentProps<typeof Item>, typeof Item> {}
interface Props extends Omit<React.ComponentProps<typeof Input>, 'onChange' | 'onSelect' | 'value' | 'children'> {
value?: string | null;
onChange?: (newValue: string | undefined | null) => void;
onInputChange?: (text: string) => void;
children: OptionType[] | OptionType;
}

const ComboBoxInner: React.FC<Props> = ({
children,
id,
value: propValue,
onChange: propOnChange,
onInputChange,
...textFieldProps
}) => {
const [innerValue, onChange] = useValue(propValue, propOnChange);
const [textValue, setTextValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const idSeed = useUIDSeed();
const isOpen = useOpenState();
const allowNewElement = !!onInputChange;

const allItems = useMemo<RenderedItems>(() => {
let items: RenderedItems = {};
React.Children.forEach(children, (option: OptionType) => {
const { label, value } = option.props || {};
const id = uid(idSeed('option') + value);
const selected = value === innerValue;
items[value] = {
id,
value,
selected,
label,
};
});
return items;
}, [children, idSeed, innerValue]);

const [renderedItems, setRenderedItems] = useState<RenderedItems>(allItems);

useEffect(() => {
if (!isOpen) return;
if (textValue === '') return setRenderedItems(allItems);
setRenderedItems(
Object.fromEntries(
Object.entries(allItems).filter(([value, { label }]) => label.toLowerCase().includes(textValue.toLowerCase()))
)
);
}, [allItems, textValue, isOpen]);

const [focusedItemId, focusControls] = useFocusControls(renderedItems);

useEffect(() => {
if (!innerValue) return;
const label = allItems[innerValue]?.label;
if (label) {
setTextValue(label);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [innerValue]);

useEffect(() => {
if (isOpen && innerValue) {
focusControls.focus(allItems[innerValue]?.id);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);

const handleValueChange = useAllHandlers((newValue?: string, newText?: string) => {
onChange(newValue);
setTextValue(newText || '');
onInputChange?.(newText || '');
});

const handleTextChange = useAllHandlers<string>((textValue) => {
if (allowNewElement || textValue === '') {
onChange(null);
}
setTextValue(textValue);
}, onInputChange);

const handleBlur = useAllHandlers(
textFieldProps.onBlur,
allowNewElement
? undefined
: () => {
if (textValue === '') {
onChange(null);
} else if (innerValue) {
setTextValue(allItems[innerValue]?.label);
} else {
setTextValue('');
}
}
);

const handleSelect = useAllHandlers<void>(() => {
const newValue = Object.keys(allItems).find((key) => allItems[key].id === focusedItemId);
if (newValue) {
handleValueChange(newValue, allItems[newValue].label);
}
});

const contextValue: ComboBoxContext = {
inputRef,
selectedItemValue: innerValue,
onValueChange: handleValueChange,
focusedItemId,
renderedItems,
focusControls,
allowNewElement,
};

const popover = useMemo(
() => (
<Popover triggerRef={inputRef}>
<ListBox role="listbox" id={idSeed('listbox')} aria-labelledby={idSeed('input')}>
{children}
</ListBox>
</Popover>
),
[children, idSeed]
);

return (
<ComboBoxProvider value={contextValue}>
<Input
{...textFieldProps}
aria-controls={idSeed('listbox')}
value={textValue}
onChange={handleTextChange}
onBlur={handleBlur}
onSelect={handleSelect}
/>
{popover}
</ComboBoxProvider>
);
};

const ComboBox = withOpenStateProvider<Props>(ComboBoxInner) as React.FC<Props> & { Item: typeof Item };

ComboBox.Item = Item;

export default ComboBox;

/**
* Duplicate state to be able to use the element uncontrolled
*/
function useValue(propValue: Props['value'], propOnChange: Props['onChange']) {
const [value, setValue] = useState(propValue);

const onChange = useCallback<Required<Props>['onChange']>(
(newValue) => {
// we expect `propOnChange` to change also `value` prop, so useEffect would update internal value
if (typeof propOnChange === 'function') {
propOnChange?.(newValue);
} else {
setValue(newValue);
}
},
[propOnChange]
);

useEffect(() => {
setValue(propValue);
}, [propValue]);

return [value, onChange] as const;
}

function useFocusControls(renderedItems: RenderedItems) {
const [focusedItemId, setFocusedItemId] = useState<string>();
const itemsRef = useRef(renderedItems);

useEffect(() => {
itemsRef.current = renderedItems;
}, [renderedItems]);

const focusControls = useMemo<FocusControls>(() => {
return {
focus: setFocusedItemId,
focusNext: () =>
setFocusedItemId((currentId) => {
const options = Object.values(itemsRef.current || {});
if (!options.length) return undefined;
const i = options.findIndex((el) => el.id === currentId);
const newIndex = (i + 1) % options.length;
return options[newIndex].id;
}),
focusPrev: () =>
setFocusedItemId((currentId) => {
const options = Object.values(itemsRef.current || {});
if (!options.length) return undefined;
const i = options.findIndex((el) => el.id === currentId);
const newIndex = i > 0 ? i - 1 : options.length - 1;
return options[newIndex].id;
}),
};
}, [itemsRef]);

return [focusedItemId, focusControls] as const;
}
Loading

1 comment on commit 09bc988

@vercel
Copy link

@vercel vercel bot commented on 09bc988 Dec 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.