diff --git a/src/lib/index.ts b/src/lib/index.ts index cdce13af..13d935bf 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,4 +1,5 @@ export { positionRelativeGet } from './position-relative'; export { keyboardEventHandle } from './keyboard-event'; export { keyHandlerGet } from './select-handlers'; +export { keyHandlerList } from './listHandlers'; export { Variant } from './types'; diff --git a/src/lib/listHandlers.ts b/src/lib/listHandlers.ts new file mode 100644 index 00000000..686fdb5c --- /dev/null +++ b/src/lib/listHandlers.ts @@ -0,0 +1,33 @@ +const LI_TAG = 'LI'; + +export const onKeyDown = (listNode: HTMLElement) => () => { + if (document.activeElement?.tagName === LI_TAG) { + (document.activeElement?.nextElementSibling as HTMLElement)?.focus(); + } else { + (listNode?.firstChild as HTMLElement)?.focus(); + } +}; + +export const onKeyUp = (listNode: HTMLElement) => () => { + if (document.activeElement?.tagName === LI_TAG) { + (document?.activeElement?.previousElementSibling as HTMLElement)?.focus(); + } else { + (listNode.lastChild as HTMLElement)?.focus(); + } +}; + +const onItemEnter = (listNode: HTMLElement) => () => { + if (document.activeElement?.tagName === LI_TAG) { + (document?.activeElement as HTMLElement).click(); + } else { + (listNode?.lastChild as HTMLElement).focus(); + } +}; + +export const keyHandlerList = ({ + listNode, +}: any) => ({ + arrowDown: onKeyDown(listNode), + arrowUp: onKeyUp(listNode), + enter: onItemEnter(listNode), +}); diff --git a/src/lib/select-handlers.ts b/src/lib/select-handlers.ts index eecf9118..4fa5e665 100644 --- a/src/lib/select-handlers.ts +++ b/src/lib/select-handlers.ts @@ -13,15 +13,15 @@ const LI_TAG = 'LI'; const onArrowDown = (dropdownNode: HTMLElement) => () => { if (document.activeElement?.tagName === LI_TAG) { - (document.activeElement.nextElementSibling as HTMLElement).focus(); + (document.activeElement?.nextElementSibling as HTMLElement).focus(); } else { - (dropdownNode.firstChild as HTMLElement).focus(); + (dropdownNode?.firstChild as HTMLElement).focus(); } }; const onArrowUp = (dropdownNode: HTMLElement) => () => { if (document.activeElement?.tagName === LI_TAG) { - (document.activeElement.previousElementSibling as HTMLElement).focus(); + (document?.activeElement?.previousElementSibling as HTMLElement).focus(); } else { (dropdownNode.lastChild as HTMLElement).focus(); } diff --git a/src/ui/atoms/button/index.tsx b/src/ui/atoms/button/index.tsx index 3712244a..5a229e66 100644 --- a/src/ui/atoms/button/index.tsx +++ b/src/ui/atoms/button/index.tsx @@ -60,9 +60,9 @@ export const Button = styled(ButtonBase)` &[data-outlined='true'] { background-color: transparent; color: var(--local-shape-color); - svg > path { - fill: var(--local-shape-color); - } + svg > path { + fill: var(--local-shape-color); + } } [data-icon] { diff --git a/src/ui/atoms/list/index.tsx b/src/ui/atoms/list/index.tsx index 8283d843..5a6b69c4 100644 --- a/src/ui/atoms/list/index.tsx +++ b/src/ui/atoms/list/index.tsx @@ -1,116 +1,196 @@ import * as React from 'react'; import styled, { StyledComponent } from 'styled-components'; import { Variant } from 'lib/types'; - -/** - * --woly-list-padding - * --woly-list-color - * --woly-list-background - * --woly-rounding - * --woly-shadow - * --woly-canvas - * --woly-background-hover - * --woly-color - * --woly-color-hover - * --woly-line-height - */ - +import { keyHandlerList, keyboardEventHandle } from 'lib'; interface List { className?: string; + disabled?: boolean; list: Array<{ left?: React.ReactNode; right?: React.ReactNode; text: React.ReactNode; id: string; disabled?: boolean; - onClick?: React.MouseEventHandler; + onClick?: React.EventHandler; }>; } -const ListBase: React.FC = ({ className, list, variant = 'default' }) => ( -
    - {list.map(({ left, right, text, id, disabled, onClick }) => ( -
  • - {left && {left}} - {text} - {right && {right}} -
  • - ))} -
-); +const ListBase: React.FC = ({ + className, + list, + variant = 'default', + disabled = false, +}) => { + const tabIndex = disabled ? -1 : 0; + const itemListRef = React.useRef(null); + + const onKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + const listNode = itemListRef.current; + + if (!document || !listNode || event.key === 'Tab') { + return; + } + + event.preventDefault(); + + const handlerList = keyHandlerList({ + listNode, + }); + + keyboardEventHandle({ + event, + keyHandler: handlerList, + }); + }, + [itemListRef], + ); + return ( +
    + {list.map(({ left, right, text, id, onClick, disabled }) => ( +
  • + {left && {left}} + {text} + {right && {right}} +
  • + ))} +
+ ); +} + export const List = styled(ListBase)` - --woly-vertical: calc(1px * var(--woly-component-level) * var(--woly-main-level)); - --woly-horizontal: calc( - var(--woly-const-m) + (1px * var(--woly-main-level)) + var(--woly-vertical) + --local-vertical: calc(1px * var(--woly-component-level) * var(--woly-main-level)); + --local-horizontal: calc( + var(--woly-const-m) + (1px * var(--woly-main-level)) + var(--local-vertical) ); - --woly-width: 100%; + --local-gap: var(--local-vertical); + --local-compensate: var(--woly-const-m); + --local-margin: var(--woly-border-width); + + --local-color: var(--woly-canvas-text-default); + --local-background: var(--woly-shape-text-default); + --local-item-background: var(--woly-canvas-default); + --local-border: var(--woly-canvas-default); box-sizing: border-box; - width: var(--woly-width); + + width: 100%; + outline: none; padding: 0; + margin: 0; - color: var(--woly-color, #000000); + border: var(--woly-border-width) solid var(--local-border); list-style-type: none; - background-color: var(--woly-canvas, #ffffff); - border-radius: var(--woly-rounding, 3px); - box-shadow: var(--woly-shadow, 3px 3px 8px rgba(11, 31, 53, 0.04)); - - [data-icon] { - display: flex; - flex-shrink: 0; - align-items: center; - justify-content: center; - - width: var(--woly-line-height, 24px); - height: var(--woly-line-height, 24px); - padding-right: 6px; - - svg > path { - fill: var(--woly-color-disabled, #000000); - } - } li[data-type='list-item'] { display: flex; - flex: 1; align-items: center; - padding: var(--woly-vertical, 12px) var(--woly-horizontal, 18px 12px); + padding: var(--local-vertical) 0; + margin-bottom: var(--local-margin); font-size: var(--woly-font-size, 15px); line-height: var(--woly-line-height, 24px); + color: var(--local-color); + background: var(--local-item-background); + cursor: pointer; + outline: none; - [data-block='content'] { + [data-text] { + display: flex; flex: 1; + padding: 0 var(--local-horizontal); } - &:hover { - background-color: var(--woly-background-hover, #f5f5f5); + & > [data-text]:not(:only-child, :last-child ){ + padding-right: 0; } - &:focus, + [data-icon] { + --local-icon-size: var(--woly-line-height); + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + + width: var(--local-icon-size); + height: var(--local-icon-size); + + svg > path { + fill: var(--local-color); + } + } + + [data-icon='left'] { + padding: 0 0 0 calc(var(--local-horizontal) - var(--local-compensate)); + } + + [data-icon='right'] { + padding: 0 calc(var(--local-horizontal) - var(--local-compensate)) 0 0; + } + + [data-icon='left'] ~ [data-text], + [data-text] ~ [data-icon='right'] { + padding-left: var(--local-gap); + } + + &:hover { + --local-item-background: var(--woly-canvas-disabled); + } + &:focus { + box-shadow: 0 0 0 var(--woly-border-width) var(--woly-focus); + } &:active { - border-color: var(--woly-border-focus, #1f68f5); - border-style: solid; - border-width: var(--woly-border-width, 1.5px); + --local-item-background: var(--woly-focus); + --local-color: var(--woly-shape-text-active); } &[data-disabled='true'] { - color: var(--woly-color-disabled, #c4c4c4); + --local-color: var(--woly-canvas-text-disabled); + --local-item-background: var(--woly-canvas-disabled); pointer-events: none; [data-icon] { - svg > path { - fill: var(--woly-canvas, #c4c4c4); - } + --local-color: var(--woly-canvas-text-disabled); } } } + + &:focus { + box-shadow: 0 0 0 var(--woly-border-width) var(--woly-focus); + } + + &[data-disabled='true'] { + pointer-events: none; + + li[data-type='list-item'] { + --local-color: var(--woly-canvas-text-disabled); + --local-item-background: var(--woly-canvas-disabled); + } + + [data-icon] { + --local-color: var(--woly-canvas-text-disabled); + } + } ` as StyledComponent<'ul', Record, List & Variant>; diff --git a/src/ui/atoms/list/usage.mdx b/src/ui/atoms/list/usage.mdx index d9178e3a..9cbb0ac0 100644 --- a/src/ui/atoms/list/usage.mdx +++ b/src/ui/atoms/list/usage.mdx @@ -4,7 +4,7 @@ category: atoms package: 'woly' --- -import { Playground, block } from 'box-styles' +import { Playground, block, StateEvent } from 'box-styles' import { List } from 'ui'; import { InfoIcon, Check } from 'icons'; @@ -22,37 +22,35 @@ It does not need to be a direct child of the List component. ### Example - + alert('Item clicked')}> + {(value, change) => ( + + )} + - - ### Icons -listList items can consist icon on the left side or the right. Use icons on the both sides is not recommended. - -{' '} +List items can consist icon on the left side or the right. Use icons on the both sides is not recommended. - + , - id: 0, - text: 'Red', - onClick: () => alert('Item clicked'), - }, + { left: , id: 0, text: 'Red' }, { left: , id: 1, text: 'Green' }, { left: , id: 2, text: 'Blue' }, ]} /> }, { id: 1, text: 'Green', right: }, @@ -60,6 +58,7 @@ listList items can consist icon on the left side or the right. Use icons on the ]} /> , @@ -72,22 +71,94 @@ listList items can consist icon on the left side or the right. Use icons on the { left: , id: 2, text: 'Blue', right: }, ]} /> + + +List items can be clickable + + + alert('Item clicked')}> + {(value, change) => ( + , + id: 0, + text: 'Red', + right: , + onClick: change, + }, + { + left: , + id: 1, + text: 'Green', + right: , + onClick: change, + }, + { + left: , + id: 2, + text: 'Blue', + right: , + onClick: change, + }, + ]} + /> + )} + + + +or not + , id: 0, text: 'Red', right: , - disabled: true, }, - { left: , id: 1, text: 'Green', right: }, + { + left: , + id: 1, + text: 'Green', + right: , + }, + { + left: , + id: 2, + text: 'Blue', + right: , + }, + ]} + /> + + +### Disabled + + + , + id: 0, + text: 'Red', + right: , + }, + { + left: , + id: 1, + text: 'Green', + right: , + }, { left: , id: 2, text: 'Blue', right: , - disabled: true, }, ]} /> @@ -100,6 +171,7 @@ Size controlled by the `component-level` block property not from the props. , id: 0, text: 'Red' }, { left: , id: 1, text: 'Green' }, @@ -107,8 +179,19 @@ Size controlled by the `component-level` block property not from the props. ]} /> + + , id: 0, text: 'Red' }, + { left: , id: 1, text: 'Green' }, + { left: , id: 2, text: 'Blue' }, + ]} + /> + , id: 0, text: 'Red' }, { left: , id: 1, text: 'Green' }, @@ -118,6 +201,7 @@ Size controlled by the `component-level` block property not from the props. , id: 0, text: 'Red' }, { left: , id: 1, text: 'Green' }, @@ -125,6 +209,16 @@ Size controlled by the `component-level` block property not from the props. ]} /> + + , id: 0, text: 'Red' }, + { left: , id: 1, text: 'Green' }, + { left: , id: 2, text: 'Blue' }, + ]} + /> + ### Kinds @@ -139,3 +233,4 @@ Size controlled by the `component-level` block property not from the props. | --------- | ------------------------ | ----------- | ------------------------------------ | | `list` | `Array` | | An array of List items | | `variant` | `string` | `'default'` | Variant prop to style List component | +| `disabled`| `boolean` | | Disable list |