@purpurds/listbox 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ ._purpur-listbox_qhoi7_1{padding:0;margin:0;list-style-type:none;border:var(--purpur-border-width-xs) solid var(--purpur-color-border-interactive-subtle);border-radius:var(--purpur-border-radius-sm);color:var(--purpur-color-brand-off-black);width:100%;background-color:var(--purpur-color-brand-white);max-height:calc(2 * var(--purpur-spacing-1200));overflow-y:scroll;box-sizing:border-box}._purpur-listbox-item_qhoi7_15{list-style:none;padding:var(--purpur-spacing-150);border:var(--purpur-border-width-xs) solid transparent;cursor:pointer;max-width:100%;word-break:break-word;transition:background var(--purpur-motion-duration-150) ease;display:flex;justify-content:space-between;align-items:center;gap:var(--purpur-spacing-100)}@media (hover: hover) and (pointer: fine){._purpur-listbox-item--hovered_qhoi7_29{background:var(--purpur-color-background-interactive-transparent-hover)}}._purpur-listbox-item--highlighted_qhoi7_37{outline:var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);outline-offset:calc(-1 * var(--purpur-border-width-sm))}._purpur-listbox-item_qhoi7_15:active:not(._purpur-listbox-item--noninteractive_qhoi7_41){background:var(--purpur-color-background-interactive-transparent-active)}._purpur-listbox-item--disabled_qhoi7_44{color:var(--purpur-color-text-weak);cursor:default}._purpur-listbox-item--noninteractive_qhoi7_41{cursor:default}._purpur-listbox-item__icon_qhoi7_51{color:var(--purpur-color-text-interactive-selected)}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@purpurds/listbox",
3
+ "version": "0.0.1",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/listbox.cjs.js",
6
+ "types": "./dist/listbox.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/listbox.cjs.js",
10
+ "types": "./dist/listbox.d.ts",
11
+ "default": "./dist/listbox.es.js"
12
+ },
13
+ "./styles": "./dist/styles.css"
14
+ },
15
+ "source": "src/listbox.tsx",
16
+ "dependencies": {
17
+ "classnames": "~2.5.0",
18
+ "@purpurds/icon": "5.14.0",
19
+ "@purpurds/paragraph": "5.14.0",
20
+ "@purpurds/tokens": "5.14.0"
21
+ },
22
+ "devDependencies": {
23
+ "@rushstack/eslint-patch": "~1.10.0",
24
+ "@storybook/blocks": "^8.2.6",
25
+ "@storybook/react": "^8.2.6",
26
+ "@telia/base-rig": "~8.2.0",
27
+ "@telia/react-rig": "~3.2.0",
28
+ "@testing-library/dom": "~9.3.3",
29
+ "@testing-library/jest-dom": "~6.4.0",
30
+ "@testing-library/react": "~14.3.0",
31
+ "@types/node": "20.12.12",
32
+ "@types/react-dom": "^18.3.0",
33
+ "@types/react": "^18.3.3",
34
+ "eslint-plugin-testing-library": "~6.2.0",
35
+ "eslint": "^8.57.0",
36
+ "jsdom": "~22.1.0",
37
+ "lint-staged": "~10.5.3",
38
+ "prettier": "~2.8.8",
39
+ "react-dom": "^18.3.1",
40
+ "react": "^18.3.1",
41
+ "storybook": "^8.2.6",
42
+ "typescript": "^5.5.4",
43
+ "vite": "5.3.4",
44
+ "vitest": "~1.5.0",
45
+ "@purpurds/component-rig": "1.0.0"
46
+ },
47
+ "scripts": {
48
+ "build:dev": "vite",
49
+ "build:watch": "vite build --watch",
50
+ "build": "vite build",
51
+ "ci:build": "rushx build",
52
+ "coverage": "vitest run --coverage",
53
+ "lint:fix": "eslint . --fix",
54
+ "lint": "lint-staged --no-stash 2>&1",
55
+ "sbdev": "rush sbdev",
56
+ "test:unit": "vitest run --passWithNoTests",
57
+ "test:watch": "vitest --watch",
58
+ "test": "rushx test:unit",
59
+ "typecheck": "tsc -p ./tsconfig.json"
60
+ }
61
+ }
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
@@ -0,0 +1,70 @@
1
+ import type {
2
+ ComponentPropsWithRef,
3
+ ForwardedRef,
4
+ ReactElement,
5
+ ReactNode,
6
+ ReactPortal,
7
+ } from "react";
8
+ import React, { forwardRef, isValidElement } from "react";
9
+ import { IconCheckmark } from "@purpurds/icon";
10
+ import { Paragraph } from "@purpurds/paragraph";
11
+ import c from "classnames/bind";
12
+
13
+ import styles from "./listbox.module.scss";
14
+
15
+ const cx = c.bind(styles);
16
+
17
+ const rootClassName = "purpur-listbox";
18
+
19
+ export type ListboxItemProps = Omit<ComponentPropsWithRef<"li">, "role"> & {
20
+ "data-testid"?: string;
21
+ highlighted?: boolean;
22
+ hovered?: boolean;
23
+ key?: string;
24
+ selected?: boolean;
25
+ disabled?: boolean;
26
+ noninteractive?: boolean;
27
+ };
28
+
29
+ export const ListboxItem = forwardRef(
30
+ (props: ListboxItemProps, ref: ForwardedRef<HTMLLIElement>) => {
31
+ const { disabled, highlighted, hovered, selected, children, noninteractive, ...liProps } =
32
+ props;
33
+ const className = cx(`${rootClassName}-item`, liProps.className, {
34
+ [`${rootClassName}-item--highlighted`]: highlighted,
35
+ [`${rootClassName}-item--selected`]: selected,
36
+ [`${rootClassName}-item--hovered`]: hovered,
37
+ [`${rootClassName}-item--disabled`]: disabled,
38
+ [`${rootClassName}-item--noninteractive`]: noninteractive,
39
+ });
40
+
41
+ return (
42
+ <li
43
+ {...liProps}
44
+ ref={ref}
45
+ className={className}
46
+ aria-selected={!!selected}
47
+ role="option"
48
+ aria-disabled={!!disabled}
49
+ >
50
+ {typeof children === "string" ? <Paragraph>{children}</Paragraph> : children}
51
+ {selected && <IconCheckmark size="xs" className={cx(`${rootClassName}-item__icon`)} />}
52
+ </li>
53
+ );
54
+ }
55
+ );
56
+
57
+ export const isListboxItem = (
58
+ child:
59
+ | ReactElement
60
+ | Iterable<ReactNode>
61
+ | ReactPortal
62
+ | string
63
+ | number
64
+ | boolean
65
+ | null
66
+ | undefined
67
+ ): child is ReactElement<ListboxItemProps> =>
68
+ isValidElement<ListboxItemProps>(child) && child?.type === ListboxItem;
69
+
70
+ ListboxItem.displayName = "ListBoxItem";
@@ -0,0 +1,60 @@
1
+ .purpur-listbox {
2
+ padding: 0;
3
+ margin: 0;
4
+ width: 100%;
5
+ list-style-type: none;
6
+ border: var(--purpur-border-width-xs) solid var(--purpur-color-border-interactive-subtle);
7
+ border-radius: var(--purpur-border-radius-sm);
8
+ color: var(--purpur-color-brand-off-black);
9
+ width: 100%;
10
+ background-color: var(--purpur-color-brand-white);
11
+ max-height: calc(2 * var(--purpur-spacing-1200));
12
+ overflow-y: scroll;
13
+ box-sizing: border-box;
14
+
15
+ &-item {
16
+ $listboxItemRoot: &;
17
+
18
+ cursor: pointer;
19
+ list-style: none;
20
+ padding: var(--purpur-spacing-150);
21
+ border: var(--purpur-border-width-xs) solid transparent;
22
+ cursor: pointer;
23
+ max-width: 100%;
24
+ word-break: break-word;
25
+ transition: background var(--purpur-motion-duration-150) ease;
26
+ display: flex;
27
+ justify-content: space-between;
28
+ align-items: center;
29
+ gap: var(--purpur-spacing-100);
30
+
31
+ &--hovered {
32
+ /* Enable only on non-touch devices */
33
+ @media (hover: hover) and (pointer: fine) {
34
+ background: var(--purpur-color-background-interactive-transparent-hover);
35
+ }
36
+ }
37
+
38
+ &--highlighted {
39
+ outline: var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);
40
+ outline-offset: calc(-1 * var(--purpur-border-width-sm));
41
+ }
42
+
43
+ &:active:not(#{$listboxItemRoot}--noninteractive) {
44
+ background: var(--purpur-color-background-interactive-transparent-active);
45
+ }
46
+
47
+ &--disabled {
48
+ color: var(--purpur-color-text-weak);
49
+ cursor: default;
50
+ }
51
+
52
+ &--noninteractive {
53
+ cursor: default;
54
+ }
55
+
56
+ &__icon {
57
+ color: var(--purpur-color-text-interactive-selected);
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+
4
+ import { Listbox } from "./listbox";
5
+
6
+ const meta = {
7
+ title: "Components/Listbox",
8
+ component: Listbox,
9
+ subcomponents: {
10
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
11
+ //@ts-ignore
12
+ "Listbox.Item": Listbox.Item,
13
+ },
14
+ parameters: {
15
+ design: [
16
+ {
17
+ name: "Listbox",
18
+ type: "figma",
19
+ url: "https://www.figma.com/design/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?node-id=47667-13626",
20
+ },
21
+ ],
22
+ },
23
+ argTypes: {
24
+ ["aria-label"]: {
25
+ control: { type: "text" },
26
+ table: { type: { summary: "string" } },
27
+ },
28
+ ["aria-expanded"]: { control: { type: "boolean" }, table: { type: { summary: "boolean" } } },
29
+ },
30
+ } satisfies Meta<typeof Listbox>;
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof Listbox>;
34
+
35
+ export const Showcase: Story = {
36
+ args: {
37
+ children: ["orange", "mocka", "frappuccino"],
38
+ },
39
+ render: ({ children, ...args }) => (
40
+ <Listbox {...args} style={{ width: "200px" }}>
41
+ {children instanceof Array &&
42
+ children?.map((child: string) => <Listbox.Item key={child}>{child}</Listbox.Item>)}
43
+ </Listbox>
44
+ ),
45
+ };
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { render, screen } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import { Listbox } from "./listbox";
7
+
8
+ expect.extend(matchers);
9
+
10
+ describe("Listbox", () => {
11
+ it("should have tests", () => {
12
+ render(
13
+ <Listbox aria-expanded={false} aria-label="order">
14
+ Some text content
15
+ </Listbox>
16
+ );
17
+ expect(screen.getByRole("listbox")).toBeVisible();
18
+ });
19
+ });
@@ -0,0 +1,44 @@
1
+ import type {
2
+ ComponentPropsWithRef,
3
+ ForwardedRef,
4
+ ForwardRefExoticComponent,
5
+ PropsWithoutRef,
6
+ } from "react";
7
+ import React, { Children, forwardRef } from "react";
8
+ import c from "classnames/bind";
9
+
10
+ import styles from "./listbox.module.scss";
11
+ import { isListboxItem, ListboxItem } from "./listbox-item";
12
+
13
+ const cx = c.bind(styles);
14
+
15
+ const rootClassName = "purpur-listbox";
16
+
17
+ export type ListboxProps = Omit<ComponentPropsWithRef<"ul">, "role"> & {
18
+ "data-testid"?: string;
19
+ "aria-label": NonNullable<ComponentPropsWithRef<"ul">["aria-label"]>;
20
+ "aria-expanded": NonNullable<ComponentPropsWithRef<"ul">["aria-expanded"]>;
21
+ };
22
+
23
+ type ListboxStatic = {
24
+ Item: typeof ListboxItem;
25
+ };
26
+
27
+ type ListboxComponent = ForwardRefExoticComponent<PropsWithoutRef<ListboxProps>> & ListboxStatic;
28
+
29
+ export const Listbox = forwardRef(
30
+ ({ children, ...listboxProps }: ListboxProps, ref: ForwardedRef<HTMLUListElement>) => (
31
+ <ul
32
+ {...listboxProps}
33
+ ref={ref}
34
+ className={cx(rootClassName, listboxProps.className)}
35
+ role="listbox"
36
+ >
37
+ {Children.toArray(children).filter(isListboxItem)}
38
+ </ul>
39
+ )
40
+ ) as ListboxComponent;
41
+
42
+ export type { ListboxItemProps } from "./listbox-item";
43
+ Listbox.Item = ListboxItem;
44
+ Listbox.displayName = "ListBox";