diff --git a/CHANGELOG.md b/CHANGELOG.md index c012a81..1eccc04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 |-|-|-|-| |Initial components|-|-|-| +## [0.0.2](https://github.com/iancharlesdouglas/carbon-icons-qwik/releases/tag/0.0.2) - 2023-06-17 + +|Added|Fixed|Changed|Removed| +|-|-|-|-| +|Dropdown compoenent|-|-|-| + diff --git a/COMPONENTS.md b/COMPONENTS.md index 82b88df..07754be 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -5,6 +5,7 @@ |-|-|-| |Button|0.0.1-1|| |Checkbox|0.0.1-1|| +|Dropdown|0.0.2|| |FluidForm|0.0.1-1|| |Form|0.0.1-1|| |Grid|0.0.1-1|Incl. Row, Column. Grid is CSS grid| @@ -35,7 +36,6 @@ - DefinitionTooltip - Dialog - Disclosure -- Dropdown - ErrorBoundary - ExpandableSearch - FileUploader diff --git a/package.json b/package.json index 215d113..37b66f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "carbon-components-qwik", - "version": "0.0.1-2.2", + "version": "0.0.2", "description": "Carbon Design System components implemented as Qwik components", "license": "Apache-2.0", "main": "./lib/index.qwik.mjs", diff --git a/src/components/dropdown/dropdown.tsx b/src/components/dropdown/dropdown.tsx index ae720c9..e2f1bfb 100644 --- a/src/components/dropdown/dropdown.tsx +++ b/src/components/dropdown/dropdown.tsx @@ -10,24 +10,30 @@ import { useStore, useTask$, QwikMouseEvent, - useVisibleTask$, } from '@builder.io/qwik'; import { usePrefix } from '../../internal/hooks/use-prefix'; import { formContext } from '../../internal/contexts/form-context'; import classNames from 'classnames'; import _ from 'lodash'; import { ListBox } from '../list-box/list-box'; -import { ListBoxMenu } from '../list-box/list-box-menu'; +import { ListBoxDimensions, ListBoxMenu } from '../list-box/list-box-menu'; import { Checkmark, WarningAltFilled, WarningFilled } from 'carbon-icons-qwik'; import { ListBoxMenuIcon } from '../list-box/list-box-menu-icon'; import { ListBoxMenuItem } from '../list-box/list-box-menu-item'; import { uniqueId } from '../../internal/unique/unique-id'; import { KeyCodes } from '../../internal/key-codes'; +/** + * Item with a label property + */ +type Labelled = { + label: string; +}; + /** * List item type */ -export type Item = string | { label: string }; +export type Item = string | Labelled; /** * Function that takes an item and returns a string representation of it @@ -46,6 +52,15 @@ export const defaultItemToString: ItemToString = (item: Item) => { return item?.label; }; +const itemsEqual = (item1: Item, item2: Item) => { + if (typeof item1 === 'string' && typeof item2 === 'string') { + return item1 === item2; + } + const labelledItem1 = item1 as Labelled; + const labelledItem2 = item2 as Labelled; + return labelledItem1.label === labelledItem2.label; +}; + const ariaNormalize = ( isOpen: boolean, disabled: boolean, @@ -64,7 +79,7 @@ const ariaNormalize = ( let selectedId: string | undefined; let selectedOption: Item | undefined; if (items && selectedItem) { - selectedIndex = items.findIndex((item) => item === selectedItem); + selectedIndex = items.findIndex((item) => itemsEqual(item, selectedItem)); selectedOption = selectedItem; } else if (items && initialSelectedItem) { const initialItems = Array.isArray(initialSelectedItem) ? initialSelectedItem : [initialSelectedItem]; @@ -168,6 +183,7 @@ export const Dropdown = component$((props: DropdownProps) => { const selectedOption = useSignal(modifiedSelectedItem); const highlightedOption = useSignal(); const listBoxElement = useSignal(); + const listBoxDimensions = useStore({ height: 0, itemHeight: 0, visibleRows: 0 }); const comboboxElement = useSignal(); const { ariaLabel, @@ -189,24 +205,6 @@ export const Dropdown = component$((props: DropdownProps) => { warnText, } = props; - useVisibleTask$(() => { - console.log('vis task - listbox ref', listBoxElement.value); - - // const listBoxDiv = document.getElementById(`${prefix}--list-box__menu`); - // console.log('listBoxDiv height', listBoxDiv?.clientHeight); - // if (listBoxElement.value) { - // console.log('dropdown vis task - listbox height', listBoxElement.value.clientHeight); - // } - }); - - useTask$( - ({ track }) => { - track(listBoxElement); - console.log('tracked listboxel. ref.', listBoxElement.value); - }, - { eagerness: 'load' } - ); - type Keys = { typed: string[]; reset: boolean; @@ -233,11 +231,6 @@ export const Dropdown = component$((props: DropdownProps) => { } }); - useTask$(({ track }) => { - track(props); - console.log('props task'); - }); - const inline = type === 'inline'; const showWarning = !invalid && warn; @@ -347,6 +340,7 @@ export const Dropdown = component$((props: DropdownProps) => { state.isOpen = false; } else if (event.keyCode !== KeyCodes.Tab) { state.isOpen = true; + event.stopPropagation(); } break; } @@ -370,6 +364,28 @@ export const Dropdown = component$((props: DropdownProps) => { } break; } + case KeyCodes.PageDown: { + if (listBoxDimensions.visibleRows) { + if (highlightedOption.value && items) { + const currentIndex = items.indexOf(highlightedOption.value); + if (currentIndex > -1 && currentIndex + listBoxDimensions.visibleRows <= items.length) { + highlightedOption.value = items[currentIndex + listBoxDimensions.visibleRows]; + } + } + } + break; + } + case KeyCodes.PageUp: { + if (listBoxDimensions.visibleRows) { + if (highlightedOption.value && items) { + const currentIndex = items.indexOf(highlightedOption.value); + if (currentIndex > -1 && currentIndex - listBoxDimensions.visibleRows >= 0) { + highlightedOption.value = items[currentIndex - listBoxDimensions.visibleRows]; + } + } + } + break; + } case KeyCodes.Escape: { state.isOpen = false; break; @@ -428,22 +444,33 @@ export const Dropdown = component$((props: DropdownProps) => { state.isOpen = false; } })} + preventdefault:keydown > {(selectedOption.value && (RenderSelectedItem ? : itemToString(selectedOption.value))) || label} - + { + listBoxDimensions.visibleRows = dimensions.visibleRows; + })} + preventdefault:keydown + > {state.isOpen && items?.map((item: Item, index: number) => { const title = itemToString(item); - const itemSelected = selectedOption.value === item; + const itemSelected = selectedOption.value ? itemsEqual(selectedOption.value, item) : undefined; return ( { diff --git a/src/components/list-box/list-box-menu.tsx b/src/components/list-box/list-box-menu.tsx index db8efc2..031f896 100644 --- a/src/components/list-box/list-box-menu.tsx +++ b/src/components/list-box/list-box-menu.tsx @@ -1,7 +1,16 @@ -import { QwikIntrinsicElements, Slot, component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'; +import { PropFunction, QwikIntrinsicElements, Slot, component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'; import { usePrefix } from '../../internal/hooks/use-prefix'; import { Item } from '../dropdown/dropdown'; +/** + * Measured dimensions + */ +export type ListBoxDimensions = { + height: number; + itemHeight: number; + visibleRows: number; +}; + /** * ListBoxMenu props * @property {string} id - ID @@ -12,6 +21,7 @@ export type ListBoxMenuProps = QwikIntrinsicElements['div'] & { id?: string; items?: Item[]; highlightedItem?: Item; + onMeasure$?: PropFunction<(dimensions: ListBoxDimensions) => void>; }; /** @@ -19,15 +29,15 @@ export type ListBoxMenuProps = QwikIntrinsicElements['div'] & { */ export const ListBoxMenu = component$((props: ListBoxMenuProps) => { const prefix = usePrefix(); - const { id, items, highlightedItem } = props; + const { id, items, highlightedItem, onMeasure$ } = props; const listBoxElement = useSignal(); useVisibleTask$(({ track }) => { track(props); if (items && highlightedItem && listBoxElement.value) { + const children = Array.from(listBoxElement.value.children); + const itemHeight = children[0]?.clientHeight; const highlightedIndex = items.indexOf(highlightedItem); if (highlightedIndex > -1) { - const children = Array.from(listBoxElement.value.children); - const itemHeight = children[0]?.clientHeight; const itemTop = highlightedIndex * itemHeight; if (itemTop < listBoxElement.value.scrollTop) { listBoxElement.value.scrollTo(0, itemTop); @@ -35,6 +45,10 @@ export const ListBoxMenu = component$((props: ListBoxMenuProps) => { listBoxElement.value.scrollTo(0, itemTop - itemHeight); } } + if (listBoxElement.value.clientHeight && itemHeight) { + onMeasure$ && + onMeasure$({ height: listBoxElement.value.clientHeight, itemHeight, visibleRows: Math.floor(listBoxElement.value.clientHeight / itemHeight) }); + } } listBoxElement.value?.focus(); }); diff --git a/src/internal/key-codes.ts b/src/internal/key-codes.ts index 927c677..ec3b9de 100644 --- a/src/internal/key-codes.ts +++ b/src/internal/key-codes.ts @@ -8,6 +8,8 @@ export enum KeyCodes { Escape = 27, Home = 36, LeftArrow = 37, + PageDown = 34, + PageUp = 33, RightArrow = 39, Space = 32, Tab = 9, diff --git a/src/test/test.tsx b/src/test/test.tsx index e5207c9..a1cdb6e 100644 --- a/src/test/test.tsx +++ b/src/test/test.tsx @@ -34,7 +34,7 @@ const Test = component$(() => { 'Jackfruit', ].map((label) => ({ label, key: label })); // const ItemComponent = component$(({ item }: ItemProps) => {defaultItemToString(item)}); - const selectedItem = useSignal({ label: '' }); + const selectedItem = useSignal(items.find((item) => (item as { label: string }).label === 'Banana')!); return ( @@ -67,7 +67,7 @@ const Test = component$(() => { (item as { label: string }).label === 'Banana')} + selectedItem={selectedItem.value} renderSelectedItem={SelectedItemRenderComp} items={items} helperText="Optional"