From e29f4bf13a69888b7bf222691f2d43895a3ac422 Mon Sep 17 00:00:00 2001 From: Barsnes Date: Wed, 11 Dec 2024 09:57:29 +0100 Subject: [PATCH 01/20] feat(Suggestion): :sparkles: new component --- packages/react/package.json | 1 + .../src/components/Suggestion/Suggestion.mdx | 11 +++++++ .../Suggestion/Suggestion.stories.tsx | 10 ++++++ .../src/components/Suggestion/Suggestion.tsx | 32 +++++++++++++++++++ .../components/Suggestion/SuggestionEmpty.tsx | 19 +++++++++++ .../components/Suggestion/SuggestionInput.tsx | 15 +++++++++ .../components/Suggestion/SuggestionList.tsx | 29 +++++++++++++++++ .../react/src/components/Suggestion/index.ts | 15 +++++++++ yarn.lock | 8 +++++ 9 files changed, 140 insertions(+) create mode 100644 packages/react/src/components/Suggestion/Suggestion.mdx create mode 100644 packages/react/src/components/Suggestion/Suggestion.stories.tsx create mode 100644 packages/react/src/components/Suggestion/Suggestion.tsx create mode 100644 packages/react/src/components/Suggestion/SuggestionEmpty.tsx create mode 100644 packages/react/src/components/Suggestion/SuggestionInput.tsx create mode 100644 packages/react/src/components/Suggestion/SuggestionList.tsx create mode 100644 packages/react/src/components/Suggestion/index.ts diff --git a/packages/react/package.json b/packages/react/package.json index f924d4d472..cdf1abb847 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -47,6 +47,7 @@ "@navikt/aksel-icons": "^6.14.0", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-virtual": "^3.10.7", + "@u-elements/u-datalist": "^0.0.9", "@u-elements/u-details": "^0.1.0", "clsx": "^2.1.1" }, diff --git a/packages/react/src/components/Suggestion/Suggestion.mdx b/packages/react/src/components/Suggestion/Suggestion.mdx new file mode 100644 index 0000000000..3b6ffb8814 --- /dev/null +++ b/packages/react/src/components/Suggestion/Suggestion.mdx @@ -0,0 +1,11 @@ +import { Meta, Canvas, Controls, Primary } from '@storybook/blocks'; +import * as SuggestionStories from './Suggestion.stories'; + + + +# ToggleGroup + +Med `ToggleGroup` kan brukerne velge alternativer som påvirker innholdet på en side. Komponenten består av en gruppe knapper som henger sammen, der kun én knapp er mulig å velge om gangen. + + + diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx new file mode 100644 index 0000000000..dbced23abd --- /dev/null +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -0,0 +1,10 @@ +import type { Meta, StoryFn } from '@storybook/react'; +import { Suggestion } from './Suggestion'; +export default { + title: 'Komponenter/Suggestion', + component: Suggestion, +} as Meta; + +export const Preview: StoryFn = (args) => { + return ; +}; diff --git a/packages/react/src/components/Suggestion/Suggestion.tsx b/packages/react/src/components/Suggestion/Suggestion.tsx new file mode 100644 index 0000000000..9433179437 --- /dev/null +++ b/packages/react/src/components/Suggestion/Suggestion.tsx @@ -0,0 +1,32 @@ +import { useMergeRefs } from '@floating-ui/react'; +import cl from 'clsx/lite'; +import { createContext, forwardRef, useId, useRef, useState } from 'react'; +import type { DefaultProps } from '../../types'; + +type SuggestionContextType = { + listId?: string; + setListId?: (id: string) => void; +}; + +export const SuggestionContext = createContext({}); + +export type SuggestionProps = + DefaultProps & {} & React.HTMLAttributes; + +export const Suggestion = forwardRef( + function Suggestion({ className, ...rest }, ref) { + const [listId, setListId] = useState(useId()); + const innerRef = useRef(null); + const mergedRefs = useMergeRefs([innerRef, ref]); + + return ( + +
+ + ); + }, +); diff --git a/packages/react/src/components/Suggestion/SuggestionEmpty.tsx b/packages/react/src/components/Suggestion/SuggestionEmpty.tsx new file mode 100644 index 0000000000..697401d9db --- /dev/null +++ b/packages/react/src/components/Suggestion/SuggestionEmpty.tsx @@ -0,0 +1,19 @@ +import type { HTMLAttributes } from 'react'; +import { forwardRef } from 'react'; +import type { DefaultProps } from '../../types'; + +export type SuggestionEmptyProps = HTMLAttributes & + DefaultProps; +export const SuggestionEmpty = forwardRef( + function SuggestionEmpty(rest, ref) { + return ( +
+ ); + }, +); diff --git a/packages/react/src/components/Suggestion/SuggestionInput.tsx b/packages/react/src/components/Suggestion/SuggestionInput.tsx new file mode 100644 index 0000000000..a9d6612c86 --- /dev/null +++ b/packages/react/src/components/Suggestion/SuggestionInput.tsx @@ -0,0 +1,15 @@ +import { forwardRef, useContext } from 'react'; +import { Input, type InputProps } from '../Input'; +import { SuggestionContext } from './Suggestion'; + +export type SuggestionInputProps = InputProps; + +export const SuggestionInput = forwardRef< + HTMLInputElement, + SuggestionInputProps +>(function SuggestionList(rest, ref) { + const { listId } = useContext(SuggestionContext); + + /* We need an empty placeholder for the clear button to be able to show/hide */ + return ; +}); diff --git a/packages/react/src/components/Suggestion/SuggestionList.tsx b/packages/react/src/components/Suggestion/SuggestionList.tsx new file mode 100644 index 0000000000..d6211f8be6 --- /dev/null +++ b/packages/react/src/components/Suggestion/SuggestionList.tsx @@ -0,0 +1,29 @@ +import type { HTMLAttributes } from 'react'; +import { forwardRef, useContext, useEffect } from 'react'; +import '@u-elements/u-datalist'; + +import type { DefaultProps } from '../../types'; +import { SuggestionContext } from './Suggestion'; + +export type SuggestionListProps = HTMLAttributes & + DefaultProps; + +export const SuggestionList = forwardRef< + HTMLDataListElement, + SuggestionListProps +>(function SuggestionList({ className, id, ...rest }, ref) { + const { listId, setListId } = useContext(SuggestionContext); + + useEffect(() => { + if (id && listId !== id) setListId?.(id); + }, [listId, id, setListId]); + + return ( + + ); +}); diff --git a/packages/react/src/components/Suggestion/index.ts b/packages/react/src/components/Suggestion/index.ts new file mode 100644 index 0000000000..c7ead3fdeb --- /dev/null +++ b/packages/react/src/components/Suggestion/index.ts @@ -0,0 +1,15 @@ +import { Suggestion as SuggestionRoot } from './Suggestion'; +import { SuggestionEmpty } from './SuggestionEmpty'; +import { SuggestionInput } from './SuggestionInput'; +import { SuggestionList } from './SuggestionList'; + +const Suggestion = Object.assign(SuggestionRoot, { + List: SuggestionList, + Input: SuggestionInput, + Empty: SuggestionEmpty, +}); + +export { Suggestion, SuggestionList, SuggestionInput, SuggestionEmpty }; +export type { SuggestionProps } from './Suggestion'; +export type { SuggestionListProps } from './SuggestionList'; +export type { SuggestionInputProps } from './SuggestionInput'; diff --git a/yarn.lock b/yarn.lock index 03459f5752..d17843ef67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1720,6 +1720,7 @@ __metadata: "@testing-library/jest-dom": "npm:^6.4.8" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.5.2" + "@u-elements/u-datalist": "npm:^0.0.9" "@u-elements/u-details": "npm:^0.1.0" clsx: "npm:^2.1.1" copyfiles: "npm:^2.4.1" @@ -5525,6 +5526,13 @@ __metadata: languageName: node linkType: hard +"@u-elements/u-datalist@npm:^0.0.9": + version: 0.0.9 + resolution: "@u-elements/u-datalist@npm:0.0.9" + checksum: 10/35e517271bec2c67aaee5806fa90d6f8755ed86cbdc339bb47d1584d8f424668ebe78052828a5a353b1143cff5f91bfb5b3105b5408949468fe33d363d12c439 + languageName: node + linkType: hard + "@u-elements/u-details@npm:^0.1.0": version: 0.1.0 resolution: "@u-elements/u-details@npm:0.1.0" From 7557be6afb65d1fedd7b7990492b468d29f9535e Mon Sep 17 00:00:00 2001 From: Barsnes Date: Wed, 11 Dec 2024 12:07:12 +0100 Subject: [PATCH 02/20] add option to list and popove --- .../src/components/Suggestion/Suggestion.mdx | 6 +-- .../Suggestion/Suggestion.stories.tsx | 14 ++++++- .../src/components/Suggestion/Suggestion.tsx | 17 ++++---- .../components/Suggestion/SuggestionInput.tsx | 7 +++- .../components/Suggestion/SuggestionList.tsx | 40 +++++++++++++++---- .../Suggestion/SuggestionOption.tsx | 19 +++++++++ .../react/src/components/Suggestion/index.ts | 4 ++ 7 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 packages/react/src/components/Suggestion/SuggestionOption.tsx diff --git a/packages/react/src/components/Suggestion/Suggestion.mdx b/packages/react/src/components/Suggestion/Suggestion.mdx index 3b6ffb8814..5ba6f80f96 100644 --- a/packages/react/src/components/Suggestion/Suggestion.mdx +++ b/packages/react/src/components/Suggestion/Suggestion.mdx @@ -1,11 +1,11 @@ import { Meta, Canvas, Controls, Primary } from '@storybook/blocks'; import * as SuggestionStories from './Suggestion.stories'; - + -# ToggleGroup +# Suggestion -Med `ToggleGroup` kan brukerne velge alternativer som påvirker innholdet på en side. Komponenten består av en gruppe knapper som henger sammen, der kun én knapp er mulig å velge om gangen. +Søkbar "select". diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index dbced23abd..5005060e12 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -1,10 +1,20 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { Suggestion } from './Suggestion'; +import { Suggestion } from './'; export default { title: 'Komponenter/Suggestion', component: Suggestion, } as Meta; export const Preview: StoryFn = (args) => { - return ; + return ( + + + Tomt + Option 1 + Option 2 + Option 3 + + + + ); }; diff --git a/packages/react/src/components/Suggestion/Suggestion.tsx b/packages/react/src/components/Suggestion/Suggestion.tsx index 9433179437..95333eff70 100644 --- a/packages/react/src/components/Suggestion/Suggestion.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.tsx @@ -2,6 +2,7 @@ import { useMergeRefs } from '@floating-ui/react'; import cl from 'clsx/lite'; import { createContext, forwardRef, useId, useRef, useState } from 'react'; import type { DefaultProps } from '../../types'; +import { Popover } from '../Popover'; type SuggestionContextType = { listId?: string; @@ -20,13 +21,15 @@ export const Suggestion = forwardRef( const mergedRefs = useMergeRefs([innerRef, ref]); return ( - -
- + + +
+ + ); }, ); diff --git a/packages/react/src/components/Suggestion/SuggestionInput.tsx b/packages/react/src/components/Suggestion/SuggestionInput.tsx index a9d6612c86..95f2d1c03d 100644 --- a/packages/react/src/components/Suggestion/SuggestionInput.tsx +++ b/packages/react/src/components/Suggestion/SuggestionInput.tsx @@ -1,5 +1,6 @@ import { forwardRef, useContext } from 'react'; import { Input, type InputProps } from '../Input'; +import { Popover } from '../Popover'; import { SuggestionContext } from './Suggestion'; export type SuggestionInputProps = InputProps; @@ -11,5 +12,9 @@ export const SuggestionInput = forwardRef< const { listId } = useContext(SuggestionContext); /* We need an empty placeholder for the clear button to be able to show/hide */ - return ; + return ( + + + + ); }); diff --git a/packages/react/src/components/Suggestion/SuggestionList.tsx b/packages/react/src/components/Suggestion/SuggestionList.tsx index d6211f8be6..8e8b1ccd62 100644 --- a/packages/react/src/components/Suggestion/SuggestionList.tsx +++ b/packages/react/src/components/Suggestion/SuggestionList.tsx @@ -1,8 +1,10 @@ import type { HTMLAttributes } from 'react'; -import { forwardRef, useContext, useEffect } from 'react'; +import { forwardRef, useContext, useEffect, useRef, useState } from 'react'; import '@u-elements/u-datalist'; +import { useMergeRefs } from '@floating-ui/react'; import type { DefaultProps } from '../../types'; +import { Popover } from '../Popover'; import { SuggestionContext } from './Suggestion'; export type SuggestionListProps = HTMLAttributes & @@ -14,16 +16,40 @@ export const SuggestionList = forwardRef< >(function SuggestionList({ className, id, ...rest }, ref) { const { listId, setListId } = useContext(SuggestionContext); + const localRef = useRef(null); + const [open, setOpen] = useState(true); + + const mergedRefs = useMergeRefs([localRef, ref]); + useEffect(() => { if (id && listId !== id) setListId?.(id); }, [listId, id, setListId]); + /* if listRef does not have hidden, it is open */ + useEffect(() => { + const observer = new MutationObserver((cb) => { + for (const mutation of cb) { + if (mutation.attributeName === 'hidden') { + setOpen(!(mutation.target as Element).hasAttribute('hidden')); + } + } + }); + + observer.observe(localRef.current as Node, { + attributes: true, + }); + + return () => observer.disconnect(); + }, [localRef]); + return ( - + + + ); }); diff --git a/packages/react/src/components/Suggestion/SuggestionOption.tsx b/packages/react/src/components/Suggestion/SuggestionOption.tsx new file mode 100644 index 0000000000..9bea5ab014 --- /dev/null +++ b/packages/react/src/components/Suggestion/SuggestionOption.tsx @@ -0,0 +1,19 @@ +import type { OptionHTMLAttributes } from 'react'; +import { forwardRef } from 'react'; +import type { DefaultProps } from '../../types'; +import '@u-elements/u-datalist'; + +export type SuggestionOptionProps = OptionHTMLAttributes & + DefaultProps; +export const SuggestionOption = forwardRef< + HTMLOptionElement, + SuggestionOptionProps +>(function SuggestionOption({ className, ...rest }, ref) { + return ( + + ); +}); diff --git a/packages/react/src/components/Suggestion/index.ts b/packages/react/src/components/Suggestion/index.ts index c7ead3fdeb..99e79fcb9b 100644 --- a/packages/react/src/components/Suggestion/index.ts +++ b/packages/react/src/components/Suggestion/index.ts @@ -2,14 +2,18 @@ import { Suggestion as SuggestionRoot } from './Suggestion'; import { SuggestionEmpty } from './SuggestionEmpty'; import { SuggestionInput } from './SuggestionInput'; import { SuggestionList } from './SuggestionList'; +import { SuggestionOption } from './SuggestionOption'; const Suggestion = Object.assign(SuggestionRoot, { List: SuggestionList, Input: SuggestionInput, Empty: SuggestionEmpty, + Option: SuggestionOption, }); export { Suggestion, SuggestionList, SuggestionInput, SuggestionEmpty }; export type { SuggestionProps } from './Suggestion'; export type { SuggestionListProps } from './SuggestionList'; export type { SuggestionInputProps } from './SuggestionInput'; +export type { SuggestionEmptyProps } from './SuggestionEmpty'; +export type { SuggestionOptionProps } from './SuggestionOption'; From bf5bbe05b4570e12efcf06355a1d1557c451783d Mon Sep 17 00:00:00 2001 From: Barsnes Date: Wed, 11 Dec 2024 13:01:27 +0100 Subject: [PATCH 03/20] remove popover, add clear --- packages/css/src/index.css | 1 + packages/css/src/suggestion.css | 90 +++++++++++++++++++ .../Suggestion/Suggestion.stories.tsx | 9 ++ .../src/components/Suggestion/Suggestion.tsx | 17 ++-- .../components/Suggestion/SuggestionClear.tsx | 76 ++++++++++++++++ .../components/Suggestion/SuggestionInput.tsx | 7 +- .../components/Suggestion/SuggestionList.tsx | 41 ++------- .../react/src/components/Suggestion/index.ts | 9 ++ 8 files changed, 200 insertions(+), 50 deletions(-) create mode 100644 packages/css/src/suggestion.css create mode 100644 packages/react/src/components/Suggestion/SuggestionClear.tsx diff --git a/packages/css/src/index.css b/packages/css/src/index.css index 590bdb88d3..39590c0a14 100644 --- a/packages/css/src/index.css +++ b/packages/css/src/index.css @@ -13,6 +13,7 @@ @import url('./input.css') layer(ds.components); @import url('./alert.css') layer(ds.components); @import url('./popover.css') layer(ds.components); +@import url('./suggestion.css') layer(ds.components); @import url('./skiplink.css') layer(ds.components); @import url('./details.css') layer(ds.components); @import url('./search.css') layer(ds.components); diff --git a/packages/css/src/suggestion.css b/packages/css/src/suggestion.css new file mode 100644 index 0000000000..954887993d --- /dev/null +++ b/packages/css/src/suggestion.css @@ -0,0 +1,90 @@ +.ds-suggestion { + --dsc-suggestion-option-background--selected: var(--ds-color-background-subtle); + --dsc-suggestion-option-border-color: var(--ds-color-base-default); + --dsc-combobox-clear-gap: var(--ds-spacing-2); + --dsc-combobox-clear-padding: var(--ds-sizing-1); + --dsc-combobox-clear-size: var(--ds-sizing-9); + --dsc-combobox-clear-icon-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath fill='currentColor' d='M6.53 5.47a.75.75 0 0 0-1.06 1.06L10.94 12l-5.47 5.47a.75.75 0 1 0 1.06 1.06L12 13.06l5.47 5.47a.75.75 0 1 0 1.06-1.06L13.06 12l5.47-5.47a.75.75 0 0 0-1.06-1.06L12 10.94z'/%3E%3C/svg%3E"); + + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + gap: var(--ds-spacing-1); + position: relative; + + & u-datalist { + background: var(--ds-color-neutral-background-default); + border-radius: var(--ds-border-radius-md); + border: 1px solid var(--ds-color-neutral-border-default); + box-shadow: var(--ds-shadow-md); + box-sizing: border-box; + color: var(--ds-color-neutral-text-default); + inset: 100% 0 auto 0; + overflow-y: auto; + position: absolute; + z-index: 1600; + + &:not(:has(u-option:hover)) > u-option:focus-visible { + border-left-color: var(--dsc-suggestion-option-border-color); + background: var(--dsc-suggestion-option-background--selected); + } + + &:has(u-option:not([hidden])) > :not(u-option) { + display: none; + } + + /* Hide if any is visible */ + &:has(u-option:not([hidden])) > :not(u-option) { + display: none; + } + + & > * { + padding: var(--ds-spacing-2); + border-left: var(--ds-spacing-1) solid transparent; + outline: none; + + /* TMP check to demonstrate selector */ + &[aria-selected='true']::before { + content: '✔ '; + } + + @media (hover: hover) and (pointer: fine) { + &:is(u-option):hover { + border-left-color: var(--dsc-suggestion-option-border-color); + background: var(--dsc-suggestion-option-background--selected); + } + } + } + } + + /** + * Clear button + */ + &:has(input:placeholder-shown) button[type='reset'], + &:has(input:is(:read-only, :disabled, [aria-disabled='true'])) button[type='reset'] { + display: none; /* We hide the clear button when input is empty */ + } + &:has(button[type='reset']) input { + padding-inline-end: calc(var(--dsc-combobox-clear-size) + var(--dsc-combobox-clear-gap)); + } + + & button[type='reset'] { + --dsc-button-size: var(--dsc-combobox-clear-size); + + align-self: center; + margin-inline-start: calc((var(--dsc-combobox-clear-size) + var(--dsc-combobox-clear-gap)) * -1); + order: 999; /* Place last */ + padding: var(--dsc-combobox-clear-padding); + position: relative; + scale: 0.75; + z-index: 2; + + &::before { + content: ''; + height: var(--dsc-combobox-clear-size); + width: var(--dsc-combobox-clear-size); + mask: var(--dsc-combobox-clear-icon-url) center / contain no-repeat; + background: currentcolor; + } + } +} diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index 5005060e12..a67c664e6b 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -15,6 +15,15 @@ export const Preview: StoryFn = (args) => { Option 3 + ); }; + +Preview.decorators = [ + (Story) => ( +
+ +
+ ), +]; diff --git a/packages/react/src/components/Suggestion/Suggestion.tsx b/packages/react/src/components/Suggestion/Suggestion.tsx index 95333eff70..9433179437 100644 --- a/packages/react/src/components/Suggestion/Suggestion.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.tsx @@ -2,7 +2,6 @@ import { useMergeRefs } from '@floating-ui/react'; import cl from 'clsx/lite'; import { createContext, forwardRef, useId, useRef, useState } from 'react'; import type { DefaultProps } from '../../types'; -import { Popover } from '../Popover'; type SuggestionContextType = { listId?: string; @@ -21,15 +20,13 @@ export const Suggestion = forwardRef( const mergedRefs = useMergeRefs([innerRef, ref]); return ( - - -
- - + +
+ ); }, ); diff --git a/packages/react/src/components/Suggestion/SuggestionClear.tsx b/packages/react/src/components/Suggestion/SuggestionClear.tsx new file mode 100644 index 0000000000..344156bec5 --- /dev/null +++ b/packages/react/src/components/Suggestion/SuggestionClear.tsx @@ -0,0 +1,76 @@ +import { forwardRef } from 'react'; +import { Button, type ButtonProps } from '../Button'; + +/* We omit children since we render the icon with css */ +export type SuggestionClearProps = Omit & { + /** + * Aria label for the clear button + * @default 'Tøm' + */ + 'aria-label'?: string; +}; + +export const SuggestionClear = forwardRef< + HTMLButtonElement, + SuggestionClearProps +>(function SuggestionClear( + { 'aria-label': label = 'Tøm', onClick, ...rest }, + ref, +) { + const handleClear = ( + event: React.MouseEvent, + ) => { + const target = event.target; + onClick?.(event); + + if (event.defaultPrevented) return; + + let input: HTMLElement | null | undefined = null; + + if (target instanceof HTMLElement) + input = target.closest('.ds-suggestion')?.querySelector('input'); + + if (!input) throw new Error('Input is missing'); + /* narrow type to make TS happy */ + if (!(input instanceof HTMLInputElement)) + throw new Error('Input is not an input element'); + + event.preventDefault(); + setReactInputValue(input, ''); + input.focus(); + }; + + return ( + + + ); +}; diff --git a/packages/react/src/components/Suggestion/Suggestion.tsx b/packages/react/src/components/Suggestion/Suggestion.tsx index 1f4884e3e6..e673ce8191 100644 --- a/packages/react/src/components/Suggestion/Suggestion.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.tsx @@ -1,7 +1,16 @@ import { useMergeRefs } from '@floating-ui/react'; import cl from 'clsx/lite'; -import { createContext, forwardRef, useId, useRef, useState } from 'react'; +import { + createContext, + forwardRef, + useEffect, + useId, + useRef, + useState, +} from 'react'; import type { DefaultProps } from '../../types'; +import type { MergeRight } from '../../utilities'; +import { setReactInputValue } from './SuggestionClear'; type SuggestionContextType = { listId?: string; @@ -11,17 +20,59 @@ type SuggestionContextType = { export const SuggestionContext = createContext({}); -export type SuggestionProps = - DefaultProps & {} & React.HTMLAttributes; +export type SuggestionProps = MergeRight< + DefaultProps & React.HTMLAttributes, + { + /** + * Callback for when the value changes + * + */ + onChange?: (value: string) => void; + /** + * The default value + */ + defaultValue?: string; + /** + * The value when controlled + */ + value?: string; + } +>; export const Suggestion = forwardRef( - function Suggestion({ className, ...rest }, ref) { + function Suggestion( + { defaultValue, value, onChange, className, ...rest }, + ref, + ) { const [listId, setListId] = useState(useId()); + const [internalValue, setInternalValue] = useState( + defaultValue || '', + ); const innerRef = useRef(null); const inputRef = useRef(null); const mergedRefs = useMergeRefs([innerRef, ref]); + // Handle onChange + useEffect(() => { + const div = innerRef.current as HTMLDivElement | null; + const handleChange = () => onChange?.(inputRef.current?.value || ''); + + div?.addEventListener('input', handleChange); + return () => div?.removeEventListener('input', handleChange); + }, [internalValue]); + + // call onChange when internalValue changes + useEffect(() => { + onChange?.(internalValue); + }, [internalValue, onChange]); + + // update internalValue and input value when value changes + useEffect(() => { + if (value && value !== internalValue) setInternalValue(value || ''); + inputRef.current && setReactInputValue(inputRef.current, value || ''); + }, [value]); + return (
Date: Thu, 12 Dec 2024 13:25:01 +0100 Subject: [PATCH 06/20] style checkmark, check desc --- packages/css/src/suggestion.css | 29 ++++++++++++++----- .../Suggestion/Suggestion.stories.tsx | 5 +++- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/css/src/suggestion.css b/packages/css/src/suggestion.css index 68b4a8501c..c41eb0e2b2 100644 --- a/packages/css/src/suggestion.css +++ b/packages/css/src/suggestion.css @@ -5,6 +5,8 @@ --dsc-combobox-clear-padding: var(--ds-sizing-1); --dsc-combobox-clear-size: var(--ds-sizing-9); --dsc-combobox-clear-icon-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath fill='currentColor' d='M6.53 5.47a.75.75 0 0 0-1.06 1.06L10.94 12l-5.47 5.47a.75.75 0 1 0 1.06 1.06L12 13.06l5.47 5.47a.75.75 0 1 0 1.06-1.06L13.06 12l5.47-5.47a.75.75 0 0 0-1.06-1.06L12 10.94z'/%3E%3C/svg%3E"); + --dsc-suggestion-option-checkmark-url: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221em%22%20height%3D%221em%22%20fill%3D%22none%22%20viewBox%3D%220%200%2024%2024%22%20focusable%3D%22false%22%20role%3D%22img%22%3E%3Cpath%20fill%3D%22currentColor%22%20fill-rule%3D%22evenodd%22%20d%3D%22M18.998%206.94a.75.75%200%200%201%20.063%201.058l-8%209a.75.75%200%200%201-1.091.032l-5-5a.75.75%200%201%201%201.06-1.06l4.438%204.437%207.471-8.405A.75.75%200%200%201%2019%206.939%22%20clip-rule%3D%22evenodd%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E'); + --dsc-suggestion-option-checkmark-size: var(--ds-sizing-7); box-sizing: border-box; display: flex; @@ -29,10 +31,6 @@ background: var(--dsc-suggestion-option-background--selected); } - &:has(u-option:not([hidden])) > :not(u-option) { - display: none; - } - /* Hide if any is visible */ &:has(u-option:not([hidden])) > :not(u-option) { display: none; @@ -41,8 +39,9 @@ & > * { grid-template-columns: 1.2em 1fr; padding: var(--ds-spacing-2) var(--ds-spacing-3); - padding-inline-start: var(--ds-spacing-4); + padding-inline-start: var(--dsc-suggestion-option-checkmark-size); border: none; + outline: none; border-left: 5px solid transparent; border-radius: var(--ds-border-radius-sm); justify-content: start; @@ -52,13 +51,27 @@ cursor: pointer; font-family: inherit; font-weight: 400; + position: relative; + + & > * { + grid-column: 1 / 1; + } - /* TMP check to demonstrate selector */ &[aria-selected='true'] { - padding-inline-start: var(--ds-spacing-1); + & > * { + grid-column: 2 / 2; + } &::before { - content: '✔ '; + content: ''; + position: absolute; + left: 0; + mask: var(--dsc-suggestion-option-checkmark-url) center / contain no-repeat; + background: currentcolor; + width: var(--dsc-suggestion-option-checkmark-size); + height: var(--dsc-suggestion-option-checkmark-size); + display: inline-block; + translate: 0 calc((1lh - var(--dsc-suggestion-option-checkmark-size)) / 2); } } diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index 695a12ca37..d36881a49e 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -36,7 +36,10 @@ export const Preview: StoryFn = (args) => { Tomt {DATA_PLACES.map((place) => ( - {place} + + {place} +
Kommune
+
))}
From 17f1f66eb3c77557c47e3f0473d725df6e3676e0 Mon Sep 17 00:00:00 2001 From: Barsnes Date: Thu, 12 Dec 2024 13:53:04 +0100 Subject: [PATCH 07/20] add controleld state --- packages/css/src/suggestion.css | 26 +++++++------ .../Suggestion/Suggestion.stories.tsx | 7 +++- .../src/components/Suggestion/Suggestion.tsx | 37 +++++++++++++++---- .../components/Suggestion/SuggestionClear.tsx | 10 ++++- .../components/Suggestion/SuggestionList.tsx | 7 +++- 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/packages/css/src/suggestion.css b/packages/css/src/suggestion.css index c41eb0e2b2..9e684ccd57 100644 --- a/packages/css/src/suggestion.css +++ b/packages/css/src/suggestion.css @@ -1,12 +1,13 @@ .ds-suggestion { --dsc-suggestion-option-background--selected: var(--ds-color-background-subtle); --dsc-suggestion-option-border-color: var(--ds-color-base-default); - --dsc-combobox-clear-gap: var(--ds-spacing-2); - --dsc-combobox-clear-padding: var(--ds-sizing-1); - --dsc-combobox-clear-size: var(--ds-sizing-9); - --dsc-combobox-clear-icon-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath fill='currentColor' d='M6.53 5.47a.75.75 0 0 0-1.06 1.06L10.94 12l-5.47 5.47a.75.75 0 1 0 1.06 1.06L12 13.06l5.47 5.47a.75.75 0 1 0 1.06-1.06L13.06 12l5.47-5.47a.75.75 0 0 0-1.06-1.06L12 10.94z'/%3E%3C/svg%3E"); + --dsc-suggestion-clear-gap: var(--ds-spacing-2); + --dsc-suggestion-clear-padding: var(--ds-sizing-1); + --dsc-suggestion-clear-size: var(--ds-sizing-9); + --dsc-suggestion-clear-icon-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath fill='currentColor' d='M6.53 5.47a.75.75 0 0 0-1.06 1.06L10.94 12l-5.47 5.47a.75.75 0 1 0 1.06 1.06L12 13.06l5.47 5.47a.75.75 0 1 0 1.06-1.06L13.06 12l5.47-5.47a.75.75 0 0 0-1.06-1.06L12 10.94z'/%3E%3C/svg%3E"); --dsc-suggestion-option-checkmark-url: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221em%22%20height%3D%221em%22%20fill%3D%22none%22%20viewBox%3D%220%200%2024%2024%22%20focusable%3D%22false%22%20role%3D%22img%22%3E%3Cpath%20fill%3D%22currentColor%22%20fill-rule%3D%22evenodd%22%20d%3D%22M18.998%206.94a.75.75%200%200%201%20.063%201.058l-8%209a.75.75%200%200%201-1.091.032l-5-5a.75.75%200%201%201%201.06-1.06l4.438%204.437%207.471-8.405A.75.75%200%200%201%2019%206.939%22%20clip-rule%3D%22evenodd%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E'); --dsc-suggestion-option-checkmark-size: var(--ds-sizing-7); + --dsc-suggestion-list-gap: var(--ds-spacing-2); box-sizing: border-box; display: flex; @@ -25,13 +26,14 @@ overflow-y: auto; position: absolute; z-index: 1600; + margin-top: var(--dsc-suggestion-list-gap); &:not(:has(u-option:hover)) > u-option:focus-visible { border-left-color: var(--dsc-suggestion-option-border-color); background: var(--dsc-suggestion-option-background--selected); } - /* Hide if any is visible */ + /* Hide if any is visible */ &:has(u-option:not([hidden])) > :not(u-option) { display: none; } @@ -92,25 +94,25 @@ display: none; /* We hide the clear button when input is empty */ } &:has(button[type='reset']) input { - padding-inline-end: calc(var(--dsc-combobox-clear-size) + var(--dsc-combobox-clear-gap)); + padding-inline-end: calc(var(--dsc-suggestion-clear-size) + var(--dsc-suggestion-clear-gap)); } & button[type='reset'] { - --dsc-button-size: var(--dsc-combobox-clear-size); + --dsc-button-size: var(--dsc-suggestion-clear-size); align-self: center; - margin-inline-start: calc((var(--dsc-combobox-clear-size) + var(--dsc-combobox-clear-gap)) * -1); + margin-inline-start: calc((var(--dsc-suggestion-clear-size) + var(--dsc-suggestion-clear-gap)) * -1); order: 999; /* Place last */ - padding: var(--dsc-combobox-clear-padding); + padding: var(--dsc-suggestion-clear-padding); position: relative; scale: 0.75; z-index: 2; &::before { content: ''; - height: var(--dsc-combobox-clear-size); - width: var(--dsc-combobox-clear-size); - mask: var(--dsc-combobox-clear-icon-url) center / contain no-repeat; + height: var(--dsc-suggestion-clear-size); + width: var(--dsc-suggestion-clear-size); + mask: var(--dsc-suggestion-clear-icon-url) center / contain no-repeat; background: currentcolor; } } diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index d36881a49e..11129834b4 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -50,6 +50,8 @@ export const Preview: StoryFn = (args) => { export const Controlled: StoryFn = (args) => { const [value, setValue] = useState(''); + console.log('controlled value is ', value); + return ( <> @@ -57,7 +59,10 @@ export const Controlled: StoryFn = (args) => { setValue(value)} + onChange={(value) => { + console.log('onChange', value); + setValue(value); + }} > diff --git a/packages/react/src/components/Suggestion/Suggestion.tsx b/packages/react/src/components/Suggestion/Suggestion.tsx index e673ce8191..bb6ae23fe4 100644 --- a/packages/react/src/components/Suggestion/Suggestion.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.tsx @@ -16,6 +16,8 @@ type SuggestionContextType = { listId?: string; setListId?: (id: string) => void; inputRef?: React.RefObject; + listRef?: React.RefObject; + handleValueChange?: (value: string) => void; }; export const SuggestionContext = createContext({}); @@ -51,30 +53,49 @@ export const Suggestion = forwardRef( const innerRef = useRef(null); const inputRef = useRef(null); + const listRef = useRef(null); const mergedRefs = useMergeRefs([innerRef, ref]); + /* function for sending value changes to consumer */ + const handleValueChange = (value: string) => { + if (value !== internalValue) { + setInternalValue(value); + onChange?.(value); + } + }; + // Handle onChange useEffect(() => { const div = innerRef.current as HTMLDivElement | null; - const handleChange = () => onChange?.(inputRef.current?.value || ''); + const handleChange = () => + handleValueChange(inputRef.current?.value || ''); div?.addEventListener('input', handleChange); return () => div?.removeEventListener('input', handleChange); }, [internalValue]); - // call onChange when internalValue changes - useEffect(() => { - onChange?.(internalValue); - }, [internalValue, onChange]); - // update internalValue and input value when value changes useEffect(() => { if (value && value !== internalValue) setInternalValue(value || ''); + + /* Update input and u-elements value */ inputRef.current && setReactInputValue(inputRef.current, value || ''); - }, [value]); + const options = listRef.current?.querySelectorAll('u-option'); + if (options) { + for (const option of options) { + if (option.value === value) { + option.selected = true; + } else { + option.selected = false; + } + } + } + }, [value, internalValue]); return ( - +
, @@ -30,6 +31,13 @@ export const SuggestionClear = forwardRef< event.preventDefault(); setReactInputValue(inputRef.current, ''); + handleValueChange?.(''); + /* Unselect selected option */ + const option = listRef?.current?.querySelector('[aria-selected="true"]'); + if (option) { + option.setAttribute('aria-selected', 'false'); + option.removeAttribute('selected'); + } inputRef.current.focus(); onClick?.(event); }; diff --git a/packages/react/src/components/Suggestion/SuggestionList.tsx b/packages/react/src/components/Suggestion/SuggestionList.tsx index 17766073aa..1acfb050fc 100644 --- a/packages/react/src/components/Suggestion/SuggestionList.tsx +++ b/packages/react/src/components/Suggestion/SuggestionList.tsx @@ -1,6 +1,7 @@ import type { HTMLAttributes } from 'react'; import { forwardRef, useContext, useEffect } from 'react'; import '@u-elements/u-datalist'; +import { useMergeRefs } from '@floating-ui/react'; import type { DefaultProps } from '../../types'; import { SuggestionContext } from './Suggestion'; @@ -11,7 +12,9 @@ export const SuggestionList = forwardRef< HTMLDataListElement, SuggestionListProps >(function SuggestionList({ className, id, ...rest }, ref) { - const { listId, setListId } = useContext(SuggestionContext); + const { listId, setListId, listRef } = useContext(SuggestionContext); + + const mergedRefs = useMergeRefs([listRef, ref]); useEffect(() => { if (id && listId !== id) setListId?.(id); @@ -21,7 +24,7 @@ export const SuggestionList = forwardRef< ); From 568aa2144e82d5555de450a695ad9850ae0e029e Mon Sep 17 00:00:00 2001 From: Barsnes Date: Thu, 12 Dec 2024 15:20:44 +0100 Subject: [PATCH 08/20] make usable in non-controlled --- packages/css/src/suggestion.css | 14 -------------- .../react/src/components/Suggestion/Suggestion.tsx | 9 ++++++++- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/css/src/suggestion.css b/packages/css/src/suggestion.css index 9e684ccd57..953f168726 100644 --- a/packages/css/src/suggestion.css +++ b/packages/css/src/suggestion.css @@ -39,7 +39,6 @@ } & > * { - grid-template-columns: 1.2em 1fr; padding: var(--ds-spacing-2) var(--ds-spacing-3); padding-inline-start: var(--dsc-suggestion-option-checkmark-size); border: none; @@ -47,23 +46,12 @@ border-left: 5px solid transparent; border-radius: var(--ds-border-radius-sm); justify-content: start; - background: none; text-align: left; - height: auto; cursor: pointer; font-family: inherit; - font-weight: 400; position: relative; - & > * { - grid-column: 1 / 1; - } - &[aria-selected='true'] { - & > * { - grid-column: 2 / 2; - } - &::before { content: ''; position: absolute; @@ -72,8 +60,6 @@ background: currentcolor; width: var(--dsc-suggestion-option-checkmark-size); height: var(--dsc-suggestion-option-checkmark-size); - display: inline-block; - translate: 0 calc((1lh - var(--dsc-suggestion-option-checkmark-size)) / 2); } } diff --git a/packages/react/src/components/Suggestion/Suggestion.tsx b/packages/react/src/components/Suggestion/Suggestion.tsx index bb6ae23fe4..f16aee4e2a 100644 --- a/packages/react/src/components/Suggestion/Suggestion.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.tsx @@ -76,7 +76,14 @@ export const Suggestion = forwardRef( // update internalValue and input value when value changes useEffect(() => { - if (value && value !== internalValue) setInternalValue(value || ''); + /* If value is not set, it is not controlled */ + if (typeof value !== 'string') return; + + if (!onChange) { + console.error("You're setting a value without an onChange handler"); + } + + if (value !== internalValue) setInternalValue(value || ''); /* Update input and u-elements value */ inputRef.current && setReactInputValue(inputRef.current, value || ''); From 735775cdeacf331ab90fc3062448d3a69a9cff1c Mon Sep 17 00:00:00 2001 From: Barsnes Date: Fri, 13 Dec 2024 08:22:45 +0100 Subject: [PATCH 09/20] start removing things --- packages/css/src/suggestion.css | 9 +++++---- .../react/src/components/Suggestion/SuggestionClear.tsx | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/css/src/suggestion.css b/packages/css/src/suggestion.css index 953f168726..bbac922615 100644 --- a/packages/css/src/suggestion.css +++ b/packages/css/src/suggestion.css @@ -40,15 +40,16 @@ & > * { padding: var(--ds-spacing-2) var(--ds-spacing-3); + font-family: inherit; + } + + & > :is(u-option) { padding-inline-start: var(--dsc-suggestion-option-checkmark-size); border: none; - outline: none; border-left: 5px solid transparent; + outline: none; border-radius: var(--ds-border-radius-sm); - justify-content: start; - text-align: left; cursor: pointer; - font-family: inherit; position: relative; &[aria-selected='true'] { diff --git a/packages/react/src/components/Suggestion/SuggestionClear.tsx b/packages/react/src/components/Suggestion/SuggestionClear.tsx index f38da9f136..0958a44c17 100644 --- a/packages/react/src/components/Suggestion/SuggestionClear.tsx +++ b/packages/react/src/components/Suggestion/SuggestionClear.tsx @@ -33,7 +33,7 @@ export const SuggestionClear = forwardRef< setReactInputValue(inputRef.current, ''); handleValueChange?.(''); /* Unselect selected option */ - const option = listRef?.current?.querySelector('[aria-selected="true"]'); + const option = listRef?.current?.querySelector('selected'); if (option) { option.setAttribute('aria-selected', 'false'); option.removeAttribute('selected'); From 92aeb134a4c5c7f84f75ec5571f0f9ff28920f00 Mon Sep 17 00:00:00 2001 From: Barsnes Date: Fri, 13 Dec 2024 08:53:02 +0100 Subject: [PATCH 10/20] misc --- packages/css/src/suggestion.css | 4 +++- .../react/src/components/Suggestion/Suggestion.stories.tsx | 1 + .../react/src/components/Suggestion/SuggestionClear.tsx | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/css/src/suggestion.css b/packages/css/src/suggestion.css index bbac922615..f401c4c7b6 100644 --- a/packages/css/src/suggestion.css +++ b/packages/css/src/suggestion.css @@ -27,10 +27,12 @@ position: absolute; z-index: 1600; margin-top: var(--dsc-suggestion-list-gap); + max-height: 200px; &:not(:has(u-option:hover)) > u-option:focus-visible { border-left-color: var(--dsc-suggestion-option-border-color); background: var(--dsc-suggestion-option-background--selected); + color: var(--ds-color-text-default); } /* Hide if any is visible */ @@ -52,7 +54,7 @@ cursor: pointer; position: relative; - &[aria-selected='true'] { + &[selected] { &::before { content: ''; position: absolute; diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index 11129834b4..11279f2db6 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -24,6 +24,7 @@ const DATA_PLACES = [ 'Stavanger', 'Brønnøysund', 'Trondheim', + 'Bergen', ]; export const Preview: StoryFn = (args) => { diff --git a/packages/react/src/components/Suggestion/SuggestionClear.tsx b/packages/react/src/components/Suggestion/SuggestionClear.tsx index 0958a44c17..500cbbfda2 100644 --- a/packages/react/src/components/Suggestion/SuggestionClear.tsx +++ b/packages/react/src/components/Suggestion/SuggestionClear.tsx @@ -33,10 +33,10 @@ export const SuggestionClear = forwardRef< setReactInputValue(inputRef.current, ''); handleValueChange?.(''); /* Unselect selected option */ - const option = listRef?.current?.querySelector('selected'); + const option = listRef?.current?.querySelector('u-option[selected]'); if (option) { - option.setAttribute('aria-selected', 'false'); - option.removeAttribute('selected'); + /* @ts-ignore -- u-option has this in its interface */ + option.selected = false; } inputRef.current.focus(); onClick?.(event); From bc5ba0a90d10d21cb20aea1077e318a6c43fd902 Mon Sep 17 00:00:00 2001 From: Barsnes Date: Fri, 13 Dec 2024 09:49:06 +0100 Subject: [PATCH 11/20] default value work, and stories --- .../src/components/Suggestion/Suggestion.mdx | 8 +++++ .../Suggestion/Suggestion.stories.tsx | 36 ++++++++++++++----- .../src/components/Suggestion/Suggestion.tsx | 19 ++++++++-- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/react/src/components/Suggestion/Suggestion.mdx b/packages/react/src/components/Suggestion/Suggestion.mdx index 5ba6f80f96..7b08860f73 100644 --- a/packages/react/src/components/Suggestion/Suggestion.mdx +++ b/packages/react/src/components/Suggestion/Suggestion.mdx @@ -9,3 +9,11 @@ Søkbar "select". + +## Kontrollert + + + +## Standard verdi + + diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index 11279f2db6..7ecc27cd3b 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -4,6 +4,7 @@ import { Button } from '../Button'; import { Divider } from '../Divider'; import { Field } from '../Field'; import { Label } from '../Label'; +import { Paragraph } from '../Paragraph'; import { Suggestion } from './'; export default { title: 'Komponenter/Suggestion', @@ -11,7 +12,7 @@ export default { /* add height by default */ decorators: [ (Story) => ( -
+
), @@ -30,9 +31,9 @@ const DATA_PLACES = [ export const Preview: StoryFn = (args) => { return ( - + - + Tomt @@ -51,21 +52,18 @@ export const Preview: StoryFn = (args) => { export const Controlled: StoryFn = (args) => { const [value, setValue] = useState(''); - console.log('controlled value is ', value); - return ( <> - + { - console.log('onChange', value); setValue(value); }} > - + Tomt @@ -78,6 +76,10 @@ export const Controlled: StoryFn = (args) => { + + Du har skrevet inn: {value} + +
), ], + parameters: { + a11y: { + // TODO: these rules should be enabled after figuring out why they occur. + // for some reason it says `aria-expanded` is not allowed + config: { + rules: [ + { + id: 'aria-allowed-attr', + enabled: false, + }, + ], + }, + }, + }, } as Meta; const DATA_PLACES = [ From 7b6f901cccc82c5f8ab04d3a4182d4ced61a99cd Mon Sep 17 00:00:00 2001 From: Barsnes Date: Fri, 13 Dec 2024 14:17:25 +0100 Subject: [PATCH 13/20] example --- .../src/components/Suggestion/Suggestion.tsx | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/react/src/components/Suggestion/Suggestion.tsx b/packages/react/src/components/Suggestion/Suggestion.tsx index 1fdb9bdd36..0f79439424 100644 --- a/packages/react/src/components/Suggestion/Suggestion.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.tsx @@ -79,13 +79,42 @@ export const Suggestion = forwardRef( /* Handle onChange */ useEffect(() => { - const div = innerRef.current as HTMLDivElement | null; - const handleChange = () => - handleValueChange(inputRef.current?.value || ''); + /* If we want input text to be value */ + /* const div = innerRef.current as HTMLDivElement | null; */ + /* const handleChange = () => + handleValueChange(inputRef.current?.value || ''); */ + const options = listRef.current?.options; + + const handleChange = (e: Event) => { + const inputEvent = e as InputEvent; + if (!inputEvent.inputType) { + handleValueChange((e.target as HTMLInputElement)?.value || ''); + } + + /* Check if input matches a value */ + if (options) { + const input = inputEvent.target as HTMLInputElement; + console.log(options); + console.log(input.value); + for (const option of options) { + console.log(option.value); + if (option.value === input.value) { + console.log('I found a match'); + /* Select option */ + option.selected = true; + break; + } + } + } + }; + + inputRef.current?.addEventListener('input', handleChange); + return () => inputRef.current?.removeEventListener('input', handleChange); - div?.addEventListener('input', handleChange); - return () => div?.removeEventListener('input', handleChange); - }, [internalValue]); + /* If we want input text to be value */ + /* div?.addEventListener('input', handleChange); + return () => div?.removeEventListener('input', handleChange); */ + }, [internalValue, listRef, inputRef, handleValueChange]); /* update internalValue and input value when value changes */ useEffect(() => { From 4dfdb3458d3c5e826245778e6e1a0d63688a91ff Mon Sep 17 00:00:00 2001 From: eirikbacker Date: Mon, 16 Dec 2024 12:00:28 +0100 Subject: [PATCH 14/20] fix(Suggestion): add stories and simplify --- packages/css/src/suggestion.css | 7 +- .../Suggestion/Suggestion.stories.tsx | 173 ++++++++++++++--- .../src/components/Suggestion/Suggestion.tsx | 176 +++++++++--------- .../components/Suggestion/SuggestionClear.tsx | 15 +- .../components/Suggestion/SuggestionList.tsx | 8 +- packages/react/src/components/index.ts | 1 + 6 files changed, 258 insertions(+), 122 deletions(-) diff --git a/packages/css/src/suggestion.css b/packages/css/src/suggestion.css index f401c4c7b6..863e1a3fa9 100644 --- a/packages/css/src/suggestion.css +++ b/packages/css/src/suggestion.css @@ -40,6 +40,11 @@ display: none; } + /* Hide datalist if no children */ + &:empty { + display: none; + } + & > * { padding: var(--ds-spacing-2) var(--ds-spacing-3); font-family: inherit; @@ -80,7 +85,7 @@ */ &:has(input:placeholder-shown) button[type='reset'], &:has(input:is(:read-only, :disabled, [aria-disabled='true'])) button[type='reset'] { - display: none; /* We hide the clear button when input is empty */ + visibility: hidden; /* We hide the clear button when input is empty */ } &:has(button[type='reset']) input { padding-inline-end: calc(var(--dsc-suggestion-clear-size) + var(--dsc-suggestion-clear-gap)); diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index 6f31c63644..3528a1cce6 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -1,11 +1,14 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { useState } from 'react'; -import { Button } from '../Button'; -import { Divider } from '../Divider'; -import { Field } from '../Field'; -import { Label } from '../Label'; -import { Paragraph } from '../Paragraph'; -import { Suggestion } from './'; +import { type ChangeEvent, useRef, useState } from 'react'; +import { + Button, + Divider, + Field, + Label, + Paragraph, + Spinner, + Suggestion, +} from '../'; export default { title: 'Komponenter/Suggestion', component: Suggestion, @@ -18,6 +21,9 @@ export default { ), ], parameters: { + customStyles: { + overflow: 'visible', // Show dropdown outside of container + }, a11y: { // TODO: these rules should be enabled after figuring out why they occur. // for some reason it says `aria-expanded` is not allowed @@ -45,9 +51,9 @@ const DATA_PLACES = [ export const Preview: StoryFn = (args) => { return ( - + - + Tomt @@ -69,15 +75,12 @@ export const Controlled: StoryFn = (args) => { return ( <> - - { - setValue(value); - }} - > - + + + setValue(event.target.value)} + /> Tomt @@ -108,9 +111,9 @@ export const Controlled: StoryFn = (args) => { export const DefaultValue: StoryFn = (args) => { return ( - - - + + + Tomt @@ -122,3 +125,131 @@ export const DefaultValue: StoryFn = (args) => { ); }; + +export const CustomFilter: StoryFn = (args) => { + const emails = ['live.com', 'icloud.com', 'hotmail.com', 'gmail.com']; + const [value, setValue] = useState(''); + const [email, setEmail] = useState(''); + + return ( + <> + + + + setValue(event.target.value)} + /> + + + Tomt + {DATA_PLACES.filter( + (_, index) => !value || index === Number(value) - 1, + ).map((text) => ( + // Setting label ensures that item is always displayed regardless of input.value + + {text} + + ))} + + + + + + + setEmail(event.target.value)} + /> + + + Tomt + {email.includes('@') && ( + {email} + )} + {emails.map((suffix) => ( + // Setting label ensures that item is always displayed regardless of input.value + + {`${email.split('@')[0]}@${suffix}`} + + ))} + + + + + ); +}; + +export const AlwaysShowAll: StoryFn = (args) => { + const [value, setValue] = useState('Sogndal'); + + return ( + + + + setValue(event.target.value)} + /> + + + Tomt + {DATA_PLACES.map((place) => ( + // Setting label ensures that item is always displayed regardless of input.value + + {place} + + ))} + + + + ); +}; + +export const FetchExternal: StoryFn = (args) => { + const [value, setValue] = useState(''); + const [options, setOptions] = useState(null); + const debounce = useRef>(); // Debounce so we do not spam the endpoint + + const handleChange = (event: ChangeEvent) => { + const isTyping = (event.nativeEvent as InputEvent).inputType; + const value = encodeURIComponent(event.target.value.trim()); + clearTimeout(debounce.current); + setValue(event.target.value); + + if (!isTyping) return; // Prevent API call if clicking on items in list + setOptions(null); // Clear options + + if (value) + debounce.current = setTimeout(async () => { + const api = `https://restcountries.com/v2/name/${value}?fields=name`; + const countries = await (await fetch(api)).json(); + setOptions( + Array.isArray(countries) ? countries.map(({ name }) => name) : [], + ); + }, 500); + }; + + return ( + + + + + + + {!!value && ( + + {options ? 'Ingen treff' : } + + )} + {options?.map((option) => ( + // Setting label ensures that item is always displayed regardless of input.value + + {option} + + ))} + + + + ); +}; diff --git a/packages/react/src/components/Suggestion/Suggestion.tsx b/packages/react/src/components/Suggestion/Suggestion.tsx index 0f79439424..c2e163b9ac 100644 --- a/packages/react/src/components/Suggestion/Suggestion.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.tsx @@ -3,20 +3,20 @@ import cl from 'clsx/lite'; import { createContext, forwardRef, - useEffect, + // useEffect, useId, useRef, useState, } from 'react'; import type { DefaultProps } from '../../types'; import type { MergeRight } from '../../utilities'; -import { setReactInputValue } from './SuggestionClear'; +// import { setReactInputValue } from './SuggestionClear'; type SuggestionContextType = { listId?: string; setListId?: (id: string) => void; inputRef?: React.RefObject; - listRef?: React.RefObject; + // listRef?: React.RefObject; handleValueChange?: (value: string) => void; }; @@ -47,103 +47,103 @@ export const Suggestion = forwardRef( ref, ) { const [listId, setListId] = useState(useId()); - const [internalValue, setInternalValue] = useState( - defaultValue || '', - ); - const innerRef = useRef(null); const inputRef = useRef(null); - const listRef = useRef(null); const mergedRefs = useMergeRefs([innerRef, ref]); + // const listRef = useRef(null); + + // const [internalValue, setInternalValue] = useState( + // defaultValue || '', + // ); /* function for sending value changes to consumer */ - const handleValueChange = (value: string) => { - if (value !== internalValue) { - setInternalValue(value); - onChange?.(value); - } - }; - - /* Handle default value */ - useEffect(() => { - if (defaultValue) { - if (!inputRef.current || !listRef.current) return; - inputRef.current && setReactInputValue(inputRef.current, defaultValue); - for (const option of listRef.current.options) { - if (option.value === defaultValue) { - option.selected = true; - } - } - } - }, [defaultValue]); + // const handleValueChange = (value: string) => { + // if (value !== internalValue) { + // setInternalValue(value); + // onChange?.(value); + // } + // }; + + // /* Handle default value */ + // useEffect(() => { + // if (defaultValue) { + // if (!inputRef.current || !listRef.current) return; + // inputRef.current && setReactInputValue(inputRef.current, defaultValue); + // for (const option of listRef.current.options) { + // if (option.value === defaultValue) { + // option.selected = true; + // } + // } + // } + // }, [defaultValue]); /* Handle onChange */ - useEffect(() => { - /* If we want input text to be value */ - /* const div = innerRef.current as HTMLDivElement | null; */ - /* const handleChange = () => - handleValueChange(inputRef.current?.value || ''); */ - const options = listRef.current?.options; - - const handleChange = (e: Event) => { - const inputEvent = e as InputEvent; - if (!inputEvent.inputType) { - handleValueChange((e.target as HTMLInputElement)?.value || ''); - } - - /* Check if input matches a value */ - if (options) { - const input = inputEvent.target as HTMLInputElement; - console.log(options); - console.log(input.value); - for (const option of options) { - console.log(option.value); - if (option.value === input.value) { - console.log('I found a match'); - /* Select option */ - option.selected = true; - break; - } - } - } - }; - - inputRef.current?.addEventListener('input', handleChange); - return () => inputRef.current?.removeEventListener('input', handleChange); - - /* If we want input text to be value */ - /* div?.addEventListener('input', handleChange); - return () => div?.removeEventListener('input', handleChange); */ - }, [internalValue, listRef, inputRef, handleValueChange]); + // useEffect(() => { + // /* If we want input text to be value */ + // /* const div = innerRef.current as HTMLDivElement | null; */ + // /* const handleChange = () => + // handleValueChange(inputRef.current?.value || ''); */ + // const options = listRef.current?.options; + + // const handleChange = (e: Event) => { + // const inputEvent = e as InputEvent; + // if (!inputEvent.inputType) { + // handleValueChange((e.target as HTMLInputElement)?.value || ''); + // } + + // /* Check if input matches a value */ + // if (options) { + // const input = inputEvent.target as HTMLInputElement; + // console.log(options); + // console.log(input.value); + // for (const option of options) { + // console.log(option.value); + // if (option.value === input.value) { + // console.log('I found a match'); + // /* Select option */ + // option.selected = true; + // break; + // } + // } + // } + // }; + + // inputRef.current?.addEventListener('input', handleChange); + // return () => inputRef.current?.removeEventListener('input', handleChange); + + // /* If we want input text to be value */ + // /* div?.addEventListener('input', handleChange); + // return () => div?.removeEventListener('input', handleChange); */ + // }, [internalValue, listRef, inputRef, handleValueChange]); /* update internalValue and input value when value changes */ - useEffect(() => { - /* If value is not set, it is not controlled */ - if (typeof value !== 'string') return; - - if (!onChange) { - console.error("You're setting a value without an onChange handler"); - } - - if (value !== internalValue) setInternalValue(value || ''); - - /* Update input and u-elements value */ - inputRef.current && setReactInputValue(inputRef.current, value || ''); - const options = listRef.current?.options; - if (options) { - for (const option of options) { - if (option.value === value) { - option.selected = true; - } else { - option.selected = false; - } - } - } - }, [value, internalValue]); + // useEffect(() => { + // /* If value is not set, it is not controlled */ + // if (typeof value !== 'string') return; + + // if (!onChange) { + // console.error("You're setting a value without an onChange handler"); + // } + + // if (value !== internalValue) setInternalValue(value || ''); + + // /* Update input and u-elements value */ + // inputRef.current && setReactInputValue(inputRef.current, value || ''); + // const options = listRef.current?.options; + // if (options) { + // for (const option of options) { + // if (option.value === value) { + // option.selected = true; + // } else { + // option.selected = false; + // } + // } + // } + // }, [value, internalValue]); return (
, @@ -31,13 +30,13 @@ export const SuggestionClear = forwardRef< event.preventDefault(); setReactInputValue(inputRef.current, ''); - handleValueChange?.(''); + // handleValueChange?.(''); /* Unselect selected option */ - const option = listRef?.current?.querySelector('u-option[selected]'); - if (option) { - /* @ts-ignore -- u-option has this in its interface */ - option.selected = false; - } + // const option = + // listRef?.current?.querySelector('u-option[selected]'); + // if (option) { + // option.selected = false; + // } inputRef.current.focus(); onClick?.(event); }; diff --git a/packages/react/src/components/Suggestion/SuggestionList.tsx b/packages/react/src/components/Suggestion/SuggestionList.tsx index 1acfb050fc..bae1841d6e 100644 --- a/packages/react/src/components/Suggestion/SuggestionList.tsx +++ b/packages/react/src/components/Suggestion/SuggestionList.tsx @@ -1,7 +1,7 @@ import type { HTMLAttributes } from 'react'; import { forwardRef, useContext, useEffect } from 'react'; import '@u-elements/u-datalist'; -import { useMergeRefs } from '@floating-ui/react'; +// import { useMergeRefs } from '@floating-ui/react'; import type { DefaultProps } from '../../types'; import { SuggestionContext } from './Suggestion'; @@ -12,9 +12,9 @@ export const SuggestionList = forwardRef< HTMLDataListElement, SuggestionListProps >(function SuggestionList({ className, id, ...rest }, ref) { - const { listId, setListId, listRef } = useContext(SuggestionContext); + const { listId, setListId } = useContext(SuggestionContext); //, listRef - const mergedRefs = useMergeRefs([listRef, ref]); + // const mergedRefs = useMergeRefs([listRef, ref]); useEffect(() => { if (id && listId !== id) setListId?.(id); @@ -24,7 +24,7 @@ export const SuggestionList = forwardRef< ); diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index c26cbe4c1e..8e3b50f65b 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -36,5 +36,6 @@ export * from './Dropdown'; export * from './Search'; export * from './Card'; export * from './Combobox'; +export * from './Suggestion'; export * from './Table'; export * from './ErrorSummary'; From b4a26178ca7204c9e487d39e73d8a3e661dc7b69 Mon Sep 17 00:00:00 2001 From: Barsnes Date: Fri, 10 Jan 2025 08:30:43 +0100 Subject: [PATCH 15/20] export as EXPERIMENTAL_Suggestion --- .../Suggestion/Suggestion.stories.tsx | 79 ++++++------------- .../react/src/components/Suggestion/index.ts | 22 ++++-- 2 files changed, 38 insertions(+), 63 deletions(-) diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index 3528a1cce6..c0a83facd5 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -1,14 +1,7 @@ import type { Meta, StoryFn } from '@storybook/react'; import { type ChangeEvent, useRef, useState } from 'react'; -import { - Button, - Divider, - Field, - Label, - Paragraph, - Spinner, - Suggestion, -} from '../'; +import { Button, Divider, Field, Label, Paragraph, Spinner } from '../'; +import { EXPERIMENTAL_Suggestion as Suggestion } from './'; export default { title: 'Komponenter/Suggestion', component: Suggestion, @@ -127,56 +120,30 @@ export const DefaultValue: StoryFn = (args) => { }; export const CustomFilter: StoryFn = (args) => { - const emails = ['live.com', 'icloud.com', 'hotmail.com', 'gmail.com']; const [value, setValue] = useState(''); - const [email, setEmail] = useState(''); return ( - <> - - - - setValue(event.target.value)} - /> - - - Tomt - {DATA_PLACES.filter( - (_, index) => !value || index === Number(value) - 1, - ).map((text) => ( - // Setting label ensures that item is always displayed regardless of input.value - - {text} - - ))} - - - - - - - setEmail(event.target.value)} - /> - - - Tomt - {email.includes('@') && ( - {email} - )} - {emails.map((suffix) => ( - // Setting label ensures that item is always displayed regardless of input.value - - {`${email.split('@')[0]}@${suffix}`} - - ))} - - - - + + + + setValue(event.target.value)} + /> + + + Tomt + {DATA_PLACES.filter( + (_, index) => !value || index === Number(value) - 1, + ).map((text) => ( + // Setting label ensures that item is always displayed regardless of input.value + + {text} + + ))} + + + ); }; diff --git a/packages/react/src/components/Suggestion/index.ts b/packages/react/src/components/Suggestion/index.ts index 36734da4e5..633bf4c1ec 100644 --- a/packages/react/src/components/Suggestion/index.ts +++ b/packages/react/src/components/Suggestion/index.ts @@ -5,7 +5,7 @@ import { SuggestionInput } from './SuggestionInput'; import { SuggestionList } from './SuggestionList'; import { SuggestionOption } from './SuggestionOption'; -const Suggestion = Object.assign(SuggestionRoot, { +const EXPERIMENTAL_Suggestion = Object.assign(SuggestionRoot, { List: SuggestionList, Input: SuggestionInput, Empty: SuggestionEmpty, @@ -13,13 +13,21 @@ const Suggestion = Object.assign(SuggestionRoot, { Clear: SuggestionClear, }); -Suggestion.List.displayName = 'Suggestion.List'; -Suggestion.Input.displayName = 'Suggestion.Input'; -Suggestion.Empty.displayName = 'Suggestion.Empty'; -Suggestion.Option.displayName = 'Suggestion.Option'; -Suggestion.Clear.displayName = 'Suggestion.Clear'; +EXPERIMENTAL_Suggestion.displayName = 'EXPERIMENTAL_Suggestion'; +EXPERIMENTAL_Suggestion.List.displayName = 'EXPERIMENTAL_Suggestion.List'; +EXPERIMENTAL_Suggestion.Input.displayName = 'EXPERIMENTAL_Suggestion.Input'; +EXPERIMENTAL_Suggestion.Empty.displayName = 'EXPERIMENTAL_Suggestion.Empty'; +EXPERIMENTAL_Suggestion.Option.displayName = 'EXPERIMENTAL_Suggestion.Option'; +EXPERIMENTAL_Suggestion.Clear.displayName = 'EXPERIMENTAL_Suggestion.Clear'; -export { Suggestion, SuggestionList, SuggestionInput, SuggestionEmpty }; +export { + EXPERIMENTAL_Suggestion, + SuggestionList as EXPERIMENTAL_SuggestionList, + SuggestionInput as EXPERIMENTAL_SuggestionInput, + SuggestionEmpty as EXPERIMENTAL_SuggestionEmpty, + SuggestionOption as EXPERIMENTAL_SuggestionOption, + SuggestionClear as EXPERIMENTAL_SuggestionClear, +}; export type { SuggestionProps } from './Suggestion'; export type { SuggestionListProps } from './SuggestionList'; export type { SuggestionInputProps } from './SuggestionInput'; From cc8dd54a38f764743fb51cafd332a18ef2ba21af Mon Sep 17 00:00:00 2001 From: Barsnes Date: Fri, 10 Jan 2025 09:35:04 +0100 Subject: [PATCH 16/20] use `ds-size-*` --- packages/css/src/suggestion.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/css/src/suggestion.css b/packages/css/src/suggestion.css index 863e1a3fa9..7644b09bb9 100644 --- a/packages/css/src/suggestion.css +++ b/packages/css/src/suggestion.css @@ -1,18 +1,18 @@ .ds-suggestion { --dsc-suggestion-option-background--selected: var(--ds-color-background-subtle); --dsc-suggestion-option-border-color: var(--ds-color-base-default); - --dsc-suggestion-clear-gap: var(--ds-spacing-2); - --dsc-suggestion-clear-padding: var(--ds-sizing-1); - --dsc-suggestion-clear-size: var(--ds-sizing-9); + --dsc-suggestion-clear-gap: var(--ds-size-2); + --dsc-suggestion-clear-padding: var(--ds-size-1); + --dsc-suggestion-clear-size: var(--ds-size-9); --dsc-suggestion-clear-icon-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath fill='currentColor' d='M6.53 5.47a.75.75 0 0 0-1.06 1.06L10.94 12l-5.47 5.47a.75.75 0 1 0 1.06 1.06L12 13.06l5.47 5.47a.75.75 0 1 0 1.06-1.06L13.06 12l5.47-5.47a.75.75 0 0 0-1.06-1.06L12 10.94z'/%3E%3C/svg%3E"); --dsc-suggestion-option-checkmark-url: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221em%22%20height%3D%221em%22%20fill%3D%22none%22%20viewBox%3D%220%200%2024%2024%22%20focusable%3D%22false%22%20role%3D%22img%22%3E%3Cpath%20fill%3D%22currentColor%22%20fill-rule%3D%22evenodd%22%20d%3D%22M18.998%206.94a.75.75%200%200%201%20.063%201.058l-8%209a.75.75%200%200%201-1.091.032l-5-5a.75.75%200%201%201%201.06-1.06l4.438%204.437%207.471-8.405A.75.75%200%200%201%2019%206.939%22%20clip-rule%3D%22evenodd%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E'); - --dsc-suggestion-option-checkmark-size: var(--ds-sizing-7); - --dsc-suggestion-list-gap: var(--ds-spacing-2); + --dsc-suggestion-option-checkmark-size: var(--ds-size-7); + --dsc-suggestion-list-gap: var(--ds-size-2); box-sizing: border-box; display: flex; flex-wrap: wrap; - gap: var(--ds-spacing-1); + gap: var(--ds-size-1); position: relative; & u-datalist { @@ -46,7 +46,7 @@ } & > * { - padding: var(--ds-spacing-2) var(--ds-spacing-3); + padding: var(--ds-size-2) var(--ds-size-3); font-family: inherit; } From a57b126033c639600919dfe045f1f25fe4d268a8 Mon Sep 17 00:00:00 2001 From: Barsnes Date: Fri, 10 Jan 2025 09:44:22 +0100 Subject: [PATCH 17/20] open suggestion when in visual tests --- .../components/Suggestion/Suggestion.stories.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index c0a83facd5..8e04c9df26 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -1,7 +1,9 @@ import type { Meta, StoryFn } from '@storybook/react'; +import { userEvent, within } from '@storybook/test'; import { type ChangeEvent, useRef, useState } from 'react'; import { Button, Divider, Field, Label, Paragraph, Spinner } from '../'; import { EXPERIMENTAL_Suggestion as Suggestion } from './'; + export default { title: 'Komponenter/Suggestion', component: Suggestion, @@ -30,8 +32,19 @@ export default { }, }, }, + play: async (ctx) => { + const storyRoot = ctx.canvasElement; + // Refactored out the play function for easier reuse in the InModal story + await testSuggestion(storyRoot); + }, } as Meta; +async function testSuggestion(el: HTMLElement) { + /* When in test mode, open suggestion by focusing input */ + const input = within(el).getByRole('combobox'); + await userEvent.click(input); +} + const DATA_PLACES = [ 'Sogndal', 'Oslo', From c52bc034711e6f756ddc767f5c16e17edc9c5946 Mon Sep 17 00:00:00 2001 From: Barsnes Date: Fri, 10 Jan 2025 09:55:45 +0100 Subject: [PATCH 18/20] disable failing role test --- .../react/src/components/Suggestion/Suggestion.stories.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/react/src/components/Suggestion/Suggestion.stories.tsx b/packages/react/src/components/Suggestion/Suggestion.stories.tsx index 8e04c9df26..28dd104f31 100644 --- a/packages/react/src/components/Suggestion/Suggestion.stories.tsx +++ b/packages/react/src/components/Suggestion/Suggestion.stories.tsx @@ -28,6 +28,11 @@ export default { id: 'aria-allowed-attr', enabled: false, }, + /* It does not like role="combobox" either */ + { + id: 'aria-allowed-role', + enabled: false, + }, ], }, }, From 8c40d3cf2a6cbfe6b29c758b37099852c431496d Mon Sep 17 00:00:00 2001 From: Tobias Barsnes Date: Fri, 10 Jan 2025 10:16:11 +0100 Subject: [PATCH 19/20] Create real-cats-suffer.md --- .changeset/real-cats-suffer.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/real-cats-suffer.md diff --git a/.changeset/real-cats-suffer.md b/.changeset/real-cats-suffer.md new file mode 100644 index 0000000000..cfe14ce74e --- /dev/null +++ b/.changeset/real-cats-suffer.md @@ -0,0 +1,6 @@ +--- +"@digdir/designsystemet-css": patch +"@digdir/designsystemet-react": patch +--- + +Experimental Suggestion: :sparkles: New component From bab420f9fa2319664cc34e336b61fc7105550c87 Mon Sep 17 00:00:00 2001 From: Barsnes Date: Fri, 10 Jan 2025 10:17:45 +0100 Subject: [PATCH 20/20] fix typo for helptext :) --- .../app/bloggen/2025/helptext-blir-fjerna-kva-gjer-du/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/storefront/app/bloggen/2025/helptext-blir-fjerna-kva-gjer-du/page.mdx b/apps/storefront/app/bloggen/2025/helptext-blir-fjerna-kva-gjer-du/page.mdx index 05c325e86b..ed0c2a93b1 100644 --- a/apps/storefront/app/bloggen/2025/helptext-blir-fjerna-kva-gjer-du/page.mdx +++ b/apps/storefront/app/bloggen/2025/helptext-blir-fjerna-kva-gjer-du/page.mdx @@ -147,7 +147,7 @@ export const HelpText = forwardRef( ### Satt saman -I CodeSanboxen under kan du sjå korleis du sett kodesnuttane over saman. +I CodeSandboxen under kan du sjå korleis du sett kodesnuttane over saman.