diff --git a/package.json b/package.json index 71cc025..ffd849c 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "arrowParens": "avoid", "printWidth": 90, "semi": false, - "singleQuote": true + "singleQuote": true, + "trailingComma": "none" } } diff --git a/packages/components/.storybook/main.ts b/packages/components/.storybook/main.ts index 85b4b31..4715d84 100644 --- a/packages/components/.storybook/main.ts +++ b/packages/components/.storybook/main.ts @@ -13,19 +13,19 @@ const config: StorybookConfig = { * @see https://github.com/storybookjs/storybook/issues/21414#issuecomment-1694357674 */ features: { - storyStoreV7: false, + storyStoreV7: false }, core: { builder: { name: '@storybook/builder-vite', options: { - viteConfigPath: '../ui/vite.config.ts', - }, - }, + viteConfigPath: '../ui/vite.config.ts' + } + } }, async viteFinal(config) { return config - }, + } } export default config diff --git a/packages/components/package.json b/packages/components/package.json index bd5903b..7ed7ad7 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -15,6 +15,10 @@ "default": "./dist/cjs/index.cjs" } }, + "./*": { + "import": "./dist/*.js", + "require": "./dist/cjs/*.js" + }, "./package.json": "./package.json" }, "types": "./dist/index.d.ts", diff --git a/packages/components/src/autoSuggest/mod.tsx b/packages/components/src/autoSuggest/mod.tsx index 047caa0..402a7d6 100644 --- a/packages/components/src/autoSuggest/mod.tsx +++ b/packages/components/src/autoSuggest/mod.tsx @@ -8,17 +8,13 @@ import { SelectWrap, SelectMenuWrap, SelectMenu, SelectItem } from '../core.js' import { focusedStyles, sizing } from '../styles.js' import { Input } from '../input/mod.js' import { PB40T, SLB30T } from '../colors.js' -import { ChevronDown } from '../chevronDown/mod.js' +import { ChevronDown } from '../icons/chevronDown/mod.js' import type { ChangeEvent, FC, ReactNode } from 'react' import type { UseComboboxStateChange, UseComboboxStateChangeTypes } from 'downshift' import type { Size } from '../types.js' -interface Item { - value: string - label: string -} -type AnItem = Item | string +type AnItem = { value: string; label: string } | string type Items = AnItem[] interface AutoSuggestProps { items: Items @@ -29,10 +25,11 @@ interface AutoSuggestProps { onClear?: boolean | (() => void) onChange: ( evt: ChangeEvent, - type?: UseComboboxStateChangeTypes, + type?: UseComboboxStateChangeTypes ) => void + onSelect?: (selected: AnItem) => void onBlur?: () => void - renderItem?: (item: Item | string) => ReactNode + renderItem?: (item: AnItem) => ReactNode isDisabled?: boolean width?: string color?: string @@ -124,17 +121,17 @@ const ToggleMenuButton = styled.button<{ size: Size }>` ` const matchSorterOptions = { keys: [(item: AnItem) => itemToString(item)], - threshold: matchSorter.rankings.CONTAINS, + threshold: matchSorter.rankings.CONTAINS } const getChangeEvt = (value?: AnItem | null): ChangeEvent => { const evt = new Event('change', { bubbles: true, - cancelable: false, + cancelable: false }) Object.defineProperty(evt, 'target', { writable: false, - value: { value }, + value: { value } }) return evt as unknown as ChangeEvent @@ -146,6 +143,7 @@ const AutoSuggest: FC = ({ onBlur, onClear, onChange, + onSelect, preload, loadItems, labelledBy, @@ -155,7 +153,7 @@ const AutoSuggest: FC = ({ size = 'medium', placeholder = '', isDisabled = false, - inputBoundByItems = false, + inputBoundByItems = false }) => { const initialLoadedItems = useRef([]) const [inputText, setInputText] = useState(itemToString(value)) @@ -176,7 +174,7 @@ const AutoSuggest: FC = ({ setInputItems(newItems) }, 250, - { leading: true }, + { leading: true } ) } @@ -188,7 +186,7 @@ const AutoSuggest: FC = ({ if (preload && typeof loadItems === 'function') { return async () => { initialLoadedItems.current = await loadItems( - typeof preload === 'string' ? preload : '', + typeof preload === 'string' ? preload : '' ) setInputItems(initialLoadedItems.current) @@ -223,7 +221,7 @@ const AutoSuggest: FC = ({ getMenuProps, getInputProps, highlightedIndex, - getItemProps, + getItemProps } = useCombobox({ itemToString, items: inputItems, @@ -236,8 +234,12 @@ const AutoSuggest: FC = ({ if (onChange) { onChange(getChangeEvt(changes.selectedItem), changes.type) } + + if (onSelect) { + onSelect(changes.selectedItem ?? '') + } }, - [onChange], + [onChange] ), onStateChange: useCallback( (changes: UseComboboxStateChange): void => { @@ -254,6 +256,10 @@ const AutoSuggest: FC = ({ if (onChange) { onChange(getChangeEvt(inputValue), type) } + + if (onSelect && inputItems.includes(inputValue)) { + onSelect(inputValue) + } } else if (inputBoundByItems) { const nextItems = matchSorter(items, inputValue, matchSorterOptions) @@ -264,6 +270,10 @@ const AutoSuggest: FC = ({ if (onChange) { onChange(getChangeEvt(inputValue), type) } + + if (onSelect && inputItems.includes(inputValue)) { + onSelect(inputValue) + } } } @@ -278,15 +288,8 @@ const AutoSuggest: FC = ({ } } }, - [ - loadItems, - inputBoundByItems, - items, - inputText, - handleOnInputValueChange, - onChange, - ], - ), + [loadItems, inputBoundByItems, items, inputText, handleOnInputValueChange, onChange] + ) }) useEffect(() => { @@ -306,7 +309,7 @@ const AutoSuggest: FC = ({ if (!isOpen) { openMenu() } - }, + } })} id={id} color={color} @@ -346,4 +349,4 @@ const AutoSuggest: FC = ({ } export { AutoSuggest } -export type { AutoSuggestProps } +export type { AutoSuggestProps, AnItem } diff --git a/packages/components/src/autoSuggest/story.tsx b/packages/components/src/autoSuggest/story.tsx index 8d5caf1..e463a60 100644 --- a/packages/components/src/autoSuggest/story.tsx +++ b/packages/components/src/autoSuggest/story.tsx @@ -3,8 +3,9 @@ import styled from 'styled-components' import { AutoSuggest } from './mod.js' +import type { AnItem } from './mod.js' import type { StoryFn } from '@storybook/react' -import type { ChangeEvent, FC } from 'react' +import type { FC } from 'react' type Story = StoryFn @@ -22,34 +23,58 @@ const Dl = styled.dl` margin: 0; } ` -const Selection: FC<{ selection: string }> = ({ selection }) => { +const Selection: FC<{ selection: AnItem }> = ({ selection }) => { return (
Selected
-
{selection || 'None'}
+
{typeof selection === 'string' ? selection : selection.value}
) } const useSelection = () => { - const [selected, setSelected] = useState('') - const onChange = useCallback((evt: ChangeEvent) => { - setSelected(evt.target.value) + const [selected, setSelected] = useState('') + const onSelect = useCallback((selection: AnItem) => { + setSelected(selection) }, []) - return { selected, onChange } + return { selected, onSelect } } const Primary: Story = args => { - const { selected, onChange } = useSelection() + const { selected, onSelect } = useSelection() return ( <> - + + + ) +} +const ItemsAsObject: Story = args => { + const { selected, onSelect } = useSelection() + const items = [ + { + label: 'One', + value: '1' + }, + { + label: 'Two', + value: '2' + }, + { + label: 'Three', + value: '3' + } + ] + + return ( + <> + + ) } const InputBoundByItems: Story = args => { - const { selected, onChange } = useSelection() + const { selected, onSelect } = useSelection() return ( <> @@ -57,7 +82,7 @@ const InputBoundByItems: Story = args => { @@ -65,8 +90,8 @@ const InputBoundByItems: Story = args => { } InputBoundByItems.argTypes = { inputBoundByItems: { - control: false, - }, + control: false + } } export default { title: 'AutoSuggest', @@ -77,37 +102,37 @@ export default { inputBoundByItems: false, isDisabled: false, color: '#000000', - placeholder: 'type to search...', + placeholder: 'type to search...' }, argTypes: { items: { - control: false, + control: false }, value: { - control: false, + control: false }, placeholder: { - control: 'text', + control: 'text' }, onChange: { - action: 'onChange', + action: 'onChange' }, isDisabled: { - control: 'boolean', + control: 'boolean' }, color: { - control: 'color', + control: 'color' }, size: { control: 'select', - options: ['small', 'medium', 'large'], + options: ['small', 'medium', 'large'] }, preload: { - control: false, + control: false }, loadItems: { - control: false, - }, - }, + control: false + } + } } -export { Primary, InputBoundByItems } +export { Primary, InputBoundByItems, ItemsAsObject } diff --git a/packages/components/src/chevronDown/mod.tsx b/packages/components/src/chevronDown/mod.tsx deleted file mode 100644 index 2997e2f..0000000 --- a/packages/components/src/chevronDown/mod.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import styled from 'styled-components' - -import { SLB30T } from '../colors.js' - -import type { FC } from 'react' -import type { Size } from '../types.js' - -interface ChevronDownProps { - color?: string - size?: Size - className?: string -} - -const getSizing = (size: Size) => { - switch (size) { - case 'small': - return '16px' - case 'medium': - return '24px' - case 'large': - return '32px' - } -} -const Icon = styled.span<{ $size: Size; $color: string }>` - display: flex; - width: ${({ $size }) => getSizing($size)}; - height: ${({ $size }) => getSizing($size)}; - color: ${({ $color }) => $color}; - cursor: pointer; - - svg { - fill: currentColor; - } - - &:focus-visible { - outline: none; - border: 1px solid ${SLB30T}; - border-radius: 50%; - } - - &.isOpen { - transform: rotate(180deg); - } -` -const ChevronDown: FC = ({ - className, - size = 'medium', - color = '#c1c1c1', -}) => { - return ( - - - - - - - - - ) -} - -export { ChevronDown } diff --git a/packages/components/src/clearIcon/story.tsx b/packages/components/src/clearIcon/story.tsx deleted file mode 100644 index c1c3eaa..0000000 --- a/packages/components/src/clearIcon/story.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { StoryObj } from '@storybook/react' - -import { ClearIcon } from './mod.js' - -type Story = StoryObj - -const Primary: Story = { - args: { - size: 'medium', - color: '#c1c1c199', - }, -} -export default { - title: 'ClearIcon', - component: ClearIcon, - argTypes: { - size: { - control: 'select', - options: ['small', 'medium', 'large'], - }, - color: { - control: 'color', - }, - onClick: { - action: 'onClick', - }, - }, -} - -export { Primary } diff --git a/packages/components/src/core.tsx b/packages/components/src/core.tsx index 8e1583e..39eee5d 100644 --- a/packages/components/src/core.tsx +++ b/packages/components/src/core.tsx @@ -5,7 +5,7 @@ import { focusedStyles, placeholderStyles, getMenuDirectionStyles, - getToggleDirectionStyles, + getToggleDirectionStyles } from './styles.js' import { PB40T, SLB30T } from './colors.js' @@ -110,5 +110,5 @@ export { SelectMenu, SelectItem, SelectPlaceholder, - SelectedItem, + SelectedItem } diff --git a/packages/components/src/dataList/mod.tsx b/packages/components/src/dataList/mod.tsx index 192313a..594ac40 100644 --- a/packages/components/src/dataList/mod.tsx +++ b/packages/components/src/dataList/mod.tsx @@ -20,7 +20,7 @@ const DataList: FC = ({ placeholder, loadOptions, onChange, - onBlur, + onBlur }) => { const [options, setOptions] = useState(items || []) const loadOptionsRef = useRef( @@ -33,8 +33,8 @@ const DataList: FC = ({ setOptions(newOptions) }, 250, - { leading: true }, - ), + { leading: true } + ) ) const [listId] = useMemo(() => { const randomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(1))) diff --git a/packages/components/src/dataList/story.tsx b/packages/components/src/dataList/story.tsx index bfc73a1..e8d1aea 100644 --- a/packages/components/src/dataList/story.tsx +++ b/packages/components/src/dataList/story.tsx @@ -7,17 +7,17 @@ type Story = StoryObj const Primary: Story = { args: { placeholder: 'enter text to search', - items: ['one', 'two', 'three', 'four'], + items: ['one', 'two', 'three', 'four'] }, argTypes: { onChange: { - action: 'onChange', - }, - }, + action: 'onChange' + } + } } export default { title: 'DataList', - component: DataList, + component: DataList } export { Primary } diff --git a/packages/components/src/icon/mod.tsx b/packages/components/src/icon/mod.tsx deleted file mode 100644 index 1576744..0000000 --- a/packages/components/src/icon/mod.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import styled from 'styled-components' - -import type { Size } from '../types.js' - -interface IconProps { - color?: string - size?: Size -} - -const Wrap = styled.span` - display: flex; -` -const Icon = () => { - return -} - -export { Icon } -export type { IconProps } diff --git a/packages/components/src/icons/chevronDown/mod.tsx b/packages/components/src/icons/chevronDown/mod.tsx new file mode 100644 index 0000000..7942f79 --- /dev/null +++ b/packages/components/src/icons/chevronDown/mod.tsx @@ -0,0 +1,23 @@ +import { Icon } from '../common.js' + +import type { FC } from 'react' +import type { IconProps } from '../common.js' + +const ChevronDown: FC = ({ + className, + size = 'medium', + color = '#c1c1c1' +}) => { + return ( + + + + + + + + + ) +} + +export { ChevronDown } diff --git a/packages/components/src/icons/chevronDown/story.tsx b/packages/components/src/icons/chevronDown/story.tsx new file mode 100644 index 0000000..058313b --- /dev/null +++ b/packages/components/src/icons/chevronDown/story.tsx @@ -0,0 +1,30 @@ +import { StoryObj } from '@storybook/react' + +import { ChevronDown } from './mod.js' + +type Story = StoryObj + +const Primary: Story = { + args: { + size: 'medium', + color: '#c1c1c1' + } +} +export default { + title: 'Icons/ChevronDown', + component: ChevronDown, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large'] + }, + color: { + control: 'color' + }, + onClick: { + action: 'onClick' + } + } +} + +export { Primary } diff --git a/packages/components/src/clearIcon/mod.tsx b/packages/components/src/icons/clear/mod.tsx similarity index 51% rename from packages/components/src/clearIcon/mod.tsx rename to packages/components/src/icons/clear/mod.tsx index f6f450d..61b8209 100644 --- a/packages/components/src/clearIcon/mod.tsx +++ b/packages/components/src/icons/clear/mod.tsx @@ -1,52 +1,16 @@ -import styled from 'styled-components' import { useCallback } from 'react' -import { SLB30T } from '../colors.js' +import { Icon } from '../common.js' import type { FC, KeyboardEvent } from 'react' -import type { Size } from '../types.js' +import type { IconProps } from '../common.js' -interface ClearIconProps { - size?: Size - color?: string - onClick?: () => void - className?: string - tabIndex?: number -} - -const getSizing = (size: Size) => { - switch (size) { - case 'small': - return '16px' - case 'medium': - return '24px' - case 'large': - return '32px' - } -} -const Icon = styled.span<{ $size: Size; $color: string }>` - display: flex; - width: ${({ $size }) => getSizing($size)}; - height: ${({ $size }) => getSizing($size)}; - color: ${({ $color }) => $color}; - cursor: pointer; - - svg { - fill: currentColor; - } - - &:focus-visible { - outline: none; - border: 1px solid ${SLB30T}; - border-radius: 50%; - } -` -const ClearIcon: FC = ({ +const Clear: FC = ({ onClick, className, size = 'medium', color = '#c1c1c199', - tabIndex = 0, + tabIndex = 0 }) => { const handleOnClick = useCallback(() => { if (typeof onClick === 'function') { @@ -59,13 +23,13 @@ const ClearIcon: FC = ({ handleOnClick() } }, - [handleOnClick], + [handleOnClick] ) return ( = ({ ) } -export { ClearIcon } -export type { ClearIconProps } +export { Clear } diff --git a/packages/components/src/icons/clear/story.tsx b/packages/components/src/icons/clear/story.tsx new file mode 100644 index 0000000..cc4a6b0 --- /dev/null +++ b/packages/components/src/icons/clear/story.tsx @@ -0,0 +1,30 @@ +import { StoryObj } from '@storybook/react' + +import { Clear } from './mod.js' + +type Story = StoryObj + +const Primary: Story = { + args: { + size: 'medium', + color: '#c1c1c199' + } +} +export default { + title: 'Icons/ClearIcon', + component: Clear, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large'] + }, + color: { + control: 'color' + }, + onClick: { + action: 'onClick' + } + } +} + +export { Primary } diff --git a/packages/components/src/icons/common.ts b/packages/components/src/icons/common.ts new file mode 100644 index 0000000..f51cead --- /dev/null +++ b/packages/components/src/icons/common.ts @@ -0,0 +1,50 @@ +import styled from 'styled-components' + +import { SLB30T } from '../colors.js' + +import type { KeyboardEvent } from 'react' +import type { Size } from '../types.js' + +interface IconProps { + color?: string + size?: Size + onClick?: () => void + onKeyDown?: (evt: KeyboardEvent) => void + className?: string + tabIndex?: number +} + +const getSizing = (size: Size) => { + switch (size) { + case 'small': + return '16px' + case 'medium': + return '24px' + case 'large': + return '32px' + } +} +const Icon = styled.span<{ size: Size; color: string }>` + display: flex; + width: ${({ size }) => getSizing(size)}; + height: ${({ size }) => getSizing(size)}; + color: ${({ color }) => color}; + cursor: pointer; + + svg { + fill: currentColor; + } + + &:focus-visible { + outline: none; + border: 1px solid ${SLB30T}; + border-radius: 50%; + } + + &.isOpen { + transform: rotate(180deg); + } +` + +export { getSizing, Icon } +export type { IconProps } diff --git a/packages/components/src/input/mod.tsx b/packages/components/src/input/mod.tsx index c625493..e05f03b 100644 --- a/packages/components/src/input/mod.tsx +++ b/packages/components/src/input/mod.tsx @@ -1,7 +1,7 @@ import { forwardRef, useCallback, useState, useRef } from 'react' import styled from 'styled-components' -import { ClearIcon } from '../clearIcon/mod.js' +import { Clear as ClearIcon } from '../icons/clear/mod.js' import { sizing } from '../styles.js' import type { KeyboardEvent, ChangeEvent, ForwardedRef, FocusEvent } from 'react' @@ -91,9 +91,9 @@ const Input = forwardRef(function Input( color = 'black', placeholder = '', borderColor = 'black', - isDisabled = false, + isDisabled = false }, - ref, + ref ) { const wrapper = useRef(null) const isClearable = typeof onClear === 'function' @@ -106,7 +106,7 @@ const Input = forwardRef(function Input( onFocus(evt) } }, - [onFocus], + [onFocus] ) const handleOnBlur = useCallback((evt: FocusEvent) => { if (!evt.currentTarget.contains(evt.relatedTarget)) { diff --git a/packages/components/src/input/story.tsx b/packages/components/src/input/story.tsx index 8b95aea..e34e858 100644 --- a/packages/components/src/input/story.tsx +++ b/packages/components/src/input/story.tsx @@ -5,7 +5,7 @@ import { Input } from './mod.js' import type { StoryFn } from '@storybook/react' import type { ChangeEvent } from 'react' -const Single: StoryFn = args => { +const Primary: StoryFn = args => { const ref = useRef(null) const [value, setValue] = useState('') const onChange = useCallback((evt: ChangeEvent) => { @@ -36,49 +36,49 @@ export default { component: Input, args: { size: 'medium', - placeholder: 'placeholder', + placeholder: 'placeholder' }, argTypes: { id: { table: { - disable: true, - }, + disable: true + } }, list: { table: { - disable: true, - }, + disable: true + } }, labelledBy: { table: { - disable: true, - }, + disable: true + } }, size: { control: 'select', - options: ['small', 'medium', 'large'], + options: ['small', 'medium', 'large'] }, fontSize: { - control: 'text', + control: 'text' }, isDisabled: { - control: 'boolean', + control: 'boolean' }, color: { - control: 'color', + control: 'color' }, borderColor: { - control: 'color', + control: 'color' }, value: { - control: false, + control: false }, placeholder: { - control: 'text', + control: 'text' }, onChange: { - action: 'onChange', - }, - }, + action: 'onChange' + } + } } -export { Single } +export { Primary } diff --git a/packages/components/src/styles.ts b/packages/components/src/styles.ts index bd2ce34..6e57fb9 100644 --- a/packages/components/src/styles.ts +++ b/packages/components/src/styles.ts @@ -11,14 +11,14 @@ const sizing = { top: '5px', left: '10px', right: '10px', - padding: '8px 14px 8px 38px', + padding: '8px 14px 8px 38px' }, clearable: { - padding: '8px 38px 8px 14px', + padding: '8px 38px 8px 14px' }, iconClearable: { - padding: '8px 38px 8px 38px', - }, + padding: '8px 38px 8px 38px' + } }, medium: { fontSize: '16px', @@ -27,14 +27,14 @@ const sizing = { top: '8px', left: '10px', right: '10px', - padding: '10px 16px 10px 40px', + padding: '10px 16px 10px 40px' }, clearable: { - padding: '10px 40px 10px 16px', + padding: '10px 40px 10px 16px' }, iconClearable: { - padding: '10px 40px 10px 40px', - }, + padding: '10px 40px 10px 40px' + } }, large: { fontSize: '16px', @@ -43,15 +43,15 @@ const sizing = { top: '12px', left: '10px', right: '10px', - padding: '14px 16px 14px 40px', + padding: '14px 16px 14px 40px' }, clearable: { - padding: '14px 40px 14px 16px', + padding: '14px 40px 14px 16px' }, iconClearable: { - padding: '14px 40px 14px 40px', - }, - }, + padding: '14px 40px 14px 40px' + } + } } const focusedStyles = css` border-color: ${SLB30T}; @@ -68,7 +68,7 @@ const placeholderStyles = css` color: ${PB40T}; ` const getToggleDirectionStyles = ({ - menuDirection, + menuDirection }: { menuDirection?: MenuDirection }) => { @@ -112,5 +112,5 @@ export { borderOutlineStyles, placeholderStyles, getMenuDirectionStyles, - getToggleDirectionStyles, + getToggleDirectionStyles } diff --git a/packages/ui/src/layout.tsx b/packages/ui/src/layout.tsx index 0129309..610d795 100644 --- a/packages/ui/src/layout.tsx +++ b/packages/ui/src/layout.tsx @@ -24,7 +24,7 @@ export const Layout: FC<{ children: ReactNode }> = ({ children }) => { <> {createPortal( , - document.querySelector('body > aside') as HTMLElement, + document.querySelector('body > aside') as HTMLElement )} {children} diff --git a/packages/ui/src/providers.tsx b/packages/ui/src/providers.tsx index 90b0d5c..b0b802c 100644 --- a/packages/ui/src/providers.tsx +++ b/packages/ui/src/providers.tsx @@ -6,9 +6,9 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, - refetchOnWindowFocus: false, - }, - }, + refetchOnWindowFocus: false + } + } }) const Providers: FC<{ children: ReactNode }> = ({ children }) => { return ( diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 6350734..69c2cf9 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -8,12 +8,12 @@ export default defineConfig(() => { babel: { babelrc: true, configFile: true, - rootMode: 'upward', - }, - }), + rootMode: 'upward' + } + }) ], server: { - host: true, - }, + host: true + } } })