@obosbbl/grunnmuren-react 2.0.0-canary.54 → 2.0.0-canary.55

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.d.mts CHANGED
@@ -1,11 +1,12 @@
1
- import { RouterProvider, ButtonProps as ButtonProps$1, LinkProps, CheckboxProps as CheckboxProps$1, CheckboxGroupProps as CheckboxGroupProps$1, ListBoxItemProps, ListBoxSectionProps, HeadingProps as HeadingProps$1, ComboBoxProps, RadioGroupProps as RadioGroupProps$1, RadioProps as RadioProps$1, SelectProps as SelectProps$1, TextFieldProps as TextFieldProps$1, NumberFieldProps as NumberFieldProps$1, ContextValue, BreadcrumbProps as BreadcrumbProps$1, BreadcrumbsProps as BreadcrumbsProps$1, DisclosureProps as DisclosureProps$1 } from 'react-aria-components';
2
- export { ListBoxItemProps as ComboboxItemProps, Form, ListBoxItemProps as SelectItemProps, DisclosureGroup as UNSAFE_DisclosureGroup, DisclosureGroupProps as UNSAFE_DisclosureGroupProps } from 'react-aria-components';
1
+ import { RouterProvider, ButtonProps as ButtonProps$1, LinkProps, CheckboxProps as CheckboxProps$1, CheckboxGroupProps as CheckboxGroupProps$1, ListBoxItemProps, ListBoxSectionProps, HeadingProps as HeadingProps$1, ComboBoxProps, RadioGroupProps as RadioGroupProps$1, RadioProps as RadioProps$1, SelectProps as SelectProps$1, TextFieldProps as TextFieldProps$1, NumberFieldProps as NumberFieldProps$1, ContextValue, BreadcrumbProps as BreadcrumbProps$1, BreadcrumbsProps as BreadcrumbsProps$1, DisclosureProps as DisclosureProps$1, FileTriggerProps as FileTriggerProps$1, TextProps, LabelProps, DialogTriggerProps as DialogTriggerProps$1, ModalOverlayProps, DialogProps as DialogProps$1, TagGroupProps as TagGroupProps$1, TagListProps as TagListProps$1, TagProps as TagProps$1 } from 'react-aria-components';
2
+ export { ListBoxItemProps as ComboboxItemProps, Form, LabelProps, ListBoxItemProps as SelectItemProps, DisclosureGroup as UNSAFE_DisclosureGroup, DisclosureGroupProps as UNSAFE_DisclosureGroupProps } from 'react-aria-components';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import * as react from 'react';
5
- import { HTMLProps, ReactNode, RefAttributes, HTMLAttributes } from 'react';
5
+ import { HTMLProps, ReactNode, RefAttributes, HTMLAttributes, RefObject, Dispatch, SetStateAction, ComponentProps } from 'react';
6
6
  import * as cva from 'cva';
7
7
  import { VariantProps } from 'cva';
8
8
  import { DateFormatterOptions } from 'react-aria';
9
+ import { FormValidationProps } from '@react-stately/form';
9
10
 
10
11
  type Locale = 'nb' | 'sv' | 'en';
11
12
  /**
@@ -503,6 +504,8 @@ type HeadingProps = HTMLProps<HTMLHeadingElement> & {
503
504
  level: 1 | 2 | 3 | 4 | 5 | 6;
504
505
  /** @private Used internally for slotted components */
505
506
  _innerWrapper?: (children: React.ReactNode) => React.ReactNode;
507
+ /** @private Used internally for slotted components */
508
+ _outerWrapper?: (children: React.ReactNode) => React.ReactNode;
506
509
  };
507
510
  declare const HeadingContext: react.Context<ContextValue<Partial<HeadingProps>, HTMLHeadingElement>>;
508
511
  declare const ContentContext: react.Context<ContextValue<Partial<ContentProps>, HTMLDivElement>>;
@@ -654,4 +657,82 @@ type DisclosurePanelProps = Omit<HTMLAttributes<HTMLDivElement>, 'role'> & {
654
657
  } & RefAttributes<HTMLDivElement>;
655
658
  declare const DisclosurePanel: ({ ref: _ref, ..._props }: DisclosurePanelProps) => react_jsx_runtime.JSX.Element;
656
659
 
657
- export { _Accordion as Accordion, _AccordionItem as AccordionItem, type AccordionItemProps, type AccordionProps, Alertbox, type Props as AlertboxProps, _Backlink as Backlink, type BacklinkProps, _Badge as Badge, type BadgeProps, _Breadcrumb as Breadcrumb, type BreadcrumbProps, _Breadcrumbs as Breadcrumbs, type BreadcrumbsProps, _Button as Button, type ButtonProps, Caption, type CaptionProps, Card, CardLink, type CardLinkProps, type CardProps, _Checkbox as Checkbox, _CheckboxGroup as CheckboxGroup, type CheckboxGroupProps, type CheckboxProps, _Combobox as Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, type ComboboxProps, ListBoxSection as ComboboxSection, _Content as Content, ContentContext, type ContentProps, DateFormatter, type DateFormatterProps, Footer, type FooterProps, GrunnmurenProvider, type GrunnmurenProviderProps, _Heading as Heading, HeadingContext, type HeadingProps, type Locale, Media, type MediaProps, _NumberField as NumberField, type NumberFieldProps, _Radio as Radio, _RadioGroup as RadioGroup, type RadioGroupProps, type RadioProps, _Select as Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, type SelectProps, ListBoxSection as SelectSection, _TextArea as TextArea, type TextAreaProps, _TextField as TextField, type TextFieldProps, Disclosure as UNSAFE_Disclosure, DisclosureButton as UNSAFE_DisclosureButton, type DisclosureButtonProps as UNSAFE_DisclosureButtonProps, DisclosurePanel as UNSAFE_DisclosurePanel, type DisclosurePanelProps as UNSAFE_DisclosurePanelProps, type DisclosureProps as UNSAFE_DisclosureProps, VideoLoop, _useLocale as useLocale };
660
+ type FileTriggerProps = Partial<FormValidationProps<File>> & FileTriggerProps$1 & Omit<HTMLAttributes<HTMLInputElement>, 'onSelect' | 'onChange' | 'required' | 'className'> & {
661
+ ref?: RefObject<HTMLInputElement | null>;
662
+ isInvalid?: boolean;
663
+ isRequired?: boolean;
664
+ };
665
+
666
+ type FileUploadProps = Omit<FileTriggerProps, 'onSelect'> & {
667
+ children: ReactNode;
668
+ files?: File[];
669
+ onChange?: Dispatch<SetStateAction<File[]>>;
670
+ validate?: (files: File) => true | string;
671
+ isRequired?: boolean;
672
+ isInvalid?: boolean;
673
+ errorMessage?: string;
674
+ };
675
+ declare const FileUpload: ({ children, files: _files, onChange, validate, isInvalid: _isInvalid, errorMessage, isRequired, allowsMultiple, ref, ...fileTriggerProps }: FileUploadProps) => react_jsx_runtime.JSX.Element;
676
+
677
+ type DescriptionProps = TextProps;
678
+ declare function Description(props: DescriptionProps): react_jsx_runtime.JSX.Element;
679
+
680
+ type ErrorMessageProps = TextProps;
681
+ declare function ErrorMessage(props: ErrorMessageProps): react_jsx_runtime.JSX.Element;
682
+
683
+ declare function Label(props: LabelProps): react_jsx_runtime.JSX.Element;
684
+
685
+ type AvatarProps = ComponentProps<'img'>;
686
+ declare const Avatar: ({ src, alt, className, onError, loading, ...rest }: AvatarProps) => react_jsx_runtime.JSX.Element;
687
+
688
+ type DialogTriggerProps = DialogTriggerProps$1;
689
+ declare const DialogTrigger: (props: DialogTriggerProps) => react_jsx_runtime.JSX.Element;
690
+ type ModalProps = Omit<ModalOverlayProps, 'isDismissable'>;
691
+ declare const Modal: ({ className, ...restProps }: ModalProps) => react_jsx_runtime.JSX.Element;
692
+ type DialogProps = DialogProps$1 & {
693
+ children: React.ReactNode;
694
+ };
695
+ declare const Dialog: ({ className, children, ...restProps }: DialogProps) => react_jsx_runtime.JSX.Element;
696
+
697
+ type TagGroupProps = Omit<TagGroupProps$1, 'className'> & RefAttributes<HTMLDivElement> & {
698
+ /**
699
+ * CSS classes to apply to the tag group
700
+ */
701
+ className?: string;
702
+ /**
703
+ * The function to call when the tag is removed
704
+ */
705
+ onRemove?: (key: React.Key) => void;
706
+ /**
707
+ * The selection mode for the tag group
708
+ * @default "single"
709
+ */
710
+ selectionMode?: 'single' | 'multiple';
711
+ };
712
+ type TagListProps = Omit<TagListProps$1<object>, 'className'> & RefAttributes<HTMLDivElement> & {
713
+ /**
714
+ * CSS classes to apply to the tag list
715
+ */
716
+ className?: string;
717
+ };
718
+ type TagProps = Omit<TagProps$1, 'className'> & RefAttributes<HTMLDivElement> & {
719
+ children: React.ReactNode;
720
+ /**
721
+ * CSS classes to apply to the tag
722
+ */
723
+ className?: string;
724
+ };
725
+ /**
726
+ * A group component for Tag components that enables selection and organization of options.
727
+ */
728
+ declare function TagGroup(props: TagGroupProps): react_jsx_runtime.JSX.Element;
729
+ /**
730
+ * A container component for Tag components within a TagGroup.
731
+ */
732
+ declare function TagList(props: TagListProps): react_jsx_runtime.JSX.Element;
733
+ /**
734
+ * Interactive tag component for selections, filtering, and categorization.
735
+ */
736
+ declare function Tag(props: TagProps): react_jsx_runtime.JSX.Element;
737
+
738
+ export { _Accordion as Accordion, _AccordionItem as AccordionItem, type AccordionItemProps, type AccordionProps, Alertbox, type Props as AlertboxProps, _Backlink as Backlink, type BacklinkProps, _Badge as Badge, type BadgeProps, _Breadcrumb as Breadcrumb, type BreadcrumbProps, _Breadcrumbs as Breadcrumbs, type BreadcrumbsProps, _Button as Button, type ButtonProps, Caption, type CaptionProps, Card, CardLink, type CardLinkProps, type CardProps, _Checkbox as Checkbox, _CheckboxGroup as CheckboxGroup, type CheckboxGroupProps, type CheckboxProps, _Combobox as Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, type ComboboxProps, ListBoxSection as ComboboxSection, _Content as Content, ContentContext, type ContentProps, DateFormatter, type DateFormatterProps, Description, type DescriptionProps, ErrorMessage, type ErrorMessageProps, Footer, type FooterProps, GrunnmurenProvider, type GrunnmurenProviderProps, _Heading as Heading, HeadingContext, type HeadingProps, Label, type Locale, Media, type MediaProps, _NumberField as NumberField, type NumberFieldProps, _Radio as Radio, _RadioGroup as RadioGroup, type RadioGroupProps, type RadioProps, _Select as Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, type SelectProps, ListBoxSection as SelectSection, type TagGroupProps, type TagListProps, type TagProps, _TextArea as TextArea, type TextAreaProps, _TextField as TextField, type TextFieldProps, Avatar as UNSAFE_Avatar, type AvatarProps as UNSAFE_AvatarProps, Dialog as UNSAFE_Dialog, type DialogProps as UNSAFE_DialogProps, DialogTrigger as UNSAFE_DialogTrigger, type DialogTriggerProps as UNSAFE_DialogTriggerProps, Disclosure as UNSAFE_Disclosure, DisclosureButton as UNSAFE_DisclosureButton, type DisclosureButtonProps as UNSAFE_DisclosureButtonProps, DisclosurePanel as UNSAFE_DisclosurePanel, type DisclosurePanelProps as UNSAFE_DisclosurePanelProps, type DisclosureProps as UNSAFE_DisclosureProps, FileUpload as UNSAFE_FileUpload, type FileUploadProps as UNSAFE_FileUploadProps, Modal as UNSAFE_Modal, type ModalProps as UNSAFE_ModalProps, Tag as UNSAFE_Tag, TagGroup as UNSAFE_TagGroup, TagList as UNSAFE_TagList, VideoLoop, _useLocale as useLocale };
package/dist/index.mjs CHANGED
@@ -1,13 +1,17 @@
1
1
  'use client';
2
- import { I18nProvider, RouterProvider, useLocale, useContextProps, Provider, Link, Button as Button$1, Text, CheckboxContext, Checkbox as Checkbox$1, FieldError, Label as Label$1, CheckboxGroup as CheckboxGroup$1, ListBoxItem as ListBoxItem$1, ListBoxSection as ListBoxSection$1, Header, ListBox as ListBox$1, ComboBox, Group, Input, Popover, RadioGroup as RadioGroup$1, Radio as Radio$1, Select as Select$1, SelectValue, TextField as TextField$1, TextArea as TextArea$1, NumberField as NumberField$1, Breadcrumbs as Breadcrumbs$1, Breadcrumb as Breadcrumb$1, ButtonContext, DisclosureContext, DisclosureGroupStateContext, DEFAULT_SLOT } from 'react-aria-components';
2
+ import { I18nProvider, RouterProvider, useLocale, useContextProps, Provider, Link, Button as Button$1, Text, CheckboxContext, Checkbox as Checkbox$1, FieldError, Label as Label$1, CheckboxGroup as CheckboxGroup$1, ListBoxItem as ListBoxItem$1, ListBoxSection as ListBoxSection$1, Header, ListBox as ListBox$1, ComboBox, Group, Input, Popover, RadioGroup as RadioGroup$1, Radio as Radio$1, Select as Select$1, SelectValue, TextField as TextField$1, TextArea as TextArea$1, NumberField as NumberField$1, Breadcrumbs as Breadcrumbs$1, Breadcrumb as Breadcrumb$1, ButtonContext, DisclosureContext, DisclosureGroupStateContext, DEFAULT_SLOT, useSlottedContext, FormContext, FieldErrorContext, LabelContext, InputContext, DialogTrigger as DialogTrigger$1, Modal as Modal$1, Dialog as Dialog$1, ModalOverlay as ModalOverlay$1, TagGroup as TagGroup$1, TagList as TagList$1, Tag as Tag$1 } from 'react-aria-components';
3
3
  export { Form, DisclosureGroup as UNSAFE_DisclosureGroup } from 'react-aria-components';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
- import { ChevronDown, LoadingSpinner, Check, Close, InfoCircle, CheckCircle, Warning, Error, ChevronRight, ChevronLeft, PlayerPause, PlayerPlay } from '@obosbbl/grunnmuren-icons-react';
6
- import { useLayoutEffect, filterDOMProps } from '@react-aria/utils';
5
+ import { ChevronDown, LoadingSpinner, Check, Close, InfoCircle, CheckCircle, Warning, Error, ChevronRight, ChevronLeft, PlayerPause, PlayerPlay, Trash, User } from '@obosbbl/grunnmuren-icons-react';
6
+ import { useLayoutEffect, filterDOMProps, useObjectRef, useFormReset, useUpdateEffect } from '@react-aria/utils';
7
7
  import { cx, cva, compose } from 'cva';
8
- import { createContext, forwardRef, Children, useId, useState, useRef, useEffect, useContext } from 'react';
9
- import { useProgressBar, useDateFormatter, useDisclosure } from 'react-aria';
8
+ import { createContext, forwardRef, Children, useId, useState, useRef, useEffect, useContext, useCallback } from 'react';
9
+ import { useProgressBar, useDateFormatter, useDisclosure, useField } from 'react-aria';
10
10
  import { useDisclosureState } from 'react-stately';
11
+ import { useFormValidation } from '@react-aria/form';
12
+ import { useFormValidationState } from '@react-stately/form';
13
+ import { useControlledState } from '@react-stately/utils';
14
+ import { PressResponder } from '@react-aria/interactions';
11
15
 
12
16
  function GrunnmurenProvider({ children, locale = 'nb', navigate, useHref }) {
13
17
  return /*#__PURE__*/ jsx(I18nProvider, {
@@ -32,14 +36,15 @@ const HeadingContext = /*#__PURE__*/ createContext({});
32
36
  const Heading = (props, ref)=>{
33
37
  // biome-ignore lint/style/noParameterAssign: fix when removing refs for React 19
34
38
  [props, ref] = useContextProps(props, ref, HeadingContext);
35
- const { children, level, className, _innerWrapper: innerWrapper, ...restProps } = props;
39
+ const { children, level, className, _innerWrapper: innerWrapper, _outerWrapper: outerWrapper, ...restProps } = props;
36
40
  const Element = `h${level}`;
37
- return /*#__PURE__*/ jsx(Element, {
41
+ const content = /*#__PURE__*/ jsx(Element, {
38
42
  ...restProps,
39
43
  className: className,
40
44
  "data-slot": "heading",
41
45
  children: innerWrapper ? innerWrapper(children) : children
42
46
  });
47
+ return outerWrapper ? outerWrapper(content) : content;
43
48
  };
44
49
  const ContentContext = /*#__PURE__*/ createContext({});
45
50
  const Content = (props, ref)=>{
@@ -211,6 +216,29 @@ function Badge(props, ref) {
211
216
  }
212
217
  const _Badge = /*#__PURE__*/ forwardRef(Badge);
213
218
 
219
+ const translations$1 = {
220
+ close: {
221
+ nb: 'Lukk',
222
+ sv: 'Stäng',
223
+ en: 'Close'
224
+ },
225
+ pending: {
226
+ nb: 'venter',
227
+ sv: 'väntar',
228
+ en: 'pending'
229
+ },
230
+ showMore: {
231
+ nb: 'Les mer',
232
+ sv: 'Läs mer',
233
+ en: 'Read more'
234
+ },
235
+ showLess: {
236
+ nb: 'Vis mindre',
237
+ sv: 'Dölj',
238
+ en: 'Show less'
239
+ }
240
+ };
241
+
214
242
  /**
215
243
  * Figma: https://www.figma.com/file/9OvSg0ZXI5E1eQYi7AWiWn/Grunnmuren-2.0-%E2%94%82-Designsystem?node-id=30%3A2574&mode=dev
216
244
  */ const buttonVariants = cva({
@@ -307,13 +335,6 @@ const _Badge = /*#__PURE__*/ forwardRef(Badge);
307
335
  function isLinkProps$1(props) {
308
336
  return !!props.href;
309
337
  }
310
- const translations$1 = {
311
- pending: {
312
- nb: 'venter',
313
- sv: 'väntar',
314
- en: 'pending'
315
- }
316
- };
317
338
  function Button(props, ref) {
318
339
  const { children: _children, color, isIconOnly, isLoading, variant, isPending: _isPending, ...restProps } = props;
319
340
  const isPending = _isPending || isLoading;
@@ -354,7 +375,7 @@ function Button(props, ref) {
354
375
  const _Button = /*#__PURE__*/ forwardRef(Button);
355
376
 
356
377
  const formField = cx('group flex flex-col gap-2');
357
- const formFieldError = cx('w-fit rounded-sm bg-red-light px-2 py-1 text-red text-sm leading-6');
378
+ const formFieldError = cx('w-fit bg-red-light px-2 py-1 text-red text-sm leading-6', 'group-data-[slot=file-upload]:rounded-lg');
358
379
  const input = cva({
359
380
  base: [
360
381
  // All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
@@ -933,23 +954,6 @@ const alertVariants = cva({
933
954
  variant: 'info'
934
955
  }
935
956
  });
936
- const translations = {
937
- close: {
938
- nb: 'Lukk',
939
- sv: 'Stäng',
940
- en: 'Close'
941
- },
942
- showMore: {
943
- nb: 'Les mer',
944
- sv: 'Läs mer',
945
- en: 'Read more'
946
- },
947
- showLess: {
948
- nb: 'Vis mindre',
949
- sv: 'Dölj',
950
- en: 'Show less'
951
- }
952
- };
953
957
  const Alertbox = ({ children, role, className, icon, variant = 'info', isDismissable = false, isDismissed, onDismiss, isExpandable })=>{
954
958
  const Icon = icon ?? iconMap[variant];
955
959
  const locale = _useLocale();
@@ -986,7 +990,7 @@ const Alertbox = ({ children, role, className, icon, variant = 'info', isDismiss
986
990
  isDismissable && /*#__PURE__*/ jsx("button", {
987
991
  className: cx('-m-2 grid h-11 w-11 place-items-center rounded-xl', 'focus-visible:-outline-offset-8 focus-visible:outline-focus'),
988
992
  onClick: close,
989
- "aria-label": translations.close[locale],
993
+ "aria-label": translations$1.close[locale],
990
994
  type: "button",
991
995
  children: /*#__PURE__*/ jsx(Close, {})
992
996
  }),
@@ -998,7 +1002,7 @@ const Alertbox = ({ children, role, className, icon, variant = 'info', isDismiss
998
1002
  "aria-controls": id,
999
1003
  type: "button",
1000
1004
  children: [
1001
- isExpanded ? translations.showLess[locale] : translations.showMore[locale],
1005
+ isExpanded ? translations$1.showLess[locale] : translations$1.showMore[locale],
1002
1006
  /*#__PURE__*/ jsx(ChevronDown, {
1003
1007
  className: cx('transition-transform duration-150 motion-reduce:transition-none', isExpanded && 'rotate-180')
1004
1008
  })
@@ -1500,4 +1504,421 @@ const DisclosurePanel = ({ ref: _ref, ..._props })=>{
1500
1504
  });
1501
1505
  };
1502
1506
 
1503
- export { _Accordion as Accordion, _AccordionItem as AccordionItem, Alertbox, _Backlink as Backlink, _Badge as Badge, _Breadcrumb as Breadcrumb, _Breadcrumbs as Breadcrumbs, _Button as Button, Caption, Card, CardLink, _Checkbox as Checkbox, _CheckboxGroup as CheckboxGroup, _Combobox as Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, _Content as Content, ContentContext, DateFormatter, Footer, GrunnmurenProvider, _Heading as Heading, HeadingContext, Media, _NumberField as NumberField, _Radio as Radio, _RadioGroup as RadioGroup, _Select as Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, _TextArea as TextArea, _TextField as TextField, Disclosure as UNSAFE_Disclosure, DisclosureButton as UNSAFE_DisclosureButton, DisclosurePanel as UNSAFE_DisclosurePanel, VideoLoop, _useLocale as useLocale };
1507
+ /**
1508
+ * A FileTrigger allows a user to access the file system with any pressable React Aria or React Spectrum component, or custom components built with usePress.
1509
+ */ const FileTrigger = (props)=>{
1510
+ const { onSelect, acceptedFileTypes, allowsMultiple, defaultCamera, children, acceptDirectory, ref, isInvalid, isRequired, name, value, ...rest } = props;
1511
+ const inputRef = useObjectRef(ref);
1512
+ return /*#__PURE__*/ jsxs(Fragment, {
1513
+ children: [
1514
+ /*#__PURE__*/ jsx(PressResponder, {
1515
+ onPress: ()=>{
1516
+ if (inputRef.current?.value) {
1517
+ inputRef.current.value = '';
1518
+ }
1519
+ inputRef.current?.click();
1520
+ },
1521
+ children: children
1522
+ }),
1523
+ /*#__PURE__*/ jsx(Input, {
1524
+ ...rest,
1525
+ required: isRequired,
1526
+ "aria-invalid": isInvalid,
1527
+ "data-invalid": isInvalid,
1528
+ "data-rac": true,
1529
+ name: Array.isArray(name) ? name.join(' ') : name,
1530
+ type: "file",
1531
+ ref: inputRef,
1532
+ accept: acceptedFileTypes?.toString(),
1533
+ onChange: (e)=>onSelect?.(e.target.files),
1534
+ capture: defaultCamera,
1535
+ multiple: allowsMultiple,
1536
+ // @ts-expect-error
1537
+ webkitdirectory: acceptDirectory ? '' : undefined,
1538
+ // This is a work around to prevent error in the console when attempting to submit a form with a required and empty file input
1539
+ // RAC uses display: none, which prevents the file input from being focused.
1540
+ // What we do instead is to hide it visually using custom CSS, so that the native HTML validation messages are still hidden. Which is why
1541
+ // we don't use the sr-only class.
1542
+ className: "absolute left-[-1000vw] opacity-0",
1543
+ // Finally, we add aria-hidden to prevent the file input from being read by screen readers
1544
+ "aria-hidden": true,
1545
+ // Prevent focus trap when tabbing (since focus is delegated to the button)
1546
+ tabIndex: -1
1547
+ })
1548
+ ]
1549
+ });
1550
+ };
1551
+
1552
+ const translations = {
1553
+ remove: {
1554
+ nb: 'Fjern',
1555
+ sv: 'Ta bort',
1556
+ en: 'Remove'
1557
+ }
1558
+ };
1559
+ /**
1560
+ * Converts an array of files to a DataTransfer object which can be used as a FileList.
1561
+ * This is necessary for setting the files on a native file input.
1562
+ * @param files An array of files
1563
+ * @returns The files as a DataTransfer object which can be used as a FileList
1564
+ */ function filesToDataTransfer(files) {
1565
+ const dataTransfer = new DataTransfer();
1566
+ for (const file of files){
1567
+ dataTransfer.items.add(file);
1568
+ }
1569
+ return dataTransfer.files;
1570
+ }
1571
+ /**
1572
+ * Generates unique file names for files with the same original name.
1573
+ * Any duplicate files will have a number in parentheses appended to their name.
1574
+ * @param files An array of files
1575
+ * @returns An array of files with unique names
1576
+ */ function uniqueFileNames(files) {
1577
+ const fileNameCounts = {};
1578
+ return files.map((file)=>{
1579
+ const fileName = file.name;
1580
+ // Filter out the extension and any trailing numbers in parentheses
1581
+ const baseName = fileName.replace(/\s*\(\d+\)|(\.[^.]+)$/g, '');
1582
+ // Extract the file extension
1583
+ const extension = fileName.match(/(\.[^.]+)$/)?.[0] || '';
1584
+ if (!fileNameCounts[baseName]) {
1585
+ // Extract any number from the file name (if any, otherwise default to 0)
1586
+ const baseNameCount = Number.parseInt(fileName.match(/\((\d+)\)/)?.[1] ?? '0');
1587
+ fileNameCounts[baseName] = baseNameCount;
1588
+ }
1589
+ fileNameCounts[baseName]++;
1590
+ if (fileNameCounts[baseName] > 1) {
1591
+ return new File([
1592
+ file
1593
+ ], // Follow the pattern of adding a number in parentheses to the base name (e.g. "file (1).txt")
1594
+ `${baseName} (${fileNameCounts[baseName] - 1})${extension}`);
1595
+ }
1596
+ return file;
1597
+ });
1598
+ }
1599
+ const FileUpload = ({ children, files: _files, onChange, validate, isInvalid: _isInvalid, errorMessage, isRequired, allowsMultiple, ref, ...fileTriggerProps })=>{
1600
+ const [files, setFiles] = useState(_files ?? []);
1601
+ const isInvalid = !!errorMessage || _isInvalid;
1602
+ const id = useId();
1603
+ const locale = _useLocale();
1604
+ const _inputRef = useRef(null);
1605
+ const inputRef = ref ?? _inputRef;
1606
+ const slottedContext = useSlottedContext(FormContext) || {};
1607
+ const validationBehavior = fileTriggerProps.validationBehavior ?? slottedContext.validationBehavior ?? 'native';
1608
+ const validateFiles = useCallback((files)=>{
1609
+ if (validate === undefined) return true;
1610
+ const errors = [];
1611
+ for (const file of files){
1612
+ if (!validate) continue;
1613
+ const validation = validate(file);
1614
+ if (typeof validation === 'string') errors.push(validation);
1615
+ }
1616
+ if (errors.length === 0) return true;
1617
+ return errors;
1618
+ }, [
1619
+ validate
1620
+ ]);
1621
+ const validationState = useFormValidationState({
1622
+ ...fileTriggerProps,
1623
+ validationBehavior,
1624
+ validate: validateFiles,
1625
+ isRequired,
1626
+ isInvalid,
1627
+ value: _files ?? files ?? null
1628
+ });
1629
+ const controlledOrUncontrolledFiles = // Use controlled files if they are provided, otherwise use internal files - but map them to File objects (remove validation prop)
1630
+ _files ?? files;
1631
+ useEffect(()=>{
1632
+ // Keep the native file input in sync with the internal file state
1633
+ if (inputRef.current) {
1634
+ inputRef.current.files = filesToDataTransfer(controlledOrUncontrolledFiles);
1635
+ }
1636
+ }, [
1637
+ controlledOrUncontrolledFiles,
1638
+ inputRef
1639
+ ]);
1640
+ const { fieldProps } = useField({
1641
+ ...fileTriggerProps,
1642
+ validate: validateFiles,
1643
+ validationBehavior,
1644
+ isInvalid,
1645
+ errorMessage
1646
+ });
1647
+ const [value, setValue] = useControlledState(_files, [], onChange);
1648
+ useFormReset(inputRef, value, setValue);
1649
+ useFormValidation({
1650
+ ...fileTriggerProps,
1651
+ validationBehavior,
1652
+ validate: validateFiles,
1653
+ isRequired,
1654
+ isInvalid
1655
+ }, validationState, inputRef);
1656
+ const buttonRef = useRef(null);
1657
+ const { displayValidation } = validationState;
1658
+ useUpdateEffect(()=>{
1659
+ // Fixes a bug where validation state is not reset after being set to customError
1660
+ // This happens if the file upload ends up in an invalid state and then is emptied: the old valdiations is still lingering
1661
+ if (controlledOrUncontrolledFiles.length === 0 && validationState.displayValidation.validationDetails.customError) {
1662
+ validationState.commitValidation();
1663
+ }
1664
+ }, [
1665
+ controlledOrUncontrolledFiles
1666
+ ]);
1667
+ return /*#__PURE__*/ jsx(Provider, {
1668
+ values: [
1669
+ [
1670
+ FieldErrorContext,
1671
+ displayValidation
1672
+ ]
1673
+ ],
1674
+ children: /*#__PURE__*/ jsxs("div", {
1675
+ "data-slot": "file-upload",
1676
+ className: "group grid w-72 max-w-full gap-2",
1677
+ "data-required": isRequired,
1678
+ children: [
1679
+ /*#__PURE__*/ jsx(Provider, {
1680
+ values: [
1681
+ [
1682
+ LabelContext,
1683
+ {
1684
+ htmlFor: id
1685
+ }
1686
+ ],
1687
+ [
1688
+ ButtonContext,
1689
+ {
1690
+ // The button acts as the trigger for the file input, which is why we connect the label to the button id
1691
+ id,
1692
+ // Needed for RAC auto-focusing behavior to work
1693
+ ref: buttonRef,
1694
+ className: 'w-fit'
1695
+ }
1696
+ ],
1697
+ [
1698
+ InputContext,
1699
+ fieldProps
1700
+ ]
1701
+ ],
1702
+ children: /*#__PURE__*/ jsx(FileTrigger, {
1703
+ ...fileTriggerProps,
1704
+ isRequired: isRequired,
1705
+ allowsMultiple: allowsMultiple,
1706
+ onSelect: (selectedFiles)=>{
1707
+ if (selectedFiles === null) return;
1708
+ const newFiles = Array.from(selectedFiles);
1709
+ // For controlled component
1710
+ onChange?.((prevFiles)=>allowsMultiple ? uniqueFileNames(prevFiles.concat(newFiles)) : newFiles);
1711
+ // For internal file state
1712
+ setFiles((prevFiles)=>allowsMultiple ? uniqueFileNames(prevFiles.concat(newFiles)) : newFiles);
1713
+ },
1714
+ isInvalid: isInvalid || validationState.displayValidation.isInvalid,
1715
+ ref: inputRef,
1716
+ // Delegate focus to the button when the hidden file input is focused (for RAC auto-focusing behavior)
1717
+ onFocus: ()=>buttonRef.current?.focus(),
1718
+ children: children
1719
+ })
1720
+ }),
1721
+ /*#__PURE__*/ jsx("ul", {
1722
+ className: "mt-4 grid gap-y-2",
1723
+ children: controlledOrUncontrolledFiles.map((file, fileIndex)=>{
1724
+ let fileName = file.name;
1725
+ if (fileTriggerProps.acceptDirectory && file.webkitRelativePath !== '') {
1726
+ fileName = file.webkitRelativePath;
1727
+ }
1728
+ const validation = validate?.(file) ?? true;
1729
+ const hasError = validation !== true;
1730
+ return /*#__PURE__*/ jsxs("li", {
1731
+ children: [
1732
+ /*#__PURE__*/ jsxs("div", {
1733
+ className: cx('flex items-center justify-between gap-2 rounded-lg border-2 px-4 py-2', hasError ? 'border-red bg-red-light' : 'border-gray bg-gray-lightest'),
1734
+ children: [
1735
+ fileName,
1736
+ ' ',
1737
+ /*#__PURE__*/ jsx("button", {
1738
+ className: cx('-m-2 grid h-11 w-11 shrink-0 place-items-center rounded-xl', // Focus styles
1739
+ 'focus-visible:-outline-offset-8 focus-visible:outline-focus'),
1740
+ onClick: ()=>{
1741
+ // For controlled component
1742
+ onChange?.((prevFiles)=>prevFiles.filter((_, index)=>index !== fileIndex));
1743
+ // For internal file state
1744
+ setFiles((prevFiles)=>prevFiles.filter((_, index)=>index !== fileIndex));
1745
+ },
1746
+ "aria-label": translations.remove[locale],
1747
+ type: "button",
1748
+ children: /*#__PURE__*/ jsx(Trash, {})
1749
+ })
1750
+ ]
1751
+ }),
1752
+ hasError && /*#__PURE__*/ jsx(ErrorMessage, {
1753
+ className: "mt-1 block w-full",
1754
+ children: validation
1755
+ })
1756
+ ]
1757
+ }, fileName);
1758
+ })
1759
+ }),
1760
+ !!errorMessage || controlledOrUncontrolledFiles.length === 0 && /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
1761
+ errorMessage: errorMessage
1762
+ })
1763
+ ]
1764
+ })
1765
+ });
1766
+ };
1767
+
1768
+ const baseClassName = 'h-20 w-20 flex-shrink-0 rounded-full';
1769
+ const Avatar = ({ src, alt = '', className, onError, loading = 'lazy', ...rest })=>{
1770
+ const [hasError, setHasError] = useState(false);
1771
+ const hasValidImage = !hasError && src;
1772
+ return hasValidImage ? /*#__PURE__*/ jsx("img", {
1773
+ ...rest,
1774
+ src: src,
1775
+ alt: alt,
1776
+ loading: loading,
1777
+ className: cx(className, baseClassName, 'object-cover'),
1778
+ onError: (event)=>{
1779
+ onError?.(event);
1780
+ setHasError(true);
1781
+ }
1782
+ }) : /*#__PURE__*/ jsx("div", {
1783
+ className: cx(className, baseClassName, 'grid place-items-center bg-gray-light text-gray-dark'),
1784
+ children: /*#__PURE__*/ jsx(User, {
1785
+ className: "scale-[2.25]"
1786
+ })
1787
+ });
1788
+ };
1789
+
1790
+ const DialogTrigger = (props)=>/*#__PURE__*/ jsx(DialogTrigger$1, {
1791
+ ...props
1792
+ });
1793
+ const ModalOverlay = (props)=>/*#__PURE__*/ jsx(ModalOverlay$1, {
1794
+ ...props,
1795
+ isDismissable: true,
1796
+ className: ({ isEntering, isExiting })=>cx('fixed inset-0 z-10 flex min-h-full items-center justify-center overflow-y-auto bg-black/25 p-4 text-center backdrop-blur', isEntering && 'fade-in animate-in duration-300 ease-out', isExiting && 'fade-out animate-out duration-200 ease-in', // Using the motion-safe class does not work, so we use motion-reduce to overwrite instead
1797
+ 'motion-reduce:animate-none')
1798
+ });
1799
+ const Modal = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ModalOverlay, {
1800
+ children: /*#__PURE__*/ jsx(Modal$1, {
1801
+ ...restProps,
1802
+ className: ({ isEntering, isExiting })=>cx(className, 'w-full max-w-md overflow-hidden rounded-2xl bg-white p-4 text-left align-middle shadow-xl', isEntering && 'zoom-in-95 animate-in duration-300 ease-out', isExiting && 'zoom-out-95 animate-out duration-200 ease-in', // Using the motion-safe class does not work, so we use motion-reduce to overwrite instead
1803
+ 'motion-reduce:animate-none')
1804
+ })
1805
+ });
1806
+ const Dialog = ({ className, children, ...restProps })=>{
1807
+ const locale = _useLocale();
1808
+ return /*#__PURE__*/ jsx(Dialog$1, {
1809
+ ...restProps,
1810
+ className: cx('relative grid gap-y-5 outline-none', // Footer
1811
+ '[&_[data-slot="footer"]]:flex [&_[data-slot="footer"]]:gap-x-2'),
1812
+ children: ({ close })=>/*#__PURE__*/ jsx(Fragment, {
1813
+ children: /*#__PURE__*/ jsx(Provider, {
1814
+ values: [
1815
+ [
1816
+ HeadingContext,
1817
+ {
1818
+ slots: {
1819
+ [DEFAULT_SLOT]: {},
1820
+ title: {
1821
+ className: 'heading-s',
1822
+ _outerWrapper: (children)=>/*#__PURE__*/ jsxs("div", {
1823
+ className: "flex items-center justify-between gap-x-2",
1824
+ children: [
1825
+ children,
1826
+ /*#__PURE__*/ jsx(_Button, {
1827
+ slot: "close" // RAC Dialog suppors one close button out of the box, so we utilize that here. For other close buttons we use ButtonContext
1828
+ ,
1829
+ variant: "tertiary",
1830
+ className: "!px-2.5 data-[focus-visible]:outline-focus-inset",
1831
+ "aria-label": translations$1.close[locale],
1832
+ children: /*#__PURE__*/ jsx(Close, {})
1833
+ })
1834
+ ]
1835
+ })
1836
+ }
1837
+ }
1838
+ }
1839
+ ],
1840
+ [
1841
+ ButtonContext,
1842
+ {
1843
+ // This is necessary to support multiple close buttons
1844
+ slots: {
1845
+ // We need to define default slot in order to also support non-slotted buttons (i.e. buttons without slot prop)
1846
+ [DEFAULT_SLOT]: {
1847
+ className: 'w-fit'
1848
+ },
1849
+ close: {
1850
+ onPress: close,
1851
+ className: 'w-fit'
1852
+ }
1853
+ }
1854
+ }
1855
+ ]
1856
+ ],
1857
+ children: children
1858
+ })
1859
+ })
1860
+ });
1861
+ };
1862
+
1863
+ const tagVariants = cva({
1864
+ base: [
1865
+ 'relative flex cursor-pointer items-center gap-1 rounded-lg px-2 py-1 font-medium text-sm transition-colors duration-200',
1866
+ //Focus
1867
+ 'focus-visible:outline-focus-offset [&:not([data-focus-visible])]:outline-none',
1868
+ //Border
1869
+ 'border border-2 border-blue-dark',
1870
+ //Backgrounds
1871
+ 'data-[hovered]:!bg-sky bg-white text-black aria-selected:bg-sky-light data-[allows-removing]:bg-sky-light',
1872
+ //Icons
1873
+ '[&_svg]:h-4 [&_svg]:w-4'
1874
+ ]
1875
+ });
1876
+ /**
1877
+ * A group component for Tag components that enables selection and organization of options.
1878
+ */ function TagGroup(props) {
1879
+ const { onRemove, selectionMode = 'single', className, children, ...restProps } = props;
1880
+ return /*#__PURE__*/ jsx(TagGroup$1, {
1881
+ ...restProps,
1882
+ className: className,
1883
+ selectionMode: onRemove ? 'none' : selectionMode,
1884
+ onRemove: onRemove,
1885
+ children: children
1886
+ });
1887
+ }
1888
+ /**
1889
+ * A container component for Tag components within a TagGroup.
1890
+ */ function TagList(props) {
1891
+ const { className, children, ...restProps } = props;
1892
+ return /*#__PURE__*/ jsx(TagList$1, {
1893
+ ...restProps,
1894
+ className: cx('flex flex-wrap gap-2', className),
1895
+ children: children
1896
+ });
1897
+ }
1898
+ /**
1899
+ * Interactive tag component for selections, filtering, and categorization.
1900
+ */ function Tag(props) {
1901
+ const { className, children, ...restProps } = props;
1902
+ const textValue = typeof children === 'string' ? children : undefined;
1903
+ return /*#__PURE__*/ jsx(Tag$1, {
1904
+ className: tagVariants({
1905
+ className
1906
+ }),
1907
+ textValue: textValue,
1908
+ ...restProps,
1909
+ children: ({ allowsRemoving })=>allowsRemoving ? /*#__PURE__*/ jsxs(Fragment, {
1910
+ children: [
1911
+ children,
1912
+ /*#__PURE__*/ jsx(Button$1, {
1913
+ className: "outline-none after:absolute after:top-0 after:right-0 after:bottom-0 after:left-0",
1914
+ slot: "remove",
1915
+ children: /*#__PURE__*/ jsx(Close, {
1916
+ className: "ml-1"
1917
+ })
1918
+ })
1919
+ ]
1920
+ }) : children
1921
+ });
1922
+ }
1923
+
1924
+ export { _Accordion as Accordion, _AccordionItem as AccordionItem, Alertbox, _Backlink as Backlink, _Badge as Badge, _Breadcrumb as Breadcrumb, _Breadcrumbs as Breadcrumbs, _Button as Button, Caption, Card, CardLink, _Checkbox as Checkbox, _CheckboxGroup as CheckboxGroup, _Combobox as Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, _Content as Content, ContentContext, DateFormatter, Description, ErrorMessage, Footer, GrunnmurenProvider, _Heading as Heading, HeadingContext, Label, Media, _NumberField as NumberField, _Radio as Radio, _RadioGroup as RadioGroup, _Select as Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, _TextArea as TextArea, _TextField as TextField, Avatar as UNSAFE_Avatar, Dialog as UNSAFE_Dialog, DialogTrigger as UNSAFE_DialogTrigger, Disclosure as UNSAFE_Disclosure, DisclosureButton as UNSAFE_DisclosureButton, DisclosurePanel as UNSAFE_DisclosurePanel, FileUpload as UNSAFE_FileUpload, Modal as UNSAFE_Modal, Tag as UNSAFE_Tag, TagGroup as UNSAFE_TagGroup, TagList as UNSAFE_TagList, VideoLoop, _useLocale as useLocale };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obosbbl/grunnmuren-react",
3
- "version": "2.0.0-canary.54",
3
+ "version": "2.0.0-canary.55",
4
4
  "description": "Grunnmuren components in React",
5
5
  "repository": {
6
6
  "url": "https://github.com/code-obos/grunnmuren"
@@ -19,11 +19,15 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@obosbbl/grunnmuren-icons-react": "^2.0.0-canary.7",
22
- "@react-aria/utils": "^3.25.1",
22
+ "@react-aria/form": "^3.0.14",
23
+ "@react-aria/interactions": "^3.24.1",
24
+ "@react-aria/utils": "^3.28.1",
25
+ "@react-stately/form": "^3.1.2",
26
+ "@react-stately/utils": "^3.10.5",
23
27
  "@types/node": "^22.0.0",
24
28
  "cva": "^1.0.0-0",
25
- "react-aria": "^3.35.1",
26
- "react-aria-components": "^1.3.1",
29
+ "react-aria": "^3.38.1",
30
+ "react-aria-components": "^1.7.1",
27
31
  "react-stately": "^3.35.0"
28
32
  },
29
33
  "peerDependencies": {