Compare commits

...
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] 6324d63745 Version Packages (#3226)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-18 14:20:08 +02:00
Halvor Haugan debfb3fd15 [Translations] Chips.Removable: Remove translations-prop (partly reverts #3232) (#3255) 2024-10-18 14:15:23 +02:00
Ken a3e8457baf change: Reduced height for actionemu examples (#3246) 2024-10-18 12:45:36 +02:00
Halvor Haugan 0560f84acd ➖ Remove faker (#3254) 2024-10-18 12:20:35 +02:00
Halvor Haugan 663dfbe8a4 🆙 Upgrade some dependencies (#3247) 2024-10-18 12:02:05 +02:00
Halvor Haugan 536f088b97 List.Item: Center icon horizontally inside it's container (#3253) 2024-10-18 11:55:46 +02:00
Ken f659599787 [ActionMenu] Removed defaultOpen-prop, moved rootElement, added Modal-support (#3240)
* change: Updated actionmenu examples

* change: Removed flipAlignment prop for better alignment

* change: Removed defaultOpen-prop from ActionMenu

* change: Moved rootElement prop to component root

* change: Added support for use inside Modal

* change: Use MenuPortalProps instead of Pick

* change: Revert to using Pick for prop inheritance

* change: Menucontext uses required definition of rootElement
2024-10-17 16:19:27 +02:00
Ken ed34bc034a [Darkside]: Move text-contrast tokens into semantic groups 2024-10-17 14:26:20 +02:00
Ken 2a2028fd95 [BasePrimitive] Hide className from basePrimitive type (#3241)
* change: Hide className from basePrimitive type

* change: Re-order baseprimitive types (#3242)
2024-10-17 13:04:50 +02:00
Ken 386145d63a [Darkside] Made breakpoints and font-tokens avaliable in css output
[Darkside] Added breakpoints to token-config
[Darkside] Added font to token-config
2024-10-17 11:51:54 +02:00
Ken 1ed809c872 [Actionmenu] Updated examples for docs, removed flipAlignment-prop (#3236)
* change: Updated actionmenu examples

* change: Removed flipAlignment prop for better alignment
2024-10-17 09:49:56 +02:00
Ken e2359ddf33 [Darkside] Bumped role-raised tokens on darkmode one tick on the scale, updated darkmode input token 2024-10-16 19:45:15 +02:00
Ken 069e3346ec [darkside] Added spacing and border-radius token support for JS/TS file-format 2024-10-16 14:34:06 +02:00
Halvor Haugan c941af9de5 Refactor examples of Vstack and HStack (#3244) 2024-10-16 14:15:08 +02:00
Halvor Haugan a273a22172 Adjust examples of VStack and HStack (#3243) 2024-10-16 13:31:09 +02:00
Ken 0e94031f1e [Darkside] Added alpha-variant to border-subtle 2024-10-16 13:01:10 +02:00
Ken 64a9762286 change: Updated exports to make docgen work (#3235) 2024-10-16 09:29:39 +02:00
Ken a05dbadfa4 [darkside]: Added support for darkside views in chromatic (#3239) 2024-10-15 16:55:05 +02:00
Halvor Haugan 7c468b18d1 Adjust dates in Date/MonthPicker examples (#3238) 2024-10-15 16:35:45 +02:00
Ken cfcd78a450 [sanity] Grunnleggende now has access to props block 2024-10-15 15:40:45 +02:00
Halvor Haugan 9e677697a7 [aksel.nav.no] Fix copy-pasting snippets in Firefox 2024-10-15 12:15:24 +02:00
Halvor Haugan 8a8bf0a274 [aksel.nav.no] Adjust list of outdated articles (#3234) 2024-10-15 10:28:03 +02:00
Halvor Haugan 1e40d96510 Chips.Removable: Added new i18n API (#3232) 2024-10-15 09:55:30 +02:00
Ken db56141b2e [Darkside] Updated CSS-readme with syntax hints 2024-10-15 09:43:14 +02:00
Ken ec351ba27f [Darkside] Added CSS-resets, utilities, fonts ++ to darkside css
[Darkside] Removed unused 'baseline' css
[Referansesider] Updated css-import to layer 'framework'
2024-10-14 22:32:39 +02:00
Ken 2e98a09540 [Linting] Update @typescript-eslint/eslint-plugin to v8 (#3222)
* linting: update ts eslint package to v8

* linting: Update ts eslint package to v8

* change: removed redundant off-toggle of native eslint rules

* change: update interface declaration
2024-10-14 16:43:46 +02:00
Halvor Haugan 2d438e1d37 [Translations] Alert: i18n support (#3231) 2024-10-14 16:42:41 +02:00
Ken 8b373e8b80 bug: Use relative units instead of tokens for storybook markdown 2024-10-14 16:21:32 +02:00
KenandHalvor Haugan 179eb51221 [Darkside] Added development environment for new CSS (#3233)
* change: Added stoybook decorators for darkside toggle

* feat: Added dynamic CSS-loading based on mode

* memo: Added readme for new CSS

* change: Added basic body styles for darkside, including fonts

* change: Added loading of new CSS to referansesider

* bug: Removed old preview file in storybook

* change: Added utf-8 to darkside css

* change: Updated darkside css readme with new instructions

* change: Removed old bg style on wrapper

* Update .storybook/preview.jsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* change: Refactored theme decorator

* Update .storybook/preview.jsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* bug: Re-added return

* change: themedecorator -> modedecorator

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-10-14 16:12:03 +02:00
Halvor Haugan d98624618c [Translations] Accordion: Remove redundant icon title (#3230) 2024-10-14 15:36:20 +02:00
Julian Nymark ecc501a0a8 Referansesider: dagoversikten tweaks to table, remove Timeline 2024-10-14 11:49:51 +02:00
Ken fec3b12d5f [Darkside] Added alpha-scale for global and moderate
[darkside] Updated naming transform to support alpha-tokens
[darkside] Updated input-token to use opacity
[darkside] Added alpha-tokens to global config
[darkside] Now auto-generates the alpha-values
[darkside] Added semantic role tokens using alpha-values
bug: removed tempfile added by vitest
2024-10-14 11:36:23 +02:00
KenandHalvor Haugan 8f99d3a102 [Icons] Some icons that should be ignored were fetched and caused exceptions (#3228)
* bug: Some icons that should be ignored were fetched and caused exceptions

* change: Added some comments

* change: Better math for icon-check

* change: Better messaging

* Update @navikt/aksel-icons/config/figma/index.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-10-14 10:19:21 +02:00
Julian Nymark 7fa7719c24 Referansesider: dagoversikten tweaks to MiniTag 2024-10-11 16:41:22 +02:00
Julian Nymark bc1b9fdad9 Referansesider: dagoversikten table placeholder 2024-10-11 16:34:15 +02:00
Julian Nymark 755d75903a Referansesider: dagoversikten correct tokens for borders 2024-10-11 16:13:57 +02:00
Julian Nymark 5ebc71470a Referansesider: dagoversikten completed rightside 2024-10-11 16:03:17 +02:00
Halvor Haugan 17daddab86 Add description to GuidePanel examples (#3224) 2024-10-11 14:14:34 +02:00
20ced9b084 ActionMenu component (#3081)
* 🎨 Setup content

* 🎨 Fungerende popover med story

* 🎨 useDescendant-API kan bruke uten context-error på nesting

* 🐛 Fikset tilfeller der oppdatering av descendant-options ikke ble reflektert i descendants-map

* 🐛 Satte enabled til optional

* bug: import av Portal brakk stories for Popover og Floating

* bug: DescenadantContext er nå riktig typet som undefined

* label: scopet navn for bruk av descendant-provider i dismissablelayer

* memo: Bedre kommentarer

* feat: Sender ikke callbacks hvis layer er disabled

* memo: Formatering og kommentarer

* memo: Kommentarer

* copy: Kopiert over basekomponent

* refactor: Oppdatert imports

* refactor: Oppdatert Primitive bruk

* feat: Støtter forwardRef

* fire: Fjernet Radix import

* feat: Bruker Floating og ikke PopperPrimitive

* type: Bedre typet composeEventHandler

* refactor: Fjernet scopedProps

* feat: Custom RowingFocus

* wip feat: Replaced rowingfocus

* wip feat: Implemented initial focus for rowingfocus

* wip feat: Removed old collection api

* refactor: Use internal Slot component

* refactor: Fixed all scoped context-use

* feat: Fullført migrering til internt API

* test: Setup stories

* bug: Content is added to descendants

* bug: Found issue for nesten descendants

* copy: Kopiert over refactor for useDescendant

* bug: Fikset descendants nesting

* feat: Fikset keyboard nav med nested descendants

* bug: Removed early return

* memo: Kommentarer

* label: Typer

* refactor: Fjernet Root export

* refactor: Fjernet typeahead

* test: Setup story for entryfocus

* refactor: Forenklet FocusScope

* refactor: bedre dokumentert utils

* refactor: Moved utils to separate file

* refactor: Moved slotted-div to separate file

* refactor: Moved MenuContext out to context-file

* 🚚 Moved focus-files

* refactor: Update syntax

* refactor: Moved MenuAnchor to file

* refactor: Moved MenuGroup to file

* refactor: Moved Separator to file

* refactor: Moved Label to file

* refactor: Moved utility-components

* refactor: Flyttet item til mappe

* refactor: Flyttet checkbox

* refactor: Content in separate file

* refactor: Radio i separate filer

* refactor: SubComponents in files

* refactor: Bedre typer

* refactor: Re-written back to single file

* refactor: Fjernet gamle commits

* bug: Fikset import feil og feil extension av type

* refactor: data-attrb fra radix -> aksel

* refactor: Fjernet console.logs

* bug: Fixed import and test-type error

* refactor: Remove unwanted exports

* feat: Re-added ItemIndicator wrapper

* refactor: Updated exports

* refactor: removed focusguards

* refactor: FocusScope now assumes Slot

* refactor: Removed extra tabIndex

* feat: Implemented ItemIndicator and styling for testing

* refactor: Rowing -> Roving

* refactor: Moved Menu-component into overlays-dir

* bug: Fixed import of Slot

* bug: Re-add tabIndex to MenuItem for correct focus-handling

* memo: Rename Tab -> menu

* refactor: removed unwanted exports in story

* Update @navikt/core/react/src/overlays/floating-menu/Menu.stories.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* bug: Added missing meta import

* bug: Aligned dash with checkmark

* bug: Circle icon now shows for radio-demo

* bug: keyframes has extra ':', breaking animations

* bug: Re-added ItemIndicator for all TickIcon

* bug: WithLabel story now shows complete dropdown

* feat: Added disabled-styling to submenu

* refactor: Now uses ownerdocument where needed instead of document

* adjustments after review 1

* adjustments after review 2

* refactor: Updated to correct function type syntax

* refactor: renamed floating content propname

* refactor: Consistent destructuring of props

* refactor: rename FocusScope props

* memo: Added explanation for why we use flushEvent

* refactor: added consistent use of composeEventHandlers

* refactor: Force userSelect none style

* demo: Allow canceling opening if pointerdown is released outside of menu

* memo: Better line-dividers

* refactor: removed data-highlighted logic

* Update @navikt/core/react/src/overlays/floating-menu/Menu.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/overlays/floating-menu/Menu.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* feat: DropdowmMenu boilerplate

* feat: MenuTrigger & MenuPortal

* feat: MenuContent

* feat: MenuGroup

* feat: lvl-1 components added

* feat: Implement each component

* feat: Implement export

* refactor: Fixeed proptypes for sub

* feat: Implemented trigger

* testing: Updated stories

* feat: Added experimental API 'requireReactelement'

* feat: Implemented content

* feat: Implemented label

* feat: Implemented Item

* feat: Implemented Item

* feat: Implemented group

* feat: Implemented radoigroup

* feat: Implemented radio

* feat: Implemented checkbox

* feat: Implemented sub-trigger

* feat: Implemented sub-content

* feat: Implemented separator classname

* feat: Added cancelable event for pointerdown

* feat: Added dropdown-menu css

* WIP: CSS

* WIP: Added label styling

* feat: Remove checkmarks whe unchecked

* feat: Allow use of 'il' in floating-element

* feat: Implemented semantic tags for items and content

* feat: Added test-markup

* Revert "feat: Implemented semantic tags for items and content"

This reverts commit 6bf473328d.

* Revert "feat: Allow use of 'il' in floating-element"

This reverts commit 5307facbca.

* css: Item active styling

* testing: Setup storybook test framework

* testing: setup basic feature stories

* testing: Un-nested variants

* testing: advanced demos

* feat: Disabled and submenu css

* feat: Added bg on open submenu-trigger

* feat: Added destructive item

* feat: Stop selecting checkbox from closing dropdown

* feat: Use actual radio and checkbox svg for components

* feat: Animate checkbox/radio checked

* feat: Added indeterminate checkbox

* feat: Added indeterminate demo

* feat: added story for shortcuts

* TODOS

* feat: added possibility to close submenus with arrowright when opened to the left of content

* refactor: Better implementation of Shortcuts and aria-keyboardshortcut

* feat: added thin scrollbar

* feat: Added animationsupport for all directions when opening dropdown

* refactor: Cleaned up CSS

* testing: Added chromatic stories for dropdown

* refactor: Dropdowm CSS -> Actionmenu CSS

* refactor: Rename files

* refactor: Move file to utils

* refactor: Inline Slot button

* refactor: rename react dropdown -> action

* refactor: Solved alignment issue with checkbox/radio and text

* feat: use div instead of removed Menu.Label

* refactor: Better useRef reference

* memo: Updated comments

* memo: removed old comments

* memo: Prop-comments

* memo: JSDOC

* bug: remove shortcut from exports

* refactor: Avoid exposing extra props for content

* refactor: Avoid exposing extra props for subcontent

* refactor: Updated CSS animation name

* feat: Updated exports making ActionMenu avaliable from the library

* memo: Added actionmenu to CSS-mappings

* feat: Added option to use links in actionmenu

* feat: Icon-prop for Item and Subtrigger

* refactor: Separator -> Divider

* feat: Updated danger variant item text color

* feat: Stronger label typings for group

* bug: Fixed edgecase where focus on trigger would close menu

* bug: removed storybook addon omit

* refactor: Used relative units and tokens for spacing/width

* feat: Added max-width, removed nowrap

* bug: removed demo story

* refactor: Removed old scale(1) property from animation

* Update @navikt/core/css/action-menu.css

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* feat: Added direct import of actionmenu

* Update @navikt/core/react/src/overlays/action-menu/ActionMenu.stories.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/overlays/action-menu/ActionMenu.stories.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Minor adjustments

* Remove unused CSS

* feat: De-select all checkboxes when every box is allready checked in indeterminate

* memo: Removed extra stories

* memo: Removed demo used for testing

* feat: More dynamic max-width with 'min'

* memo: Removed cryptic comments

* feat: Avoid spread in reduce

* refactor: Removed middle-step function to handle radio state update

* refactor: Better css-naming for marker css

* refactor: removed extra unused variables, added double underscore prefix

* refactor: Removed extra onSelect on disabled story

* feat: Actionmenu now proplery handles overflow

* feat: Avoid auto-closing modal

* feat: Menu is now Modal

* feat: Now always focuses trigger when closed

* Update @navikt/core/css/action-menu.css

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* feat: Fix space auto closing modal

* feat: Fallback to bottom of anchor when no avaliable space

* memo: Added todo

* ,memo: added todos

* memo: changeset

* refactor: Simplified SVG with groups

* [ActionMenu] Remove extra CSS-classes, fixed centering bug in safari (#3194)

* refactor: Remove extra CSS-classes, fixed center bug in safari

* refactor: Removed unused CSS

* [ActionMenu] Sub-menu now uses onClick for opening and closing instead of pointermove (#3193)

* feat: ActionMenu now only opens submeny onClick

* memo: removed TODO

* feat: Closes all adjacent submenues when opening a new one

* refactor: Undo unwrapping of onDismiss

* refactor: Remove unused code in Menu.tsx

* feat: Handle submenu keys correctly

* feat: use native onClick for Trigger

* refactor: Remove un-needed disabled check on trigger

* bug: Reuse already used context

* Update @navikt/core/react/src/overlays/floating-menu/Menu.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* bug: Rename open prop

* Memo: update comment

* feat: Use looser types for RovingFocus

* bug: Aboid auto-closing menu when opening with pointer

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/modal/ModalHeader.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/css/action-menu.css

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update .changeset/dry-flowers-dance.md

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 💄 Manually set zIndex value

* ActionMenu documentation (#3085)

* memo: ActionMenu documentation

* refactor: Update to use Icons

* bug: Fixed use of Item with nextjs link-component

* memo: Updated examples

* memo: Updated examples, removed div-wrapper

* TODOS

* memo: Oppdatert submenu med table-vising

* feat: Oppdatert eksempler

* memo: Default open first submenu in demo

* memo: Use norwegian in link docs

* Update aksel.nav.no/website/pages/eksempler/actionmenu/shortcuts.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* memo: Shortcut easter egg

* ⚡ Simplify indeterminate toggle for actionmenu

* refactor: remove redundant aria-label

* Update aksel.nav.no/website/pages/eksempler/actionmenu/sub-menu.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/pages/eksempler/actionmenu/links.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* memo: removed reference to nextjs app-router

* bug: Removed empty aria-label

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* [ActionMenu] Fixes overlapping elements for SubMenus (#3212)

* bug: Fixed scroll + wrapping issue

* bug: undo scrollbar gutter change

* change: Updated chromatic to not overlap

* change: Updated yarn.lock

* change: submenu changes floating position more flexibly than main content now

* change: Chromatic clips unused margins, so moved submenu higher in view

* change: Updated chromatic-story to better show submenu

* memo: changeset for modal head update

* Update .changeset/proud-rings-float.md

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2024-10-11 14:13:55 +02:00
Julian Nymark 6d2115cdc9 Referansesider: dagoversikten structure + completed leftside 2024-10-11 13:57:20 +02:00
Halvor Haugan 17ae19c5fe Add translations for NN and EN (#3209) 2024-10-11 12:01:11 +02:00
KenandHalvor Haugan a60298f232 [Icons] Refactored Figma fetch to TS (#3218)
* ♻️ Refactored Icon fetching from figma

* bug: Fixed types for YML-handling

* update: Lock update

* Update @navikt/aksel-icons/config/figma/index.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/aksel-icons/config/figma/index.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/aksel-icons/config/figma/make-configs.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* refactor: pages -> parts

* refactor: Better naming

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-10-11 09:10:31 +02:00
github-actions[bot]andgithub-actions[bot] 5866992e7d Version Packages (#3208)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-10 15:56:34 +02:00
KenandHalvor Haugan 8ab241a980 [Linting] Comment-cleanup in eslint (#3220)
* memo: Cleanup comments

* memo: Updated eslintRc

* bug: Removed ts from global eslint

* Update .eslintrc.js

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* memo: Updated temp comment

* Update .eslintrc.js

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-10-10 15:48:47 +02:00
KenandHalvor Haugan 363d76b9b2 [Linting] Fixed noUselessTernary and useOptionalChain-rules (#3215)
* memo: Added figma-plugin as ignore

* memo: Removed unused noSelfAssigned

* refactor: Fixed useJsxKeyInIterable

* memo: Removed unused noInnerDeclarations

* memo: Removed unused noUselessTernary

* refactor: Fixed useOptionalChan rule biome

* Update @navikt/aksel-icons/__tests__/validate-svg.test.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* feat: Better null-check

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-10-10 15:06:49 +02:00
Ken 5da970fd62 [Linting] Fixed useLiteralKeys rule from biome (#3219)
* memo: Added figma-plugin as ignore

* memo: Removed unused noSelfAssigned

* refactor: Fixed useJsxKeyInIterable

* memo: Removed unused noInnerDeclarations

* refactor: Fixed useLiteralKeys rule

* bug: Fixed typings for sanity publication action

* bug: Fixed typings for sanity publication action

* bug: Fixed typings for sanity publication action

* bug: added null-check
2024-10-10 14:47:11 +02:00
Ken 296e43cab2 [Linting] Fixed useJsxKeyInIterable ,noSelfAssign and noInnerDeclarations rules from biome (#3214)
* memo: Added figma-plugin as ignore

* memo: Removed unused noSelfAssigned

* refactor: Fixed useJsxKeyInIterable

* memo: Removed unused noInnerDeclarations
2024-10-10 14:33:26 +02:00
Ken 85fec974dc Added autoprefixer to @navikt/ds-css devDependencies (#3213)
* bug: added autoprefixer to ds-css devDeps

* ⬆️ Yarn lock
2024-10-10 13:29:56 +02:00
Vegard HaugstvedtandKen 6ddea4eaf2 Feature/combobox pageup and pagedown (#3158)
* Add support for PageUp and PageDown in Combobox.FilteredOptions

* Remove console.log statements

* Add test for PageDown/PageUp

* Open FilteredOptions when pressing PageDown

* Prevent scrolling from moving the whole page

* Consistently scroll to the active element in useVirtualFocus by using useLayoutEffect

It seems like doing it with the state update sometimes encounters a race condition preventing the scrolling.
This removes the need for a private function handling index update and scrolling, simplifying the code.

* Updated test to fit new options order

* Prevent "Home" and "End" in Combobox from scrolling as well

* Add changeset

* Update @navikt/core/react/src/form/combobox/FilteredOptions/useVirtualFocus.ts

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* useEffect seems to work the same way, and works better for SSR

* Removed code that did nothing

* Added the same check we have for PageDown, etc. Unsure if it is neccessary.

* Change behaviour for "Home" to move focus to the first element instead of Input

* Once we changed virtualFocus.moveFocusToTop to move focus to first element instead of the Input, we now need another function to move it to the Input where applicable.

* Update .changeset/tasty-cats-hear.md

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

---------

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-10-10 12:05:13 +02:00
Halvor Haugan acea9ebb78 Change "personnummer" to "fødselsnummer" (#3211) 2024-10-10 11:59:28 +02:00
Julian Nymark fdbd634012 Referansesider: dagoversikten BreadCrumbs 2024-10-09 12:37:13 +02:00
Julian Nymark dcf5452d28 Referansesider: dagoversikten content 2024-10-08 16:34:15 +02:00
Sjur 0a8c33bbbe Justerte Danger-farger i LightMode (#3210)
Noen av HEX-verdiene i Danger LightMode er endret.
2024-10-08 15:26:07 +02:00
Vegard Haugstvedt 18b0cf5fa5 Bugfix/combobox disable autocomplete in firefox on android (#3201)
* Disable autocomplete in Combobox to "fix" issue where autocomplete is broken in Firefox on Android

The bug is that after three "correct" characters are entered in a row, the autocomplete inserts the third character incorrectly, messing up the text in the input.
This also happens in other implementations of autocomple, like W3C's ARIA Authoring Practices Guide combobox with inline autocomple.

* Add changeset

* Use type checking instead of useEffect to prevent client code from running server side
2024-10-08 12:37:07 +02:00
Julian Nymark 374a679bc3 Referansesider: ✨ dagoversikten.tsx 2024-10-08 11:53:09 +02:00
195 changed files with 7505 additions and 3439 deletions

No files matched your search

+26 -12
View File
@@ -1,4 +1,11 @@
// TODO: Convert to new format: https://eslint.org/docs/latest/use/configure/migration-guide
/**
* TODO:
* - Convert to new format (v9): https://eslint.org/docs/latest/use/configure/migration-guide
* - Look into opinionated stylistic plugin
* - Replace @typescript-eslint/recommended with @typescript-eslint/recommended-type-checked
* - We should add "plugin:react/jsx-runtime", but not sure if this will cause problems for projects not using the new JSX transform
* - Consider adding the rule "id-length"
*/
module.exports = {
env: {
browser: true,
@@ -8,15 +15,13 @@ module.exports = {
extends: [
"eslint:recommended",
"plugin:react/recommended",
//"plugin:react/jsx-runtime", // Not sure if this will cause problems for projects not using the new JSX transform
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:storybook/recommended",
// TODO: Look into opinionated stylistic plugin
// TODO: Replace @typescript-eslint/recommended with @typescript-eslint/recommended-type-checked
],
/* Allows eslint-plugin-react to detect installed react-version */
settings: { react: { version: "detect" } },
rules: {
"import/no-unresolved": "off",
@@ -26,12 +31,6 @@ module.exports = {
],
"array-callback-return": "error",
"object-shorthand": "error",
// TODO: Consider adding the rule "id-length"
// Temporary:
"react/prop-types": "off",
"react/display-name": "off",
"import/no-named-as-default": "off",
// "import/no-cycle": [1],
"no-else-return": "error",
"no-console": [
"warn",
@@ -53,7 +52,12 @@ module.exports = {
],
},
],
/* Temporarily turned off until code is updated */
"react/prop-types": "off",
"react/display-name": "off",
"import/no-named-as-default": "off",
},
/* https://eslint.org/docs/latest/use/configure/rules#report-unused-eslint-disable-comments */
reportUnusedDisableDirectives: true,
overrides: [
{
@@ -66,17 +70,27 @@ module.exports = {
files: ["**/*.ts?(x)"],
extends: ["plugin:@typescript-eslint/recommended"],
rules: {
"@typescript-eslint/no-shadow": ["error", { hoist: "all" }], // TODO: Consider { builtinGlobals: true }
/*
* TODO:
* - Consider { builtinGlobals: true }
*/
"@typescript-eslint/no-shadow": ["error", { hoist: "all" }],
"@typescript-eslint/no-explicit-any": "off", // Temporary
"@typescript-eslint/array-type": "error",
"@typescript-eslint/no-unused-expressions": [
"error",
/* https://eslint.org/docs/latest/rules/no-unused-expressions#allowshortcircuit-and-allowternary */
{ allowShortCircuit: true, allowTernary: true },
],
},
},
{
files: ["**/*.test.*", "**/__tests__/*"],
plugins: ["@vitest"],
extends: [
"plugin:@vitest/legacy-recommended",
"plugin:testing-library/react",
/* Needed to support v8 eslint config-syntax */
"plugin:@vitest/legacy-recommended",
],
},
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
playwright:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.35.1-jammy
image: mcr.microsoft.com/playwright:v1.48.1-jammy
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
+6 -6
View File
@@ -136,34 +136,34 @@ import Setup from "./assets/setup.png";
width: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: var(--a-spacing-6);
grid-gap: 1.5rem;
}
.sb-link{
text-decoration: underline;
color: var(--a-text-default);
color: #111;
}
img {
object-fit: cover;
padding-bottom: var(--a-spacing-5);
padding-bottom: 1.25rem;
border-radius: 8px;
}
.sb-section-title {
margin-bottom: var(--a-spacing-8);
margin-bottom: 2rem;
}
.sb-section-item-heading {
padding-bottom: var(--a-spacing-1);
padding-bottom: 0.25rem
margin: 0 !important;
}
.sb-section-item-paragraph {
margin: 0;
padding-bottom: var(--a-spacing-3);
padding-bottom: 0.75rem;
}
@media screen and (max-width: 768px) {
+1
View File
@@ -53,6 +53,7 @@ export default {
"@storybook/addon-a11y",
"@whitespace/storybook-addon-html",
"@storybook/addon-interactions",
"@storybook/addon-themes",
{
name: "@storybook/addon-storysource",
options: {
+51 -30
View File
@@ -1,5 +1,7 @@
import React from "react";
import "@navikt/ds-css/index.css";
import { withThemeByClassName } from "@storybook/addon-themes";
import React, { useLayoutEffect } from "react";
import darksideCss from "@navikt/ds-css/darkside/index.css?inline";
import defaultCss from "@navikt/ds-css/index.css?inline";
import "./layout.css";
export const parameters = {
@@ -16,44 +18,63 @@ export const parameters = {
},
},
layout: "centered",
backgrounds: {
values: [
{
name: "Canvas",
value: "#ffffff",
},
{
name: "Darkmode",
value: "#23262a",
},
],
},
backgrounds: { disable: true },
};
export const globalTypes = {
theme: {
name: "Theme",
mode: {
name: "Darkside",
toolbar: {
icon: "circlehollow",
items: [
{ value: "light", icon: "circlehollow", title: "light" },
{ value: "dark", icon: "circle", title: "dark" },
{
value: "default",
icon: "hourglass",
title: "Default (old)",
},
{ value: "darkside", icon: "beaker", title: "Darkside (new)" },
],
showName: true,
dynamicTitle: true,
},
},
};
export const withTheme = (StoryFn, context) => {
return (
<div
data-theme={context.parameters.theme || context.globals.theme || "light"}
lang="no"
id="root"
>
<StoryFn />
</div>
);
export const initialGlobals = {
mode: "default",
};
export const decorators = [withTheme];
const ModeDecorator = ({ children, mode }) => {
useLayoutEffect(() => {
const style = document.createElement("style");
style.innerHTML = mode === "darkside" ? darksideCss : defaultCss;
document.head.appendChild(style);
if (mode === "darkside") {
document.body.style.setProperty("background", "var(--a-bg-default)");
}
return () => {
document.head.removeChild(style);
document.body.style.removeProperty("background");
};
}, [mode]);
return children;
};
export const decorators = [
withThemeByClassName({
themes: {
light: "light",
dark: "dark",
},
defaultTheme: "light",
}),
(StoryFn, context) => {
return (
<ModeDecorator mode={context.globals.mode}>
<StoryFn />
</ModeDecorator>
);
},
];
+4
View File
@@ -1,5 +1,9 @@
# @navikt/aksel-icons
## 7.3.0
## 7.2.1
## 7.2.0
## 7.1.2
@@ -0,0 +1,16 @@
import { describe, expect, test } from "vitest";
import { splitIntoParts } from "../config/figma/fetch-icons";
describe("splitIntoParts", () => {
test("should split array into parts correctly with odd count array", () => {
const arr = ["a", "b", "c", "d", "e"];
const result = splitIntoParts(arr, 4);
expect(result).toEqual(["a,b", "c,d", "e"]);
});
test("should split array into parts correctly with even count array", () => {
const arr = ["a", "b", "c", "d", "e", "f"];
const result = splitIntoParts(arr, 4);
expect(result).toEqual(["a,b", "c,d", "e,f"]);
});
});
@@ -1,9 +1,9 @@
/* eslint-disable import/no-named-as-default-member */
import fastglob from "fast-glob";
import fs from "fs";
import jsYaml from "js-yaml";
import { JSON_SCHEMA, load } from "js-yaml";
import path from "path";
import { describe, expect, test } from "vitest";
import { IconYml } from "../config/figma/make-configs";
const basePath = path.resolve(__dirname, "../icons");
@@ -14,9 +14,10 @@ const ymlList = fastglob
describe(`Each icons YML-config is valid`, () => {
ymlList.forEach((file) => {
test(`${file} has valid YML-config`, () => {
const ymlData = jsYaml.load(fs.readFileSync(`${basePath}/${file}`), {
schema: jsYaml.JSON_SCHEMA,
});
const ymlData = load(fs.readFileSync(`${basePath}/${file}`).toString(), {
schema: JSON_SCHEMA,
}) as IconYml;
expect(ymlData.name).toBeTruthy();
expect(ymlData.category).toBeTruthy();
expect(ymlData.sub_category).toBeTruthy();
@@ -31,7 +32,7 @@ describe(`Each icons YML-config is valid`, () => {
});
});
function isDate(dateStr) {
function isDate(dateStr: string) {
return !Number.isNaN(
new Date(dateStr.split(".").reverse().join(".")).getDate(),
);
@@ -21,8 +21,7 @@ describe(`Each icons has a valid code`, () => {
test(`has valid attributes on root-node`, () => {
const root = select(":root", iconAst);
const properties =
root && root.properties ? Object.keys(root.properties).sort() : [];
const properties = Object.keys(root?.properties ?? []).sort();
expect(properties).toStrictEqual(
["viewBox", "xmlns", "height", "width", "fill"].sort(),
@@ -31,45 +30,41 @@ describe(`Each icons has a valid code`, () => {
test(`has valid xml-attr`, () => {
const root = select(":root", iconAst);
const xmlns = root && root.properties ? root.properties.xmlns : null;
expect(xmlns).toBe("http://www.w3.org/2000/svg");
expect(root?.properties?.xmlns).toBe("http://www.w3.org/2000/svg");
});
test(`has valid viewbox`, () => {
const root = select(":root", iconAst);
const viewbox =
root && root.properties ? root.properties.viewBox : null;
expect(viewbox).toBe("0 0 24 24");
expect(root?.properties?.viewBox).toBe("0 0 24 24");
});
test(`root fill is none`, () => {
const root = select(":root", iconAst);
const fill = root && root.properties ? root.properties.fill : null;
expect(fill).toBe("none");
expect(root?.properties?.fill).toBe("none");
});
test(`has valid width and height`, () => {
const root = select(":root", iconAst);
const width = root && root.properties ? root.properties.width : null;
const height = root && root.properties ? root.properties.height : null;
expect(width).toBe("24");
expect(height).toBe("24");
expect(root?.properties?.width).toBe("24");
expect(root?.properties?.height).toBe("24");
});
test(`has valid stroke`, () => {
const nodes = selectAll("*", iconAst);
nodes.forEach((n) => {
n.properties?.stroke &&
if (n.properties?.stroke) {
expect(n.properties.stroke).toEqual("#23262A");
}
});
});
test(`has valid stroke-width`, () => {
const nodes = selectAll("*", iconAst);
nodes.forEach((n) => {
n.properties?.strokeWidth &&
if (n.properties?.strokeWidth) {
expect(n.properties?.strokeWidth).toEqual("1.5");
}
});
});
});
@@ -1,63 +0,0 @@
const file_key = "wEdyFjCQSBR3U7FvrMbPXa";
export const fetchIcons = async () => {
console.info("Fetching new icons from Figma...");
const data = await fetch(
`https://api.figma.com/v1/files/${file_key}/components`,
{
headers: {
"Content-type": "application/json",
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN,
},
},
)
.then((x) => x.json())
.catch((e) => {
throw e.message;
});
if (data?.err) {
throw new Error(data.err);
}
if (!data?.meta?.components || data?.meta?.components.length === 0) {
throw new Error("Fant ingen publiserte ikoner");
}
return data?.meta?.components;
};
export const fetchDownloadUrls = async (idArray) => {
const urls = { images: null };
for (let ids of idArray) {
await fetch(
`https://api.figma.com/v1/images/${file_key}/?ids=${ids}&format=svg`,
{
headers: {
"Content-type": "application/json",
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN,
},
},
)
.then((x) => x.json())
.then((x) => {
urls.images = {
...urls.images,
...x.images,
};
})
.catch((e) => {
throw e.message;
});
}
return urls;
};
export const fetchIcon = async (url) => {
return await fetch(url)
.then((x) => x.text())
.catch((e) => {
throw e.message;
});
};
@@ -0,0 +1,114 @@
import {
BadRequestErrorResponseWithErrMessage,
ErrorResponsePayloadWithErrMessage,
GetFileComponentsResponse,
GetImagesResponse,
PublishedComponent,
} from "@figma/rest-api-spec";
/* https://www.figma.com/developers/api#library-items */
const FIGMA_FILE_KEY = "wEdyFjCQSBR3U7FvrMbPXa";
export const fetchIcons = async (): Promise<PublishedComponent[]> => {
console.group("Fetching list of published icons from Figma...");
const data:
| GetFileComponentsResponse
| BadRequestErrorResponseWithErrMessage = await fetch(
`https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/components`,
{
headers: {
"Content-type": "application/json",
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN ?? "",
},
},
)
.then((x) => x.json())
.catch((e) => {
throw e.message;
});
if (data.status !== 200) {
throw new Error(data.err);
}
if (data.meta.components.length === 0) {
throw new Error("No published icons found");
}
/* Filtes out any 'random' icons not meant for the icon-library */
const validIcons = data.meta.components.filter((icon) => {
const validIcon =
!!icon.containing_frame?.pageName && !!icon.containing_frame?.name;
if (!validIcon) {
console.warn(`Ignoring icon '${icon.name}' as it is missing metadata`);
}
return validIcon;
});
console.info(
`Found ${validIcons.length} published icons in Figma. ${
data.meta.components.length - validIcons.length
} icons were ignored due to missing metadata.`,
);
console.groupEnd();
return validIcons;
};
export const fetchDownloadUrls = async (
nodeIds: string[],
): Promise<Record<string, string>> => {
/* Would just using Record be better here? Yes. But is a self-referencing return kinda cool? Also yes */
const idUrlPairs: Awaited<ReturnType<typeof fetchDownloadUrls>> = {};
/* Figma accepts a comma-separated list of ids: GET /v1/images/:key?ids=1:2,1:3,1:4 */
const paginatedNodeIds = splitIntoParts(nodeIds, 5);
for (const ids of paginatedNodeIds) {
const data: GetImagesResponse | ErrorResponsePayloadWithErrMessage =
await fetch(
`https://api.figma.com/v1/images/${FIGMA_FILE_KEY}/?ids=${ids}&format=svg`,
{
headers: {
"Content-type": "application/json",
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN ?? "",
},
},
)
.then((x) => x.json())
.catch((e) => {
throw e.message;
});
if (data.err !== null) {
throw new Error(data.err);
}
for (const [key, value] of Object.entries(data.images)) {
if (!value) {
console.warn(`No image found for ${key}`);
continue;
}
idUrlPairs[key] = value;
}
}
return idUrlPairs;
};
/**
* Splits an array of strings into parts where each part is a comma-separated string of all the strings for that part.
* @returns An array of comma-separated strings.
*/
export function splitIntoParts(arr: string[], parts: number): string[] {
const partSize = Math.ceil(arr.length / parts);
const blocks: string[] = [];
for (let i = 0; i < parts && i * partSize < arr.length; i++) {
const part = arr.slice(i * partSize, (i + 1) * partSize);
blocks.push(part.join(","));
}
return blocks;
}
@@ -1,15 +0,0 @@
import camelCase from "lodash.camelCase";
import startCase from "lodash.startcase";
export const resolveName = (icon) => {
const isVariant = icon.name.startsWith("Variant=");
if (isVariant) {
const variant = icon.name.replace("Variant=", "").replace("Stroke", "");
const actualName = icon["containing_frame"]?.["containingStateGroup"]?.name;
const name = `${actualName}${variant}`;
return `${startCase(camelCase(name)).replace(/ /g, "")}.svg`;
}
return `${startCase(camelCase(icon.name)).replace(/ /g, "")}.svg`;
};
@@ -0,0 +1,17 @@
import { PublishedComponent } from "@figma/rest-api-spec";
import { camelCase, startCase } from "lodash";
export const resolveName = (icon: PublishedComponent) => {
const isVariant = icon.name.startsWith("Variant=");
if (isVariant) {
const variant = icon.name.replace("Variant=", "").replace("Stroke", "");
/* @ts-expect-error Figma now includes the key:value pair `containingStateGroup: { name: string, nodeId: string }` */
const actualName = icon.containing_frame?.containingStateGroup?.name;
const name = `${actualName}${variant}`;
return `${startCase(camelCase(name)).replace(/ /g, "")}.svg`;
}
return `${startCase(camelCase(icon.name)).replace(/ /g, "")}.svg`;
};
@@ -1,64 +0,0 @@
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
import { resolve } from "path";
import { fetchDownloadUrls, fetchIcon, fetchIcons } from "./fetch-icons.mjs";
import { resolveName } from "./icon-name.mjs";
import { makeConfig } from "./make-configs.mjs";
import { paginate } from "./paginate.mjs";
/* https://www.figma.com/file/wEdyFjCQSBR3U7FvrMbPXa/Core-Icons-Next?node-id=277%3A1221&t=mUJzFvnsceYYXNL5-0 */
const iconFolder = "./icons";
if (!process.env.FIGMA_TOKEN) {
throw new Error("FIGMA_TOKEN not set in .env");
}
async function main() {
console.info("Started icon-update from Figma");
let icons = await fetchIcons();
const totalIcons = icons.length;
const { images } = await fetchDownloadUrls(
paginate(
icons.map((x) => x.node_id),
400,
).map((x) => x.join(",")),
);
if (existsSync(iconFolder)) {
rmSync(iconFolder, { recursive: true, force: true });
}
mkdirSync(iconFolder);
console.info(`Laster ned ${Object.keys(images).length} ikoner`);
for (const [key, value] of Object.entries(images)) {
const icon = await fetchIcon(value).catch((e) => {
throw e;
});
await new Promise((r) => setTimeout(r, 20));
icon &&
writeFileSync(
resolve(iconFolder, resolveName(icons.find((x) => x.node_id === key))),
icon,
{
encoding: "utf8",
},
);
}
makeConfig(icons, iconFolder);
readdirSync(iconFolder, (_, files) => {
if (files.length * 2 !== totalIcons) {
throw new Error(`Ikoner mangler!`);
}
});
console.info(`Success! ${Object.keys(images).length} ikoner lastet ned`);
}
main();
+106
View File
@@ -0,0 +1,106 @@
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
import { resolve } from "path";
import { fetchDownloadUrls, fetchIcons } from "./fetch-icons";
import { resolveName } from "./icon-name";
import { makeConfig } from "./make-configs";
/**
* https://www.figma.com/file/wEdyFjCQSBR3U7FvrMbPXa/Core-Icons-Next?node-id=277%3A1221&t=mUJzFvnsceYYXNL5-0
* This is the current source for all icons we are working with.
*/
const iconFolder = "./icons";
if (!process.env.FIGMA_TOKEN) {
throw new Error("FIGMA_TOKEN not set in .env");
}
main();
async function main() {
console.info("Started icon-update from Figma");
/* Icons are published as 'components' in Figma */
const publishedIconComponents = await fetchIcons();
/* When we have all the published icons, we can ask figma for URL's for downloading them as assets */
const imagesUrls = await fetchDownloadUrls(
publishedIconComponents.map((x) => x.node_id),
);
/* Lets do a clean install */
if (existsSync(iconFolder)) {
rmSync(iconFolder, { recursive: true, force: true });
}
mkdirSync(iconFolder);
console.group(`Processing ${Object.keys(imagesUrls).length} icons...`);
const fileNames = new Set<string>();
await Promise.all(
Object.entries(imagesUrls).map(async ([nodeId, iconUrl]) => {
/* Each icon is now a raw string for a complete SVG */
const iconSvg = await fetch(iconUrl)
.then((x) => x.text())
.catch((e) => {
throw e.message;
});
const matchingIcon = publishedIconComponents.find(
(x) => x.node_id === nodeId,
);
if (!matchingIcon) {
throw new Error(
`No matching icon found for ${nodeId}. It should not be possible to dowload icon without a matching icon in the list of icons fetched from Figma.`,
);
}
const fileName = resolveName(matchingIcon);
/*
* In some cases if multiple icons are published in Figma with the same name,
* we will end up overwriting the icon with the same name.
*/
if (fileNames.has(fileName)) {
console.warn(`Duplicate name detected: ${fileName}.`);
}
fileNames.add(fileName);
writeFileSync(resolve(iconFolder, resolveName(matchingIcon)), iconSvg, {
encoding: "utf8",
});
}),
);
if (fileNames.size !== publishedIconComponents.length) {
console.warn(
`Duplicate icon names from Figma leads to them being overwritten. This will cause the icon-library to be out of sync with Figma.`,
);
}
console.info(`Completed processing 🎉`);
console.groupEnd();
makeConfig(publishedIconComponents, iconFolder);
const filesInDir = readdirSync(iconFolder);
const svgFiles = filesInDir.filter((x) => x.endsWith(".svg"));
const ymlFiles = filesInDir.filter((x) => x.endsWith(".yml"));
if (
svgFiles.length !== publishedIconComponents.length ||
ymlFiles.length !== publishedIconComponents.length
) {
throw new Error(
`Icons (${svgFiles.length}) and configs (${ymlFiles.length}) written to directory does not match the expected amount of icons located in Figma (${publishedIconComponents.length}).\nThis is most likely caused by duplicate icon names from figma.`,
);
}
console.info(
`Success! A total of ${
Object.keys(imagesUrls).length
} were fetched and downloaded from Figma 🎉`,
);
}
@@ -1,17 +1,40 @@
import { PublishedComponent } from "@figma/rest-api-spec";
import { writeFileSync } from "fs";
import jsYaml from "js-yaml";
import { dump } from "js-yaml";
import { resolve } from "path";
import { resolveName } from "./icon-name.mjs";
import { resolveName } from "./icon-name";
export const makeConfig = (icons, folder) => {
icons.forEach((icon) => {
export type IconYml = {
name: string;
category: string;
sub_category: string;
keywords: string[];
variant: string;
updated_at: string;
created_at: string;
};
export const makeConfig = (
icons: PublishedComponent[],
dirLocation: string,
) => {
console.group("Creating icon yml...");
let counter = 0;
for (const icon of icons) {
const name = resolveName(icon).replace(".svg", "");
const keywords = icon.description
.split(",")
.map((x) => x.trim())
.filter(Boolean);
const config = {
if (!icon.containing_frame?.pageName || !icon.containing_frame?.name) {
throw new Error(
`Missing containing_frame.pageName or containing_frame.name for icon ${name}`,
);
}
const config: IconYml = {
name,
category: icon.containing_frame.pageName,
sub_category: icon.containing_frame.name,
@@ -33,14 +56,18 @@ export const makeConfig = (icons, folder) => {
.join("."),
};
const yml = jsYaml.dump(config, {
const yml = dump(config, {
noRefs: true,
skipInvalid: false,
quotingType: '"',
});
writeFileSync(resolve(folder, `${config.name}.yml`), yml, {
writeFileSync(resolve(dirLocation, `${config.name}.yml`), yml, {
encoding: "utf8",
});
});
counter++;
}
console.info(`Created ${counter} icon yml files`);
console.groupEnd();
};
@@ -1,12 +0,0 @@
export function paginate(arr, size) {
return arr.reduce((acc, val, i) => {
let idx = Math.floor(i / size);
if (!acc[idx]) {
acc[idx] = [];
}
let page = acc[idx];
page.push(val);
return acc;
}, []);
}
+1
View File
@@ -15,6 +15,7 @@ ymlList.forEach((file) => {
const ymlData = jsYaml.load(fs.readFileSync(`${basePath}/${file}`), {
schema: jsYaml.JSON_SCHEMA,
});
// biome-ignore lint/complexity/useOptionalChain: optional chain not supported in enviroment. Consider revriting to Typescript
if (ymlData.keywords && ymlData.keywords.includes("[ignore-docs]")) {
return;
}
@@ -27,7 +27,7 @@
"@vitejs/plugin-react": "^4.2.1",
"postcss": "^8.4.31",
"postcss-url": "^10.1.3",
"typescript": "^5.1.6",
"typescript": "5.5.4",
"vite": "^5.4.6",
"vite-plugin-singlefile": "^1.0.0",
"vite-plugin-svgr": "^4.2.0"
+8 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "7.2.0",
"version": "7.3.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
@@ -62,28 +62,30 @@
"update-metadata": "node config/metadata.js",
"write-packagejson": "echo '{\"type\": \"module\"}' > dist/react/esm/package.json",
"build": "yarn copy && yarn create-icons && concurrently \"tsc\" \"tsc -p tsconfig.esm.json && tsc-alias -p tsconfig.esm.json && yarn write-packagejson\" && node config/cleanTypes.js && yarn update-metadata && yarn parse-svg",
"fetch-new:icons": "tsx --env-file=.env config/figma/index.mjs",
"fetch-new:icons": "tsx --env-file=.env config/figma/index.ts",
"zip:icons": "node config/zip.js",
"test": "vitest run",
"test:watch": "vitest watch"
},
"devDependencies": {
"@figma/rest-api-spec": "^0.19.0",
"@svgr/cli": "8.1.0",
"@types/js-yaml": "^4.0.9",
"@types/react": "^18.3.11",
"adm-zip": "^0.5.10",
"concurrently": "7.2.1",
"concurrently": "9.0.1",
"copyfiles": "^2.4.1",
"cross-zip-cli": "1.0.0",
"fast-glob": "3.2.11",
"hast-util-select": "5.0.5",
"js-yaml": "^4.1.0",
"lodash.camelcase": "4.3.0",
"lodash.startcase": "4.4.0",
"lodash": "^4.17.21",
"prettier": "3.1.1",
"react": "^18.0.0",
"rehype-parse": "8.0.4",
"tsc-alias": "1.8.8",
"tsx": "^4.7.1",
"typescript": "^5.1.6",
"typescript": "5.5.4",
"unified": "10.1.2",
"vitest": "^1.2.2"
}
+4
View File
@@ -1,5 +1,9 @@
# @navikt/aksel-stylelint
## 7.3.0
## 7.2.1
## 7.2.0
## 7.1.2
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "7.2.0",
"version": "7.3.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,14 +35,14 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^7.2.0",
"@navikt/ds-tokens": "^7.2.0",
"concurrently": "7.2.1",
"@navikt/ds-css": "^7.3.0",
"@navikt/ds-tokens": "^7.3.0",
"concurrently": "9.0.1",
"postcss-selector-parser": "^6.0.13",
"postcss-value-parser": "^4.2.0",
"stylelint": "^14.8.5",
"stylelint-test-rule-node": "^0.2.1",
"tsx": "^4.7.1",
"typescript": "^5.1.6"
"typescript": "5.5.4"
}
}
+14
View File
@@ -1,5 +1,19 @@
# @navikt/aksel
## 7.3.0
### Patch Changes
- Updated dependencies [[`20ced9b`](https://github.com/navikt/aksel/commit/20ced9b084a1f4bb56a1e1c07ef92010c9f4d71c), [`536f088`](https://github.com/navikt/aksel/commit/536f088b97ab9ae139d6974deb2a3c69e4fb6db3)]:
- @navikt/ds-css@7.3.0
## 7.2.1
### Patch Changes
- Updated dependencies [[`6ddea4eaf`](https://github.com/navikt/aksel/commit/6ddea4eaf2e37f20f93d75e1a3a327b038fb209b)]:
- @navikt/ds-css@7.2.1
## 7.2.0
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "7.2.0",
"version": "7.3.0",
"description": "Aksel command line interface. Handles css-imports, codemods and more",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
"dependencies": {
"@navikt/ds-css": "7.2.0",
"@navikt/ds-css": "7.3.0",
"axios": "1.7.4",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
@@ -45,7 +45,7 @@
},
"devDependencies": {
"rimraf": "6.0.1",
"typescript": "^5.1.6",
"typescript": "5.5.4",
"vitest": "^1.2.2"
},
"sideEffects": false,
+1 -1
View File
@@ -43,7 +43,7 @@ export function validateGit(options: any, program: Command) {
try {
clean = isGitClean.sync(process.cwd());
} catch (err: any) {
if (err && err.stderr && err.stderr.indexOf("Not a git repository") >= 0) {
if (err?.stderr && err.stderr.indexOf("Not a git repository") >= 0) {
clean = true;
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ export async function getAllVersions() {
const npmPackageData = await axios.get(
`https://registry.npmjs.org/@navikt/ds-css`,
);
return Object.keys(npmPackageData.data["versions"]);
return Object.keys(npmPackageData.data.versions);
} catch (e) {
console.error(e);
return [];
+1 -1
View File
@@ -137,7 +137,7 @@ export async function cssImportsCommand() {
let foundComponents: string[] = [];
if (answers["autoscan"] === "yes") {
if (answers.autoscan === "yes") {
foundComponents = await new Promise((resolve) => {
exec(`node ${__dirname}/scan-code.js ${answers.scandir}`, (_, stdout) => {
resolve(
+16
View File
@@ -1,5 +1,21 @@
# @navikt/ds-css
## 7.3.0
### Minor Changes
- ActionMenu: :tada: New component! Replaces DropdownMenu as the go-to solution for floating menus. Includes support for grouping, sub-menus, checkboxes, radios and dividers. Read more in the [documentation](https://aksel.nav.no/komponenter/core/actionmenu). ([#3081](https://github.com/navikt/aksel/pull/3081))
### Patch Changes
- List.Item: Center icon horizontally inside it's container ([#3253](https://github.com/navikt/aksel/pull/3253))
## 7.2.1
### Patch Changes
- Combobox: Support PageUp/PageDown in dropdown list. ([#3158](https://github.com/navikt/aksel/pull/3158))
## 7.2.0
## 7.1.2
+227
View File
@@ -0,0 +1,227 @@
.navds-action-menu__content {
overflow: hidden;
box-shadow: var(--a-shadow-medium);
border-radius: var(--a-border-radius-large);
}
/* stylelint-disable csstools/value-no-unknown-custom-properties */
.navds-action-menu__content > .navds-action-menu__content-inner {
--__ac-action-menu-content-p: var(--a-spacing-2);
--__ac-action-menu-item-pr: var(--a-spacing-3);
--__ac-action-menu-item-pl: var(--a-spacing-2);
--__ac-action-menu-item-height: 2rem;
border-radius: var(--a-border-radius-large);
background-color: var(--a-surface-default);
min-width: 128px;
max-width: min(95vw, 640px);
transform-origin: var(--__ac-action-menu-content-transform-origin);
animation-duration: 160ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
padding: var(--__ac-action-menu-content-p);
overflow: auto;
max-height: var(--__ac-action-menu-content-available-height);
}
.navds-action-menu__content:where([data-state="open"]):where([data-side="bottom"]) {
animation-name: aksel-action-from-bottom, aksel-action-fade-in;
}
.navds-action-menu__content:where([data-state="open"]):where([data-side="top"]) {
animation-name: aksel-action-from-top, aksel-action-fade-in;
}
.navds-action-menu__content:where([data-state="open"]):where([data-side="left"]) {
animation-name: aksel-action-from-left, aksel-action-fade-in;
}
.navds-action-menu__content:where([data-state="open"]):where([data-side="right"]) {
animation-name: aksel-action-from-right, aksel-action-fade-in;
}
@keyframes aksel-action-from-bottom {
from {
transform: translateY(-4px);
}
to {
transform: translateY(0);
}
}
@keyframes aksel-action-from-top {
from {
transform: translateY(4px);
}
to {
transform: translateY(0);
}
}
@keyframes aksel-action-from-left {
from {
transform: translateX(4px);
}
to {
transform: translateY(0);
}
}
@keyframes aksel-action-from-right {
from {
transform: translateX(-4px);
}
to {
transform: translateY(0);
}
}
@keyframes aksel-action-fade-in {
from {
opacity: 0.01;
}
to {
opacity: 1;
}
}
.navds-action-menu__item {
display: flex;
align-items: center;
gap: var(--a-spacing-2);
min-height: var(--__ac-action-menu-item-height);
cursor: default;
border-radius: var(--a-border-radius-medium);
position: relative;
padding-left: var(--__ac-action-menu-item-pl);
padding-right: var(--__ac-action-menu-item-pr);
font-size: var(--a-font-size-medium);
scroll-margin-block: var(--__ac-action-menu-content-p);
line-height: 1.5;
color: var(--a-text-default);
text-decoration: none;
}
.navds-action-menu__item svg {
flex-shrink: 0;
}
.navds-action-menu__item--has-icon {
--__ac-action-menu-item-pl: var(--a-spacing-6);
}
.navds-action-menu__sub-trigger {
--__ac-action-menu-item-pr: var(--a-spacing-05);
}
.navds-action-menu__item:focus {
outline: none;
background-color: var(--a-surface-action-subtle-hover);
color: var(--a-text-default);
}
.navds-action-menu__item--danger {
color: var(--a-text-danger);
}
.navds-action-menu__item--danger:focus {
background-color: var(--a-surface-danger-subtle);
}
.navds-action-menu__marker {
display: flex;
align-items: center;
gap: var(--a-spacing-1);
}
.navds-action-menu__marker--right {
margin-left: auto;
padding-left: var(--a-spacing-4);
}
.navds-action-menu__marker--left {
position: absolute;
left: 0;
width: var(--__ac-action-menu-item-pl);
display: inline-flex;
justify-content: center;
}
.navds-action-menu__marker-icon svg {
font-size: 18px;
flex-shrink: 0;
}
.navds-action-menu__shortcut {
background-color: var(--a-surface-neutral-subtle);
color: var(--a-text-default);
border-radius: var(--a-border-radius-small);
padding: 0 var(--a-spacing-1);
min-width: 1.125rem;
height: 1.125rem;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: var(--a-font-size-small);
}
.navds-action-menu__label {
display: flex;
align-items: center;
min-height: calc(var(--__ac-action-menu-item-height) - 6px);
padding-right: var(--__ac-action-menu-item-pr);
padding-left: var(--__ac-action-menu-item-pl);
color: var(--a-text-subtle);
border-radius: var(--a-border-radius-medium);
user-select: none;
cursor: default;
font-size: var(--a-font-size-small);
}
.navds-action-menu__divider {
height: 1px;
margin-block: var(--a-spacing-2);
background-color: var(--a-border-divider);
}
.navds-action-menu__indicator {
display: grid;
place-content: center;
}
.navds-action-menu__indicator-icon {
font-size: 14px;
}
.navds-action-menu__indicator-icon--unchecked,
.navds-action-menu__indicator-icon--checked,
.navds-action-menu__indicator-icon--indeterminate {
display: none;
}
.navds-action-menu__indicator[data-state="unchecked"] .navds-action-menu__indicator-icon--unchecked {
display: block;
}
.navds-action-menu__indicator:where([data-state="checked"]) .navds-action-menu__indicator-icon--checked {
display: block;
}
.navds-action-menu__indicator:where([data-state="indeterminate"]) .navds-action-menu__indicator-icon--indeterminate {
display: block;
}
.navds-action-menu__item:where([aria-disabled="true"]) {
color: var(--a-text-subtle);
opacity: 0.5;
pointer-events: none;
}
.navds-action-menu__sub-trigger:where([data-state="open"]) {
background-color: var(--a-surface-neutral-subtle);
}
+3 -1
View File
@@ -47,6 +47,8 @@ legend {
white-space: normal;
}
/* Make elements with the HTML hidden attribute stay hidden by default. */
[hidden] {
display: none;
display: none !important;
}
+5
View File
@@ -69,6 +69,11 @@ const StyleMappings = {
dependencies: [typoCss],
},
{ component: "Alert", main: "alert.css", dependencies: [typoCss] },
{
component: "ActionMenu",
main: "action-menu.css",
dependencies: [typoCss],
},
{
component: "BodyLong",
main: typoCss,
+25
View File
@@ -0,0 +1,25 @@
# Template for writing new CSS
## File setup
1. Create new CSS-file for component: "alert.darkside.css", "button.darkside.css" etc. Remember to add `.darkside.` for easier lookup.
2. Import file in "darkside/index.css": `@import "./alert.darkside.css`
Files will not be included in `/dist` for now to avoid it potentially polluting production-code. This will need to be revisited when we want to expose the new systems outside the team.
## Development
1. Write new CSS for component. Use same prefix `.navds` as before.
2. Run storybook and change from "Default (old)" to "Darkside (new)" in toolbar. You are now developing new CSS 🎉
## Syntax
- Test using outline + outline-offset for focus-markings.
## Checklist
- Add new CSS-files under `core/css/darkside`.
- New files should include `.darkside.` for easier lookup.
- Import new CSS-files in `core/css/darkside/index.css`
- New CSS-selectors should have `.navds` as prefix, same as original code.
- Test changes in Storybook by changing mode in toolbar from "default (old)" -> "darkside (new)"
+129
View File
@@ -0,0 +1,129 @@
/* cyrillic-ext */
@font-face {
font-family: "Source Sans 3";
font-style: italic;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic-cyrillic_ext.woff2) format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: "Source Sans 3";
font-style: italic;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic-cyrillic.woff2) format("woff2");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
font-family: "Source Sans 3";
font-style: italic;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic-latin_ext.woff2) format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Source Sans 3";
font-style: italic;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: "Source Sans 3";
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal-cyrillic_ext.woff2) format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: "Source Sans 3";
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal-cyrillic.woff2) format("woff2");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
font-family: "Source Sans 3";
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal-latin_ext.woff2) format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Source Sans 3";
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Legacy support */
/* latin-ext */
@font-face {
font-family: "Source Sans Pro";
font-style: italic;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic-latin_ext.woff2) format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Source Sans Pro";
font-style: italic;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: "Source Sans Pro";
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal-latin_ext.woff2) format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Source Sans Pro";
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@@ -0,0 +1,75 @@
/* ==========================================================================
Print styles copied from https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/style.css
Inlined to avoid the additional HTTP request:
https://www.phpied.com/delay-loading-your-print-css/
========================================================================== */
@media print {
*,
*::before,
*::after {
background: #fff !important;
color: #000 !important;
/* Black prints faster */
box-shadow: none !important;
text-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]::after {
content: " (" attr(href) ")";
}
abbr[title]::after {
content: " (" attr(title) ")";
}
/*
* Don't show links that are fragment identifiers,
* or use the `javascript:` pseudo protocol
*/
a[href^="#"]::after,
a[href^="javascript:"]::after {
content: "";
}
pre {
white-space: pre-wrap !important;
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
/*
* Printing Tables:
* https://web.archive.org/web/20180815150934/http://css-discuss.incutio.com/wiki/Printing_Tables
*/
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}
@@ -0,0 +1,148 @@
/* Modern-normalize: https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css */
/* Better box-model */
*,
::before,
::after {
box-sizing: border-box;
}
html {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
line-height: 1.15; /* Correct the line height in all browsers. */
-webkit-text-size-adjust: 100%; /* Prevent adjustments of font size after orientation changes in iOS. */
tab-size: 4; /* Use a more readable tab size (opinionated). */
}
body {
margin: 0; /* Remove the margin in all browsers. */
}
b,
strong {
font-weight: bolder;
}
/**
1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
2. Correct the odd 'em' font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/**
Correct table border color inheritance in Chrome and Safari. (https://issues.chromium.org/issues/40615503, https://bugs.webkit.org/show_bug.cgi?id=195016)
*/
table {
border-color: currentColor;
}
/**
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
*/
legend {
padding: 0;
}
/**
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/**
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/**
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to 'inherit' in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
@@ -0,0 +1,10 @@
/* To support nested theming, we want to declare color on each scoped elements */
:root,
:host,
.light {
color: var(--a-text-default);
}
.dark {
color: var(--a-text-default);
}
@@ -0,0 +1,65 @@
[hidden] {
display: none !important;
}
/* https://web.dev/prefers-reduced-motion/ */
@media (prefers-reduced-motion: reduce) {
*:not(.navds-loader *):not(.navds-loader):not(.navds-progress-bar *):not(.navds-progress-bar),
::before,
::after {
animation-delay: -1ms !important;
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
background-attachment: initial !important;
scroll-behavior: auto !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
}
/*
* From https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/style.css
*
* Hide only visually, but have it available for screen readers:
* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility
*
* 1. For long content, line feeds are not interpreted as spaces and small width
* causes content to wrap 1 word per line:
* https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
*/
.sr-only,
.navds-sr-only {
border: 0;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
}
/*
* From https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/style.css
*
* Extends the .sr-only class to allow the element
* to be focusable when navigated to via the keyboard:
* https://www.drupal.org/node/897638
*/
.sr-only.focusable:active,
.sr-only.focusable:focus,
.navds-sr-only.focusable:active,
.navds-sr-only.focusable:focus {
clip: auto;
clip-path: none;
height: auto;
margin: 0;
overflow: visible;
position: static;
white-space: inherit;
width: auto;
}
@@ -0,0 +1,6 @@
/* stylelint-disable csstools/value-no-unknown-custom-properties */
/* stylelint-disable aksel/design-token-exists */
.navds-button {
background-color: var(--a-bg-accent-strong);
}
+17
View File
@@ -0,0 +1,17 @@
@charset "UTF-8";
@layer aksel.reset, aksel.typography, aksel.theme, aksel.components, aksel.utilities;
/* CSS foundations */
@import url("./baseline/reset.darkside.css") layer(aksel.reset);
@import url("./baseline/print.darkside.css") layer(aksel.reset);
@import url("./baseline/fonts.darkside.css") layer(aksel.typography);
@import url("./baseline/theme.darkside.css") layer(aksel.theme);
@import url("./typography.darkside.css") layer(aksel.typography);
@import url("./baseline/utilities.darkside.css") layer(aksel.utilities);
/* Theming */
@import url("@navikt/ds-tokens/dist/darkside/tokens.css") layer(aksel.theme);
/* Components */
@import url("./button.darkside.css") layer(aksel.components);
@@ -0,0 +1,6 @@
body,
:host {
font-family: "Source Sans 3", "Source Sans Pro", Arial, sans-serif;
line-height: 1.333;
font-size: 1.125rem;
}
+1
View File
@@ -263,6 +263,7 @@
border-radius: var(--a-border-radius-medium);
background-color: var(--ac-combobox-list-bg, var(--a-surface-default));
color: var(--ac-combobox-list-text, var(--a-text-default));
overscroll-behavior: contain;
}
.navds-combobox__list--closed {
+1
View File
@@ -8,6 +8,7 @@
@import "chips.css";
@import "copybutton.css";
@import "dropdown.css";
@import "action-menu.css";
@import "expansioncard.css";
@import "guide-panel.css";
@import "form/index.css";
+1
View File
@@ -80,5 +80,6 @@ _:future,
.navds-list__item-marker--icon {
font-size: 1.5rem;
justify-content: center;
color: var(--ac-list-marker-icon-color, var(--ac-list-marker-color, var(--a-icon-default)));
}
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "7.2.0",
"version": "7.3.0",
"description": "CSS for NAV Designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -27,10 +27,11 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^7.2.0",
"@navikt/ds-tokens": "^7.3.0",
"autoprefixer": "^10.4.20",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
"lodash": "^4.17.21",
"nodemon": "^3.0.1",
"normalize.css": "^8.0.1",
"postcss": "^8.4.31",
+28
View File
@@ -1,5 +1,33 @@
# @navikt/ds-react
## 7.3.0
### Minor Changes
- ActionMenu: :tada: New component! Replaces DropdownMenu as the go-to solution for floating menus. Includes support for grouping, sub-menus, checkboxes, radios and dividers. Read more in the [documentation](https://aksel.nav.no/komponenter/core/actionmenu). ([#3081](https://github.com/navikt/aksel/pull/3081))
### Patch Changes
- Accordion: Remove redundant icon title ([#3230](https://github.com/navikt/aksel/pull/3230))
- Modal: Close button now avoids triggering when repeated keystrokes are detected. This helps stop accidental closing of Modal when 'Enter'-key is held down. ([#3081](https://github.com/navikt/aksel/pull/3081))
- Updated dependencies []:
- @navikt/ds-tokens@7.3.0
- @navikt/aksel-icons@7.3.0
## 7.2.1
### Patch Changes
- Combobox: Disable autocomple in Firefox on Android to prevent bug ([#3201](https://github.com/navikt/aksel/pull/3201))
- Combobox: Support PageUp/PageDown in dropdown list. ([#3158](https://github.com/navikt/aksel/pull/3158))
- Updated dependencies []:
- @navikt/ds-tokens@7.2.1
- @navikt/aksel-icons@7.2.1
## 7.2.0
### Minor Changes
+15 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "7.2.0",
"version": "7.3.0",
"description": "React components from the Norwegian Labour and Welfare Administration.",
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
"license": "MIT",
@@ -50,6 +50,16 @@
"default": "./cjs/accordion/index.js"
}
},
"./ActionMenu": {
"import": {
"types": "./esm/overlays/action-menu/index.d.ts",
"default": "./esm/overlays/action-menu/index.js"
},
"require": {
"types": "./cjs/overlays/action-menu/index.d.ts",
"default": "./cjs/overlays/action-menu/index.js"
}
},
"./Alert": {
"import": {
"types": "./esm/alert/index.d.ts",
@@ -594,8 +604,8 @@
"dependencies": {
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.9",
"@navikt/aksel-icons": "^7.2.0",
"@navikt/ds-tokens": "^7.2.0",
"@navikt/aksel-icons": "^7.3.0",
"@navikt/ds-tokens": "^7.3.0",
"clsx": "^2.1.0",
"date-fns": "^3.0.0",
"react-day-picker": "8.10.0"
@@ -605,9 +615,7 @@
"@testing-library/jest-dom": "^5.16.0",
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^14.2.0",
"@types/faker": "5.5.8",
"concurrently": "7.2.1",
"faker": "5.5.3",
"concurrently": "9.0.1",
"fast-glob": "3.2.11",
"jsdom": "24.0.0",
"react-dom": "^18.0.0",
@@ -616,7 +624,7 @@
"swr": "^1.1.2",
"tsc-alias": "1.8.8",
"tsx": "^4.19.1",
"typescript": "^5.1.6",
"typescript": "5.5.4",
"vitest": "^1.2.2"
},
"peerDependencies": {
@@ -38,7 +38,6 @@ const AccordionHeader = forwardRef<HTMLButtonElement, AccordionHeaderProps>(
<span className="navds-accordion__icon-wrapper">
<ChevronDownIcon
className="navds-accordion__header-chevron"
title="Vis mer"
aria-hidden
/>
</span>
+11 -20
View File
@@ -9,6 +9,7 @@ import {
} from "@navikt/aksel-icons";
import { Button } from "../button";
import { BodyLong } from "../typography";
import { useI18n } from "../util/i18n/i18n.context";
export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
/**
@@ -54,25 +55,13 @@ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
onClose?: () => void;
}
const Icon = ({ variant, ...props }) => {
switch (variant) {
case "error":
return <XMarkOctagonFillIcon title="Feil" {...props} />;
case "warning":
return <ExclamationmarkTriangleFillIcon title="Advarsel" {...props} />;
case "info":
return <InformationSquareFillIcon title="Informasjon" {...props} />;
case "success":
return <CheckmarkCircleFillIcon title="Suksess" {...props} />;
default:
return null;
}
const IconMap = {
error: XMarkOctagonFillIcon,
warning: ExclamationmarkTriangleFillIcon,
info: InformationSquareFillIcon,
success: CheckmarkCircleFillIcon,
};
export interface AlertContextProps {
size: "medium" | "small";
}
/**
* A component for displaying alerts
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/alert)
@@ -98,6 +87,8 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
},
ref,
) => {
const translate = useI18n("Alert");
const Icon = IconMap[variant];
return (
<div
{...rest}
@@ -114,7 +105,7 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
},
)}
>
<Icon variant={variant} className="navds-alert__icon" />
<Icon title={translate(variant)} className="navds-alert__icon" />
<BodyLong
as="div"
size={size}
@@ -137,8 +128,8 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
<XMarkIcon
title={
["error", "warning"].includes(variant)
? "Lukk varsel"
: "Lukk melding"
? translate("closeAlert")
: translate("closeMessage")
}
/>
}
@@ -190,3 +190,19 @@ export const ChromaticForcedColors: Story = {
chromatic: { disable: false, forcedColors: "active" },
},
};
export const ChromaticDarksideDark: Story = {
render: Chromatic.render,
parameters: {
chromatic: { disable: false },
},
globals: { theme: "dark", mode: "darkside" },
};
export const ChromaticDarksideLight: Story = {
render: Chromatic.render,
parameters: {
chromatic: { disable: false },
},
globals: { theme: "light", mode: "darkside" },
};
+4 -4
View File
@@ -58,7 +58,7 @@ export const Size: Story = {
render: () => (
<VStack gap="4">
{SIZES.map((size) => (
<>
<React.Fragment key={size}>
<h3>{size}</h3>
<Chat
avatar="ON"
@@ -75,7 +75,7 @@ export const Size: Story = {
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
</>
</React.Fragment>
))}
</VStack>
),
@@ -85,7 +85,7 @@ export const Variants: Story = {
render: () => (
<VStack gap="4">
{VARIANTS.map((variant) => (
<>
<React.Fragment key={variant}>
<h3>{variant}</h3>
<Chat
avatar="NAV"
@@ -97,7 +97,7 @@ export const Variants: Story = {
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
</Chat>
</>
</React.Fragment>
))}
</VStack>
),
+13 -9
View File
@@ -2,6 +2,7 @@ import cl from "clsx";
import React, { forwardRef } from "react";
import { XMarkIcon } from "@navikt/aksel-icons";
import { composeEventHandlers } from "../util/composeEventHandlers";
import { useI18n } from "../util/i18n/i18n.context";
export interface ChipsRemovableProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -11,15 +12,15 @@ export interface ChipsRemovableProps
* @default "action"
*/
variant?: "action" | "neutral";
/**
* Replaces label read for screen-readers
* @default "slett filter"
*/
removeLabel?: string;
/**
* Click callback
*/
onDelete?: () => void;
/**
* Replaces label read for screen-readers
* @default "slett"
*/
removeLabel?: string;
}
export const RemovableChips = forwardRef<
@@ -28,17 +29,18 @@ export const RemovableChips = forwardRef<
>(
(
{
className,
children,
variant = "action",
removeLabel = "slett",
onDelete,
type = "button",
removeLabel,
className,
onClick,
type = "button",
...rest
},
ref,
) => {
const translate = useI18n("Chips");
return (
<button
{...rest}
@@ -49,7 +51,9 @@ export const RemovableChips = forwardRef<
className,
`navds-chips__removable--${variant}`,
)}
aria-label={`${children} ${removeLabel}`}
aria-label={`${children} ${
removeLabel ?? translate("Removable.labelSuffix")
}`}
onClick={composeEventHandlers(onClick, onDelete)}
>
<span className="navds-chips__chip-text">{children}</span>
@@ -27,8 +27,8 @@ interface DatePickerComponent
* ```jsx
* <DatePicker.Standalone
* dropdownCaption
* fromDate={new Date("2020-10-01")}
* toDate={new Date("2024-10-01")}
* fromDate={new Date("2022-10-01")}
* toDate={new Date("2026-10-01")}
* />
* ```
*/
@@ -19,8 +19,8 @@ interface MonthPickerComponent
* ```jsx
* <MonthPicker.Standalone
* dropdownCaption
* fromDate={new Date("1 Oct 2020")}
* toDate={new Date("1 Oct 2024")}
* fromDate={new Date("1 Oct 2022")}
* toDate={new Date("1 Oct 2026")}
* />
* ```
*/
@@ -82,7 +82,7 @@ export const Menu = forwardRef<HTMLDivElement, DropdownMenuProps>(
open={isOpen}
onClose={() => {
handleToggle(false);
onClose && onClose();
onClose?.();
}}
/>
);
@@ -1,5 +1,4 @@
import { fireEvent, render, screen } from "@testing-library/react";
import faker from "faker";
import React from "react";
import { describe, expect, test, vi } from "vitest";
import { Checkbox, CheckboxGroup } from ".";
@@ -10,8 +9,8 @@ const firstArgumentOfFirstCall = (fn: ReturnType<typeof vi.fn>) =>
test("checkbox group chains onChange calls", async () => {
const onGroupChange = vi.fn();
const onChange = vi.fn();
const value = faker.datatype.string();
const label = faker.datatype.string();
const value = "Checkbox value";
const label = "My pretty label";
render(
<CheckboxGroup legend="legend" onChange={onGroupChange}>
@@ -44,8 +44,8 @@ const useCheckbox = (props: CheckboxProps) => {
if (readOnly) {
return;
}
props.onChange && props.onChange(e);
checkboxGroup && checkboxGroup.toggleValue(props.value);
props.onChange?.(e);
checkboxGroup?.toggleValue(props.value);
},
onClick: (e) => {
if (readOnly) {
@@ -51,13 +51,21 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
value,
onChange,
onClear,
shouldAutocomplete,
shouldAutocomplete: externalShouldAutocomplete,
size,
...rest
} = props;
const options = mapToComboboxOptionArray(externalOptions) || [];
const filteredOptions = mapToComboboxOptionArray(externalFilteredOptions);
const selectedOptions = mapToComboboxOptionArray(externalSelectedOptions);
const userAgent =
typeof navigator === "undefined" ? "" : navigator.userAgent;
const isFirefoxOnAndroid =
userAgent.includes("Android") && userAgent.includes("Firefox/");
const shouldAutocomplete =
!isFirefoxOnAndroid && externalShouldAutocomplete;
return (
<InputContextProvider
value={{
@@ -140,7 +140,7 @@ const FilteredOptionsProvider = ({
if (disabled || readOnly) {
return;
}
virtualFocus.moveFocusToTop();
virtualFocus.resetFocus();
if (newState ?? !isInternalListOpen) {
setHideCaret(!!maxSelected?.isLimitReached);
}
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
export type VirtualFocusType = {
activeElement: HTMLElement | undefined;
@@ -10,6 +10,9 @@ export type VirtualFocusType = {
moveFocusToElement: (id: string) => void;
moveFocusToTop: () => void;
moveFocusToBottom: () => void;
moveFocusUpBy: (numberOfElements: number) => void;
moveFocusDownBy: (numberOfElements: number) => void;
resetFocus: () => void;
};
const useVirtualFocus = (
@@ -40,11 +43,6 @@ const useVirtualFocus = (
: false;
};
const _moveFocusAndScrollTo = (_element?: HTMLElement) => {
setActiveElement(_element);
_element?.scrollIntoView?.({ block: "nearest" });
};
const moveFocusUp = () => {
if (!activeElement) {
return;
@@ -55,14 +53,14 @@ const useVirtualFocus = (
if (_currentIndex === 0) {
setActiveElement(undefined);
} else {
_moveFocusAndScrollTo(elementAbove);
setActiveElement(elementAbove);
}
};
const moveFocusDown = () => {
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
if (!activeElement) {
_moveFocusAndScrollTo(elementsAbleToReceiveFocus[0]);
setActiveElement(elementsAbleToReceiveFocus[0]);
return;
}
const _currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
@@ -70,13 +68,17 @@ const useVirtualFocus = (
return;
}
_moveFocusAndScrollTo(elementsAbleToReceiveFocus[_currentIndex + 1]);
setActiveElement(elementsAbleToReceiveFocus[_currentIndex + 1]);
};
const moveFocusToTop = () => _moveFocusAndScrollTo(undefined);
const resetFocus = () => setActiveElement(undefined);
const moveFocusToTop = () => {
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
setActiveElement(elementsAbleToReceiveFocus[0]);
};
const moveFocusToBottom = () => {
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
return _moveFocusAndScrollTo(
setActiveElement(
elementsAbleToReceiveFocus[elementsAbleToReceiveFocus.length - 1],
);
};
@@ -89,6 +91,32 @@ const useVirtualFocus = (
}
};
const moveFocusUpBy = (numberOfElements: number) => {
if (!activeElement) {
return;
}
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
const currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
const newIndex = Math.max(currentIndex - numberOfElements, 0);
setActiveElement(elementsAbleToReceiveFocus[newIndex]);
};
const moveFocusDownBy = (numberOfElements: number) => {
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
const currentIndex = activeElement
? elementsAbleToReceiveFocus.indexOf(activeElement)
: -1;
const newIndex = Math.min(
currentIndex + numberOfElements,
elementsAbleToReceiveFocus.length - 1,
);
setActiveElement(elementsAbleToReceiveFocus[newIndex]);
};
useEffect(() => {
activeElement?.scrollIntoView?.({ block: "nearest" });
}, [activeElement]);
return {
activeElement,
getElementById,
@@ -99,6 +127,9 @@ const useVirtualFocus = (
moveFocusToElement,
moveFocusToTop,
moveFocusToBottom,
moveFocusUpBy,
moveFocusDownBy,
resetFocus,
};
};
@@ -134,14 +134,6 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
case "Accept":
onEnter(e);
break;
case "Home":
toggleIsListOpen(false);
virtualFocus.moveFocusToTop();
break;
case "End":
toggleIsListOpen(true);
virtualFocus.moveFocusToBottom();
break;
default:
break;
}
@@ -202,6 +194,24 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
}
virtualFocus.moveFocusUp();
}
} else if (e.key === "Home") {
e.preventDefault();
virtualFocus.moveFocusToTop();
} else if (e.key === "End") {
e.preventDefault();
if (virtualFocus.activeElement === null || !isListOpen) {
toggleIsListOpen(true);
}
virtualFocus.moveFocusToBottom();
} else if (e.key === "PageUp") {
e.preventDefault();
virtualFocus.moveFocusUpBy(6);
} else if (e.key === "PageDown") {
e.preventDefault();
if (virtualFocus.activeElement === null || !isListOpen) {
toggleIsListOpen(true);
}
virtualFocus.moveFocusDownBy(6);
}
},
[
@@ -230,10 +240,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
} else if (filteredOptions.length === 0) {
toggleIsListOpen(false);
}
virtualFocus.moveFocusToTop();
onChange(newValue);
},
[filteredOptions.length, virtualFocus, onChange, toggleIsListOpen],
[filteredOptions.length, onChange, toggleIsListOpen],
);
return (
@@ -291,4 +291,40 @@ describe("Render combobox", () => {
);
});
});
describe("has keyboard navigation", () => {
test("for PageDown and PageUp", async () => {
render(<App options={options} />);
const combobox = screen.getByRole("combobox", {
name: "Hva er dine favorittfrukter?",
});
const pressKey = async (key: string) => {
await act(async () => {
await userEvent.keyboard(`{${key}}`);
});
};
const hasVirtualFocus = (option: string) =>
expect(combobox.getAttribute("aria-activedescendant")).toBe(
screen.getByRole("option", { name: option }).id,
);
await act(async () => {
await userEvent.click(combobox);
});
await pressKey("ArrowDown");
hasVirtualFocus("apple");
await pressKey("PageDown");
hasVirtualFocus("mango");
await pressKey("PageDown");
hasVirtualFocus("watermelon");
await pressKey("PageUp");
hasVirtualFocus("mango");
await pressKey("PageUp");
hasVirtualFocus("apple");
});
});
});
@@ -1,11 +1,10 @@
import { render, screen } from "@testing-library/react";
import faker from "faker";
import React from "react";
import { expect, test } from "vitest";
import { ConfirmationPanel } from ".";
test("omits error id from input", async () => {
const label = faker.datatype.string();
const label = "My label";
render(<ConfirmationPanel label={label} errorId="wat"></ConfirmationPanel>);
@@ -1,14 +1,13 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import faker from "faker";
import React from "react";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { Radio, RadioGroup } from ".";
const value1 = faker.datatype.string();
const label1 = faker.datatype.string();
const value2 = faker.datatype.string();
const label2 = faker.datatype.string();
const value1 = "My first value";
const label1 = "World's best radio label";
const value2 = "Life changing value";
const label2 = "Radio label of the year";
const Group = (props) => (
<RadioGroup {...props} legend="legend">
@@ -41,8 +41,8 @@ export const useRadio = (props: RadioProps) => {
if (readOnly) {
return;
}
props.onChange && props.onChange(e);
radioGroup?.onChange && radioGroup.onChange(props.value);
props.onChange?.(e);
radioGroup?.onChange?.(props.value);
},
onClick: (e) => {
if (readOnly) {
@@ -144,7 +144,7 @@ export const Search = forwardRef<HTMLInputElement, SearchProps>(
(event: SearchClearEvent) => {
onClear?.(event);
handleChange("");
searchRef.current && searchRef.current?.focus?.();
searchRef.current?.focus?.();
},
[handleChange, onClear],
);
@@ -122,7 +122,7 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
return;
}
setChecked(e.target.checked);
props.onChange && props.onChange(e);
props.onChange?.(e);
}}
onClick={(e) => {
if (readOnly) {
@@ -49,7 +49,7 @@ Default.argTypes = {
export const Open: StoryFn = () => {
const ref = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
ref.current && ref.current.click();
ref.current?.click();
}, []);
return (
+1
View File
@@ -1,5 +1,6 @@
"use client";
export { Accordion, type AccordionProps } from "./accordion";
export { ActionMenu, type ActionMenuProps } from "./overlays/action-menu";
export { Alert, type AlertProps } from "./alert";
export { Button, type ButtonProps } from "./button";
export { Chat, type ChatProps } from "./chat";
@@ -5,6 +5,9 @@ import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
import { ResponsiveProp, SpacingScale } from "../utilities/types";
export type PrimitiveProps = {
/**
* @private Hides prop from documentation
*/
className?: string;
/**
* Padding around children.
+35 -36
View File
@@ -19,42 +19,41 @@ import {
SurfaceColorToken,
} from "../utilities/types";
export type BoxProps = PrimitiveProps &
PrimitiveAsChildProps &
React.HTMLAttributes<HTMLDivElement> & {
/**
* CSS `background-color` property.
* Accepts a [background/surface color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#afff774dad80).
*/
background?: BackgroundColorToken | SurfaceColorToken;
/**
* CSS `border-color` property.
* Accepts a [border color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#adb1767e2f87).
*/
borderColor?: BorderColorToken;
/**
* CSS `border-radius` property.
* Accepts a [radius token](https://aksel.nav.no/grunnleggende/styling/design-tokens#6d79c5605d31)
* or an object of radius tokens for different breakpoints.
* @example
* borderRadius='full'
* borderRadius='0 full large small'
* borderRadius={{xs: 'small large', sm: '0', md: 'large', lg: 'full'}}
*/
borderRadius?: ResponsiveProp<SpaceDelimitedAttribute<BorderRadiiToken>>;
/**
* CSS `border-width` property. If this is not set there will be no border.
* @example
* borderWidth='2'
* borderWidth='1 2 3 4'
*/
borderWidth?: SpaceDelimitedAttribute<"0" | "1" | "2" | "3" | "4" | "5">;
/** Shadow on box. Accepts a shadow token.
* @example
* shadow='small'
*/
shadow?: ShadowToken;
};
export type BoxProps = React.HTMLAttributes<HTMLDivElement> & {
/**
* CSS `background-color` property.
* Accepts a [background/surface color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#afff774dad80).
*/
background?: BackgroundColorToken | SurfaceColorToken;
/**
* CSS `border-color` property.
* Accepts a [border color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#adb1767e2f87).
*/
borderColor?: BorderColorToken;
/**
* CSS `border-radius` property.
* Accepts a [radius token](https://aksel.nav.no/grunnleggende/styling/design-tokens#6d79c5605d31)
* or an object of radius tokens for different breakpoints.
* @example
* borderRadius='full'
* borderRadius='0 full large small'
* borderRadius={{xs: 'small large', sm: '0', md: 'large', lg: 'full'}}
*/
borderRadius?: ResponsiveProp<SpaceDelimitedAttribute<BorderRadiiToken>>;
/**
* CSS `border-width` property. If this is not set there will be no border.
* @example
* borderWidth='2'
* borderWidth='1 2 3 4'
*/
borderWidth?: SpaceDelimitedAttribute<"0" | "1" | "2" | "3" | "4" | "5">;
/** Shadow on box. Accepts a shadow token.
* @example
* shadow='small'
*/
shadow?: ShadowToken;
} & PrimitiveProps &
PrimitiveAsChildProps;
/**
* Foundational Layout-primitive for generic encapsulation & styling.
+26 -27
View File
@@ -10,33 +10,32 @@ import { PrimitiveAsChildProps } from "../base/PrimitiveAsChildProps";
import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
import { ResponsiveProp, SpacingScale } from "../utilities/types";
export type HGridProps = PrimitiveProps &
PrimitiveAsChildProps &
React.HTMLAttributes<HTMLDivElement> & {
/**
* Number of columns to display. Can be a number, a string, or an object with values for specific breakpoints.
* Sets `grid-template-columns`, so `fr`, `minmax` etc. works.
* @example
* columns={{ sm: 1, md: 1, lg: "1fr auto", xl: "1fr auto"}}
* columns={3}
* columns="repeat(3, minmax(0, 1fr))"
*/
columns?: ResponsiveProp<number | string>;
/**
* Spacing between columns.
* Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213)
* or an object of spacing tokens for different breakpoints.
* @example
* gap="6"
* gap="8 4"
* gap={{ sm: "2", md: "2", lg: "6", xl: "6"}}
*/
gap?: ResponsiveProp<SpacingScale | `${SpacingScale} ${SpacingScale}`>;
/**
* Vertical alignment of children. Elements will by default stretch to the height of parent-element.
*/
align?: "start" | "center" | "end";
};
export type HGridProps = React.HTMLAttributes<HTMLDivElement> & {
/**
* Number of columns to display. Can be a number, a string, or an object with values for specific breakpoints.
* Sets `grid-template-columns`, so `fr`, `minmax` etc. works.
* @example
* columns={{ sm: 1, md: 1, lg: "1fr auto", xl: "1fr auto"}}
* columns={3}
* columns="repeat(3, minmax(0, 1fr))"
*/
columns?: ResponsiveProp<number | string>;
/**
* Spacing between columns.
* Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213)
* or an object of spacing tokens for different breakpoints.
* @example
* gap="6"
* gap="8 4"
* gap={{ sm: "2", md: "2", lg: "6", xl: "6"}}
*/
gap?: ResponsiveProp<SpacingScale | `${SpacingScale} ${SpacingScale}`>;
/**
* Vertical alignment of children. Elements will by default stretch to the height of parent-element.
*/
align?: "start" | "center" | "end";
} & PrimitiveProps &
PrimitiveAsChildProps;
/**
* Horizontal Grid Primitive with dynamic columns and gap based on breakpoints.
*
+53 -54
View File
@@ -11,60 +11,59 @@ import { PrimitiveAsChildProps } from "../base/PrimitiveAsChildProps";
import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
import { ResponsiveProp, SpacingScale } from "../utilities/types";
export type StackProps = PrimitiveProps &
PrimitiveAsChildProps &
HTMLAttributes<HTMLDivElement> & {
/**
* CSS `justify-content` property.
*
* @example
* justify='center'
* justify={{xs: 'start', sm: 'center', md: 'end', lg: 'space-around', xl: 'space-between'}}
*/
justify?: ResponsiveProp<
| "start"
| "center"
| "end"
| "space-around"
| "space-between"
| "space-evenly"
>;
/**
* CSS `align-items` property.
* @default "stretch"
*
* @example
* align='center'
* align={{xs: 'start', sm: 'center', md: 'end', lg: 'baseline', xl: 'stretch'}}
*/
align?: ResponsiveProp<"start" | "center" | "end" | "baseline" | "stretch">;
/**
* Sets the CSS `flex-wrap` property.
*/
wrap?: boolean;
/**
* CSS `gap` property.
* Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213)
* or an object of spacing tokens for different breakpoints.
*
* @example
* gap='4'
* gap='8 4'
* gap={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}}
*/
gap?: ResponsiveProp<SpacingScale | `${SpacingScale} ${SpacingScale}`>;
/**
* CSS `flex-direction` property.
* @default "row"
*
* @example
* direction='row'
* direction={{xs: 'row', sm: 'column'}}
*/
direction?: ResponsiveProp<
"row" | "column" | "row-reverse" | "column-reverse"
>;
};
export type StackProps = HTMLAttributes<HTMLDivElement> & {
/**
* CSS `justify-content` property.
*
* @example
* justify='center'
* justify={{xs: 'start', sm: 'center', md: 'end', lg: 'space-around', xl: 'space-between'}}
*/
justify?: ResponsiveProp<
| "start"
| "center"
| "end"
| "space-around"
| "space-between"
| "space-evenly"
>;
/**
* CSS `align-items` property.
* @default "stretch"
*
* @example
* align='center'
* align={{xs: 'start', sm: 'center', md: 'end', lg: 'baseline', xl: 'stretch'}}
*/
align?: ResponsiveProp<"start" | "center" | "end" | "baseline" | "stretch">;
/**
* Sets the CSS `flex-wrap` property.
*/
wrap?: boolean;
/**
* CSS `gap` property.
* Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213)
* or an object of spacing tokens for different breakpoints.
*
* @example
* gap='4'
* gap='8 4'
* gap={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}}
*/
gap?: ResponsiveProp<SpacingScale | `${SpacingScale} ${SpacingScale}`>;
/**
* CSS `flex-direction` property.
* @default "row"
*
* @example
* direction='row'
* direction={{xs: 'row', sm: 'column'}}
*/
direction?: ResponsiveProp<
"row" | "column" | "row-reverse" | "column-reverse"
>;
} & PrimitiveProps &
PrimitiveAsChildProps;
export const Stack: OverridableComponent<StackProps, HTMLDivElement> =
forwardRef(
+13 -2
View File
@@ -1,7 +1,6 @@
import { Meta, StoryObj } from "@storybook/react";
import React from "react";
import {
BrailleIcon,
CheckmarkIcon,
ForkIcon,
HeadHeartIcon,
@@ -322,7 +321,19 @@ export const Icons: Story = {
<List title="Best title">
<List.Item
title="Lorem Ipsum Dolor Sit Amet"
icon={<BrailleIcon aria-hidden />}
icon={
<svg
width="12"
height="8"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ transform: "rotate(45deg)" }}
aria-hidden
>
<rect width="24" height="24" rx="6" fill="var(--a-icon-alt-3)" />
</svg>
}
>
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
@@ -25,6 +25,12 @@ const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(
className="navds-modal__button"
size="small"
variant="tertiary-neutral"
onKeyDown={(event) => {
/* Prevents autofocus used in combination with holding down keys from closing modal */
if (["Enter", " "].includes(event.key) && event.repeat) {
event.preventDefault();
}
}}
onClick={context.closeHandler}
icon={<XMarkIcon title="Lukk" />}
/>
@@ -518,7 +518,7 @@ dialogPolyfill.isInlinePositionSetByStylesheet = function (element) {
// Some browsers throw on cssRules.
try {
cssRules = styleSheet.cssRules;
} catch (e) {
} catch {
/* empty */
}
if (!cssRules) {
@@ -530,7 +530,7 @@ dialogPolyfill.isInlinePositionSetByStylesheet = function (element) {
// Ignore errors on invalid selector texts.
try {
selectedNodes = document.querySelectorAll(rule.selectorText);
} catch (e) {
} catch {
/* empty */
}
if (!selectedNodes || !inNodeList(selectedNodes, element)) {
@@ -0,0 +1,608 @@
import { Meta, StoryObj } from "@storybook/react";
import React, { useEffect, useRef, useState } from "react";
import { PencilIcon, StarIcon } from "@navikt/aksel-icons";
import { Button } from "../../button";
import { HStack, VStack } from "../../layout/stack";
import { Modal } from "../../modal";
import { Tooltip } from "../../tooltip";
import { BodyShort } from "../../typography";
import { ActionMenu } from "./ActionMenu";
export default {
title: "ds-react/ActionMenu",
parameters: {
layout: "padded",
chromatic: { disable: true },
},
} satisfies Meta<typeof ActionMenu>;
type Story = StoryObj<typeof ActionMenu>;
const DemoDecorator: Story["decorators"] = (Story, { name }) => {
return (
<VStack gap="4" align="start">
<h2>{name}</h2>
<p>Placeholder before button</p>
<button>Focusable item before action</button>
<Story />
<button>Focusable item after action</button>
<p>Placeholder after button</p>
</VStack>
);
};
export const OnlyItems: Story = {
render: (props) => {
return (
<ActionMenu open={props.open}>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.Item onSelect={() => console.log("Item 1 clicked")}>
Item 1
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 2 clicked")}>
Item 2
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 3 clicked")}>
Item 3
</ActionMenu.Item>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const GroupedItems: Story = {
render: (props) => {
return (
<ActionMenu open={props.open}>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.Group label="Group 1">
<ActionMenu.Item onSelect={() => console.log("Item 1 clicked")}>
Item 1
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 2 clicked")}>
Item 2
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 3 clicked")}>
Item 3
</ActionMenu.Item>
</ActionMenu.Group>
<ActionMenu.Group label="Group 2">
<ActionMenu.Item onSelect={() => console.log("Item 4 clicked")}>
Item 4
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 5 clicked")}>
Item 5
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 6 clicked")}>
Item 6
</ActionMenu.Item>
</ActionMenu.Group>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const ShortcutsAndIcons: Story = {
render: (props) => {
const [checkedItems, setCheckedItems] = useState({
checkbox1: false,
checkbox2: false,
});
const handleCheckboxChange = (checkboxId: string) => {
setCheckedItems((prevState) => ({
...prevState,
[checkboxId]: !prevState[checkboxId],
}));
};
return (
<ActionMenu open={props.open}>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<VStack asChild align="start" gap="1" paddingBlock="2">
<ActionMenu.Label>
<BodyShort weight="semibold" as="div">
Ola Normann
</BodyShort>
<div>D123456</div>
</ActionMenu.Label>
</VStack>
<ActionMenu.Divider />
<ActionMenu.Group label="Group 1">
<ActionMenu.CheckboxItem
checked={checkedItems.checkbox1}
onCheckedChange={() => handleCheckboxChange("checkbox1")}
shortcut="⌘+T"
>
Checkbox 1
</ActionMenu.CheckboxItem>
<ActionMenu.CheckboxItem
checked={checkedItems.checkbox2}
onCheckedChange={() => handleCheckboxChange("checkbox2")}
shortcut="⇧+⌘+N"
>
Checkbox 2
</ActionMenu.CheckboxItem>
</ActionMenu.Group>
<ActionMenu.Divider />
<ActionMenu.Group label="Group 2">
<ActionMenu.Item
shortcut="⌘+T"
onSelect={() => console.log("Item 1 clicked")}
icon={<StarIcon aria-hidden />}
>
Item 1
</ActionMenu.Item>
<ActionMenu.Item
shortcut="⇧+⌘+N"
onSelect={() => console.log("Item 2 clicked")}
icon={<PencilIcon aria-hidden />}
>
Item 2
</ActionMenu.Item>
</ActionMenu.Group>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const Checkboxes: Story = {
render: (props) => {
const [checkedItems, setCheckedItems] = useState({
checkbox1: false,
checkbox2: true,
checkbox3: false,
checkbox4: false,
});
const handleCheckboxChange = (checkboxId: string) => {
setCheckedItems((prevState) => ({
...prevState,
[checkboxId]: !prevState[checkboxId],
}));
};
return (
<ActionMenu open={props.open}>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.CheckboxItem
checked={
Object.values(checkedItems).every(Boolean)
? true
: Object.values(checkedItems).some(Boolean)
? "indeterminate"
: false
}
onCheckedChange={() =>
Object.values(checkedItems).every(Boolean)
? setCheckedItems((prevState) =>
Object.keys(prevState).reduce(
(acc, key) => {
acc[key] = false;
return acc;
},
{} as typeof checkedItems,
),
)
: setCheckedItems((prevState) =>
Object.keys(prevState).reduce(
(acc, key) => {
acc[key] = true;
return acc;
},
{} as typeof checkedItems,
),
)
}
>
Select all
</ActionMenu.CheckboxItem>
<ActionMenu.Group label="Group 1">
<ActionMenu.CheckboxItem
checked={checkedItems.checkbox1}
onCheckedChange={() => handleCheckboxChange("checkbox1")}
>
Checkbox 1
</ActionMenu.CheckboxItem>
<ActionMenu.CheckboxItem
checked={checkedItems.checkbox2}
onCheckedChange={() => handleCheckboxChange("checkbox2")}
>
Checkbox 1
</ActionMenu.CheckboxItem>
</ActionMenu.Group>
<ActionMenu.Group label="Group 2">
<ActionMenu.CheckboxItem
checked={checkedItems.checkbox3}
onCheckedChange={() => handleCheckboxChange("checkbox3")}
>
Checkbox 3
</ActionMenu.CheckboxItem>
<ActionMenu.CheckboxItem
checked={checkedItems.checkbox4}
onCheckedChange={() => handleCheckboxChange("checkbox4")}
>
Checkbox 4
</ActionMenu.CheckboxItem>
</ActionMenu.Group>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const RadioGroups: Story = {
render: (props) => {
const [group1Value, setGroup1Value] = useState("1");
const [group2Value, setGroup2Value] = useState("4");
return (
<ActionMenu open={props.open}>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.RadioGroup
onValueChange={setGroup1Value}
value={group1Value}
label="Group 1"
>
<ActionMenu.RadioItem value="1">Radio 1</ActionMenu.RadioItem>
<ActionMenu.RadioItem value="2">Radio 2</ActionMenu.RadioItem>
</ActionMenu.RadioGroup>
<ActionMenu.RadioGroup
onValueChange={setGroup2Value}
value={group2Value}
label="Group 2"
>
<ActionMenu.RadioItem value="3">Radio 3</ActionMenu.RadioItem>
<ActionMenu.RadioItem value="4">Radio 4</ActionMenu.RadioItem>
</ActionMenu.RadioGroup>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const Dividers: Story = {
render: (props) => {
return (
<ActionMenu open={props.open}>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.Group label="Group 1">
<ActionMenu.Item onSelect={() => console.log("Item 1 clicked")}>
Item 1
</ActionMenu.Item>
</ActionMenu.Group>
<ActionMenu.Divider />
<ActionMenu.Group label="Group 2">
<ActionMenu.Item onSelect={() => console.log("Item 4 clicked")}>
Item 4
</ActionMenu.Item>
</ActionMenu.Group>
<ActionMenu.Divider />
<ActionMenu.Item onSelect={() => console.log("Item 7 clicked")}>
Item 7
</ActionMenu.Item>
<ActionMenu.Divider />
<ActionMenu.Item onSelect={() => console.log("Item 8 clicked")}>
Item 8
</ActionMenu.Item>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const Submenus: Story = {
render: (props) => {
return (
<ActionMenu open={props.open}>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.Item onSelect={() => console.log("Item 1 clicked")}>
Item 1
</ActionMenu.Item>
<ActionMenu.Sub open={props.open}>
<ActionMenu.SubTrigger>Submenu 1</ActionMenu.SubTrigger>
<ActionMenu.SubContent>
<ActionMenu.Item
onSelect={() => console.log("Subitem 1 clicked")}
>
Subitem 1
</ActionMenu.Item>
<ActionMenu.Item
onSelect={() => console.log("Subitem 2 clicked")}
>
Subitem 2
</ActionMenu.Item>
<ActionMenu.Sub open={props.open}>
<ActionMenu.SubTrigger>Nested submenu 1</ActionMenu.SubTrigger>
<ActionMenu.SubContent>
<ActionMenu.Item
onSelect={() => console.log("Nested Subitem 1 clicked")}
>
Nested Subitem 1
</ActionMenu.Item>
<ActionMenu.Item
onSelect={() => console.log("Nested Subitem 2 clicked")}
>
Nested Subitem 2
</ActionMenu.Item>
</ActionMenu.SubContent>
</ActionMenu.Sub>
</ActionMenu.SubContent>
</ActionMenu.Sub>
<ActionMenu.Item onSelect={() => console.log("Item 3 clicked")}>
Item 3
</ActionMenu.Item>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const Disabled: Story = {
render: (props) => {
return (
<ActionMenu open={props.open}>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.Item disabled shortcut="T+W">
Item 1
</ActionMenu.Item>
<ActionMenu.Sub>
<ActionMenu.SubTrigger disabled>Submenu 1</ActionMenu.SubTrigger>
<ActionMenu.SubContent>
<ActionMenu.Item
onSelect={() => console.log("Subitem 1 clicked")}
>
Subitem 1
</ActionMenu.Item>
</ActionMenu.SubContent>
</ActionMenu.Sub>
<ActionMenu.CheckboxItem checked disabled shortcut="T+W">
Checkbox disabled
</ActionMenu.CheckboxItem>
<ActionMenu.CheckboxItem>Checkbox 2</ActionMenu.CheckboxItem>
<ActionMenu.RadioGroup value="1" label="Radiogroup">
<ActionMenu.RadioItem disabled value="1">
Radio disabled
</ActionMenu.RadioItem>
<ActionMenu.RadioItem value="2">Radio</ActionMenu.RadioItem>
</ActionMenu.RadioGroup>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const TriggerWithTooltip: Story = {
render: () => {
return (
<ActionMenu>
<Tooltip content="Tooltip!">
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
</Tooltip>
<ActionMenu.Content>
<ActionMenu.Item onSelect={() => console.log("Item 1 clicked")}>
Item 1
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 2 clicked")}>
Item 2
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 3 clicked")}>
Item 3
</ActionMenu.Item>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const ModalTrigger: Story = {
render: () => {
const ref = useRef<HTMLDialogElement>(null);
return (
<div>
<ActionMenu>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.Item onSelect={() => ref.current?.showModal()}>
open modal
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 2 clicked")}>
Item 2
</ActionMenu.Item>
</ActionMenu.Content>
</ActionMenu>
<Modal ref={ref} header={{ heading: "Heading" }}>
<Modal.Body>
Culpa aliquip ut cupidatat laborum minim quis ex in aliqua.
</Modal.Body>
<Modal.Footer>
<Button type="button" onClick={() => ref.current?.close()}>
Close
</Button>
</Modal.Footer>
</Modal>
</div>
);
},
decorators: [DemoDecorator],
};
export const OpenInsideModal: Story = {
render: () => {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
ref.current?.showModal();
}, []);
return (
<div>
<button onClick={() => ref.current?.showModal()}>Open modal</button>
<Modal ref={ref} header={{ heading: "Heading" }}>
<Modal.Body>
Culpa aliquip ut cupidatat laborum minim quis ex in aliqua.
<ActionMenu>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.Item onSelect={() => console.log("Item 1 clicked")}>
Item 1
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 2 clicked")}>
Item 2
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 3 clicked")}>
Item 3
</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
</ActionMenu.Content>
</ActionMenu>
</Modal.Body>
<Modal.Footer>
<Button type="button" onClick={() => ref.current?.close()}>
Close
</Button>
</Modal.Footer>
</Modal>
</div>
);
},
decorators: [DemoDecorator],
};
export const Links: Story = {
render: (props) => {
return (
<ActionMenu open={props.open}>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.Item as="a" href="#1">
Item 1
</ActionMenu.Item>
<ActionMenu.Item as="a" href="#2">
Item 2
</ActionMenu.Item>
<ActionMenu.Item as="a" href="#3">
Item 3
</ActionMenu.Item>
</ActionMenu.Content>
</ActionMenu>
);
},
decorators: [DemoDecorator],
};
export const Chromatic: Story = {
render: (args, context) => {
const newArgs = { ...args, open: true };
return (
<VStack style={{ gap: "32rem" }}>
<div>
<h2>Submenus</h2>
{Submenus.render?.(newArgs, context)}
</div>
<HStack gap="12">
<div>
<h2>OnlyItems</h2>
{OnlyItems.render?.(newArgs, context)}
</div>
<div>
<h2>GroupedItems</h2>
{GroupedItems.render?.(newArgs, context)}
</div>
<div>
<h2>Shortcut</h2>
{ShortcutsAndIcons.render?.(newArgs, context)}
</div>
</HStack>
<HStack gap="12">
<div>
<h2>Checkboxes</h2>
{Checkboxes.render?.(newArgs, context)}
</div>
<div>
<h2>RadioGroups</h2>
{RadioGroups.render?.(newArgs, context)}
</div>
</HStack>
<div>
<h2>Dividers</h2>
{Dividers.render?.(newArgs, context)}
</div>
<HStack gap="12">
<div>
<h2>Links</h2>
{Links.render?.(newArgs, context)}
</div>
<div>
<h2>Disabled</h2>
{Disabled.render?.(newArgs, context)}
</div>
</HStack>
</VStack>
);
},
parameters: {
chromatic: { disable: false },
},
};
@@ -0,0 +1,971 @@
import cl from "clsx";
import React, { forwardRef, useRef } from "react";
import { ChevronRightIcon } from "@navikt/aksel-icons";
import { useModalContext } from "../../modal/Modal.context";
import { Slot } from "../../slot/Slot";
import { OverridableComponent, useId } from "../../util";
import { composeEventHandlers } from "../../util/composeEventHandlers";
import { createContext } from "../../util/create-context";
import { useMergeRefs } from "../../util/hooks";
import { useControllableState } from "../../util/hooks/useControllableState";
import { requireReactElement } from "../../util/requireReactElement";
import { Menu, MenuPortalProps } from "../floating-menu/Menu";
/* -------------------------------------------------------------------------- */
/* ActionMenu */
/* -------------------------------------------------------------------------- */
type ActionMenuContextValue = {
triggerId: string;
triggerRef: React.RefObject<HTMLButtonElement>;
contentId: string;
open: boolean;
onOpenChange: (open: boolean) => void;
onOpenToggle: () => void;
rootElement: MenuPortalProps["rootElement"];
};
const [ActionMenuProvider, useActionMenuContext] =
createContext<ActionMenuContextValue>({
name: "ActionMenuContext",
errorMessage:
"ActionMenu sub-components cannot be rendered outside the ActionMenu component.",
});
type ActionMenuProps = {
children?: React.ReactNode;
/**
* Whether the menu is open or not.
* Only needed if you want manually control state.
*/
open?: boolean;
/**
* Callback for when the menu is opened or closed.
*/
onOpenChange?: (open: boolean) => void;
} & Pick<MenuPortalProps, "rootElement">;
interface ActionMenuComponent extends React.FC<ActionMenuProps> {
/**
* Acts as a trigger and anchor for the menu.
* Must be wrapped around a button. If you use your own component, make sure to forward ref and props.
* @example
* ```jsx
* <ActionMenu.Trigger>
* <button>Open Menu</button>
* </ActionMenu.Trigger>
* ```
*/
Trigger: typeof ActionMenuTrigger;
/**
* The menu content, containing all the items.
* @example
* ```jsx
* <ActionMenu.Content>
* <ActionMenu.Item>
* Item 1
* </ActionMenu.Item>
* <ActionMenu.Item>
* Item 2
* </ActionMenu.Item>
* </ActionMenu.Content>
* ```
*/
Content: typeof ActionMenuContent;
/**
* Semantically and visually groups items together with a label.
* This is the prefered way to group items, as it provides better accessibility
* rather than using a standalone `ActionMenu.Label`.
*
* It is required to use either `label` or `aria-label` to provide an accessible name for the group.
* @example
* ```jsx
* <ActionMenu.Content>
* <ActionMenu.Group label="Group 1">
* <ActionMenu.Item>
* Item 1
* </ActionMenu.Item>
* <ActionMenu.Item>
* Item 2
* </ActionMenu.Item>
* </ActionMenu.Group>
* <ActionMenu.Group label="Group 2">
* <ActionMenu.Item>
* Item 3
* </ActionMenu.Item>
* <ActionMenu.Item>
* Item 4
* </ActionMenu.Item>
* </ActionMenu.Group>
* </ActionMenu.Content>
* ```
*/
Group: typeof ActionMenuGroup;
/**
* Separate labeling option for the menu.
* This is not for grouping items, but rather for adding a label to the menu at the top. For grouping items, use `ActionMenu.Group`.
* @example
* ```jsx
* <ActionMenu.Content>
* <ActionMenu.Label>
* Label
* </ActionMenu.Label>
* <ActionMenu.Divider />
* </ActionMenu.Content
* ```
*/
Label: typeof ActionMenuLabel;
/**
* A single item in the menu. Can be used standalone or grouped with other items.
* Use `onSelect` to handle the action when the item is selected, like navigating to a new page or performing an action.
* @example
* ```jsx
* <ActionMenu.Content>
* // Grouped
* <ActionMenu.Group label="Group 1">
* <ActionMenu.Item onSelect={navigate}>
* Item 1
* </ActionMenu.Item>
* <ActionMenu.Item onSelect={navigate}>
* Item 2
* </ActionMenu.Item>
* </ActionMenu.Group>
* <ActionMenu.Divider />
* // Standalone
* <ActionMenu.Item onSelect={updateState}>
* Item 3
* </ActionMenu.Item>
* </ActionMenu.Content>
* ```
* @example As link
* ```jsx
* <ActionMenu.Item as="a" href="#">
* Item
* </ActionMenu.Item>
* ```
*/
Item: typeof ActionMenuItem;
/**
* A checkbox item in the menu. Can be used standalone or grouped with other items.
* @example
* ```jsx
* <ActionMenu.CheckboxItem
* checked={isChecked}
* onCheckedChange={handleChange}
* >
* Checkbox 1
* </ActionMenu.CheckboxItem>
* ```
*/
CheckboxItem: typeof ActionMenuCheckboxItem;
/**
* A radio group in the menu.
*
* It is required to use either `label` or `aria-label` to provide an accessible name for the group.
* @example
* ```jsx
* <ActionMenu.RadioGroup
* onValueChange={handleValueChange}
* value={radioValue}
* label="Radio group"
* >
* <ActionMenu.RadioItem value="1">Radio 1</ActionMenu.RadioItem>
* <ActionMenu.RadioItem value="2">Radio 2</ActionMenu.RadioItem>
* </ActionMenu.RadioGroup>
* ```
*/
RadioGroup: typeof ActionMenuRadioGroup;
/**
* A radio item in the menu. Should always be grouped with an `ActionMenu.RadioGroup`.
* @example
* ```jsx
* <ActionMenu.RadioGroup
* onValueChange={handleValueChange}
* value={radioValue}
* label="Radio group"
* >
* <ActionMenu.RadioItem value="1">Radio 1</ActionMenu.RadioItem>
* <ActionMenu.RadioItem value="2">Radio 2</ActionMenu.RadioItem>
* </ActionMenu.RadioGroup>
* ```
*/
RadioItem: typeof ActionMenuRadioItem;
/**
* A simple divider to separate items in the menu.
*/
Divider: typeof ActionMenuDivider;
/**
* A sub-menu that can be nested inside the menu.
* The sub-menu can be nested inside other sub-menus allowing for multiple levels of nesting.
* @example
* ```jsx
* <ActionMenu.Sub>
* <ActionMenu.SubTrigger>Submenu 1</ActionMenu.SubTrigger>
* <ActionMenu.SubContent>
* <ActionMenu.Item>
* Subitem 1
* </ActionMenu.Item>
* <ActionMenu.Item>
* Subitem 2
* </ActionMenu.Item>
* </ActionMenu.SubContent>
* </ActionMenu.Sub>
* ```
*/
Sub: typeof ActionMenuSub;
/**
* Acts as a trigger for a sub-menu.
* In contrast to `ActionMenu.Trigger`, this trigger is a standalone component and should not be wrapped around a React.ReactNode.
* @example
* ```jsx
* <ActionMenu.Sub>
* <ActionMenu.SubTrigger>Submenu 1</ActionMenu.SubTrigger>
* </ActionMenu.Sub>
* ```
*/
SubTrigger: typeof ActionMenuSubTrigger;
/**
* The content of a sub-menu.
* @example
* ```jsx
* <ActionMenu.Sub>
* <ActionMenu.SubContent>
* <ActionMenu.Item>
* Subitem 1
* </ActionMenu.Item>
* <ActionMenu.Item>
* Subitem 2
* </ActionMenu.Item>
* </ActionMenu.SubContent>
* </ActionMenu.Sub>
* ```
*/
SubContent: typeof ActionMenuSubContent;
}
const ActionMenuRoot = ({
children,
open: openProp,
onOpenChange,
rootElement: rootElementProp,
}: ActionMenuProps) => {
const triggerRef = useRef<HTMLButtonElement>(null);
const modalContext = useModalContext(false);
const rootElement = modalContext ? modalContext.ref.current : rootElementProp;
const [open = false, setOpen] = useControllableState({
value: openProp,
defaultValue: false,
onChange: onOpenChange,
});
return (
<ActionMenuProvider
triggerId={useId()}
triggerRef={triggerRef}
contentId={useId()}
open={open}
onOpenChange={setOpen}
onOpenToggle={() => setOpen((prevOpen) => !prevOpen)}
rootElement={rootElement}
>
<Menu open={open} onOpenChange={setOpen} modal>
{children}
</Menu>
</ActionMenuProvider>
);
};
export const ActionMenu = ActionMenuRoot as ActionMenuComponent;
/* -------------------------------------------------------------------------- */
/* ActionMenuTrigger */
/* -------------------------------------------------------------------------- */
interface ActionMenuTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactElement;
}
export const ActionMenuTrigger = forwardRef<
HTMLButtonElement,
ActionMenuTriggerProps
>(
(
{ children, onKeyDown, style, onClick, ...rest }: ActionMenuTriggerProps,
ref,
) => {
const context = useActionMenuContext();
const mergedRefs = useMergeRefs(ref, context.triggerRef);
return (
<Menu.Anchor asChild>
<Slot
type="button"
id={context.triggerId}
aria-haspopup="menu"
aria-expanded={context.open}
aria-controls={context.open ? context.contentId : undefined}
data-state={context.open ? "open" : "closed"}
ref={mergedRefs}
{...rest}
style={{ ...style, pointerEvents: context.open ? "auto" : undefined }}
onClick={composeEventHandlers(onClick, context.onOpenToggle)}
onKeyDown={composeEventHandlers(onKeyDown, (event) => {
if (event.key === "ArrowDown") {
context.onOpenChange(true);
/* Stop keydown from scrolling window */
event.preventDefault();
}
})}
>
{requireReactElement(children)}
</Slot>
</Menu.Anchor>
);
},
);
/* -------------------------------------------------------------------------- */
/* ActionMenuContent */
/* -------------------------------------------------------------------------- */
interface ActionMenuContentProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "id"> {
children?: React.ReactNode;
}
export const ActionMenuContent = forwardRef<
HTMLDivElement,
ActionMenuContentProps
>(({ children, className, style, ...rest }: ActionMenuContentProps, ref) => {
const context = useActionMenuContext();
return (
<Menu.Portal rootElement={context.rootElement} asChild>
<Menu.Content
ref={ref}
id={context.contentId}
aria-labelledby={context.triggerId}
className={cl("navds-action-menu__content", className)}
{...rest}
align="start"
sideOffset={4}
collisionPadding={10}
onCloseAutoFocus={() => {
context.triggerRef.current?.focus();
}}
safeZone={{ anchor: context.triggerRef.current }}
style={{
...style,
...{
"--__ac-action-menu-content-transform-origin":
"var(--ac-floating-transform-origin)",
"--__ac-action-menu-content-available-height":
"var(--ac-floating-available-height)",
},
}}
>
<div className="navds-action-menu__content-inner">{children}</div>
</Menu.Content>
</Menu.Portal>
);
});
/* -------------------------------------------------------------------------- */
/* ActionMenuLabel */
/* -------------------------------------------------------------------------- */
interface ActionMenuLabelProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export const ActionMenuLabel = forwardRef<HTMLDivElement, ActionMenuLabelProps>(
({ children, className, ...rest }: ActionMenuLabelProps, ref) => {
return (
<div
ref={ref}
{...rest}
className={cl("navds-action-menu__label", className)}
>
{children}
</div>
);
},
);
/* -------------------------------------------------------------------------- */
/* ActionMenuGroup */
/* -------------------------------------------------------------------------- */
type ActionMenuGroupElement = React.ElementRef<typeof Menu.Group>;
type MenuGroupProps = React.ComponentPropsWithoutRef<typeof Menu.Group>;
type ActionMenuGroupLabelingProps =
| {
/**
* Adds a visual and accessible label to the group.
*/
label: string;
/**
* Adds an aria-label to the group.
*/
"aria-label"?: never;
}
| {
/**
* Adds an aria-label to the group.
*/
"aria-label": string;
/**
* Adds a visual and accessible label to the group.
*/
label?: never;
};
type ActionMenuGroupProps = Omit<MenuGroupProps, "asChild"> &
ActionMenuGroupLabelingProps;
export const ActionMenuGroup = forwardRef<
ActionMenuGroupElement,
ActionMenuGroupProps
>(({ children, className, label, ...rest }: ActionMenuGroupProps, ref) => {
const labelId = useId();
return (
<Menu.Group
ref={ref}
{...rest}
className={cl("navds-action-menu__group", className)}
asChild={false}
aria-labelledby={label ? labelId : undefined}
>
{label && (
<ActionMenu.Label id={labelId} aria-hidden>
{label}
</ActionMenu.Label>
)}
{children}
</Menu.Group>
);
});
/* -------------------------------------------------------------------------- */
/* Utility components */
/* -------------------------------------------------------------------------- */
type MarkerProps = {
children: React.ReactNode;
className?: string;
placement: "left" | "right";
};
const Marker = ({ children, className, placement }: MarkerProps) => {
return (
<div
aria-hidden
className={cl(
className,
"navds-action-menu__marker",
`navds-action-menu__marker--${placement}`,
)}
>
{children}
</div>
);
};
type ShortcutProps = {
children: string;
};
const Shortcut = ({ children }: ShortcutProps) => {
/**
* Assumes the user will input either a single keyboard key
* or keys separated by "+"
*/
const parsed = children.split("+").filter((str) => str !== "");
return (
<Marker placement="right">
{parsed.map((char, index) => (
<span key={char + index} className="navds-action-menu__shortcut">
{char}
</span>
))}
</Marker>
);
};
/* -------------------------------------------------------------------------- */
/* ActionMenuItem */
/* -------------------------------------------------------------------------- */
type ActionMenuItemElement = React.ElementRef<typeof Menu.Item>;
type MenuItemProps = React.ComponentPropsWithoutRef<typeof Menu.Item>;
interface ActionMenuItemProps extends Omit<MenuItemProps, "asChild"> {
/**
* Shows connected shortcut-keys for the item.
* This is only a visual representation, you will have to implement the actual shortcut yourself.
*/
shortcut?: string;
/**
* Styles the item as a destructive action.
*/
variant?: "danger";
/**
* Adds an icon on the left side. The icon will always have aria-hidden.
*/
icon?: React.ReactNode;
}
export const ActionMenuItem: OverridableComponent<
ActionMenuItemProps,
ActionMenuItemElement
> = forwardRef(
(
{
children,
as: Component = "div",
className,
icon,
shortcut,
variant,
...rest
},
ref,
) => {
return (
<Menu.Item
{...rest}
className={cl("navds-action-menu__item", className, {
"navds-action-menu__item--danger": variant === "danger",
"navds-action-menu__item--has-icon": icon,
})}
aria-keyshortcuts={shortcut ?? undefined}
asChild
>
<Component ref={ref}>
{children}
{icon && (
<Marker placement="left" className="navds-action-menu__marker-icon">
{icon}
</Marker>
)}
{shortcut && <Shortcut>{shortcut}</Shortcut>}
</Component>
</Menu.Item>
);
},
);
/* -------------------------------------------------------------------------- */
/* ActionMenuCheckboxItem */
/* -------------------------------------------------------------------------- */
type ActionMenuCheckboxItemElement = React.ElementRef<typeof Menu.CheckboxItem>;
type MenuCheckboxItemProps = React.ComponentPropsWithoutRef<
typeof Menu.CheckboxItem
>;
interface ActionMenuCheckboxItemProps
extends Omit<MenuCheckboxItemProps, "asChild"> {
children: React.ReactNode;
/**
* Shows connected shortcut-keys for the item.
* This is only a visual representation, you will have to implement the actual shortcut yourself.
*/
shortcut?: string;
}
export const ActionMenuCheckboxItem = forwardRef<
ActionMenuCheckboxItemElement,
ActionMenuCheckboxItemProps
>(
(
{
children,
className,
shortcut,
onSelect,
...rest
}: ActionMenuCheckboxItemProps,
ref,
) => {
return (
<Menu.CheckboxItem
ref={ref}
{...rest}
onSelect={composeEventHandlers(onSelect, (event) => {
/**
* Prevent default to avoid the menu from closing when clicking the checkbox
*/
event.preventDefault();
})}
asChild={false}
className={cl(
"navds-action-menu__item navds-action-menu__item--has-icon",
className,
)}
aria-keyshortcuts={shortcut}
>
{children}
<Marker placement="left">
<Menu.ItemIndicator className="navds-action-menu__indicator">
<svg
width="1em"
height="1em"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="navds-action-menu__indicator-icon"
aria-hidden
>
<g className="navds-action-menu__indicator-icon--unchecked">
<rect
width="24"
height="24"
rx="4"
fill="var(--a-border-default)"
/>
<rect
x="1"
y="1"
width="22"
height="22"
rx="3"
fill="var(--a-surface-default)"
strokeWidth="2"
/>
</g>
<g className="navds-action-menu__indicator-icon--indeterminate">
<rect
width="24"
height="24"
rx="4"
fill="var(--a-surface-action-selected)"
/>
<rect
x="6"
y="10"
width="12"
height="4"
rx="1"
fill="var(--a-surface-default)"
/>
</g>
<g className="navds-action-menu__indicator-icon--checked">
<rect
width="24"
height="24"
rx="4"
fill="var(--a-surface-action-selected)"
/>
<path
d="M10.0352 13.4148L16.4752 7.40467C17.0792 6.83965 18.029 6.86933 18.5955 7.47478C19.162 8.08027 19.1296 9.03007 18.5245 9.59621L11.0211 16.5993C10.741 16.859 10.3756 17 10.0002 17C9.60651 17 9.22717 16.8462 8.93914 16.5611L6.43914 14.0611C5.85362 13.4756 5.85362 12.5254 6.43914 11.9399C7.02467 11.3544 7.97483 11.3544 8.56036 11.9399L10.0352 13.4148Z"
fill="var(--a-surface-default)"
/>
</g>
</svg>
</Menu.ItemIndicator>
</Marker>
{shortcut && <Shortcut>{shortcut}</Shortcut>}
</Menu.CheckboxItem>
);
},
);
/* -------------------------------------------------------------------------- */
/* ActionMenuRadioGroup */
/* -------------------------------------------------------------------------- */
type ActionMenuRadioGroupElement = React.ElementRef<typeof Menu.RadioGroup>;
type MenuRadioGroupProps = React.ComponentPropsWithoutRef<
typeof Menu.RadioGroup
>;
type ActionMenuRadioGroupProps = ActionMenuGroupLabelingProps &
Omit<MenuRadioGroupProps, "asChild"> & {
children: React.ReactNode;
};
export const ActionMenuRadioGroup = forwardRef<
ActionMenuRadioGroupElement,
ActionMenuRadioGroupProps
>(({ children, label, ...rest }: ActionMenuRadioGroupProps, ref) => {
const labelId = useId();
return (
<Menu.RadioGroup
ref={ref}
{...rest}
asChild={false}
aria-labelledby={label ? labelId : undefined}
>
{label && (
<ActionMenu.Label id={labelId} aria-hidden>
{label}
</ActionMenu.Label>
)}
{children}
</Menu.RadioGroup>
);
});
/* -------------------------------------------------------------------------- */
/* ActionMenuRadioItem */
/* -------------------------------------------------------------------------- */
type ActionMenuRadioItemElement = React.ElementRef<typeof Menu.RadioItem>;
type MenuRadioItemProps = React.ComponentPropsWithoutRef<typeof Menu.RadioItem>;
interface ActionMenuRadioItemProps extends Omit<MenuRadioItemProps, "asChild"> {
children: React.ReactNode;
}
export const ActionMenuRadioItem = forwardRef<
ActionMenuRadioItemElement,
ActionMenuRadioItemProps
>(
(
{ children, className, onSelect, ...rest }: ActionMenuRadioItemProps,
ref,
) => {
return (
<Menu.RadioItem
ref={ref}
{...rest}
onSelect={composeEventHandlers(onSelect, (event) => {
/**
* Prevent default to avoid the menu from closing when clicking the radio
*/
event.preventDefault();
})}
asChild={false}
className={cl(
"navds-action-menu__item navds-action-menu__item--has-icon",
className,
)}
>
{children}
<Marker placement="left">
<Menu.ItemIndicator className="navds-action-menu__indicator">
<svg
width="1em"
height="1em"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="navds-action-menu__indicator-icon"
aria-hidden
>
<g className="navds-action-menu__indicator-icon--unchecked">
<rect
width="24"
height="24"
rx="12"
fill="var(--a-border-default)"
/>
<rect
x="1"
y="1"
width="22"
height="22"
rx="11"
strokeWidth="2"
fill="var(--a-surface-default)"
/>
</g>
<g className="navds-action-menu__indicator-icon--checked">
<rect
x="1"
y="1"
width="22"
height="22"
rx="11"
fill="var(--a-surface-default)"
/>
<rect
x="1"
y="1"
width="22"
height="22"
rx="11"
stroke="var(--a-surface-action-selected)"
strokeWidth="2"
/>
<path
d="M20 12C20 16.4178 16.4178 20 12 20C7.58222 20 4 16.4178 4 12C4 7.58222 7.58222 4 12 4C16.4178 4 20 7.58222 20 12Z"
fill="var(--a-surface-action-selected)"
/>
</g>
</svg>
</Menu.ItemIndicator>
</Marker>
</Menu.RadioItem>
);
},
);
/* -------------------------------------------------------------------------- */
/* ActionMenuDivider */
/* -------------------------------------------------------------------------- */
type ActionMenuDividerElement = React.ElementRef<typeof Menu.Divider>;
type MenuDividerProps = React.ComponentPropsWithoutRef<typeof Menu.Divider>;
type ActionMenuDividerProps = Omit<MenuDividerProps, "asChild">;
export const ActionMenuDivider = forwardRef<
ActionMenuDividerElement,
ActionMenuDividerProps
>(({ className, ...rest }: ActionMenuDividerProps, ref) => {
return (
<Menu.Divider
ref={ref}
asChild={false}
{...rest}
className={cl("navds-action-menu__divider", className)}
/>
);
});
/* -------------------------------------------------------------------------- */
/* ActionMenuSub */
/* -------------------------------------------------------------------------- */
interface ActionMenuSubProps {
children?: React.ReactNode;
/**
* Whether the sub-menu is open or not. Only needed if you want to manually control state.
*/
open?: boolean;
/**
* Callback for when the sub-menu is opened or closed.
*/
onOpenChange?: (open: boolean) => void;
}
export const ActionMenuSub = (props: ActionMenuSubProps) => {
const { children, open: openProp, onOpenChange } = props;
const [open = false, setOpen] = useControllableState({
value: openProp,
defaultValue: false,
onChange: onOpenChange,
});
return (
<Menu.Sub open={open} onOpenChange={setOpen}>
{children}
</Menu.Sub>
);
};
/* -------------------------------------------------------------------------- */
/* ActionMenuSubTrigger */
/* -------------------------------------------------------------------------- */
type ActionMenuSubTriggerElement = React.ElementRef<typeof Menu.SubTrigger>;
type MenuSubTriggerProps = React.ComponentPropsWithoutRef<
typeof Menu.SubTrigger
>;
interface ActionMenuSubTriggerProps
extends Omit<MenuSubTriggerProps, "asChild"> {
icon?: React.ReactNode;
}
export const ActionMenuSubTrigger = forwardRef<
ActionMenuSubTriggerElement,
ActionMenuSubTriggerProps
>(({ children, className, icon, ...rest }: ActionMenuSubTriggerProps, ref) => {
return (
<Menu.SubTrigger
ref={ref}
{...rest}
asChild={false}
className={cl(
"navds-action-menu__item navds-action-menu__sub-trigger",
className,
{ "navds-action-menu__item--has-icon": icon },
)}
>
{children}
{icon && (
<Marker placement="left" className="navds-action-menu__marker-icon">
{icon}
</Marker>
)}
<Marker placement="right" className="navds-action-menu__marker-icon">
<ChevronRightIcon aria-hidden />
</Marker>
</Menu.SubTrigger>
);
});
/* -------------------------------------------------------------------------- */
/* ActionMenuSubContent */
/* -------------------------------------------------------------------------- */
type ActionMenuSubContentElement = React.ElementRef<typeof Menu.Content>;
interface ActionMenuSubContentProps
extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export const ActionMenuSubContent = forwardRef<
ActionMenuSubContentElement,
ActionMenuSubContentProps
>(({ children, className, style, ...rest }: ActionMenuSubContentProps, ref) => {
const context = useActionMenuContext();
return (
<Menu.Portal rootElement={context.rootElement}>
<Menu.SubContent
ref={ref}
alignOffset={-4}
sideOffset={1}
collisionPadding={10}
{...rest}
className={cl(
"navds-action-menu__content navds-action-menu__sub-content",
className,
)}
style={{
...style,
...{
"--ac-action-menu-content-transform-origin":
"var(--ac-floating-transform-origin)",
"--ac-action-menu-content-available-width":
"var(--ac-floating-available-width)",
"--ac-action-menu-content-available-height":
"var(--ac-floating-available-height)",
"--ac-action-menu-trigger-width": "var(--ac-floating-anchor-width)",
"--ac-action-menu-trigger-height":
"var(--ac-floating-anchor-height)",
},
}}
>
<div className="navds-action-menu__content-inner">{children}</div>
</Menu.SubContent>
</Menu.Portal>
);
});
/* -------------------------------------------------------------------------- */
ActionMenu.Trigger = ActionMenuTrigger;
ActionMenu.Content = ActionMenuContent;
ActionMenu.Group = ActionMenuGroup;
ActionMenu.Label = ActionMenuLabel;
ActionMenu.Item = ActionMenuItem;
ActionMenu.CheckboxItem = ActionMenuCheckboxItem;
ActionMenu.RadioGroup = ActionMenuRadioGroup;
ActionMenu.RadioItem = ActionMenuRadioItem;
ActionMenu.Divider = ActionMenuDivider;
ActionMenu.Sub = ActionMenuSub;
ActionMenu.SubTrigger = ActionMenuSubTrigger;
ActionMenu.SubContent = ActionMenuSubContent;
export type {
ActionMenuItemProps,
ActionMenuCheckboxItemProps,
ActionMenuContentProps,
ActionMenuDividerProps,
ActionMenuGroupProps,
ActionMenuLabelProps,
ActionMenuProps,
ActionMenuRadioGroupProps,
ActionMenuRadioItemProps,
ActionMenuSubContentProps,
ActionMenuSubProps,
ActionMenuSubTriggerProps,
ActionMenuTriggerProps,
};
@@ -0,0 +1,29 @@
"use client";
export {
ActionMenu,
ActionMenuCheckboxItem,
ActionMenuContent,
ActionMenuGroup,
ActionMenuItem,
ActionMenuLabel,
ActionMenuRadioGroup,
ActionMenuRadioItem,
ActionMenuDivider,
ActionMenuSub,
ActionMenuSubContent,
ActionMenuSubTrigger,
ActionMenuTrigger,
type ActionMenuItemProps,
type ActionMenuCheckboxItemProps,
type ActionMenuContentProps,
type ActionMenuGroupProps,
type ActionMenuLabelProps,
type ActionMenuProps,
type ActionMenuRadioGroupProps,
type ActionMenuRadioItemProps,
type ActionMenuDividerProps,
type ActionMenuSubContentProps,
type ActionMenuSubProps,
type ActionMenuSubTriggerProps,
type ActionMenuTriggerProps,
} from "./ActionMenu";
@@ -22,7 +22,7 @@ export const Styled = () => (
<Menu.Item className="item" onSelect={() => window.alert("redo")}>
Redo
</Menu.Item>
<Menu.Separator className="separator" />
<Menu.Divider className="separator" />
<Menu.Item className="item" disabled onSelect={() => window.alert("cut")}>
Cut
</Menu.Item>
@@ -103,7 +103,7 @@ export const Submenus = () => {
</Menu.Item>
</Submenu>
<Menu.Separator className="separator" />
<Menu.Divider className="separator" />
<Menu.Item className="item" disabled onSelect={() => window.alert("cut")}>
Cut
</Menu.Item>
@@ -179,7 +179,7 @@ export const WithLabels = () => (
</Menu.Item>
))}
{index < foodGroups.length - 1 && (
<Menu.Separator className="separator" />
<Menu.Divider className="separator" />
)}
</Menu.Group>
))}
@@ -217,7 +217,7 @@ export const CheckboxItems = () => {
Select all
{selection.length === options.length ? <TickIcon /> : <div>—</div>}
</Menu.CheckboxItem>
<Menu.Separator className="separator" />
<Menu.Divider className="separator" />
{options.map((option) => (
<Menu.CheckboxItem
key={option}
@@ -256,7 +256,7 @@ export const RadioItems = () => {
<Menu.Item className="item" onSelect={() => window.alert("smaller")}>
Smaller
</Menu.Item>
<Menu.Separator className="separator" />
<Menu.Divider className="separator" />
<Menu.RadioGroup value={file} onValueChange={setFile}>
{files.map((_file) => (
<Menu.RadioItem key={_file} className="item" value={_file}>
@@ -426,7 +426,7 @@ export const MenuWithOpenButton = () => {
<Menu.Item className="item" onSelect={() => window.alert("redo")}>
Redo
</Menu.Item>
<Menu.Separator className="separator" />
<Menu.Divider className="separator" />
<Menu.Item
className="item"
disabled
@@ -1,10 +1,4 @@
import React, {
forwardRef,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import React, { forwardRef, useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import { Portal } from "../../portal";
import { composeEventHandlers } from "../../util/composeEventHandlers";
@@ -24,17 +18,10 @@ import {
/* -------------------------------------------------------------------------- */
/* Constants */
/* -------------------------------------------------------------------------- */
const SELECTION_KEYS = ["Enter", " "];
const SUB_OPEN_KEYS = [...SELECTION_KEYS, "ArrowRight"];
const SUB_CLOSE_KEYS = ["ArrowLeft"];
const FIRST_KEYS = ["ArrowDown", "PageUp", "Home"];
const LAST_KEYS = ["ArrowUp", "PageDown", "End"];
const FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];
type Point = { x: number; y: number };
type Polygon = Point[];
type SubMenuSide = "left" | "right";
type GraceIntent = { area: Polygon; side: SubMenuSide };
type CheckedState = boolean | "indeterminate";
/* -------------------------------------------------------------------------- */
@@ -56,7 +43,7 @@ interface MenuComponent extends React.FC<MenuProps> {
CheckboxItem: typeof MenuCheckboxItem;
RadioGroup: typeof MenuRadioGroup;
RadioItem: typeof MenuRadioItem;
Separator: typeof MenuSeparator;
Divider: typeof MenuDivider;
Sub: typeof MenuSub;
SubTrigger: typeof MenuSubTrigger;
SubContent: typeof MenuSubContent;
@@ -68,7 +55,12 @@ const [
useMenuDescendantsContext,
useMenuDescendants,
useMenuDescendant,
] = createDescendantContext<SlottedDivElementRef>();
] = createDescendantContext<
SlottedDivElementRef,
{
closeMenu: () => void;
}
>();
type MenuContentElementRef = React.ElementRef<typeof Floating.Content>;
@@ -180,22 +172,8 @@ const MenuAnchor = forwardRef<MenuAnchorElement, MenuAnchorProps>(
/* -------------------------------------------------------------------------- */
/* Menu Content */
/* -------------------------------------------------------------------------- */
type MenuContentContextValue = {
onItemEnter: (event: React.PointerEvent) => void;
onItemLeave: (event: React.PointerEvent) => void;
onPointerLeaveTrigger: (event: React.PointerEvent) => void;
pointerGraceTimerRef: React.MutableRefObject<number>;
onPointerGraceIntentChange: (intent: GraceIntent | null) => void;
};
const [MenuContentProvider, useMenuContentContext] =
createContext<MenuContentContextValue>({
providerName: "MenuContentProvider",
hookName: "useMenuContentContext",
});
type MenuContentElement = MenuContentInternalElement;
interface MenuContentProps extends MenuContentInternalTypeProps {}
type MenuContentProps = MenuContentInternalTypeProps;
const MenuContent = React.forwardRef<
MenuContentInternalElement,
@@ -286,6 +264,7 @@ interface MenuContentInternalProps
onPointerDownOutside?: DismissableLayerProps["onPointerDownOutside"];
onFocusOutside?: DismissableLayerProps["onFocusOutside"];
onInteractOutside?: DismissableLayerProps["onInteractOutside"];
safeZone?: DismissableLayerProps["safeZone"];
}
const MenuContentInternal = forwardRef<
@@ -303,6 +282,7 @@ const MenuContentInternal = forwardRef<
onFocusOutside,
onInteractOutside,
onDismiss,
safeZone,
...rest
}: MenuContentInternalProps,
forwardedRef,
@@ -318,149 +298,80 @@ const MenuContentInternal = forwardRef<
contentRef,
context.onContentChange,
);
const pointerGraceTimerRef = React.useRef(0);
const pointerGraceIntentRef = React.useRef<GraceIntent | null>(null);
const pointerDirRef = React.useRef<SubMenuSide>("right");
const lastPointerXRef = React.useRef(0);
const isPointerMovingToSubmenu = React.useCallback(
(event: React.PointerEvent) => {
const isMovingTowards =
pointerDirRef.current === pointerGraceIntentRef.current?.side;
return (
isMovingTowards &&
isPointerInGraceArea(event, pointerGraceIntentRef.current?.area)
);
},
[],
);
return (
<MenuContentProvider
onItemEnter={React.useCallback(
(event) => {
if (isPointerMovingToSubmenu(event)) event.preventDefault();
},
[isPointerMovingToSubmenu],
)}
onItemLeave={React.useCallback(
(event) => {
if (isPointerMovingToSubmenu(event)) return;
/**
* Resets focus from current active item to content area
* This is to prevent focus from being stuck on an item when we move pointer outside the menu or onto a disabled item
*/
contentRef.current?.focus();
},
[isPointerMovingToSubmenu],
)}
onPointerLeaveTrigger={React.useCallback(
(event) => {
if (isPointerMovingToSubmenu(event)) event.preventDefault();
},
[isPointerMovingToSubmenu],
)}
pointerGraceTimerRef={pointerGraceTimerRef}
onPointerGraceIntentChange={React.useCallback((intent) => {
pointerGraceIntentRef.current = intent;
}, [])}
<FocusScope
onMountHandler={composeEventHandlers(onOpenAutoFocus, (event) => {
// when opening, explicitly focus the content area only and leave
// `onEntryFocus` in control of focusing first item
event.preventDefault();
contentRef.current?.focus({ preventScroll: true });
})}
onUnmountHandler={onCloseAutoFocus}
>
<FocusScope
onMountHandler={composeEventHandlers(onOpenAutoFocus, (event) => {
// when opening, explicitly focus the content area only and leave
// `onEntryFocus` in control of focusing first item
event.preventDefault();
contentRef.current?.focus({ preventScroll: true });
})}
onUnmountHandler={onCloseAutoFocus}
<DismissableLayer
asChild
disableOutsidePointerEvents={disableOutsidePointerEvents}
onEscapeKeyDown={onEscapeKeyDown}
onPointerDownOutside={onPointerDownOutside}
onFocusOutside={onFocusOutside}
onInteractOutside={onInteractOutside}
onDismiss={onDismiss}
safeZone={safeZone}
>
<DismissableLayer
<RovingFocus
asChild
disableOutsidePointerEvents={disableOutsidePointerEvents}
onEscapeKeyDown={onEscapeKeyDown}
onPointerDownOutside={onPointerDownOutside}
onFocusOutside={onFocusOutside}
onInteractOutside={onInteractOutside}
onDismiss={onDismiss}
descendants={descendants}
onEntryFocus={composeEventHandlers(onEntryFocus, (event) => {
// only focus first item when using keyboard
if (!rootContext.isUsingKeyboardRef.current)
event.preventDefault();
})}
>
<RovingFocus
asChild
descendants={descendants}
onEntryFocus={composeEventHandlers(onEntryFocus, (event) => {
// only focus first item when using keyboard
if (!rootContext.isUsingKeyboardRef.current)
event.preventDefault();
<Floating.Content
role="menu"
aria-orientation="vertical"
data-state={getOpenState(context.open)}
data-aksel-menu-content=""
dir="ltr"
{...rest}
ref={composedRefs}
style={{ outline: "none", ...rest.style }}
onKeyDown={composeEventHandlers(rest.onKeyDown, (event) => {
// submenu key events bubble through portals. We only care about keys in this menu.
const target = event.target as HTMLElement;
const isKeyDownInside =
target.closest("[data-aksel-menu-content]") ===
event.currentTarget;
if (isKeyDownInside) {
// menus should not be navigated using tab key so we prevent it
if (event.key === "Tab") event.preventDefault();
}
// focus first/last item based on key pressed
const content = contentRef.current;
if (event.target !== content) return;
if (!FIRST_LAST_KEYS.includes(event.key)) return;
event.preventDefault();
if (LAST_KEYS.includes(event.key)) {
descendants.lastEnabled()?.node?.focus();
return;
}
descendants.firstEnabled()?.node?.focus();
})}
>
<Floating.Content
role="menu"
aria-orientation="vertical"
data-state={getOpenState(context.open)}
data-aksel-menu-content=""
dir="ltr"
{...rest}
ref={composedRefs}
style={{ outline: "none", ...rest.style }}
onKeyDown={composeEventHandlers(rest.onKeyDown, (event) => {
// submenu key events bubble through portals. We only care about keys in this menu.
const target = event.target as HTMLElement;
const isKeyDownInside =
target.closest("[data-aksel-menu-content]") ===
event.currentTarget;
if (isKeyDownInside) {
// menus should not be navigated using tab key so we prevent it
if (event.key === "Tab") event.preventDefault();
}
// focus first/last item based on key pressed
const content = contentRef.current;
if (event.target !== content) return;
if (!FIRST_LAST_KEYS.includes(event.key)) return;
event.preventDefault();
if (LAST_KEYS.includes(event.key)) {
descendants.lastEnabled()?.node?.focus();
return;
}
descendants.firstEnabled()?.node?.focus();
})}
onPointerMove={composeEventHandlers(
rest.onPointerMove,
whenMouse((event) => {
const target = event.target as HTMLElement;
const pointerXHasChanged =
lastPointerXRef.current !== event.clientX;
// We don't use `event.movementX` for this check because Safari will
// always return `0` on a pointer event.
if (
event.currentTarget.contains(target) &&
pointerXHasChanged
) {
const newDir =
event.clientX > lastPointerXRef.current
? "right"
: "left";
pointerDirRef.current = newDir;
lastPointerXRef.current = event.clientX;
}
}),
)}
/>
</RovingFocus>
</DismissableLayer>
</FocusScope>
</MenuContentProvider>
/>
</RovingFocus>
</DismissableLayer>
</FocusScope>
);
},
);
interface MenuContentInternalTypeProps
extends Omit<
MenuContentInternalProps,
keyof MenuContentInternalPrivateProps
> {}
type MenuContentInternalTypeProps = Omit<
MenuContentInternalProps,
keyof MenuContentInternalPrivateProps
>;
/* -------------------------------------------------------------------------- */
/* Menu item */
@@ -482,6 +393,7 @@ const MenuItem = forwardRef<MenuItemElement, MenuItemProps>(
onPointerUp,
onPointerDown,
onKeyDown,
onKeyUp,
...rest
}: MenuItemProps,
forwardedRef,
@@ -510,6 +422,28 @@ const MenuItem = forwardRef<MenuItemElement, MenuItemProps>(
} else {
rootContext.onClose();
}
} else if (!disabled && menuItem) {
rootContext.onClose();
}
};
const handleKey = (
event: React.KeyboardEvent<HTMLDivElement>,
key: "Enter" | " ",
) => {
if (disabled || event.repeat) {
return;
}
if (key === event.key) {
event.currentTarget.click();
/**
* We prevent default browser behaviour for selection keys as they should only trigger
* selection.
* - Prevents space from scrolling the page.
* - If keydown causes focus to move, prevents keydown from firing on the new target.
*/
event.preventDefault();
}
};
@@ -519,7 +453,13 @@ const MenuItem = forwardRef<MenuItemElement, MenuItemProps>(
tabIndex={disabled ? -1 : 0}
ref={composedRefs}
disabled={disabled}
onClick={composeEventHandlers(onClick, handleSelect)}
onClick={composeEventHandlers(onClick, handleSelect, {
/**
* Nextjs prevents default on click when using Link component, so we have to force click-event
* https://github.com/vercel/next.js/blob/77dcd4c66a35d0e8ef639bda4d05873bd3c0f52d/packages/next/src/client/link.tsx#L211
*/
checkForDefaultPrevented: false,
})}
onPointerDown={composeEventHandlers(
onPointerDown,
() => {
@@ -533,21 +473,12 @@ const MenuItem = forwardRef<MenuItemElement, MenuItemProps>(
// prevent Firefox from getting stuck in text selection mode when the menu closes.
if (!isPointerDownRef.current) event.currentTarget?.click();
})}
onKeyDown={composeEventHandlers(onKeyDown, (event) => {
if (disabled) {
return;
}
if (SELECTION_KEYS.includes(event.key)) {
event.currentTarget.click();
/**
* We prevent default browser behaviour for selection keys as they should only trigger
* selection.
* - Prevents space from scrolling the page.
* - If keydown causes focus to move, prevents keydown from firing on the new target.
*/
event.preventDefault();
}
})}
onKeyDown={composeEventHandlers(onKeyDown, (event) =>
handleKey(event, "Enter"),
)}
onKeyUp={composeEventHandlers(onKeyUp, (event) =>
handleKey(event, " "),
)}
/>
);
},
@@ -573,9 +504,16 @@ const MenuItemInternal = forwardRef<
}: MenuItemInternalProps,
forwardedRef,
) => {
const { register } = useMenuDescendant({ disabled });
const context = useMenuContext();
const { register } = useMenuDescendant({
disabled,
closeMenu: () => {
rest["data-submenu-trigger"] &&
context.open &&
context.onOpenChange(false);
},
});
const contentContext = useMenuContentContext();
const ref = useRef<HTMLDivElement>(null);
const composedRefs = useMergeRefs(forwardedRef, ref, register);
@@ -601,18 +539,15 @@ const MenuItemInternal = forwardRef<
* In the edgecase the focus is still stuck on a previous item, we make sure to reset it
* even when the disabled item can't be focused itself to reset it.
*/
contentContext.onItemLeave(event);
context.content?.focus();
} else {
contentContext.onItemEnter(event);
if (!event.defaultPrevented) {
event.currentTarget.focus();
}
event.currentTarget.focus();
}
}),
)}
onPointerLeave={composeEventHandlers(
onPointerLeave,
whenMouse(contentContext.onItemLeave),
whenMouse(() => context.content?.focus()),
)}
/>
);
@@ -622,7 +557,7 @@ const MenuItemInternal = forwardRef<
/* -------------------------------------------------------------------------- */
/* Menu Group */
/* -------------------------------------------------------------------------- */
interface MenuGroupProps extends SlottedDivProps {}
type MenuGroupProps = SlottedDivProps;
const MenuGroup = forwardRef<SlottedDivElementRef, MenuGroupProps>(
(props: MenuGroupProps, ref) => {
@@ -696,7 +631,7 @@ const [MenuItemIndicatorProvider, useMenuItemIndicatorContext] = createContext<{
hookName: "useMenuItemIndicatorContext",
});
interface MenuItemIndicatorProps extends SlottedDivProps {}
type MenuItemIndicatorProps = SlottedDivProps;
const MenuItemIndicator = forwardRef<
SlottedDivElementRef,
@@ -786,12 +721,12 @@ const MenuCheckboxItem = forwardRef<MenuItemElement, MenuCheckboxItemProps>(
);
/* -------------------------------------------------------------------------- */
/* Menu Separator */
/* Menu Divider */
/* -------------------------------------------------------------------------- */
interface MenuSeparatorProps extends SlottedDivProps {}
type MenuDividerProps = SlottedDivProps;
const MenuSeparator = forwardRef<SlottedDivElementRef, MenuSeparatorProps>(
(props: MenuSeparatorProps, ref) => {
const MenuDivider = forwardRef<SlottedDivElementRef, MenuDividerProps>(
(props: MenuDividerProps, ref) => {
return (
<SlottedDivElement
role="separator"
@@ -833,6 +768,8 @@ const MenuSub: React.FC<MenuSubProps> = ({
}: MenuSubProps) => {
const parentMenuContext = useMenuContext();
const { values } = useMenuDescendantsContext();
const [trigger, setTrigger] = useState<MenuItemElement | null>(null);
const [content, setContent] = useState<MenuContentInternalElement | null>(
null,
@@ -851,7 +788,17 @@ const MenuSub: React.FC<MenuSubProps> = ({
<Floating>
<MenuProvider
open={open}
onOpenChange={handleOpenChange}
onOpenChange={(_open) => {
handleOpenChange(_open);
if (_open) {
/* Makes sure to close all adjacent submenus if they are open */
values().forEach((descendant) => {
if (descendant.node !== trigger) {
descendant.closeMenu();
}
});
}
}}
content={content}
onContentChange={setContent}
>
@@ -871,34 +818,31 @@ const MenuSub: React.FC<MenuSubProps> = ({
/* -------------------------------------------------------------------------- */
/* Menu SubMenu Trigger */
/* -------------------------------------------------------------------------- */
interface MenuSubTriggerProps extends MenuItemInternalProps {}
type MenuSubTriggerProps = MenuItemInternalProps;
const MenuSubTrigger = forwardRef<MenuItemElement, MenuSubTriggerProps>(
(props: MenuSubTriggerProps, forwardedRef) => {
const context = useMenuContext();
const subContext = useMenuSubContext();
const contentContext = useMenuContentContext();
const openTimerRef = useRef<number | null>(null);
const { pointerGraceTimerRef, onPointerGraceIntentChange } = contentContext;
const composedRefs = useMergeRefs(forwardedRef, subContext.onTriggerChange);
const clearOpenTimer = useCallback(() => {
if (openTimerRef.current) {
window.clearTimeout(openTimerRef.current);
const handleKey = (
event: React.KeyboardEvent<HTMLDivElement>,
keys: string[],
) => {
if (props.disabled) {
return;
}
openTimerRef.current = null;
}, []);
React.useEffect(() => clearOpenTimer, [clearOpenTimer]);
React.useEffect(() => {
const pointerGraceTimer = pointerGraceTimerRef.current;
return () => {
window.clearTimeout(pointerGraceTimer);
onPointerGraceIntentChange(null);
};
}, [pointerGraceTimerRef, onPointerGraceIntentChange]);
if (keys.includes(event.key)) {
context.onOpenChange(true);
// The trigger may hold focus if opened via pointer interaction
// so we ensure content is given focus again when switching to keyboard.
context.content?.focus();
// prevent window from scrolling
event.preventDefault();
}
};
return (
<MenuAnchor asChild>
@@ -910,85 +854,25 @@ const MenuSubTrigger = forwardRef<MenuItemElement, MenuSubTriggerProps>(
data-state={getOpenState(context.open)}
{...props}
ref={composedRefs}
/**
* onClick is added to solve edgecase where the user clicks the trigger,
* but the focus is outside browser-window or viewport at first.
*/
data-submenu-trigger
onClick={(event) => {
props.onClick?.(event);
if (props.disabled || event.defaultPrevented) return;
event.currentTarget.focus();
if (!context.open) context.onOpenChange(true);
}}
onPointerMove={composeEventHandlers(
props.onPointerMove,
whenMouse((event) => {
if (event.defaultPrevented) return;
if (!props.disabled && !context.open && !openTimerRef.current) {
contentContext.onPointerGraceIntentChange(null);
openTimerRef.current = window.setTimeout(() => {
context.onOpenChange(true);
clearOpenTimer();
}, 100);
}
}),
)}
onPointerLeave={composeEventHandlers(
props.onPointerLeave,
whenMouse((event) => {
clearOpenTimer();
const contentRect = context.content?.getBoundingClientRect();
if (contentRect) {
const side = context.content?.dataset.side as SubMenuSide;
const rightSide = side === "right";
const bleed = rightSide ? -5 : +5;
const contentNearEdge =
contentRect[rightSide ? "left" : "right"];
const contentFarEdge =
contentRect[rightSide ? "right" : "left"];
contentContext.onPointerGraceIntentChange({
area: [
// Apply a bleed on clientX to ensure that our exit point is
// consistently within polygon bounds
{ x: event.clientX + bleed, y: event.clientY },
{ x: contentNearEdge, y: contentRect.top },
{ x: contentFarEdge, y: contentRect.top },
{ x: contentFarEdge, y: contentRect.bottom },
{ x: contentNearEdge, y: contentRect.bottom },
],
side,
});
window.clearTimeout(pointerGraceTimerRef.current);
pointerGraceTimerRef.current = window.setTimeout(
() => contentContext.onPointerGraceIntentChange(null),
300,
);
} else {
contentContext.onPointerLeaveTrigger(event);
if (event.defaultPrevented) return;
// There's 100ms where the user may leave an item before the submenu was opened.
contentContext.onPointerGraceIntentChange(null);
}
}),
)}
onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
if (props.disabled) {
if (props.disabled || event.defaultPrevented) {
return;
}
if (SUB_OPEN_KEYS.includes(event.key)) {
context.onOpenChange(true);
// The trigger may hold focus if opened via pointer interaction
// so we ensure content is given focus again when switching to keyboard.
context.content?.focus();
// prevent window from scrolling
event.preventDefault();
}
})}
props.onClick?.(event);
/*
* Solves edgecase where the user clicks the trigger,
* but the focus is outside browser-window or viewport at first.
*/
event.currentTarget.focus();
context.onOpenChange(!context.open);
}}
onKeyDown={composeEventHandlers(props.onKeyDown, (event) =>
handleKey(event, ["Enter", "ArrowRight"]),
)}
onKeyUp={composeEventHandlers(props.onKeyUp, (event) =>
handleKey(event, [" "]),
)}
/>
</MenuAnchor>
);
@@ -998,15 +882,14 @@ const MenuSubTrigger = forwardRef<MenuItemElement, MenuSubTriggerProps>(
/* -------------------------------------------------------------------------- */
/* Menu SubMenu Content */
/* -------------------------------------------------------------------------- */
interface MenuSubContentProps
extends Omit<
MenuContentInternalProps,
| keyof MenuContentInternalPrivateProps
| "onCloseAutoFocus"
| "onEntryFocus"
| "side"
| "align"
> {}
type MenuSubContentProps = Omit<
MenuContentInternalProps,
| keyof MenuContentInternalPrivateProps
| "onCloseAutoFocus"
| "onEntryFocus"
| "side"
| "align"
>;
const MenuSubContent = forwardRef<
MenuContentInternalElement,
@@ -1037,14 +920,8 @@ const MenuSubContent = forwardRef<
}
event.preventDefault();
}}
// The menu might close because of focusing another menu item in the parent menu. We
// don't want it to refocus the trigger in that case so we handle trigger focus ourselves.
/* Since we manually focus Subtrigger, we prevent use of auto-focus */
onCloseAutoFocus={(event) => event.preventDefault()}
onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) => {
// We prevent closing when the trigger is focused to avoid triggering a re-open animation
// on pointer interaction.
if (event.target !== subContext.trigger) context.onOpenChange(false);
})}
onEscapeKeyDown={composeEventHandlers(
props.onEscapeKeyDown,
(event) => {
@@ -1058,7 +935,7 @@ const MenuSubContent = forwardRef<
const isKeyDownInside = event.currentTarget.contains(
event.target as HTMLElement,
);
let isCloseKey = SUB_CLOSE_KEYS.includes(event.key);
let isCloseKey = event.key === "ArrowLeft";
/* When submenu opens to the left, we allow closing it with ArrowRight */
if (context.content?.dataset.side === "left") {
@@ -1097,32 +974,6 @@ function getCheckedState(checked: CheckedState) {
: "unchecked";
}
/**
* Determine if a point is inside of a polygon.
*/
function isPointInPolygon(point: Point, polygon: Polygon) {
const { x, y } = point;
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x;
const yi = polygon[i].y;
const xj = polygon[j].x;
const yj = polygon[j].y;
// prettier-ignore
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
function isPointerInGraceArea(event: React.PointerEvent, area?: Polygon) {
if (!area) return false;
const cursorPos = { x: event.clientX, y: event.clientY };
return isPointInPolygon(cursorPos, area);
}
function whenMouse<E>(
handler: React.PointerEventHandler<E>,
): React.PointerEventHandler<E> {
@@ -1139,7 +990,7 @@ Menu.Item = MenuItem;
Menu.CheckboxItem = MenuCheckboxItem;
Menu.RadioGroup = MenuRadioGroup;
Menu.RadioItem = MenuRadioItem;
Menu.Separator = MenuSeparator;
Menu.Divider = MenuDivider;
Menu.Sub = MenuSub;
Menu.SubTrigger = MenuSubTrigger;
Menu.SubContent = MenuSubContent;
@@ -1150,19 +1001,20 @@ export {
MenuAnchor,
MenuCheckboxItem,
MenuContent,
MenuDivider,
MenuGroup,
MenuItem,
MenuItemIndicator,
MenuPortal,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuSub,
MenuSubContent,
MenuSubTrigger,
type MenuAnchorProps,
type MenuCheckboxItemProps,
type MenuContentProps,
type MenuDividerProps,
type MenuGroupProps,
type MenuItemElement,
type MenuItemIndicatorProps,
@@ -1170,7 +1022,6 @@ export {
type MenuProps,
type MenuRadioGroupProps,
type MenuRadioItemProps,
type MenuSeparatorProps,
type MenuSubContentProps,
type MenuSubProps,
type MenuSubTriggerProps,
@@ -7,7 +7,7 @@ import { DescendantsManager } from "../../../util/hooks/descendants/descendant";
interface RovingFocusProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "tabIndex"> {
asChild?: boolean;
descendants: DescendantsManager<HTMLDivElement, object>;
descendants: DescendantsManager<HTMLDivElement, any>;
onEntryFocus?: (event: Event) => void;
}
@@ -25,8 +25,8 @@ const RovingFocus = forwardRef<HTMLDivElement, RovingFocusProps>(
onMouseDown,
onFocus,
...rest
}: RovingFocusProps,
ref,
},
ref: React.Ref<HTMLDivElement>,
) => {
const _ref = React.useRef<HTMLDivElement>(null);
const composedRefs = useMergeRefs(ref, _ref);
@@ -55,19 +55,19 @@ const RovingFocus = forwardRef<HTMLDivElement, RovingFocusProps>(
const nextItem = () => {
const next = descendants.nextEnabled(idx, loop);
next && next.node?.focus();
next?.node?.focus();
};
const prevItem = () => {
const prev = descendants.prevEnabled(idx, loop);
prev && prev.node?.focus();
prev?.node?.focus();
};
const firstItem = () => {
const first = descendants.firstEnabled();
first && first.node?.focus();
first?.node?.focus();
};
const lastItem = () => {
const last = descendants.lastEnabled();
last && last.node?.focus();
last?.node?.focus();
};
const keyMap: Record<string, React.KeyboardEventHandler> = {
@@ -1,4 +1,5 @@
import {
FlipOptions,
Placement,
autoUpdate,
flip,
@@ -217,9 +218,6 @@ const FloatingContent = forwardRef<HTMLDivElement, FloatingContentProps>(
) => {
const context = useFloatingContext();
const [content, setContent] = useState<HTMLDivElement | null>(null);
const mergeRefs = useMergeRefs(forwardedRef, setContent);
const arrowDefaults = {
padding: 5,
width: 0,
@@ -251,11 +249,13 @@ const FloatingContent = forwardRef<HTMLDivElement, FloatingContentProps>(
return value !== null;
}
const detectOverflowOptions = {
const detectOverflowOptions: FlipOptions = {
padding: collisionPadding,
boundary: boundary.filter(isNotNull),
// with `strategy: 'fixed'`, this is the only way to get it to respect boundaries
altBoundary: hasExplicitBoundaries,
/* https://floating-ui.com/docs/flip#fallbackaxissidedirection */
fallbackAxisSideDirection: "end",
};
const { refs, floatingStyles, placement, isPositioned, middlewareData } =
@@ -282,7 +282,6 @@ const FloatingContent = forwardRef<HTMLDivElement, FloatingContentProps>(
mainAxis: true,
crossAxis: false,
limiter: limitShift(),
...detectOverflowOptions,
}),
avoidCollisions && flip({ ...detectOverflowOptions }),
size({
@@ -332,11 +331,6 @@ const FloatingContent = forwardRef<HTMLDivElement, FloatingContentProps>(
const arrowY = middlewareData.arrow?.y;
const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;
const [contentZIndex, setContentZIndex] = useState<string>();
useClientLayoutEffect(() => {
if (content) setContentZIndex(window.getComputedStyle(content).zIndex);
}, [content]);
return (
<div
ref={refs.setFloating}
@@ -347,7 +341,7 @@ const FloatingContent = forwardRef<HTMLDivElement, FloatingContentProps>(
? floatingStyles.transform
: "translate(0, -200%)", // keep off the page when measuring
minWidth: "max-content",
zIndex: contentZIndex,
zIndex: "9999999",
["--ac-floating-transform-origin" as any]: [
middlewareData.transformOrigin?.x,
middlewareData.transformOrigin?.y,
@@ -365,7 +359,7 @@ const FloatingContent = forwardRef<HTMLDivElement, FloatingContentProps>(
hideArrow={cannotCenterArrow}
>
<div
ref={mergeRefs}
ref={forwardedRef}
data-side={placedSide}
data-align={placedAlign}
{...contentProps}
@@ -1,10 +1,7 @@
import type { Middleware, Placement } from "@floating-ui/react-dom";
const SIDE_OPTIONS = ["top", "right", "bottom", "left"] as const;
const ALIGN_OPTIONS = ["start", "center", "end"] as const;
type Side = (typeof SIDE_OPTIONS)[number];
type Align = (typeof ALIGN_OPTIONS)[number];
type Side = "top" | "right" | "bottom" | "left";
type Align = "start" | "center" | "end";
type Measurable = { getBoundingClientRect(): DOMRect };
/**
@@ -69,7 +69,10 @@ export const getSteps = ({
count,
boundaryCount = 1,
siblingCount = 1,
}) => {
}: Pick<
PaginationProps,
"page" | "count" | "boundaryCount" | "siblingCount"
>) => {
const range = (start: number, end: number) =>
Array.from({ length: end - start + 1 }, (_, i) => start + i);
+15 -16
View File
@@ -1,18 +1,17 @@
import faker from "faker";
import { describe, expect, test } from "vitest";
import { getSteps } from "./Pagination";
describe("getSteps", () => {
test("lists all pages when count is <= 7", () => {
const count = faker.datatype.number({ min: 1, max: 7 });
const count = 5;
expect(getSteps({ page: 1, count })).toEqual(
Array.from({ length: count }, (_, i) => i + 1),
);
});
test("has an end ellipsis when count >= 8", () => {
const count = faker.datatype.number({ min: 8 });
const page = faker.datatype.number({ min: 1, max: 4 });
const count = 8;
const page = 3;
expect(
getSteps({
page,
@@ -22,8 +21,8 @@ describe("getSteps", () => {
});
test("has a start ellipsis when count - page >= 3", () => {
const count = faker.datatype.number({ min: 8 });
const page = faker.datatype.number({ min: count - 3, max: count });
const count = 8;
const page = 5;
expect(
getSteps({
page,
@@ -41,8 +40,8 @@ describe("getSteps", () => {
});
test("has start & end ellipsis when count is high", () => {
const count = faker.datatype.number({ min: 9 });
const page = faker.datatype.number({ min: 5, max: count - 4 });
const count = 9;
const page = 5;
expect(
getSteps({
page,
@@ -52,8 +51,8 @@ describe("getSteps", () => {
});
test("can have a reduced siblingCount", () => {
const count = faker.datatype.number({ min: 7 });
const page = faker.datatype.number({ min: 4, max: count - 3 });
const count = 7;
const page = 4;
expect(
getSteps({
page,
@@ -64,8 +63,8 @@ describe("getSteps", () => {
});
test("can have an increased siblingCount", () => {
const count = faker.datatype.number({ min: 11 });
const page = faker.datatype.number({ min: 6, max: count - 5 });
const count = 11;
const page = 6;
expect(
getSteps({
page,
@@ -86,8 +85,8 @@ describe("getSteps", () => {
});
test("can have an reduced boundaryCount", () => {
const count = faker.datatype.number({ min: 7 });
const page = faker.datatype.number({ min: 4, max: count - 3 });
const count = 7;
const page = 4;
expect(
getSteps({
page,
@@ -98,8 +97,8 @@ describe("getSteps", () => {
});
test("can have an increased boundaryCount", () => {
const count = faker.datatype.number({ min: 11 });
const page = faker.datatype.number({ min: 6, max: count - 5 });
const count = 11;
const page = 6;
expect(
getSteps({
page,
@@ -1,12 +1,12 @@
import React, { createContext, useContext } from "react";
import { TranslationDictionary } from "../../util/i18n/i18n.types";
import { PartialTranslations } from "../../util/i18n/i18n.types";
import nb from "../../util/i18n/locales/nb";
export interface LanguageProviderContextType {
/**
* Merged with the default language translations object (officially provided translations).
*/
translations: TranslationDictionary | TranslationDictionary[];
translations: PartialTranslations | PartialTranslations[];
}
export const LanguageProviderContext =
@@ -16,7 +16,7 @@ export const LanguageProviderContext =
export interface LanguageProviderProps {
children?: React.ReactNode;
translations?: TranslationDictionary | TranslationDictionary[];
translations?: PartialTranslations | PartialTranslations[];
}
export const useProvider = () => useContext(LanguageProviderContext);
+4 -6
View File
@@ -1,13 +1,11 @@
import cl from "clsx";
import React, { forwardRef } from "react";
export interface BodyProps
extends React.HTMLAttributes<HTMLTableSectionElement> {}
export type BodyProps = React.HTMLAttributes<HTMLTableSectionElement>;
export interface BodyType
extends React.ForwardRefExoticComponent<
BodyProps & React.RefAttributes<HTMLTableSectionElement>
> {}
export type BodyType = React.ForwardRefExoticComponent<
BodyProps & React.RefAttributes<HTMLTableSectionElement>
>;
export const Body: BodyType = forwardRef(({ className, ...rest }, ref) => (
<tbody {...rest} ref={ref} className={cl("navds-table__body", className)} />
@@ -19,10 +19,9 @@ export interface ColumnHeaderProps extends HeaderCellProps {
sortable?: boolean;
}
export interface ColumnHeaderType
extends React.ForwardRefExoticComponent<
ColumnHeaderProps & React.RefAttributes<HTMLTableCellElement>
> {}
export type ColumnHeaderType = React.ForwardRefExoticComponent<
ColumnHeaderProps & React.RefAttributes<HTMLTableCellElement>
>;
export const ColumnHeader: ColumnHeaderType = forwardRef(
({ className, children, sortable = false, sortKey, ...rest }, ref) => {
@@ -49,10 +49,9 @@ export interface ExpandableRowProps extends Omit<RowProps, "content"> {
colSpan?: number;
}
export interface ExpandableRowType
extends React.ForwardRefExoticComponent<
ExpandableRowProps & React.RefAttributes<HTMLTableRowElement>
> {}
export type ExpandableRowType = React.ForwardRefExoticComponent<
ExpandableRowProps & React.RefAttributes<HTMLTableRowElement>
>;
export const ExpandableRow: ExpandableRowType = forwardRef(
(
+4 -6
View File
@@ -1,13 +1,11 @@
import cl from "clsx";
import React, { forwardRef } from "react";
export interface HeaderProps
extends React.HTMLAttributes<HTMLTableSectionElement> {}
export type HeaderProps = React.HTMLAttributes<HTMLTableSectionElement>;
export interface HeaderType
extends React.ForwardRefExoticComponent<
HeaderProps & React.RefAttributes<HTMLTableSectionElement>
> {}
export type HeaderType = React.ForwardRefExoticComponent<
HeaderProps & React.RefAttributes<HTMLTableSectionElement>
>;
export const Header: HeaderType = forwardRef(({ className, ...rest }, ref) => (
<thead {...rest} ref={ref} className={cl("navds-table__header", className)} />
+3 -4
View File
@@ -15,10 +15,9 @@ export interface HeaderCellProps
textSize?: "medium" | "small";
}
export interface HeaderCellType
extends React.ForwardRefExoticComponent<
HeaderCellProps & React.RefAttributes<HTMLTableCellElement>
> {}
export type HeaderCellType = React.ForwardRefExoticComponent<
HeaderCellProps & React.RefAttributes<HTMLTableCellElement>
>;
export const HeaderCell: HeaderCellType = forwardRef(
({ className, children, align, textSize, ...rest }, ref) => {
+3 -4
View File
@@ -14,10 +14,9 @@ export interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
shadeOnHover?: boolean;
}
export interface RowType
extends React.ForwardRefExoticComponent<
RowProps & React.RefAttributes<HTMLTableRowElement>
> {}
export type RowType = React.ForwardRefExoticComponent<
RowProps & React.RefAttributes<HTMLTableRowElement>
>;
export const Row: RowType = forwardRef(
({ className, selected = false, shadeOnHover = true, ...rest }, ref) => (
@@ -41,7 +41,7 @@ export function useScrollButtons(listRef: React.RefObject<HTMLDivElement>) {
return () => {
win.removeEventListener("resize", handleResize);
resizeObserver && resizeObserver.disconnect();
resizeObserver?.disconnect();
updateScrollButtonState.clear();
};
}, [listRef, updateScrollButtonState]);
@@ -26,19 +26,19 @@ export function useTabList() {
const nextTab = () => {
const next = descendants.nextEnabled(idx, loop);
next && next.node?.focus();
next?.node?.focus();
};
const prevTab = () => {
const prev = descendants.prevEnabled(idx, loop);
prev && prev.node?.focus();
prev?.node?.focus();
};
const firstTab = () => {
const first = descendants.firstEnabled();
first && first.node?.focus();
first?.node?.focus();
};
const lastTab = () => {
const last = descendants.lastEnabled();
last && last.node?.focus();
last?.node?.focus();
};
const keyMap: Record<string, React.KeyboardEventHandler> = {
+20 -21
View File
@@ -91,27 +91,26 @@ export const TimelineRow = forwardRef<HTMLOListElement, TimelineRowProps>(
}
}}
>
{periods &&
periods.map((period) => {
return (
<li key={`period-${period.id}`}>
<PeriodContext.Provider
value={{
periodId: period.id,
firstFocus: firstFocusable?.id === period.id,
restProps: period?.restProps,
}}
>
<Period
start={period.start}
end={period.endInclusive}
icon={period.icon}
ref={period?.ref}
/>
</PeriodContext.Provider>
</li>
);
})}
{periods?.map((period) => {
return (
<li key={`period-${period.id}`}>
<PeriodContext.Provider
value={{
periodId: period.id,
firstFocus: firstFocusable?.id === period.id,
restProps: period?.restProps,
}}
>
<Period
start={period.start}
end={period.endInclusive}
icon={period.icon}
ref={period?.ref}
/>
</PeriodContext.Provider>
</li>
);
})}
</ol>
</div>
</>
@@ -55,19 +55,19 @@ export function useToggleItem<P extends UseToggleItemProps>(
const nextTab = () => {
const next = descendants.nextEnabled(idx, false);
next && next.node?.focus();
next?.node?.focus();
};
const prevTab = () => {
const prev = descendants.prevEnabled(idx, false);
prev && prev.node?.focus();
prev?.node?.focus();
};
const firstTab = () => {
const first = descendants.firstEnabled();
first && first.node?.focus();
first?.node?.focus();
};
const lastTab = () => {
const last = descendants.lastEnabled();
last && last.node?.focus();
last?.node?.focus();
};
const keyMap: Record<string, React.KeyboardEventHandler> = {
@@ -152,14 +152,14 @@ export const FontWeight: Story = {
return (
<>
{fontWeights.map((weight) => (
<>
<React.Fragment key={weight}>
<h2>{weight}</h2>
<FontComponent
key={weight}
weight={weight}
family="Source Sans 3"
/>
</>
</React.Fragment>
))}
</>
);
@@ -40,7 +40,7 @@ const checkState = (
* https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/ownerWindow/ownerWindow.ts
*/
const ownerWindow = (node: Node | undefined): Window => {
const doc = (node && node.ownerDocument) || document;
const doc = node?.ownerDocument || document;
return doc.defaultView || window;
};
@@ -223,7 +223,7 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
};
const mainStyle: React.CSSProperties = {
["--__ac-textarea-height"]: state.outerHeightStyle + "px",
"--__ac-textarea-height": state.outerHeightStyle + "px",
// Need a large enough difference to allow scrolling.
// This prevents infinite rendering loop.
overflow:
@@ -60,7 +60,7 @@ export class DescendantsManager<
this.descendants.forEach((descendant) => {
const index = descendants.indexOf(descendant.node);
descendant.index = index;
descendant.node.dataset["index"] = descendant.index.toString();
descendant.node.dataset.index = descendant.index.toString();
});
};
@@ -52,7 +52,7 @@ export function createDescendantContext<
useClientLayoutEffect(() => {
if (!ref.current) return;
const dataIndex = Number(ref.current.dataset["index"]);
const dataIndex = Number(ref.current.dataset.index);
if (index !== dataIndex && !Number.isNaN(dataIndex)) {
setIndex(dataIndex);
}
Loaded 100 of 195 files, more files were not shown because too many files have changed in this diff. Show more