@kinetixui/ui 0.17.0 → 0.18.0
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.
- package/dist/index.js +377 -237
- package/package.json +2 -2
- package/src/components/audio-player.tsx +2 -2
- package/src/components/badge.tsx +1 -1
- package/src/components/banner.tsx +1 -1
- package/src/components/button.tsx +8 -8
- package/src/components/calendar.tsx +1 -1
- package/src/components/chart.tsx +2 -2
- package/src/components/checkbox.tsx +2 -2
- package/src/components/circular-progress.tsx +1 -1
- package/src/components/code-block.tsx +1 -1
- package/src/components/color-picker.tsx +4 -0
- package/src/components/data-grid.tsx +169 -25
- package/src/components/diff-viewer.tsx +9 -8
- package/src/components/fab.tsx +1 -1
- package/src/components/file-upload.tsx +13 -15
- package/src/components/input-group.tsx +1 -1
- package/src/components/input-otp.tsx +2 -1
- package/src/components/input.tsx +2 -2
- package/src/components/json-viewer.tsx +12 -10
- package/src/components/list.tsx +41 -27
- package/src/components/markdown-editor.tsx +3 -2
- package/src/components/message-bubble.tsx +3 -3
- package/src/components/multi-select.tsx +14 -3
- package/src/components/native-select.tsx +2 -2
- package/src/components/notification-center.tsx +2 -2
- package/src/components/progress.tsx +1 -1
- package/src/components/radio-group.tsx +3 -3
- package/src/components/scroll-area.tsx +1 -1
- package/src/components/select.tsx +2 -2
- package/src/components/skeleton.tsx +1 -1
- package/src/components/slider.tsx +2 -2
- package/src/components/sonner.tsx +1 -1
- package/src/components/spinner.tsx +3 -3
- package/src/components/stepper.tsx +3 -3
- package/src/components/switch.tsx +1 -1
- package/src/components/tab-bar.tsx +1 -1
- package/src/components/table-of-contents.tsx +1 -1
- package/src/components/textarea.tsx +2 -2
- package/src/components/timeline.tsx +1 -1
- package/src/components/tooltip.tsx +1 -1
- package/src/components/virtual-list.tsx +2 -1
- package/src/components-a11y.test.tsx +4 -25
- package/src/components-behavior.test.tsx +1 -1
- package/src/components-keyboard.test.tsx +236 -33
- package/src/stories/AppBar.stories.tsx +1 -1
- package/src/stories/Checkbox.stories.tsx +1 -1
- package/src/stories/CircularProgress.stories.tsx +4 -4
- package/src/stories/InputOTP.stories.tsx +2 -2
- package/src/stories/List.stories.tsx +2 -2
- package/src/stories/MarkdownEditor.stories.tsx +1 -1
- package/src/stories/MultiSelect.stories.tsx +1 -0
- package/src/stories/NativeSelect.stories.tsx +1 -1
- package/src/stories/NumberInput.stories.tsx +3 -3
- package/src/stories/Progress.stories.tsx +3 -3
- package/src/stories/Select.stories.tsx +2 -2
- package/src/stories/Slider.stories.tsx +3 -3
- package/src/stories/Switch.stories.tsx +1 -1
- package/src/stories/ToggleGroup.stories.tsx +6 -6
- package/tailwind.config.ts +14 -0
|
@@ -4,6 +4,8 @@ import userEvent from "@testing-library/user-event";
|
|
|
4
4
|
import { describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { Checkbox } from "./components/checkbox";
|
|
6
6
|
import { ColorPicker } from "./components/color-picker";
|
|
7
|
+
import { FileUpload } from "./components/file-upload";
|
|
8
|
+
import { List, ListItem } from "./components/list";
|
|
7
9
|
import { DataGrid, type DataGridColumn } from "./components/data-grid";
|
|
8
10
|
import { KinetixDirectionProvider } from "./components/direction-provider";
|
|
9
11
|
import {
|
|
@@ -432,84 +434,225 @@ describe("DataGrid", () => {
|
|
|
432
434
|
{ id: 2, name: "Abe" },
|
|
433
435
|
{ id: 3, name: "Bea" },
|
|
434
436
|
];
|
|
435
|
-
function setup(onCellEdit
|
|
437
|
+
function setup(opts: { onCellEdit?: ReturnType<typeof vi.fn>; data?: Row[]; wrapper?: (n: React.ReactNode) => React.ReactNode } = {}) {
|
|
438
|
+
const onCellEdit = opts.onCellEdit ?? vi.fn();
|
|
436
439
|
const columns: DataGridColumn<Row>[] = [
|
|
437
440
|
{ id: "name", header: "Name", cell: (r) => r.name, value: (r) => r.name, sortable: true, editable: true, onCellEdit },
|
|
438
|
-
{ id: "id", header: "ID", cell: (r) => r.id, value: (r) => r.id },
|
|
441
|
+
{ id: "id", header: "ID", cell: (r) => r.id, value: (r) => r.id, minWidth: 60, width: 100 },
|
|
439
442
|
];
|
|
440
|
-
|
|
443
|
+
const grid = <DataGrid columns={columns} data={opts.data ?? rows} height={200} getRowId={(r) => r.id} />;
|
|
444
|
+
render(
|
|
445
|
+
<>
|
|
446
|
+
{opts.wrapper ? opts.wrapper(grid) : grid}
|
|
447
|
+
<button>after</button>
|
|
448
|
+
</>,
|
|
449
|
+
);
|
|
441
450
|
return onCellEdit;
|
|
442
451
|
}
|
|
452
|
+
const header = (name: string) => screen.getByRole("columnheader", { name });
|
|
453
|
+
const cell = (r: number, c: number) => screen.getAllByRole("row")[r + 1]!.querySelectorAll<HTMLElement>('[role="gridcell"]')[c]!;
|
|
443
454
|
const names = () => screen.getAllByRole("gridcell").filter((c) => /^(Cara|Abe|Bea)$/.test(c.textContent ?? "")).map((c) => c.textContent);
|
|
444
455
|
|
|
445
|
-
it("
|
|
456
|
+
it("is a single tab stop: Tab enters on the first header, the next Tab leaves the grid", async () => {
|
|
446
457
|
const user = userEvent.setup();
|
|
447
458
|
setup();
|
|
448
|
-
const header = screen.getByRole("columnheader", { name: "Name" });
|
|
449
|
-
expect(header).toHaveAttribute("aria-sort", "none");
|
|
450
459
|
await user.tab();
|
|
451
|
-
expect(header).toHaveFocus();
|
|
460
|
+
expect(header("Name")).toHaveFocus();
|
|
461
|
+
expect(header("ID")).toHaveAttribute("tabindex", "-1");
|
|
462
|
+
expect(cell(0, 0)).toHaveAttribute("tabindex", "-1");
|
|
463
|
+
await user.tab();
|
|
464
|
+
expect(screen.getByRole("button", { name: "after" })).toHaveFocus();
|
|
465
|
+
});
|
|
452
466
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
expect(
|
|
458
|
-
expect(
|
|
467
|
+
it("exposes grid dimensions and row / column indexes (rows are virtualized, so they must be explicit)", () => {
|
|
468
|
+
setup();
|
|
469
|
+
const grid = screen.getByRole("grid");
|
|
470
|
+
expect(grid).toHaveAttribute("aria-rowcount", "4"); // header + 3
|
|
471
|
+
expect(grid).toHaveAttribute("aria-colcount", "2");
|
|
472
|
+
expect(screen.getAllByRole("row")[0]).toHaveAttribute("aria-rowindex", "1");
|
|
473
|
+
expect(screen.getAllByRole("row")[3]).toHaveAttribute("aria-rowindex", "4");
|
|
474
|
+
expect(cell(0, 1)).toHaveAttribute("aria-colindex", "2");
|
|
459
475
|
});
|
|
460
476
|
|
|
461
|
-
it("
|
|
477
|
+
it("moves between cells with the arrow keys, header row included, and stops at the edges", async () => {
|
|
462
478
|
const user = userEvent.setup();
|
|
463
479
|
setup();
|
|
464
|
-
expect(screen.getByRole("columnheader", { name: "ID" })).not.toHaveAttribute("tabindex");
|
|
465
480
|
await user.tab(); // Name header
|
|
466
|
-
await user.
|
|
467
|
-
expect(
|
|
481
|
+
await user.keyboard("{ArrowRight}");
|
|
482
|
+
expect(header("ID")).toHaveFocus();
|
|
483
|
+
await user.keyboard("{ArrowRight}"); // last column: stays
|
|
484
|
+
expect(header("ID")).toHaveFocus();
|
|
485
|
+
await user.keyboard("{ArrowDown}");
|
|
486
|
+
expect(cell(0, 1)).toHaveFocus();
|
|
487
|
+
await user.keyboard("{ArrowLeft}");
|
|
488
|
+
expect(cell(0, 0)).toHaveFocus();
|
|
489
|
+
await user.keyboard("{ArrowDown}{ArrowDown}{ArrowDown}"); // past the last row: stays
|
|
490
|
+
expect(cell(2, 0)).toHaveFocus();
|
|
491
|
+
await user.keyboard("{ArrowUp}{ArrowUp}{ArrowUp}");
|
|
492
|
+
expect(header("Name")).toHaveFocus();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("Home / End go to the row ends; Ctrl+Home / Ctrl+End to the grid corners", async () => {
|
|
496
|
+
const user = userEvent.setup();
|
|
497
|
+
setup();
|
|
498
|
+
await user.tab();
|
|
499
|
+
await user.keyboard("{ArrowDown}{ArrowDown}"); // row 1, Name
|
|
500
|
+
await user.keyboard("{End}");
|
|
501
|
+
expect(cell(1, 1)).toHaveFocus();
|
|
502
|
+
await user.keyboard("{Home}");
|
|
503
|
+
expect(cell(1, 0)).toHaveFocus();
|
|
504
|
+
await user.keyboard("{Control>}{End}{/Control}");
|
|
505
|
+
expect(cell(2, 1)).toHaveFocus();
|
|
506
|
+
await user.keyboard("{Control>}{Home}{/Control}");
|
|
507
|
+
expect(header("Name")).toHaveFocus();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("remembers the active cell: leaving and re-entering the grid returns to it", async () => {
|
|
511
|
+
const user = userEvent.setup();
|
|
512
|
+
setup();
|
|
513
|
+
await user.tab();
|
|
514
|
+
await user.keyboard("{ArrowDown}{ArrowRight}");
|
|
515
|
+
expect(cell(0, 1)).toHaveFocus();
|
|
516
|
+
await user.tab(); // out
|
|
517
|
+
expect(screen.getByRole("button", { name: "after" })).toHaveFocus();
|
|
518
|
+
await user.tab({ shift: true }); // back in
|
|
519
|
+
expect(cell(0, 1)).toHaveFocus();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("sorts from the keyboard: Enter / Space on a sortable header", async () => {
|
|
523
|
+
const user = userEvent.setup();
|
|
524
|
+
setup();
|
|
525
|
+
await user.tab();
|
|
526
|
+
expect(header("Name")).toHaveAttribute("aria-sort", "none");
|
|
527
|
+
await user.keyboard("{Enter}");
|
|
528
|
+
expect(header("Name")).toHaveAttribute("aria-sort", "ascending");
|
|
529
|
+
expect(names()).toEqual(["Abe", "Bea", "Cara"]);
|
|
530
|
+
await user.keyboard(" ");
|
|
531
|
+
expect(header("Name")).toHaveAttribute("aria-sort", "descending");
|
|
532
|
+
expect(names()).toEqual(["Cara", "Bea", "Abe"]);
|
|
468
533
|
});
|
|
469
534
|
|
|
470
535
|
it("edits a cell from the keyboard: Enter to edit, Enter to commit, focus returns to the cell", async () => {
|
|
471
536
|
const user = userEvent.setup();
|
|
472
537
|
const onCellEdit = setup();
|
|
473
|
-
await user.tab();
|
|
474
|
-
await user.
|
|
475
|
-
const
|
|
476
|
-
expect(
|
|
538
|
+
await user.tab();
|
|
539
|
+
await user.keyboard("{ArrowDown}");
|
|
540
|
+
const target = cell(0, 0);
|
|
541
|
+
expect(target).toHaveFocus();
|
|
477
542
|
|
|
478
543
|
await user.keyboard("{Enter}");
|
|
479
|
-
const input = within(
|
|
544
|
+
const input = within(target).getByRole("textbox");
|
|
480
545
|
expect(input).toHaveFocus();
|
|
481
546
|
await user.clear(input);
|
|
482
547
|
await user.type(input, "Zed{Enter}");
|
|
483
548
|
|
|
484
549
|
expect(onCellEdit).toHaveBeenCalledTimes(1);
|
|
485
550
|
expect(onCellEdit).toHaveBeenCalledWith(rows[0], 0, "Zed");
|
|
486
|
-
await waitFor(() => expect(within(
|
|
487
|
-
expect(
|
|
551
|
+
await waitFor(() => expect(within(target).queryByRole("textbox")).not.toBeInTheDocument());
|
|
552
|
+
expect(target).toHaveFocus();
|
|
553
|
+
await user.keyboard("{ArrowDown}"); // navigation still works after an edit
|
|
554
|
+
expect(cell(1, 0)).toHaveFocus();
|
|
488
555
|
});
|
|
489
556
|
|
|
490
557
|
it("Escape cancels an edit without committing and returns focus to the cell", async () => {
|
|
491
558
|
const user = userEvent.setup();
|
|
492
559
|
const onCellEdit = setup();
|
|
493
560
|
await user.tab();
|
|
494
|
-
await user.
|
|
495
|
-
const
|
|
561
|
+
await user.keyboard("{ArrowDown}");
|
|
562
|
+
const target = cell(0, 0);
|
|
496
563
|
await user.keyboard("{F2}");
|
|
497
|
-
await user.type(within(
|
|
564
|
+
await user.type(within(target).getByRole("textbox"), "x{Escape}");
|
|
498
565
|
|
|
499
566
|
expect(onCellEdit).not.toHaveBeenCalled();
|
|
500
|
-
expect(
|
|
567
|
+
expect(target).toHaveFocus();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("Alt+Arrow reorders a column from its header, keeps focus on it, and announces the move", async () => {
|
|
571
|
+
const user = userEvent.setup();
|
|
572
|
+
setup();
|
|
573
|
+
await user.tab(); // Name header
|
|
574
|
+
const headersBefore = screen.getAllByRole("columnheader").map((h) => h.textContent);
|
|
575
|
+
expect(headersBefore).toEqual(["Name", "ID"]);
|
|
576
|
+
|
|
577
|
+
await user.keyboard("{Alt>}{ArrowRight}{/Alt}");
|
|
578
|
+
await waitFor(() => expect(screen.getAllByRole("columnheader").map((h) => h.textContent)).toEqual(["ID", "Name"]));
|
|
579
|
+
expect(header("Name")).toHaveFocus();
|
|
580
|
+
expect(screen.getByRole("status")).toHaveTextContent("Name moved to position 2 of 2");
|
|
581
|
+
|
|
582
|
+
await user.keyboard("{Alt>}{ArrowRight}{/Alt}"); // already last
|
|
583
|
+
expect(screen.getByRole("status")).toHaveTextContent("already the last movable column");
|
|
501
584
|
});
|
|
502
585
|
|
|
503
|
-
|
|
504
|
-
// plus keyboard equivalents for column reorder and resize. Not implemented yet; `it.fails` turns
|
|
505
|
-
// this into an error the day it starts passing, so the gap can't be forgotten or silently closed.
|
|
506
|
-
it.fails("moves between cells with the arrow keys", async () => {
|
|
586
|
+
it("Shift+Arrow resizes a column from its header and announces the width", async () => {
|
|
507
587
|
const user = userEvent.setup();
|
|
508
588
|
setup();
|
|
509
589
|
await user.tab();
|
|
590
|
+
await user.keyboard("{ArrowRight}"); // ID header, width 100, min 60
|
|
591
|
+
expect(header("ID")).toHaveStyle({ width: "100px" });
|
|
592
|
+
await user.keyboard("{Shift>}{ArrowRight}{/Shift}");
|
|
593
|
+
expect(header("ID")).toHaveStyle({ width: "110px" });
|
|
594
|
+
expect(screen.getByRole("status")).toHaveTextContent("ID column width 110 pixels");
|
|
595
|
+
await user.keyboard("{Shift>}{ArrowLeft}{ArrowLeft}{ArrowLeft}{ArrowLeft}{ArrowLeft}{/Shift}");
|
|
596
|
+
expect(header("ID")).toHaveStyle({ width: "60px" }); // clamped to minWidth
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("advertises the header chords with aria-keyshortcuts", () => {
|
|
600
|
+
setup();
|
|
601
|
+
expect(header("Name")).toHaveAttribute("aria-keyshortcuts", expect.stringContaining("Alt+ArrowLeft"));
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("mirrors the arrow keys under RTL", async () => {
|
|
605
|
+
const user = userEvent.setup();
|
|
606
|
+
setup({ wrapper: (n) => <div dir="rtl" style={{ direction: "rtl" }}>{n}</div> });
|
|
510
607
|
await user.tab();
|
|
608
|
+
expect(header("Name")).toHaveFocus();
|
|
609
|
+
await user.keyboard("{ArrowLeft}"); // in RTL the next column is to the left
|
|
610
|
+
expect(header("ID")).toHaveFocus();
|
|
511
611
|
await user.keyboard("{ArrowRight}");
|
|
512
|
-
expect(
|
|
612
|
+
expect(header("Name")).toHaveFocus();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
describe("with many rows (virtualized)", () => {
|
|
616
|
+
const many: Row[] = Array.from({ length: 200 }, (_, i) => ({ id: i + 1, name: `Person ${i + 1}` }));
|
|
617
|
+
|
|
618
|
+
it("scrolls the target row into the rendered window and focuses it", async () => {
|
|
619
|
+
const user = userEvent.setup();
|
|
620
|
+
setup({ data: many });
|
|
621
|
+
await user.tab();
|
|
622
|
+
await user.keyboard("{ArrowDown}");
|
|
623
|
+
for (let i = 0; i < 30; i++) await user.keyboard("{ArrowDown}");
|
|
624
|
+
await waitFor(() => expect(document.activeElement).toHaveTextContent("Person 31"));
|
|
625
|
+
expect(document.activeElement).toHaveAttribute("data-cell", "30:name");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("PageDown / Ctrl+End jump by a page and to the last row", async () => {
|
|
629
|
+
const user = userEvent.setup();
|
|
630
|
+
setup({ data: many });
|
|
631
|
+
await user.tab();
|
|
632
|
+
await user.keyboard("{ArrowDown}");
|
|
633
|
+
await user.keyboard("{PageDown}");
|
|
634
|
+
await waitFor(() => expect(document.activeElement?.getAttribute("data-cell")).toMatch(/^(\d+):name$/));
|
|
635
|
+
const jumped = Number(document.activeElement!.getAttribute("data-cell")!.split(":")[0]);
|
|
636
|
+
expect(jumped).toBeGreaterThan(2);
|
|
637
|
+
|
|
638
|
+
await user.keyboard("{Control>}{End}{/Control}");
|
|
639
|
+
await waitFor(() => expect(document.activeElement).toHaveAttribute("data-cell", "199:id"));
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("stays reachable by Tab even after the active row scrolls out of the rendered window", async () => {
|
|
643
|
+
const user = userEvent.setup();
|
|
644
|
+
setup({ data: many });
|
|
645
|
+
await user.tab();
|
|
646
|
+
await user.keyboard("{ArrowDown}");
|
|
647
|
+
await user.keyboard("{Control>}{End}{/Control}");
|
|
648
|
+
await waitFor(() => expect(document.activeElement).toHaveAttribute("data-cell", "199:id"));
|
|
649
|
+
await user.tab(); // out
|
|
650
|
+
const grid = screen.getByRole("grid");
|
|
651
|
+
grid.scrollTop = 0;
|
|
652
|
+
grid.dispatchEvent(new Event("scroll"));
|
|
653
|
+
await user.tab({ shift: true });
|
|
654
|
+
expect(grid.contains(document.activeElement)).toBe(true);
|
|
655
|
+
});
|
|
513
656
|
});
|
|
514
657
|
});
|
|
515
658
|
|
|
@@ -569,3 +712,63 @@ describe("Slider", () => {
|
|
|
569
712
|
expect(onValueChange).toHaveBeenLastCalledWith([100]);
|
|
570
713
|
});
|
|
571
714
|
});
|
|
715
|
+
|
|
716
|
+
describe("MultiSelect chips", () => {
|
|
717
|
+
it("Backspace in the empty search field removes the last chip", async () => {
|
|
718
|
+
const user = userEvent.setup();
|
|
719
|
+
const onValueChange = vi.fn();
|
|
720
|
+
render(
|
|
721
|
+
<MultiSelect
|
|
722
|
+
aria-label="Fruit"
|
|
723
|
+
options={[
|
|
724
|
+
{ value: "a", label: "Apple" },
|
|
725
|
+
{ value: "b", label: "Banana" },
|
|
726
|
+
]}
|
|
727
|
+
defaultValue={["a", "b"]}
|
|
728
|
+
onValueChange={onValueChange}
|
|
729
|
+
/>,
|
|
730
|
+
);
|
|
731
|
+
await user.tab();
|
|
732
|
+
await user.keyboard("{Enter}");
|
|
733
|
+
await user.keyboard("{Backspace}");
|
|
734
|
+
expect(onValueChange).toHaveBeenLastCalledWith(["a"]);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
describe("List", () => {
|
|
739
|
+
it("a pressable row is a listitem containing a button, and Enter / Space activate it", async () => {
|
|
740
|
+
const user = userEvent.setup();
|
|
741
|
+
const onSelect = vi.fn();
|
|
742
|
+
render(
|
|
743
|
+
<List>
|
|
744
|
+
<ListItem title="Profile" onSelect={onSelect} />
|
|
745
|
+
<ListItem title="Static" />
|
|
746
|
+
</List>,
|
|
747
|
+
);
|
|
748
|
+
const items = screen.getAllByRole("listitem");
|
|
749
|
+
expect(items).toHaveLength(2);
|
|
750
|
+
const button = within(items[0]!).getByRole("button", { name: "Profile" });
|
|
751
|
+
await user.tab();
|
|
752
|
+
expect(button).toHaveFocus();
|
|
753
|
+
await user.keyboard("{Enter}");
|
|
754
|
+
await user.keyboard(" ");
|
|
755
|
+
expect(onSelect).toHaveBeenCalledTimes(2);
|
|
756
|
+
expect(within(items[1]!).queryByRole("button")).not.toBeInTheDocument();
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
describe("FileUpload", () => {
|
|
761
|
+
it("has exactly one keyboard target — the Browse button — not a nested control or the hidden input", async () => {
|
|
762
|
+
const user = userEvent.setup();
|
|
763
|
+
render(
|
|
764
|
+
<>
|
|
765
|
+
<FileUpload />
|
|
766
|
+
<button>after</button>
|
|
767
|
+
</>,
|
|
768
|
+
);
|
|
769
|
+
await user.tab();
|
|
770
|
+
expect(screen.getByRole("button", { name: "Browse files" })).toHaveFocus();
|
|
771
|
+
await user.tab();
|
|
772
|
+
expect(screen.getByRole("button", { name: "after" })).toHaveFocus();
|
|
773
|
+
});
|
|
774
|
+
});
|
|
@@ -8,7 +8,7 @@ import { Bell } from "lucide-react";
|
|
|
8
8
|
const Demo = () => (
|
|
9
9
|
<AppBar className="w-full max-w-2xl rounded-md border">
|
|
10
10
|
<AppBarBrand>
|
|
11
|
-
<span className="grid size-6 place-items-center rounded bg-primary text-
|
|
11
|
+
<span className="grid size-6 place-items-center rounded bg-primary text-[11px] font-bold text-primary-foreground">
|
|
12
12
|
A
|
|
13
13
|
</span>
|
|
14
14
|
Acme
|
|
@@ -24,6 +24,6 @@ const meta = {
|
|
|
24
24
|
|
|
25
25
|
export default meta;
|
|
26
26
|
|
|
27
|
-
export const Playground: StoryObj<typeof meta> = { render: (args) => <Checkbox {...args} /> };
|
|
27
|
+
export const Playground: StoryObj<typeof meta> = { render: (args) => <Checkbox aria-label="Accept terms" {...args} /> };
|
|
28
28
|
|
|
29
29
|
export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
|
|
@@ -12,9 +12,9 @@ const Demo = () => {
|
|
|
12
12
|
}, []);
|
|
13
13
|
return (
|
|
14
14
|
<div className="flex items-center gap-6">
|
|
15
|
-
<CircularProgress value={v} showValue />
|
|
16
|
-
<CircularProgress value={v} size={64} strokeWidth={6} showValue />
|
|
17
|
-
<CircularProgress value={v} />
|
|
15
|
+
<CircularProgress value={v} showValue aria-label="Upload progress" />
|
|
16
|
+
<CircularProgress value={v} size={64} strokeWidth={6} showValue aria-label="Upload progress" />
|
|
17
|
+
<CircularProgress value={v} aria-label="Upload progress" />
|
|
18
18
|
</div>
|
|
19
19
|
);
|
|
20
20
|
};
|
|
@@ -37,6 +37,6 @@ const meta = {
|
|
|
37
37
|
|
|
38
38
|
export default meta;
|
|
39
39
|
|
|
40
|
-
export const Playground: StoryObj<typeof meta> = { render: (args) => <CircularProgress {...args} /> };
|
|
40
|
+
export const Playground: StoryObj<typeof meta> = { render: (args) => <CircularProgress aria-label="Upload progress" {...args} /> };
|
|
41
41
|
|
|
42
42
|
export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
|
|
@@ -5,7 +5,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
|
5
5
|
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@kinetixui/ui";
|
|
6
6
|
|
|
7
7
|
const Demo = () => (
|
|
8
|
-
<InputOTP maxLength={6}>
|
|
8
|
+
<InputOTP maxLength={6} aria-label="One-time code">
|
|
9
9
|
<InputOTPGroup>
|
|
10
10
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
11
11
|
<InputOTPSlot key={i} index={i} />
|
|
@@ -18,7 +18,7 @@ const meta = {
|
|
|
18
18
|
title: "Form Inputs/InputOTP",
|
|
19
19
|
parameters: {
|
|
20
20
|
layout: "centered",
|
|
21
|
-
docs: { source: { code: "<InputOTP maxLength={6}>\\n <InputOTPGroup>\\n <InputOTPSlot index={0} />\\n ...\\n <InputOTPSlot index={5} />\\n </InputOTPGroup>\\n</InputOTP>", language: "tsx" } },
|
|
21
|
+
docs: { source: { code: "<InputOTP maxLength={6} aria-label=\\\"One-time code\\\">\\n <InputOTPGroup>\\n <InputOTPSlot index={0} />\\n ...\\n <InputOTPSlot index={5} />\\n </InputOTPGroup>\\n</InputOTP>", language: "tsx" } },
|
|
22
22
|
},
|
|
23
23
|
} satisfies Meta;
|
|
24
24
|
|
|
@@ -8,7 +8,7 @@ import { Bell, Settings, User } from "lucide-react";
|
|
|
8
8
|
const Demo = () => (
|
|
9
9
|
<List className="w-full max-w-sm rounded-md border">
|
|
10
10
|
<ListItem leading={<User className="size-5 text-muted-foreground" />} title="Profile" description="Name, photo, and personal details" trailing={<Badge variant="outline">2</Badge>} onSelect={() => {}} />
|
|
11
|
-
<ListItem leading={<Bell className="size-5 text-muted-foreground" />} title="Notifications" description="Push, email and SMS preferences" trailing={<Switch />} />
|
|
11
|
+
<ListItem leading={<Bell className="size-5 text-muted-foreground" />} title="Notifications" description="Push, email and SMS preferences" trailing={<Switch aria-label="Notifications" />} />
|
|
12
12
|
<ListItem leading={<Settings className="size-5 text-muted-foreground" />} title="Settings" onSelect={() => {}} />
|
|
13
13
|
</List>
|
|
14
14
|
);
|
|
@@ -17,7 +17,7 @@ const meta = {
|
|
|
17
17
|
title: "Data Display/List",
|
|
18
18
|
parameters: {
|
|
19
19
|
layout: "centered",
|
|
20
|
-
docs: { source: { code: "<List>\\n <ListItem\\n leading={<User />}\\n title=\"Profile\"\\n description=\"Name, photo, and personal details\"\\n onSelect={() => router.push(\"/profile\")}\\n />\\n <ListItem leading={<Bell />} title=\"Notifications\" trailing={<Switch />} />\\n</List>", language: "tsx" } },
|
|
20
|
+
docs: { source: { code: "<List>\\n <ListItem\\n leading={<User />}\\n title=\"Profile\"\\n description=\"Name, photo, and personal details\"\\n onSelect={() => router.push(\"/profile\")}\\n />\\n <ListItem leading={<Bell />} title=\"Notifications\" trailing={<Switch aria-label=\"Notifications\" />} />\\n</List>", language: "tsx" } },
|
|
21
21
|
},
|
|
22
22
|
} satisfies Meta;
|
|
23
23
|
|
|
@@ -9,7 +9,7 @@ const Demo = () => {
|
|
|
9
9
|
const [text, setText] = React.useState(
|
|
10
10
|
"## Release notes\n\nShipped **MarkdownEditor** — a toolbar over a plain textarea, not `contenteditable`.\n\n- Bold, italic, links\n- Bulleted and numbered lists\n- An optional preview pane",
|
|
11
11
|
);
|
|
12
|
-
return <MarkdownEditor className="w-full" value={text} onChange={setText} rows={8} />;
|
|
12
|
+
return <MarkdownEditor aria-label="Release notes" className="w-full" value={text} onChange={setText} rows={8} />;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const meta = {
|
|
@@ -5,7 +5,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
|
5
5
|
import { NativeSelect, NativeSelectOptGroup, NativeSelectOption, Select } from "@kinetixui/ui";
|
|
6
6
|
|
|
7
7
|
const Demo = () => (
|
|
8
|
-
<NativeSelect className="max-w-xs" defaultValue="">
|
|
8
|
+
<NativeSelect aria-label="Fruit" className="max-w-xs" defaultValue="">
|
|
9
9
|
<NativeSelectOption value="" disabled>
|
|
10
10
|
Select a fruit…
|
|
11
11
|
</NativeSelectOption>
|
|
@@ -6,7 +6,7 @@ import { NumberInput } from "@kinetixui/ui";
|
|
|
6
6
|
|
|
7
7
|
const Demo = () => {
|
|
8
8
|
const [v, setV] = React.useState(2);
|
|
9
|
-
return <NumberInput value={v} onChange={setV} min={0} max={10} className="w-32" />;
|
|
9
|
+
return <NumberInput aria-label="Quantity" value={v} onChange={setV} min={0} max={10} className="w-32" />;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
const meta = {
|
|
@@ -18,12 +18,12 @@ const meta = {
|
|
|
18
18
|
},
|
|
19
19
|
parameters: {
|
|
20
20
|
layout: "centered",
|
|
21
|
-
docs: { source: { code: "<NumberInput value={qty} onChange={setQty} min={0} max={10} />", language: "tsx" } },
|
|
21
|
+
docs: { source: { code: "<NumberInput aria-label=\\\"Quantity\\\" value={qty} onChange={setQty} min={0} max={10} />", language: "tsx" } },
|
|
22
22
|
},
|
|
23
23
|
} satisfies Meta;
|
|
24
24
|
|
|
25
25
|
export default meta;
|
|
26
26
|
|
|
27
|
-
export const Playground: StoryObj<typeof meta> = { render: (args) => <NumberInput {...args} className="w-32" /> };
|
|
27
|
+
export const Playground: StoryObj<typeof meta> = { render: (args) => <NumberInput aria-label="Quantity" {...args} className="w-32" /> };
|
|
28
28
|
|
|
29
29
|
export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
|
|
@@ -10,7 +10,7 @@ const Demo = () => {
|
|
|
10
10
|
const t = setTimeout(() => setV(66), 600);
|
|
11
11
|
return () => clearTimeout(t);
|
|
12
12
|
}, []);
|
|
13
|
-
return <Progress value={v} className="w-[60%]" />;
|
|
13
|
+
return <Progress value={v} aria-label="Upload progress" className="w-[60%]" />;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const meta = {
|
|
@@ -20,12 +20,12 @@ const meta = {
|
|
|
20
20
|
argTypes: { value: { control: { type: "range", min: 0, max: 100 } } },
|
|
21
21
|
parameters: {
|
|
22
22
|
layout: "centered",
|
|
23
|
-
docs: { source: { code: "const [value, setValue] = React.useState(13)\\n// ...\\n<Progress value={value} />", language: "tsx" } },
|
|
23
|
+
docs: { source: { code: "const [value, setValue] = React.useState(13)\\n// ...\\n<Progress value={value} aria-label=\\\"Upload progress\\\" />", language: "tsx" } },
|
|
24
24
|
},
|
|
25
25
|
} satisfies Meta;
|
|
26
26
|
|
|
27
27
|
export default meta;
|
|
28
28
|
|
|
29
|
-
export const Playground: StoryObj<typeof meta> = { render: (args) => <Progress {...args} className="w-60" /> };
|
|
29
|
+
export const Playground: StoryObj<typeof meta> = { render: (args) => <Progress aria-label="Upload progress" {...args} className="w-60" /> };
|
|
30
30
|
|
|
31
31
|
export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
|
|
@@ -6,7 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|
|
6
6
|
|
|
7
7
|
const Demo = () => (
|
|
8
8
|
<Select>
|
|
9
|
-
<SelectTrigger className="w-[220px]">
|
|
9
|
+
<SelectTrigger className="w-[220px]" aria-label="Fruit">
|
|
10
10
|
<SelectValue placeholder="Select a fruit" />
|
|
11
11
|
</SelectTrigger>
|
|
12
12
|
<SelectContent>
|
|
@@ -32,7 +32,7 @@ export default meta;
|
|
|
32
32
|
|
|
33
33
|
export const Playground: StoryObj<typeof meta> = { render: (args) => (
|
|
34
34
|
<Select {...args}>
|
|
35
|
-
<SelectTrigger className="w-[220px]">
|
|
35
|
+
<SelectTrigger className="w-[220px]" aria-label="Fruit">
|
|
36
36
|
<SelectValue placeholder="Select a fruit" />
|
|
37
37
|
</SelectTrigger>
|
|
38
38
|
<SelectContent>
|
|
@@ -4,7 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
5
5
|
import { Slider } from "@kinetixui/ui";
|
|
6
6
|
|
|
7
|
-
const Demo = () => <Slider defaultValue={[50]} max={100} step={1} className="w-[60%]" />;
|
|
7
|
+
const Demo = () => <Slider aria-label="Volume" defaultValue={[50]} max={100} step={1} className="w-[60%]" />;
|
|
8
8
|
|
|
9
9
|
const meta = {
|
|
10
10
|
title: "Form Inputs/Slider",
|
|
@@ -13,12 +13,12 @@ const meta = {
|
|
|
13
13
|
argTypes: { disabled: { control: "boolean" } },
|
|
14
14
|
parameters: {
|
|
15
15
|
layout: "centered",
|
|
16
|
-
docs: { source: { code: "<Slider defaultValue={[50]} max={100} step={1} />", language: "tsx" } },
|
|
16
|
+
docs: { source: { code: "<Slider aria-label=\"Volume\" defaultValue={[50]} max={100} step={1} />", language: "tsx" } },
|
|
17
17
|
},
|
|
18
18
|
} satisfies Meta;
|
|
19
19
|
|
|
20
20
|
export default meta;
|
|
21
21
|
|
|
22
|
-
export const Playground: StoryObj<typeof meta> = { render: (args) => <Slider {...args} defaultValue={[50]} max={100} step={1} className="w-60" /> };
|
|
22
|
+
export const Playground: StoryObj<typeof meta> = { render: (args) => <Slider aria-label="Volume" {...args} defaultValue={[50]} max={100} step={1} className="w-60" /> };
|
|
23
23
|
|
|
24
24
|
export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
|
|
@@ -24,6 +24,6 @@ const meta = {
|
|
|
24
24
|
|
|
25
25
|
export default meta;
|
|
26
26
|
|
|
27
|
-
export const Playground: StoryObj<typeof meta> = { render: (args) => <Switch {...args} /> };
|
|
27
|
+
export const Playground: StoryObj<typeof meta> = { render: (args) => <Switch aria-label="Airplane mode" {...args} /> };
|
|
28
28
|
|
|
29
29
|
export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
|
|
@@ -7,9 +7,9 @@ import { Bold, Italic, Underline } from "lucide-react";
|
|
|
7
7
|
|
|
8
8
|
const Demo = () => (
|
|
9
9
|
<ToggleGroup type="multiple">
|
|
10
|
-
<ToggleGroupItem value="bold"><Bold className="size-4" /></ToggleGroupItem>
|
|
11
|
-
<ToggleGroupItem value="italic"><Italic className="size-4" /></ToggleGroupItem>
|
|
12
|
-
<ToggleGroupItem value="underline"><Underline className="size-4" /></ToggleGroupItem>
|
|
10
|
+
<ToggleGroupItem value="bold" aria-label="Bold"><Bold className="size-4" /></ToggleGroupItem>
|
|
11
|
+
<ToggleGroupItem value="italic" aria-label="Italic"><Italic className="size-4" /></ToggleGroupItem>
|
|
12
|
+
<ToggleGroupItem value="underline" aria-label="Underline"><Underline className="size-4" /></ToggleGroupItem>
|
|
13
13
|
</ToggleGroup>
|
|
14
14
|
);
|
|
15
15
|
|
|
@@ -28,9 +28,9 @@ export default meta;
|
|
|
28
28
|
|
|
29
29
|
export const Playground: StoryObj<typeof meta> = { render: (args) => (
|
|
30
30
|
<ToggleGroup {...args}>
|
|
31
|
-
<ToggleGroupItem value="bold"><Bold className="size-4" /></ToggleGroupItem>
|
|
32
|
-
<ToggleGroupItem value="italic"><Italic className="size-4" /></ToggleGroupItem>
|
|
33
|
-
<ToggleGroupItem value="underline"><Underline className="size-4" /></ToggleGroupItem>
|
|
31
|
+
<ToggleGroupItem value="bold" aria-label="Bold"><Bold className="size-4" /></ToggleGroupItem>
|
|
32
|
+
<ToggleGroupItem value="italic" aria-label="Italic"><Italic className="size-4" /></ToggleGroupItem>
|
|
33
|
+
<ToggleGroupItem value="underline" aria-label="Underline"><Underline className="size-4" /></ToggleGroupItem>
|
|
34
34
|
</ToggleGroup>
|
|
35
35
|
) };
|
|
36
36
|
|
package/tailwind.config.ts
CHANGED
|
@@ -66,6 +66,20 @@ export default {
|
|
|
66
66
|
"on-container": c("--semantic-on-info-container"),
|
|
67
67
|
},
|
|
68
68
|
tertiary: { DEFAULT: c("--tertiary"), foreground: c("--tertiary-foreground") },
|
|
69
|
+
// Role tokens layered over `primary` (which stays the shadcn-compatible source). `action`, `link`
|
|
70
|
+
// and `focus` default to it, so a theme that only sets `--primary` still works; override them
|
|
71
|
+
// individually to split the interactive colour, link colour and focus colour. `brand` is identity
|
|
72
|
+
// (logos, headers), not a control colour.
|
|
73
|
+
brand: { DEFAULT: c("--brand"), foreground: c("--brand-foreground") },
|
|
74
|
+
action: {
|
|
75
|
+
DEFAULT: c("--action"),
|
|
76
|
+
foreground: c("--action-foreground"),
|
|
77
|
+
// explicit states for native ports; on the web, hover/pressed derive from `action` (bg-action/90, /85)
|
|
78
|
+
hover: c("--action-hover"),
|
|
79
|
+
pressed: c("--action-pressed"),
|
|
80
|
+
},
|
|
81
|
+
link: c("--link"),
|
|
82
|
+
focus: c("--focus"),
|
|
69
83
|
border: c("--border"),
|
|
70
84
|
input: c("--input"),
|
|
71
85
|
ring: c("--ring"),
|