A design system in a box. hip-ui.tngl.io/docs/introduction
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

checkbox listbox

+258 -28
+39 -5
apps/docs/src/components/listbox/index.tsx
··· 1 1 import * as stylex from "@stylexjs/stylex"; 2 2 import { Check } from "lucide-react"; 3 - import { use } from "react"; 3 + import { createContext, use } from "react"; 4 4 import { 5 5 ListBoxProps as AriaListBoxProps, 6 6 ListBoxItem as AriaListBoxItem, ··· 10 10 ListBoxItemProps as AriaListBoxItemProps, 11 11 Header, 12 12 SeparatorProps, 13 + ListStateContext, 13 14 } from "react-aria-components"; 14 15 16 + import { Checkbox, CheckboxProps } from "../checkbox"; 15 17 import { SizeContext } from "../context"; 16 18 import { Separator } from "../separator"; 17 19 import { ui } from "../theme/semantic-color.stylex"; ··· 45 47 }, 46 48 }); 47 49 50 + type ListBoxVariant = "default" | "checkbox"; 51 + 52 + const ListboxVariantContext = createContext<ListBoxVariant>("default"); 53 + 48 54 export interface ListBoxProps<T extends object> 49 55 extends StyleXComponentProps<AriaListBoxProps<T>> { 50 56 size?: Size; 51 57 items?: Iterable<T>; 52 58 children: React.ReactNode | ((item: T) => React.ReactNode); 59 + variant?: ListBoxVariant; 53 60 } 54 61 55 62 export function ListBox<T extends object>({ 56 63 size: sizeProp, 57 64 style, 65 + variant = "default", 58 66 ...props 59 67 }: ListBoxProps<T>) { 60 68 const size = sizeProp || use(SizeContext); 61 69 62 70 return ( 63 - <SizeContext value={size}> 64 - <AriaListBox {...stylex.props(styles.listBox, style)} {...props} /> 65 - </SizeContext> 71 + <ListboxVariantContext value={variant}> 72 + <SizeContext value={size}> 73 + <AriaListBox {...stylex.props(styles.listBox, style)} {...props} /> 74 + </SizeContext> 75 + </ListboxVariantContext> 66 76 ); 67 77 } 68 78 ··· 73 83 suffix?: React.ReactNode; 74 84 } 75 85 86 + function ListBoxCheckbox({ id = "", ...props }: CheckboxProps) { 87 + const listboxItemState = use(ListStateContext); 88 + 89 + return ( 90 + <Checkbox 91 + {...props} 92 + isSelected={Boolean( 93 + listboxItemState?.selectionManager.selectedKeys.has(id), 94 + )} 95 + onChange={() => listboxItemState?.selectionManager.select(id)} 96 + /> 97 + ); 98 + } 99 + 76 100 export function ListBoxItem({ 77 101 style, 78 102 children, ··· 81 105 ...props 82 106 }: ListBoxItemProps) { 83 107 const listBoxItemStyles = useListBoxItemStyles(); 108 + const variant = use(ListboxVariantContext); 84 109 85 110 return ( 86 111 <AriaListBoxItem ··· 93 118 > 94 119 {({ isSelected }) => ( 95 120 <div {...stylex.props(listBoxItemStyles.inner)}> 121 + {variant === "checkbox" && ( 122 + <div {...stylex.props(listBoxItemStyles.addon)}> 123 + <ListBoxCheckbox 124 + isSelected={isSelected} 125 + id={props.id || (props.value as { id: string })?.id} 126 + onPress={(e) => e.continuePropagation()} 127 + /> 128 + </div> 129 + )} 96 130 {prefix != null && ( 97 131 <div {...stylex.props(listBoxItemStyles.addon)}>{prefix}</div> 98 132 )} ··· 100 134 {suffix != null && ( 101 135 <div {...stylex.props(listBoxItemStyles.addon)}>{suffix}</div> 102 136 )} 103 - {isSelected && ( 137 + {isSelected && variant === "default" && ( 104 138 <Check size={16} {...stylex.props(listBoxItemStyles.check)} /> 105 139 )} 106 140 </div>
+180 -18
apps/docs/src/showcases/todo-table.tsx
··· 10 10 ChevronsRight, 11 11 Circle, 12 12 MoreHorizontal, 13 - Plus, 13 + PlusCircle, 14 14 Settings2, 15 15 } from "lucide-react"; 16 16 import { useState } from "react"; 17 - import { ResizableTableContainer, Pressable } from "react-aria-components"; 17 + import { 18 + ResizableTableContainer, 19 + Pressable, 20 + Autocomplete, 21 + useFilter, 22 + } from "react-aria-components"; 18 23 19 24 import { Avatar } from "@/components/avatar"; 20 25 import { Badge } from "@/components/badge"; ··· 23 28 import { Flex } from "@/components/flex"; 24 29 import { IconButton } from "@/components/icon-button"; 25 30 import { Kbd } from "@/components/kbd"; 31 + import { ListBox, ListBoxItem } from "@/components/listbox"; 26 32 import { Menu, MenuItem, MenuSeparator } from "@/components/menu"; 33 + import { Popover } from "@/components/popover"; 27 34 import { SearchField } from "@/components/search-field"; 28 35 import { Select, SelectItem } from "@/components/select"; 36 + import { Separator } from "@/components/separator"; 29 37 import { 30 38 Table, 31 39 TableHeader, ··· 62 70 }, 63 71 emptyState: { 64 72 padding: spacing["8"], 73 + }, 74 + noPadding: { 75 + paddingBottom: 0, 76 + paddingLeft: 0, 77 + paddingRight: 0, 78 + paddingTop: 0, 79 + }, 80 + filterSection: { 81 + padding: spacing["2"], 65 82 }, 66 83 }); 67 84 ··· 276 293 277 294 const hideableColumns = ["status", "priority"]; 278 295 296 + function Filter<T extends string>({ 297 + title, 298 + selectedKeys, 299 + onSelectionChange, 300 + children, 301 + }: { 302 + title: string; 303 + selectedKeys: T[]; 304 + onSelectionChange: (keys: T[]) => void; 305 + children: React.ReactNode; 306 + }) { 307 + const { contains } = useFilter({ sensitivity: "base" }); 308 + 309 + return ( 310 + <Popover 311 + style={styles.noPadding} 312 + trigger={ 313 + <Button variant="outline"> 314 + <PlusCircle /> 315 + {title} 316 + {selectedKeys.length > 0 && ( 317 + <Badge size="sm" variant="default"> 318 + {selectedKeys.join(" / ")} 319 + </Badge> 320 + )} 321 + </Button> 322 + } 323 + > 324 + <Autocomplete filter={contains}> 325 + <Flex direction="column" gap="1" style={styles.filterSection}> 326 + <SearchField placeholder={`Search ${title}...`} /> 327 + </Flex> 328 + <Separator /> 329 + <ListBox 330 + style={styles.filterSection} 331 + variant="checkbox" 332 + selectionMode="multiple" 333 + selectedKeys={selectedKeys} 334 + onSelectionChange={(keys) => { 335 + if (keys === "all") { 336 + // 337 + } else { 338 + onSelectionChange([...keys] as T[]); 339 + } 340 + }} 341 + > 342 + {children} 343 + </ListBox> 344 + {selectedKeys.length > 0 && ( 345 + <> 346 + <Separator /> 347 + <Flex direction="column" style={styles.filterSection}> 348 + <Button variant="outline" onClick={() => onSelectionChange([])}> 349 + Clear 350 + </Button> 351 + </Flex> 352 + </> 353 + )} 354 + </Autocomplete> 355 + </Popover> 356 + ); 357 + } 358 + 279 359 export function TodoTable() { 280 360 const [search, setSearch] = useState(""); 281 361 const [rowsPerPage, setRowsPerPage] = useState(10); 282 362 const [selectedRows, setSelectedRows] = useState<Task[]>([]); 283 363 const [page, setPage] = useState(1); 364 + const [priorityFilter, setPriorityFilter] = useState<Task["priority"][]>([]); 365 + const [statusFilter, setStatusFilter] = useState<Task["status"][]>([]); 284 366 const currentPageTasks = tasks 285 367 .filter((task) => task.title.toLowerCase().includes(search.toLowerCase())) 368 + .filter( 369 + (task) => 370 + priorityFilter.includes(task.priority) || 371 + statusFilter.includes(task.status) || 372 + (priorityFilter.length === 0 && statusFilter.length === 0), 373 + ) 286 374 .slice((page - 1) * rowsPerPage, page * rowsPerPage); 287 375 const [visibleColumns, setVisibleColumns] = 288 376 useState<string[]>(hideableColumns); ··· 332 420 </Flex> 333 421 334 422 <Flex justify="between" align="center" wrap gap="3"> 335 - <div style={{ minWidth: "400px" }}> 336 - <SearchField 337 - placeholder="Filter tasks..." 338 - value={search} 339 - onChange={setSearch} 340 - /> 341 - </div> 342 - 343 423 <Flex gap="2"> 344 - <Button variant="secondary" size="sm"> 345 - <Plus size={16} /> 346 - Status 347 - </Button> 348 - <Button variant="secondary" size="sm"> 349 - <Plus size={16} /> 350 - Priority 351 - </Button> 424 + <div style={{ minWidth: "400px" }}> 425 + <SearchField 426 + placeholder="Filter tasks..." 427 + value={search} 428 + onChange={setSearch} 429 + /> 430 + </div> 431 + <Filter 432 + title="Status" 433 + selectedKeys={statusFilter} 434 + onSelectionChange={setStatusFilter} 435 + > 436 + <ListBoxItem 437 + id="Todo" 438 + suffix={ 439 + <Badge size="sm" variant="default"> 440 + {tasks.filter((task) => task.status === "Todo").length} 441 + </Badge> 442 + } 443 + > 444 + Todo 445 + </ListBoxItem> 446 + <ListBoxItem 447 + id="In Progress" 448 + suffix={ 449 + <Badge size="sm" variant="default"> 450 + {tasks.filter((task) => task.status === "In Progress").length} 451 + </Badge> 452 + } 453 + > 454 + In Progress 455 + </ListBoxItem> 456 + <ListBoxItem 457 + id="Done" 458 + suffix={ 459 + <Badge size="sm" variant="default"> 460 + {tasks.filter((task) => task.status === "Done").length} 461 + </Badge> 462 + } 463 + > 464 + Done 465 + </ListBoxItem> 466 + <ListBoxItem id="Backlog">Backlog</ListBoxItem> 467 + <ListBoxItem 468 + id="Canceled" 469 + suffix={ 470 + <Badge size="sm" variant="default"> 471 + {tasks.filter((task) => task.status === "Canceled").length} 472 + </Badge> 473 + } 474 + > 475 + Canceled 476 + </ListBoxItem> 477 + </Filter> 478 + <Filter 479 + title="Priority" 480 + selectedKeys={priorityFilter} 481 + onSelectionChange={setPriorityFilter} 482 + > 483 + <ListBoxItem 484 + id="High" 485 + suffix={ 486 + <Badge size="sm" variant="default"> 487 + {tasks.filter((task) => task.priority === "High").length} 488 + </Badge> 489 + } 490 + > 491 + High 492 + </ListBoxItem> 493 + <ListBoxItem 494 + id="Medium" 495 + suffix={ 496 + <Badge size="sm" variant="default"> 497 + {tasks.filter((task) => task.priority === "Medium").length} 498 + </Badge> 499 + } 500 + > 501 + Medium 502 + </ListBoxItem> 503 + <ListBoxItem 504 + id="Low" 505 + suffix={ 506 + <Badge size="sm" variant="default"> 507 + {tasks.filter((task) => task.priority === "Low").length} 508 + </Badge> 509 + } 510 + > 511 + Low 512 + </ListBoxItem> 513 + </Filter> 352 514 </Flex> 353 515 354 516 <Flex gap="2">
+39 -5
packages/hip-ui/src/components/listbox/index.tsx
··· 1 1 import * as stylex from "@stylexjs/stylex"; 2 2 import { Check } from "lucide-react"; 3 - import { use } from "react"; 3 + import { createContext, use } from "react"; 4 4 import { 5 5 ListBoxProps as AriaListBoxProps, 6 6 ListBoxItem as AriaListBoxItem, ··· 10 10 ListBoxItemProps as AriaListBoxItemProps, 11 11 Header, 12 12 SeparatorProps, 13 + ListStateContext, 13 14 } from "react-aria-components"; 14 15 16 + import { Checkbox, CheckboxProps } from "../checkbox"; 15 17 import { SizeContext } from "../context"; 16 18 import { Separator } from "../separator"; 17 19 import { ui } from "../theme/semantic-color.stylex"; ··· 45 47 }, 46 48 }); 47 49 50 + type ListBoxVariant = "default" | "checkbox"; 51 + 52 + const ListboxVariantContext = createContext<ListBoxVariant>("default"); 53 + 48 54 export interface ListBoxProps<T extends object> 49 55 extends StyleXComponentProps<AriaListBoxProps<T>> { 50 56 size?: Size; 51 57 items?: Iterable<T>; 52 58 children: React.ReactNode | ((item: T) => React.ReactNode); 59 + variant?: ListBoxVariant; 53 60 } 54 61 55 62 export function ListBox<T extends object>({ 56 63 size: sizeProp, 57 64 style, 65 + variant = "default", 58 66 ...props 59 67 }: ListBoxProps<T>) { 60 68 const size = sizeProp || use(SizeContext); 61 69 62 70 return ( 63 - <SizeContext value={size}> 64 - <AriaListBox {...stylex.props(styles.listBox, style)} {...props} /> 65 - </SizeContext> 71 + <ListboxVariantContext value={variant}> 72 + <SizeContext value={size}> 73 + <AriaListBox {...stylex.props(styles.listBox, style)} {...props} /> 74 + </SizeContext> 75 + </ListboxVariantContext> 66 76 ); 67 77 } 68 78 ··· 73 83 suffix?: React.ReactNode; 74 84 } 75 85 86 + function ListBoxCheckbox({ id = "", ...props }: CheckboxProps) { 87 + const listboxItemState = use(ListStateContext); 88 + 89 + return ( 90 + <Checkbox 91 + {...props} 92 + isSelected={Boolean( 93 + listboxItemState?.selectionManager.selectedKeys.has(id), 94 + )} 95 + onChange={() => listboxItemState?.selectionManager.select(id)} 96 + /> 97 + ); 98 + } 99 + 76 100 export function ListBoxItem({ 77 101 style, 78 102 children, ··· 81 105 ...props 82 106 }: ListBoxItemProps) { 83 107 const listBoxItemStyles = useListBoxItemStyles(); 108 + const variant = use(ListboxVariantContext); 84 109 85 110 return ( 86 111 <AriaListBoxItem ··· 93 118 > 94 119 {({ isSelected }) => ( 95 120 <div {...stylex.props(listBoxItemStyles.inner)}> 121 + {variant === "checkbox" && ( 122 + <div {...stylex.props(listBoxItemStyles.addon)}> 123 + <ListBoxCheckbox 124 + isSelected={isSelected} 125 + id={props.id || (props.value as { id: string })?.id} 126 + onPress={(e) => e.continuePropagation()} 127 + /> 128 + </div> 129 + )} 96 130 {prefix != null && ( 97 131 <div {...stylex.props(listBoxItemStyles.addon)}>{prefix}</div> 98 132 )} ··· 100 134 {suffix != null && ( 101 135 <div {...stylex.props(listBoxItemStyles.addon)}>{suffix}</div> 102 136 )} 103 - {isSelected && ( 137 + {isSelected && variant === "default" && ( 104 138 <Check size={16} {...stylex.props(listBoxItemStyles.check)} /> 105 139 )} 106 140 </div>