diff --git a/.changeset/brave-windows-tap.md b/.changeset/brave-windows-tap.md new file mode 100644 index 000000000..896c96bb8 --- /dev/null +++ b/.changeset/brave-windows-tap.md @@ -0,0 +1,70 @@ +--- +"@obosbbl/grunnmuren-react": patch +--- + +## Breaking Beta change +The `` API has now been refactored to support headings inside link lists. + +- `` no longer supports link props, the component must now receive a `` as a child to which link props are passed +- The `isExternal` prop has been removed ``. External links are now identified byt the `rel` prop on the `` child (e.g ``) + + +### Before +``` tsx + + Les mer + + Medlemsvilkår + + + Tryg forsikring + + +``` + +### Now +``` tsx + + + Bolig + + + + Medlemsvilkår + + + + + Tryg forsikring + + + +``` + +## Use Headings (with links) + +``` tsx + + + OBOS + + + + Bolig + + + Bank + + + Medlem + + + +``` \ No newline at end of file diff --git a/.changeset/mean-coats-admire.md b/.changeset/mean-coats-admire.md new file mode 100644 index 000000000..48cac5a96 --- /dev/null +++ b/.changeset/mean-coats-admire.md @@ -0,0 +1,5 @@ +--- +"@obosbbl/grunnmuren-react": patch +--- + +Better screen reader support in the `` component: annonuce external links. diff --git a/.changeset/orange-breads-flow.md b/.changeset/orange-breads-flow.md new file mode 100644 index 000000000..3c956c20e --- /dev/null +++ b/.changeset/orange-breads-flow.md @@ -0,0 +1,5 @@ +--- +"@obosbbl/grunnmuren-tailwind": patch +--- + +Styles for headings and icons inside link lists. diff --git a/packages/react/src/link-list/link-list.stories.tsx b/packages/react/src/link-list/link-list.stories.tsx index ae15325b6..cf11bff6c 100644 --- a/packages/react/src/link-list/link-list.stories.tsx +++ b/packages/react/src/link-list/link-list.stories.tsx @@ -1,4 +1,6 @@ import type { Meta } from '@storybook/react-vite'; +import { Heading } from '../content'; +import { UNSAFE_Link as Link } from '../link'; import { UNSAFE_LinkList as LinkList, UNSAFE_LinkListContainer as LinkListContainer, @@ -16,191 +18,360 @@ export default meta; export const Default = () => ( - Bolig - Bank - Medlem + + Bolig + + + Bank + + + Medlem + ); export const Download = () => ( - - Medlemsvilkår + + + Medlemsvilkår + - - Samtykke + + + Samtykke + ); + +const LayoutWrapper = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + export const ExternalLinkListItems = () => ( - Forsikring - - Les mer om trygg forsikring + + + OBOS Nett - Min side + + + + + Les mer om trygg forsikring + ); +export const WithHeadings = () => ( + + + + OBOS + + + + Bolig + + + Bank + + + Medlem + + + + + + + OBOS EXTERN + + + + + + OBOS Nett - Min side + + + + + Les mer om trygg forsikring + + + + + + + + Årsrapport + + + + + + Klimarisikorapport + + + + + Klimagassberegning + + + + + +); + export const AutoResponsive = () => ( -
-

2 items

+ + 2 items - - Medlemsvilkår + + + Medlemsvilkår + - - Samtykke + + + Samtykke + -

3 items

+ 3 items - Bolig - Bank - Medlem + + Bolig + + + Bank + + + Medlem + -

5 items

+ 5 items - Konsernledelsen - Styret - - Representantskapet + + Konsernledelsen + + + Styret - - Boligpriser og statistikk + + Representantskapet - - Investor Relations + + + Boligpriser og statistikk + + + + Investor Relations -

6 items

+ 6 items - Konsernledelsen - Styret - - Representantskapet + + Konsernledelsen + + + Styret + + + Representantskapet - - Boligpriser og statistikk + + + Boligpriser og statistikk + - - Investor Relations + + Investor Relations - - Digital årsrapport + + Digital årsrapport -

7 items

+ 7 items - Konsernledelsen - Styret - - Representantskapet + + Konsernledelsen - - Boligpriser og statistikk + + Styret - - Investor Relations + + Representantskapet - - Digital årsrapport + + + Boligpriser og statistikk + + + + Investor Relations + + + Digital årsrapport + + + Jobb i OBOS - Jobb i OBOS -

9 items

+ 9 items - Konsernledelsen - Styret - - Representantskapet + + Konsernledelsen + + + Styret + + + Representantskapet + + + + Boligpriser og statistikk + + + + Investor Relations - - Boligpriser og statistikk + + Digital årsrapport - - Investor Relations + + Jobb i OBOS - - Digital årsrapport + + Presse + + + Logoer - Jobb i OBOS - Presse - Logoer -

10 items

+ 10 items - Konsernledelsen - Styret - - Representantskapet + + Konsernledelsen + + + Styret + + + Representantskapet + + + + Boligpriser og statistikk + - - Boligpriser og statistikk + + Investor Relations - - Investor Relations + + Digital årsrapport - - Digital årsrapport + + Jobb i OBOS - Jobb i OBOS - Presse - Logoer - - OBOS Boligkonferanse + + Presse + + + Logoer + + + OBOS Boligkonferanse -

15 items

+ 15 items - Konsernledelsen - Styret - - Representantskapet + + Konsernledelsen + + + Styret + + + Representantskapet + + + + Boligpriser og statistikk + + + + Investor Relations + + + Digital årsrapport + + + Jobb i OBOS + + + Presse + + + Logoer - - Boligpriser og statistikk + + OBOS Boligkonferanse - - Investor Relations + + OBOS-ligaen - - Digital årsrapport + + Datterselskaper - Jobb i OBOS - Presse - Logoer - - OBOS Boligkonferanse + + Vedtekter - OBOS-ligaen - Datterselskaper - Vedtekter - - Generalforsamlingen i OBOS + + + Generalforsamlingen i OBOS + - - Strategi og styrende dokumenter + + + Strategi og styrende dokumenter + -
+ ); diff --git a/packages/react/src/link-list/link-list.tsx b/packages/react/src/link-list/link-list.tsx index 355c2d63a..f223b8fe7 100644 --- a/packages/react/src/link-list/link-list.tsx +++ b/packages/react/src/link-list/link-list.tsx @@ -5,71 +5,78 @@ import { } from '@obosbbl/grunnmuren-icons-react'; import { cx } from 'cva'; import type { JSX, ReactNode } from 'react'; -import { - UNSAFE_Link as Link, - type UNSAFE_LinkProps as LinkProps, -} from '../link'; +import { type LinkRenderProps, Provider } from 'react-aria-components'; +import { HeadingContext } from '../content'; +import { _LinkContext, type UNSAFE_LinkProps as LinkProps } from '../link'; type LinkListContainerProps = React.HTMLProps & { children: JSX.Element | JSX.Element[]; }; +// Sets the correct icons for each link in the link list +const _LinkProvider = ({ children }: { children: ReactNode }) => ( + + (values: LinkRenderProps) => { + let Icon = ArrowRight; + + if (download) { + Icon = Download; + } else if (rel?.includes('external')) { + Icon = LinkExternal; + } + + return ( + <> + {typeof children === 'function' + ? children({ ...values, defaultChildren: null }) + : children} + + + ); + }, + }, + ], + ]} + > + {children} + +); + const LinkListContainer = ({ className, ...restProps }: LinkListContainerProps) => ( -
+ // Dual providers makes for easier typing and more readable code + + <_LinkProvider> +
+ + ); -type LinkListProps = React.HTMLProps & { +type LinkListProps = React.HTMLProps & { children: JSX.Element | JSX.Element[]; }; -const LinkList = ({ className, children, ...restProps }: LinkListProps) => ( - -
    {children}
-
+const LinkList = (props: LinkListProps) => ( + <_LinkProvider> +
    + ); -type LinkListItemProps = LinkProps & { +type LinkListItemProps = React.HTMLProps & { children: ReactNode; - isExternal?: boolean; }; -const LinkListItem = ({ - children, - isExternal, - className, - ...restProps -}: LinkListItemProps) => { - let Icon = ArrowRight; - let iconTransition = cx('group-hover:motion-safe:translate-x-1'); - - if (restProps.download) { - Icon = Download; - iconTransition = cx('group-hover:motion-safe:translate-y-1'); - } else if (isExternal) { - iconTransition = cx( - 'group-hover:motion-safe:-translate-y-0.5 group-hover:motion-safe:translate-x-0.5', - ); - Icon = LinkExternal; - } - - return ( -
  • - - {children} - - -
  • - ); -}; +const LinkListItem = (props: LinkListItemProps) => ( +
  • +); export { LinkList as UNSAFE_LinkList, diff --git a/packages/react/src/link/link.tsx b/packages/react/src/link/link.tsx index 2436b89e7..b71b32c28 100644 --- a/packages/react/src/link/link.tsx +++ b/packages/react/src/link/link.tsx @@ -1,23 +1,55 @@ import { cx } from 'cva'; -import type { ReactNode } from 'react'; +import { createContext } from 'react'; import { Link as _Link, type LinkProps as _LinkProps, + type ContextValue, + useContextProps, } from 'react-aria-components'; +import { translations } from '../translations'; +import { useLocale } from '../use-locale'; type LinkProps = _LinkProps & React.RefAttributes & { - children: ReactNode; + /** @private Used internally for slotted components */ + _innerWrapper?: (props: _LinkProps) => LinkProps['children']; }; -/** - * A basic link component that extends react-aria-components Link with consistent styling. - * Provides accessible focus styles and maintains design system consistency. - */ -const Link = ({ children, className, ...restProps }: LinkProps) => { +const _LinkContext = createContext< + ContextValue, HTMLAnchorElement> +>({}); + +const Link = ({ ref: _ref, ..._props }: LinkProps) => { + const [props, ref] = useContextProps(_props, _ref, _LinkContext); + const { className, _innerWrapper, children: _children, ...restProps } = props; + + const locale = useLocale(); + const externalLinkSR = props.rel?.includes('external') ? ( + {translations.externalLink[locale]} + ) : null; + + const reactNodeChildren = ( + <> + {_children} + {externalLinkSR} + + ); + + const children = _innerWrapper + ? _innerWrapper({ + ...restProps, + children: + typeof _children === 'function' + ? (values) => _children(values) + : reactNodeChildren, + }) + : reactNodeChildren; + return ( <_Link {...restProps} + ref={ref} + data-slot="link" className={cx( className, 'inline-flex cursor-pointer items-center gap-1 font-medium hover:no-underline focus-visible:outline-current focus-visible:outline-focus-offset [&>svg]:shrink-0 [&>svg]:transition-transform', @@ -28,4 +60,8 @@ const Link = ({ children, className, ...restProps }: LinkProps) => { ); }; -export { Link as UNSAFE_Link, type LinkProps as UNSAFE_LinkProps }; +export { + _LinkContext, + Link as UNSAFE_Link, + type LinkProps as UNSAFE_LinkProps, +}; diff --git a/packages/react/src/translations.ts b/packages/react/src/translations.ts index 8df4a8da6..cf0c3ea83 100644 --- a/packages/react/src/translations.ts +++ b/packages/react/src/translations.ts @@ -39,6 +39,11 @@ const translations: Translations = { sv: 'Nästa', en: 'Next', }, + externalLink: { + nb: '(ekstern lenke)', + sv: '(extern länk)', + en: '(external link)', + }, }; export { translations, type Translation, type Translations }; diff --git a/packages/tailwind/tailwind-base.css b/packages/tailwind/tailwind-base.css index bfefdc434..612fcf0ea 100644 --- a/packages/tailwind/tailwind-base.css +++ b/packages/tailwind/tailwind-base.css @@ -200,6 +200,28 @@ .link-list-container { @apply @container; + &:has([data-slot="heading"]) { + .link-list, + [data-slot="link-list"] { + @apply overflow-visible; + } + } + + [data-slot="heading"] { + @apply p-1.25; + + [data-slot="link"] { + @apply py-2.25; + & > svg { + @apply text-base; + } + } + + &:not(:has([data-slot="link"])) { + @apply my-2.25; + } + } + [data-slot="link-list"] { @apply @sm:gap-x-4 @md:gap-x-9 @lg:gap-x-12 @xl:gap-x-16; @@ -227,6 +249,30 @@ .link-list-item, [data-slot="link-list-item"] { /** Creates divider lines that works in any grid layout and with the focus ring */ - @apply after:-top-px relative p-0.75 after:absolute after:right-0 after:left-0 after:h-px after:w-full after:bg-gray-light; + @apply after:-top-px relative p-1.25 after:absolute after:right-0 after:left-0 after:h-px after:w-full after:bg-gray-light; + [data-slot="link"] { + @apply paragraph; + } + } + + .link-list-container, + .link-list-item, + [data-slot="link-list-item"] { + [data-slot="link"] { + @apply flex w-full cursor-pointer justify-between gap-x-2 py-3.5 font-medium no-underline focus-visible:outline-focus; + } + } + + .link-list-container [data-slot="link"], + [data-slot="link-list-item"] [data-slot="link"] { + &:hover:not([download]):not([rel~="external"]) svg { + @apply motion-safe:translate-x-1; + } + &[download]:hover svg { + @apply motion-safe:translate-y-1; + } + &[rel~="external"]:hover svg { + @apply motion-safe:-translate-y-0.5 motion-safe:translate-x-0.5; + } } }