Compare commits

..
Author SHA1 Message Date
github-actions[bot] f6042d01aa Lerna versionbump [ci skip]
- @navikt/ds-css@0.16.5
 - @navikt/ds-react@0.17.6
 - @navikt/ds-react-internal@0.12.6
2022-03-16 16:18:56 +00:00
Andreas Nordahl 3f4c35b0ef [fix] Read more component (#1456)
* scrips yo

* unused import

* stylez

* aria-expanded

* add link to storybook

* oopsie

* read more focus style

* read more collapse animation
2022-03-16 17:09:06 +01:00
github-actions[bot] b2a71fd227 Lerna versionbump [ci skip]
- @navikt/ds-icons@0.8.5
 - @navikt/ds-react@0.17.5
 - @navikt/ds-react-internal@0.12.5
2022-03-16 15:53:15 +00:00
Ken d4aed1b59a [FIX] Oppdatert ikon-template, SSR-safe uuid (#1458)
* oppdatert ikon-template

* oppdatert pakkversjoner

* fikset pakkeversjoner

* oppdatert ikon-template

* fikset config
2022-03-16 16:44:24 +01:00
github-actions[bot] ddc0db1905 Lerna versionbump [ci skip]
- @navikt/ds-css@0.16.4
 - @navikt/ds-react@0.17.4
 - @navikt/ds-react-internal@0.12.4
2022-03-14 12:29:03 +00:00
211bd751cd [fix] Helptext uu fix (#1407)
* storybook no div in p fix

* move title inside svg

* extra helptext in story

* remove duplicate title

* remove aria-props and refactor

* remove unused style

* simplify styling some

* default help text content to body-short

* cleanup

* tabindex 0 på popover

* flyttet story

* fjernet tabindex

* Update @navikt/core/react/src/help-text/HelpText.tsx

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

Co-authored-by: Ken <ken.aleksander@gmail.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2022-03-14 13:18:43 +01:00
github-actions[bot] 3e334453e3 Lerna versionbump [ci skip]
- @navikt/ds-css@0.16.3
2022-03-14 12:00:11 +00:00
Ken 70819de284 [FIX] Bedre width for search (#1455) 2022-03-14 12:51:53 +01:00
github-actions[bot] 4baebb4340 Lerna versionbump [ci skip]
- @navikt/ds-react@0.17.3
 - @navikt/ds-react-internal@0.12.3
2022-03-08 10:50:29 +00:00
Ken 7866333ae2 [FIX] Search: form -> div (#1454) 2022-03-08 11:40:25 +01:00
github-actions[bot] 2f13905d61 Lerna versionbump [ci skip]
- @navikt/ds-css@0.16.2
2022-03-07 15:23:32 +00:00
KenandAndreas Nordahl 0160383ff0 [FIX] Synket ConfirmationPanel med Figma (#1437)
Co-authored-by: Andreas Nordahl <andnorda@gmail.com>
2022-03-07 16:14:04 +01:00
github-actions[bot] dd1c128aac Lerna versionbump [ci skip]
- @navikt/ds-react@0.17.2
 - @navikt/ds-react-internal@0.12.2
2022-03-07 11:25:20 +00:00
Ken f1a6d768fc [FIX] onSubmit kjører onSearch callback i Search (#1452) 2022-03-07 12:16:56 +01:00
github-actions[bot] c47967589a Lerna versionbump [ci skip]
- @navikt/ds-react@0.17.1
 - @navikt/ds-react-internal@0.12.1
2022-03-07 10:13:39 +00:00
Ken 51654edbfb [FIX] parentElement prop på Modal (#1450) 2022-03-07 11:05:02 +01:00
Ken 5c3683a89e Search clearbutton label comment 2022-03-04 17:19:59 +01:00
github-actions[bot] 9d9bf898e9 Lerna versionbump [ci skip]
- @navikt/ds-css@0.16.1
2022-03-04 16:06:56 +00:00
Ken e8e530b764 [FIX] Search label layout hotfix 2022-03-04 16:57:09 +01:00
github-actions[bot] d2d08f58ce Lerna versionbump [ci skip]
- @navikt/ds-css@0.16.0
 - @navikt/ds-react@0.17.0
 - @navikt/ds-react-internal@0.12.0
2022-03-04 15:18:02 +00:00
KenandAndreas Nordahl f75abaff4b [FEATURE] Nytt søkefelt (#1382)
Co-authored-by: Andreas Nordahl <andnorda@gmail.com>
2022-03-04 16:08:26 +01:00
github-actions[bot] 7e8899df34 Lerna versionbump [ci skip]
- @navikt/ds-react@0.16.20
 - @navikt/ds-react-internal@0.11.20
2022-03-02 10:10:05 +00:00
Ken 3926b24350 [FIX] Modal aria-label (#1449) 2022-03-02 11:01:45 +01:00
github-actions[bot] f1c2c718e5 Lerna versionbump [ci skip]
- @navikt/ds-css@0.15.12
 - @navikt/ds-react@0.16.19
 - @navikt/ds-react-internal@0.11.19
2022-03-01 10:13:40 +00:00
Ken ddcc6b8bd7 [FIX] Ny versjon av Radix ToggleGroup (#1448) 2022-03-01 11:03:45 +01:00
github-actions[bot] 6f5fee9628 Lerna versionbump [ci skip]
- @navikt/ds-css@0.15.11
 - @navikt/ds-react@0.16.18
 - @navikt/ds-react-internal@0.11.18
2022-03-01 08:33:43 +00:00
ad191179ab [fix] Table sort (#1445)
* story baseline

* pagination first take

* sort first take

* fix sort logic

* sort styling

* fix widths

* scope row

* steps

* pagination styles

* sortable birth year

* simplify comparator

* async table story

* HeaderCell allows sorting

* remove redundant story. fix some styling

* fix chromatic

* refactor column header

* overflow auto

* refactor pagination

* fix pagination steps test

* separate pagination comp

* xsmall button in table

* fikset type-error i ds-react-internal

* Update @navikt/core/css/button.css

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

* Update @navikt/core/css/pagination.css

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

* Update @navikt/core/css/pagination.css

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

* Update @navikt/core/css/table.css

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

* Update @navikt/core/react/src/pagination/Pagination.tsx

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

* Update @navikt/core/react/src/pagination/Pagination.tsx

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

* Update @navikt/core/react/src/table/ColumnHeader.tsx

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

* move storybook static conf

* sort arrow fix

* new, adjustable step logic

* fix ellipsis key bug

* pagination styling

* some a11y things

* lint

* some sizing

* next prev focus style

* add pagination component and styles

* pagination css custom props

* 1px #fff linje mellom bg og shadow

* fix page logic on story

* user defined sort change logic

* fix table padding

* fix focus bug

* fix tilbake title bug

* digihot table story

* no active style, selected blue

* yarn.lock

* remove duplicate pagination stories

* fix storybook

* bump

* table cell alignment

* Update @navikt/core/react/src/table/Table.tsx

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

* Update @navikt/core/react/src/table/stories/table-async.stories.tsx

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

* Update @navikt/core/css/table.css

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

Co-authored-by: Ken <ken.aleksander@gmail.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2022-03-01 09:23:54 +01:00
github-actions[bot] 9c86becab0 Lerna versionbump [ci skip]
- @navikt/ds-react@0.16.17
 - @navikt/ds-react-internal@0.11.17
2022-02-28 13:12:38 +00:00
Ken 200d420723 [FIX] readme 2022-02-28 14:05:20 +01:00
Ken 44ab348ecf force v0.1.4 for ToggleGroup 2022-02-28 14:03:31 +01:00
64 changed files with 5179 additions and 3061 deletions

No files matched your search

+1
View File
@@ -33,4 +33,5 @@ module.exports = {
core: {
builder: "webpack5",
},
staticDirs: ["./public"],
};
+5
View File
@@ -1,5 +1,10 @@
import "@navikt/ds-css/index.css";
import "@navikt/ds-css-internal/index.css";
import { initialize, mswDecorator } from "msw-storybook-addon";
initialize();
export const decorators = [mswDecorator];
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
+338
View File
@@ -0,0 +1,338 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (0.36.5).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = "02f4ad4a2797f85668baf196e553d929";
const bypassHeaderName = "x-msw-bypass";
const activeClientIds = new Set();
self.addEventListener("install", function () {
return self.skipWaiting();
});
self.addEventListener("activate", async function (event) {
return self.clients.claim();
});
self.addEventListener("message", async function (event) {
const clientId = event.source.id;
if (!clientId || !self.clients) {
return;
}
const client = await self.clients.get(clientId);
if (!client) {
return;
}
const allClients = await self.clients.matchAll();
switch (event.data) {
case "KEEPALIVE_REQUEST": {
sendToClient(client, {
type: "KEEPALIVE_RESPONSE",
});
break;
}
case "INTEGRITY_CHECK_REQUEST": {
sendToClient(client, {
type: "INTEGRITY_CHECK_RESPONSE",
payload: INTEGRITY_CHECKSUM,
});
break;
}
case "MOCK_ACTIVATE": {
activeClientIds.add(clientId);
sendToClient(client, {
type: "MOCKING_ENABLED",
payload: true,
});
break;
}
case "MOCK_DEACTIVATE": {
activeClientIds.delete(clientId);
break;
}
case "CLIENT_CLOSED": {
activeClientIds.delete(clientId);
const remainingClients = allClients.filter((client) => {
return client.id !== clientId;
});
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister();
}
break;
}
}
});
// Resolve the "main" client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId);
if (client.frameType === "top-level") {
return client;
}
const allClients = await self.clients.matchAll();
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === "visible";
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id);
});
}
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event);
const response = await getResponse(event, client, requestId);
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
(async function () {
const clonedResponse = response.clone();
sendToClient(client, {
type: "RESPONSE",
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body:
clonedResponse.body === null ? null : await clonedResponse.text(),
headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected,
},
});
})();
}
return response;
}
async function getResponse(event, client, requestId) {
const { request } = event;
const requestClone = request.clone();
const getOriginalResponse = () => fetch(requestClone);
// Bypass mocking when the request client is not active.
if (!client) {
return getOriginalResponse();
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return await getOriginalResponse();
}
// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === "true") {
const cleanRequestHeaders = serializeHeaders(requestClone.headers);
// Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName];
const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders),
});
return fetch(originalRequest);
}
// Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers);
const body = await request.text();
const clientMessage = await sendToClient(client, {
type: "REQUEST",
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
});
switch (clientMessage.type) {
case "MOCK_SUCCESS": {
return delayPromise(
() => respondWithMock(clientMessage),
clientMessage.payload.delay
);
}
case "MOCK_NOT_FOUND": {
return getOriginalResponse();
}
case "NETWORK_ERROR": {
const { name, message } = clientMessage.payload;
const networkError = new Error(message);
networkError.name = name;
// Rejecting a request Promise emulates a network error.
throw networkError;
}
case "INTERNAL_ERROR": {
const parsedBody = JSON.parse(clientMessage.payload.body);
console.error(
`\
[MSW] Uncaught exception in the request handler for "%s %s":
${parsedBody.location}
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url
);
return respondWithMock(clientMessage);
}
}
return getOriginalResponse();
}
self.addEventListener("fetch", function (event) {
const { request } = event;
const accept = request.headers.get("accept") || "";
// Bypass server-sent events.
if (accept.includes("text/event-stream")) {
return;
}
// Bypass navigation requests.
if (request.mode === "navigate") {
return;
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === "only-if-cached" && request.mode !== "same-origin") {
return;
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return;
}
const requestId = uuidv4();
return event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === "NetworkError") {
console.warn(
'[MSW] Successfully emulated a network error for the "%s %s" request.',
request.method,
request.url
);
return;
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`
);
})
);
});
function serializeHeaders(headers) {
const reqHeaders = {};
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name]
? [].concat(reqHeaders[name]).concat(value)
: value;
});
return reqHeaders;
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error);
}
resolve(event.data);
};
client.postMessage(JSON.stringify(message), [channel.port2]);
});
}
function delayPromise(cb, duration) {
return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration);
});
}
function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
});
}
function uuidv4() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
+7 -1
View File
@@ -99,6 +99,11 @@
min-width: 32px;
}
.navds-button--xsmall {
padding: 0.125rem 0.375rem;
min-width: 24px;
}
.navds-button:focus {
outline: none;
box-shadow: var(--navds-shadow-focus);
@@ -115,7 +120,8 @@
font-size: 1.5rem;
}
.navds-button--small svg {
.navds-button--small svg,
.navds-button--xsmall svg {
font-size: 1.25rem;
}
+11 -4
View File
@@ -1,15 +1,21 @@
:root {
--navds-confirmation-panel-color-background: var(
--navds-semantic-color-feedback-warning-background
--navds-global-color-orange-50
);
--navds-confirmation-panel-color-background-checked: var(
--navds-semantic-color-feedback-success-background
--navds-global-color-green-50
);
--navds-confirmation-panel-color-border: var(
--navds-semantic-color-feedback-warning-border
);
--navds-confirmation-panel-color-border-checked: var(
--navds-semantic-color-feedback-success-background
--navds-semantic-color-feedback-success-border
);
--navds-confirmation-panel-color-border-error: var(
--navds-semantic-color-feedback-danger-background
);
--navds-confirmation-panel-color-background-error: var(
--navds-semantic-color-feedback-danger-background
--navds-global-color-red-50
);
--navds-confirmation-panel-color-checkbox-background-checked-hover: var(
--navds-global-color-green-700
@@ -34,6 +40,7 @@
gap: var(--navds-spacing-4);
padding: var(--navds-spacing-4);
border-radius: 4px;
border: 1px solid var(--navds-confirmation-panel-color-border);
background-color: var(--navds-confirmation-panel-color-background);
transition: background-color linear 100ms;
}
+1 -1
View File
@@ -11,4 +11,4 @@
@import "switch.css";
@import "text-field.css";
@import "textarea.css";
@import "search-field.css";
@import "search.css";
-59
View File
@@ -1,59 +0,0 @@
:root {
--navds-search-field-color-shadow-error: var(
--navds-semantic-color-interaction-danger
);
--navds-search-field-color-border-error: var(
--navds-semantic-color-interaction-danger
);
--navds-search-field-color-clearbutton-border: var(
--navds-semantic-color-border
);
}
.navds-search-field__input-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.navds-form-field--small .navds-search-field__input {
padding: 0.15rem;
min-height: 32px;
}
.navds-search-field__input-wrapper > *:not(:first-child):not(:last-child) {
border-radius: 0;
}
.navds-search-field__input-wrapper > :first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.navds-search-field__input-wrapper > :last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.navds-search-field__input-wrapper > :last-child:first-child {
border-radius: 4px;
}
.navds-search-field__input-wrapper :focus {
z-index: var(--navds-z-index-focus);
}
.navds-search-field__button,
.navds-search-field__clear-button {
flex-shrink: 0;
}
.navds-search-field--error
.navds-search-field__input:not(:hover):not(:focus):not(:disabled) {
box-shadow: 0 0 0 1px var(--navds-search-field-color-shadow-error) inset;
border-color: var(--navds-search-field-color-border-error);
}
.navds-search-field--disabled {
opacity: 0.3;
}
+125
View File
@@ -0,0 +1,125 @@
:root {
--navds-search-color-border: var(--navds-semantic-color-border);
--navds-search-color-border-hover: var(
--navds-semantic-color-interaction-primary
);
}
.navds-search {
display: flex;
flex-direction: column;
width: 100%;
}
.navds-search__wrapper-inner {
position: relative;
width: 100%;
}
.navds-search__wrapper {
display: inline-flex;
align-items: center;
}
.navds-search__input {
padding-right: var(--navds-spacing-12);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.navds-form-field--small .navds-search__input {
min-height: 32px;
padding: 0.15rem;
padding-right: var(--navds-spacing-8);
}
.navds-search__button-clear {
position: absolute;
right: 0.75rem;
border-radius: var(--navds-border-radius-medium);
color: var(--navds-semantic-color-text);
height: 32px;
width: 32px;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background: none;
border: none;
font-size: 1rem;
padding: 0;
}
.navds-form-field--small .navds-search__button-clear {
right: 0.5rem;
height: 24px;
width: 24px;
}
.navds-search__button-clear:hover {
color: var(--navds-global-color-blue-500);
}
.navds-search__button-clear:focus {
box-shadow: var(--navds-shadow-focus);
outline: none;
}
.navds-search__button-search {
flex-shrink: 0;
min-width: 64px;
min-height: 32px;
border-radius: 0;
border-top-right-radius: var(--navds-border-radius-medium);
border-bottom-right-radius: var(--navds-border-radius-medium);
}
.navds-form-field--small .navds-search__button-search svg {
font-size: 1rem;
}
.navds-form-field--small .navds-search__button-search {
min-width: 40px;
}
.navds-search__button-search.navds-button--tertiary {
box-shadow: -1px 0 0 0 var(--navds-search-color-border) inset,
0 1px 0 0 var(--navds-search-color-border) inset,
0 -1px 0 0 var(--navds-search-color-border) inset;
}
.navds-search__button-search.navds-button--tertiary:hover {
box-shadow: -1px 0 0 0 var(--navds-search-color-border-hover) inset,
0 1px 0 0 var(--navds-search-color-border-hover) inset,
0 -1px 0 0 var(--navds-search-color-border-hover) inset,
-1px 0 0 0 var(--navds-search-color-border-hover);
z-index: 1;
}
.navds-search__wrapper-inner:focus-within
+ .navds-search__button-search.navds-button--tertiary:hover {
z-index: auto;
}
.navds-search__button-search.navds-button--tertiary:focus {
box-shadow: 0 0 0 1px var(--navds-search-color-border) inset,
var(--navds-shadow-focus);
}
.navds-search__button-search.navds-button--tertiary:focus:hover {
box-shadow: 0 0 0 1px var(--navds-search-color-border-hover) inset,
var(--navds-shadow-focus);
}
.navds-search__button-search.navds-button--tertiary:focus:active {
box-shadow: 0 0 0 1px white inset, var(--navds-shadow-focus);
}
/* Focus layering */
.navds-search__input:focus,
.navds-search__button-clear,
.navds-search__button-search:focus {
z-index: 1;
}
+2
View File
@@ -1,4 +1,5 @@
:root {
--navds-text-field-color-text: var(--navds-semantic-color-text);
--navds-text-field-color-background: var(
--navds-semantic-color-component-background-light
);
@@ -27,6 +28,7 @@
border: 1px solid var(--navds-text-field-color-border);
min-height: 48px;
width: 100%;
color: var(--navds-text-field-color-text);
}
.navds-form-field--small > .navds-text-field__input {
+23 -28
View File
@@ -1,22 +1,21 @@
:root {
--navds-help-text-color-button-background: transparent;
--navds-help-text-color-icon: var(--navds-semantic-color-interaction-primary);
--navds-help-text-color-icon-hover: var(
--navds-help-text-color: var(--navds-semantic-color-interaction-primary);
--navds-help-text-color-hover: var(
--navds-semantic-color-component-background-light
);
--navds-help-text-color-icon-background-hover: var(
--navds-semantic-color-interaction-primary
);
--navds-help-text-color-icon-shadow-hover: var(
--navds-semantic-color-interaction-primary
);
--navds-help-text-color-icon-focus: var(
--navds-help-text-color-focus: var(
--navds-semantic-color-component-background-light
);
--navds-help-text-color-icon-background-focus: var(
--navds-help-text-color-background-hover: var(
--navds-semantic-color-interaction-primary
);
--navds-help-text-color-icon-shadow-focus: var(--navds-semantic-color-focus);
--navds-help-text-color-background-focus: var(
--navds-semantic-color-interaction-primary
);
--navds-help-text-color-shadow-hover: var(
--navds-semantic-color-interaction-primary
);
--navds-help-text-color-shadow-focus: var(--navds-semantic-color-focus);
--navds-help-text-color-popover-background: var(
--navds-semantic-color-feedback-info-background
);
@@ -27,34 +26,30 @@
padding: 0;
border: 0;
cursor: pointer;
background-color: var(--navds-help-text-color-button-background);
background-color: transparent;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: var(--navds-help-text-color);
font-size: 1.5rem;
}
.navds-help-text__button:hover {
color: var(--navds-help-text-color-hover);
background: var(--navds-help-text-color-background-hover);
box-shadow: 0 0 0 2px var(--navds-help-text-color-shadow-hover);
}
.navds-help-text__button:focus {
outline: none;
color: var(--navds-help-text-color-focus);
background: var(--navds-help-text-color-background-focus);
box-shadow: 0 0 0 3px var(--navds-help-text-color-shadow-focus);
}
.navds-help-text__icon {
border-radius: 50%;
font-size: 1.5rem;
color: var(--navds-help-text-color-icon);
}
.navds-help-text__button:hover > .navds-help-text__icon {
color: var(--navds-help-text-color-icon-hover);
background: var(--navds-help-text-color-icon-background-hover);
box-shadow: 0 0 0 2px var(--navds-help-text-color-icon-shadow-hover);
}
.navds-help-text__button:focus > .navds-help-text__icon {
outline: none;
color: var(--navds-help-text-color-icon-focus);
background: var(--navds-help-text-color-icon-background-focus);
box-shadow: 0 0 0 3px var(--navds-help-text-color-icon-shadow-focus);
}
.navds-help-text__popover > .navds-popover__arrow::before {
+1
View File
@@ -20,6 +20,7 @@
@import "toggle-group.css";
@import "panel.css";
@import "link-panel.css";
@import "read-more.css";
@import "speech-bubble.css";
@import "step-indicator.css";
@import "table.css";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "0.15.10",
"version": "0.16.5",
"private": false,
"description": "Css for NAV Designsystem components",
"author": "NAV Designsystem team",
+52
View File
@@ -0,0 +1,52 @@
.navds-read-more {
cursor: pointer;
margin: 0;
padding: 0;
text-decoration: underline;
border: none;
background: none;
display: flex;
align-items: center;
gap: var(--navds-spacing-2);
color: var(--navds-semantic-color-link);
}
.navds-read-more:hover {
text-decoration: none;
}
.navds-read-more:focus {
outline: none;
color: var(--navds-link-color-text-focus);
text-decoration: none;
background-color: var(--navds-link-color-background-focus);
box-shadow: 0 0 0 2px var(--navds-semantic-color-focus);
}
.navds-read-more__expand-icon {
font-size: 1.25rem;
}
.navds-read-more--open > .navds-read-more__expand-icon {
transform: rotate(180deg);
}
.navds-read-more + .ReactCollapse--collapse {
transition: height 250ms cubic-bezier(0.4, 0, 0.2, 1);
padding-top: var(--navds-spacing-2);
}
.navds-read-more__content {
display: flex;
align-items: stretch;
gap: var(--navds-spacing-2);
}
.navds-read-more__content::before {
content: "";
display: block;
width: 2px;
background-color: var(--navds-semantic-color-border-muted);
margin: 0 9px;
flex-shrink: 0;
}
+64 -3
View File
@@ -16,8 +16,8 @@
}
.navds-table {
border-collapse: collapse;
width: 100%;
border-collapse: collapse;
display: table;
}
@@ -54,11 +54,21 @@
.navds-table__header-cell,
.navds-table__data-cell {
display: table-cell;
padding: var(--navds-spacing-4);
padding: var(--navds-spacing-4) var(--navds-spacing-3);
border-bottom: 1px solid var(--navds-table-cell-color-border);
text-align: left;
}
.navds-table__header-cell--align-right,
.navds-table__data-cell--align-right {
text-align: right;
}
.navds-table__header-cell--align-center,
.navds-table__data-cell--align-center {
text-align: center;
}
.navds-table__body .navds-table__row:hover .navds-table__header-cell,
.navds-table__body .navds-table__row:hover .navds-table__data-cell {
border-color: var(--navds-table-cell-color-border-hover);
@@ -66,7 +76,7 @@
.navds-table--small .navds-table__header-cell,
.navds-table--small .navds-table__data-cell {
padding: var(--navds-spacing-2);
padding: var(--navds-spacing-2) var(--navds-spacing-3);
}
.navds-table .navds-checkbox .navds-checkbox__input {
@@ -80,3 +90,54 @@
.navds-table .navds-checkbox .navds-checkbox__label {
padding: 0;
}
.navds-table__header-cell[aria-sort] {
padding: 0;
}
.navds-table__sort-button {
appearance: none;
background: none;
color: var(--navds-semantic-color-link);
border: none;
cursor: pointer;
margin: 0;
padding: var(--navds-spacing-4) var(--navds-spacing-3);
width: 100%;
display: flex;
gap: 0.5rem;
align-items: center;
line-height: inherit;
font-weight: inherit;
}
.navds-table--small .navds-table__sort-button {
padding: var(--navds-spacing-2) var(--navds-spacing-3);
}
.navds-table__sort-button:hover {
background-color: var(--navds-semantic-color-canvas-background);
}
.navds-table__sort-button:focus {
outline: none;
box-shadow: inset 0 0 0 2px var(--navds-semantic-color-focus);
}
.navds-table__header-cell[aria-sort="ascending"] .navds-table__sort-button,
.navds-table__header-cell[aria-sort="descending"] .navds-table__sort-button {
background-color: var(--navds-global-color-blue-50);
}
.navds-table__header-cell--align-right .navds-table__sort-button {
justify-content: end;
}
.navds-table__header-cell--align-center .navds-table__sort-button {
justify-content: center;
}
.navds-table__sort-button svg {
font-size: 1rem;
flex-shrink: 0;
}
+2 -1
View File
@@ -69,7 +69,8 @@
flex-shrink: 0;
}
.navds-toggle-group__button[aria-pressed="true"] {
.navds-toggle-group__button[aria-pressed="true"],
.navds-toggle-group__button[aria-checked="true"] {
background-color: var(--navds-toggle-group-color-background-pressed);
color: var(--navds-toggle-group-color-text-pressed);
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-icons",
"version": "0.8.4",
"version": "0.8.5",
"private": false,
"description": "NAVs egne ikoner i SVG og React format",
"repository": {
@@ -29,7 +29,7 @@
"create-package-json-pointers-to-esm": "node ../../../utilities/scripts/createPackageJsonsWithESMPointers.js",
"create-icons": "svgr --out-dir src svg",
"clean": "rimraf src cjs esm",
"build": "yarn clean && yarn create-icons && tsc && tsc -p tsconfig.esm.json && yarn create-package-json-pointers-to-esm",
"build": "yarn clean && copyfiles util/* src && yarn create-icons && tsc && tsc -p tsconfig.esm.json && yarn create-package-json-pointers-to-esm",
"update": "ts-node figma/index.ts dotenv/config"
},
"dependencies": {
+7 -7
View File
@@ -15,13 +15,13 @@ function defaultTemplate(
type: "ImportSpecifier",
imported: {
type: "Identifier",
name: "v4",
name: "useId",
},
},
],
source: {
type: "StringLiteral",
value: "uuid",
value: "./util/useId",
},
});
@@ -34,12 +34,12 @@ function defaultTemplate(
return typeScriptTpl.ast`
${imports}
${interfaces}
function ${componentName}(${props}) {
const titleId = _titleId ?? (title ?
"icon-title-" + v4() : undefined);
const ${componentName} = React.forwardRef((${props}) => {
let titleId: string | undefined = useId(_titleId);
titleId = title ? titleId : undefined;
return ${jsx};
}
${exports}`;
});
export default ${componentName}`;
}
module.exports = defaultTemplate;
+1 -1
View File
@@ -4,5 +4,5 @@
"outDir": "./cjs",
"module": "commonjs"
},
"exclude": ["./figma", "./scripts", "**/*.stories.*"]
"exclude": ["./figma", "./util", "./scripts", "**/*.stories.*"]
}
+22
View File
@@ -0,0 +1,22 @@
import { useLayoutEffect, useState } from "react";
import { v4 } from "uuid";
const canUseDOM = (): boolean => {
return (
typeof window !== "undefined" &&
typeof window.document !== "undefined" &&
typeof window.document.createElement !== "undefined"
);
};
export const useClientLayoutEffect = canUseDOM() ? useLayoutEffect : () => {};
export const useId: (id?: string) => string = (id) => {
const [newId, setNewId] = useState<string | undefined>(undefined);
useClientLayoutEffect(() => {
setNewId(v4());
}, []);
return id ?? newId ?? "";
};
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "0.16.16",
"version": "0.17.6",
"private": false,
"description": "NAV designsystem react components",
"author": "NAV Designsystem team",
@@ -35,9 +35,9 @@
},
"dependencies": {
"@material-ui/core": "^4.12.3",
"@navikt/ds-icons": "^0.8.4",
"@navikt/ds-icons": "^0.8.5",
"@popperjs/core": "^2.10.1",
"@radix-ui/react-toggle-group": "^0.1.4",
"@radix-ui/react-toggle-group": "0.1.5",
"classnames": "^2.2.6",
"react-collapse": "^5.1.0",
"react-merge-refs": "^1.1.0",
@@ -51,6 +51,7 @@
"@testing-library/user-event": "^13.2.1",
"@types/faker": "^5.5.8",
"@types/jest": "^27.0.1",
"@types/react-collapse": "^5.0.1",
"@types/react-modal": "^3.13.1",
"@types/styled-components": "^5.1.14",
"copyfiles": "^2.4.1",
@@ -59,6 +60,7 @@
"react-router-dom": "^6.1.0",
"rimraf": "3.0.2",
"styled-components": "^5.3.1",
"swr": "^1.1.2",
"ts-jest": "^27.0.5"
},
"peerDependencies": {
+7 -3
View File
@@ -19,7 +19,7 @@ export interface ButtonProps
* Changes padding, height and font-size
* @default "medium"
*/
size?: "medium" | "small";
size?: "medium" | "small" | "xsmall";
/**
* Prevent the user from interacting with the button: it cannot be pressed or focused.
* @note Avoid using if possible for accessibility purposes
@@ -43,6 +43,7 @@ const Button: OverridableComponent<ButtonProps, HTMLButtonElement> = forwardRef(
size = "medium",
loading = false,
disabled,
style,
...rest
},
ref
@@ -76,13 +77,16 @@ const Button: OverridableComponent<ButtonProps, HTMLButtonElement> = forwardRef(
"navds-button--loading": widthOverride,
}
)}
style={{ width: widthOverride }}
style={{
...style,
width: widthOverride,
}}
disabled={disabled ?? widthOverride ? true : undefined}
>
<BodyShort
as="span"
className="navds-button__inner"
size={size}
size={size === "medium" ? "medium" : "small"}
aria-live="polite"
>
{widthOverride ? <Loader size={size} /> : children}
@@ -81,6 +81,14 @@ export const All = () => {
</Button>
))}
</Section>
<h2>Xsmall</h2>
<Section>
{variants.map((variant) => (
<Button key={variant} variant={variant} size="xsmall">
{varSwitch[variant]}
</Button>
))}
</Section>
<h2>Button w/icon</h2>
<Section>
{variants.map((variant) => (
+1 -1
View File
@@ -10,7 +10,7 @@ export {
FieldsetProps,
} from "./Fieldset";
export { Radio, RadioGroup, RadioGroupProps, RadioProps } from "./radio";
export { SearchField, SearchFieldProps } from "./search-field";
export { Search, SearchProps, SearchClearEvent } from "./search";
export { default as Select, SelectProps } from "./Select";
export { default as Switch, SwitchProps } from "./Switch";
export { ErrorSummary, ErrorSummaryProps } from "./error-summary";
@@ -1,132 +0,0 @@
import cl from "classnames";
import React, { forwardRef } from "react";
import { BodyShort, Label, omit } from "../..";
import ErrorMessage from "../ErrorMessage";
import { FormFieldProps, useFormField } from "../useFormField";
import SearchFieldButton, { SearchFieldButtonType } from "./SearchFieldButton";
import SearchFieldClearButton, {
SearchFieldClearButtonType,
} from "./SearchFieldClearButton";
import SearchFieldInput, { SearchFieldInputType } from "./SearchFieldInput";
export interface SearchFieldContextProps {
inputProps: {
id: string;
"aria-invalid": boolean;
"aria-describedby"?: string;
disabled?: boolean;
};
size?: "medium" | "small";
}
export const SearchFieldContext = React.createContext<SearchFieldContextProps | null>(
null
);
export interface SearchFieldProps
extends FormFieldProps,
React.HTMLAttributes<HTMLDivElement> {
/**
* SearchFieldInput & SearchFieldButton
*/
children: React.ReactNode;
/**
* If enabled shows the label and description for screenreaders only
*/
hideLabel?: boolean;
/**
* SearchField label
*/
label: React.ReactNode;
}
interface SearchFieldComponent
extends React.ForwardRefExoticComponent<
SearchFieldProps & React.RefAttributes<HTMLDivElement>
> {
Button: SearchFieldButtonType;
Clear: SearchFieldClearButtonType;
Input: SearchFieldInputType;
}
const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
(props, ref) => {
const {
inputProps,
errorId,
showErrorMsg,
hasError,
size,
inputDescriptionId,
} = useFormField(props, "searchfield");
const {
className,
hideLabel,
children,
label,
description,
error,
...rest
} = props;
return (
<div
ref={ref}
{...omit(rest, ["id", "error", "errorId", "size", "disabled"])}
className={cl(
className,
"navds-form-field",
`navds-form-field--${size ?? "medium"}`,
"navds-search-field",
{
"navds-search-field--error": hasError,
"navds-search-field--disabled": !!inputProps.disabled,
}
)}
>
<Label
htmlFor={inputProps.id}
size={size}
as="label"
className={cl("navds-text-field__label", {
"navds-sr-only": hideLabel,
})}
>
{label}
</Label>
{!!description && (
<BodyShort
as="div"
className={cl("navds-text-field__description", {
"navds-sr-only": hideLabel,
})}
id={inputDescriptionId}
size={size}
>
{description}
</BodyShort>
)}
<div className="navds-search-field__input-wrapper">
<SearchFieldContext.Provider
value={{
inputProps,
size,
}}
>
{children}
</SearchFieldContext.Provider>
</div>
<div id={errorId} aria-relevant="additions removals" aria-live="polite">
{showErrorMsg && <ErrorMessage size={size}>{error}</ErrorMessage>}
</div>
</div>
);
}
) as SearchFieldComponent;
SearchField.Button = SearchFieldButton;
SearchField.Clear = SearchFieldClearButton;
SearchField.Input = SearchFieldInput;
export default SearchField;
@@ -1,47 +0,0 @@
import React, { forwardRef, useContext } from "react";
import cl from "classnames";
import { SearchFieldContext } from "./SearchField";
import { Button, ButtonProps } from "../..";
export interface SearchFieldButtonProps extends Omit<ButtonProps, "size"> {
/**
* Button text
* @example <Search /> <span>Søk</span>
*/
children: React.ReactNode;
/**
* Changes design and interactions
* @default "primary"
*/
variant?: "primary" | "secondary";
}
export type SearchFieldButtonType = React.ForwardRefExoticComponent<
SearchFieldButtonProps & React.RefAttributes<HTMLButtonElement>
>;
const SearchFieldButton: SearchFieldButtonType = forwardRef(
({ className, variant = "primary", disabled, ...rest }, ref) => {
const searchField = useContext(SearchFieldContext);
if (searchField === null) {
console.warn("SearchFieldButton has to be wrapped in <SearchField />");
return null;
}
const { size, inputProps } = searchField;
return (
<Button
ref={ref}
className={cl(className, "navds-search-field__button")}
{...rest}
size={size}
variant={variant}
disabled={disabled ?? inputProps?.disabled}
/>
);
}
);
export default SearchFieldButton;
@@ -1,49 +0,0 @@
import React, { forwardRef, useContext } from "react";
import cl from "classnames";
import { SearchFieldContext } from "./SearchField";
import { Button, ButtonProps } from "../..";
export interface SearchFieldClearButtonProps extends Omit<ButtonProps, "size"> {
/**
* Button text
* @example <Close /> <span>Søk</span>
*/
children: React.ReactNode;
/**
* Changes design and interactions
* @default "secondary"
*/
variant?: "primary" | "secondary";
}
export type SearchFieldClearButtonType = React.ForwardRefExoticComponent<
SearchFieldClearButtonProps & React.RefAttributes<HTMLButtonElement>
>;
const SearchFieldClearButton: SearchFieldClearButtonType = forwardRef(
({ className, variant = "secondary", disabled, ...rest }, ref) => {
const searchField = useContext(SearchFieldContext);
if (searchField === null) {
console.warn(
"SearchFieldClearButton has to be wrapped in <SearchField />"
);
return null;
}
const { size, inputProps } = searchField;
return (
<Button
{...rest}
ref={ref}
className={cl(className, "navds-search-field__clear-button")}
size={size}
variant={variant}
disabled={disabled ?? inputProps?.disabled}
/>
);
}
);
export default SearchFieldClearButton;
@@ -1,42 +0,0 @@
import React, { forwardRef, InputHTMLAttributes, useContext } from "react";
import cl from "classnames";
import { SearchFieldContext } from "./SearchField";
export interface SearchFieldInputProps
extends InputHTMLAttributes<HTMLInputElement> {}
export type SearchFieldInputType = React.ForwardRefExoticComponent<
SearchFieldInputProps & React.RefAttributes<HTMLInputElement>
>;
const SearchFieldInput: SearchFieldInputType = forwardRef(
({ className, ...rest }, ref) => {
const searchField = useContext(SearchFieldContext);
if (searchField === null) {
console.warn("SearchFieldInput has to be wrapped in <SearchField />");
return null;
}
const { size, inputProps } = searchField;
return (
<input
{...rest}
{...inputProps}
type="search"
role="searchbox"
ref={ref}
className={cl(
className,
"navds-search-field__input",
"navds-text-field__input",
"navds-body-short",
`navds-body-${size ?? "medium"}`
)}
/>
);
}
);
export default SearchFieldInput;
@@ -1,2 +0,0 @@
export { default as SearchField, SearchFieldProps } from "./SearchField";
export * from "./SearchField";
@@ -1,25 +0,0 @@
import React, { useState } from "react";
import { Close, Search } from "@navikt/ds-icons";
import { SearchField } from "../index";
export const Example = ({ size = "medium" }: { size: "medium" | "small" }) => {
const [value, setValue] = useState("");
return (
<SearchField size={size} label="Skriv i søkefeltet for å vise clearbutton">
<SearchField.Input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
{!!value && (
<SearchField.Clear onClick={() => setValue("")}>
<Close />
Tøm
</SearchField.Clear>
)}
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
);
};
@@ -1,156 +0,0 @@
import { Meta, Canvas } from "@storybook/addon-docs";
import { SearchField } from "../index";
import { Example } from "./search-field-example.tsx";
import { Search } from "@navikt/ds-icons";
<Meta title="ds-react/form/search-field/intro" />
# Hvordan ta i bruk SearchField
## Viktig
Komponenten er ikke testet eller gått gjennom et design-review, så anbefalses og vente med å ta den i bruk.
## Bruk
```jsx
<SearchField label="Mollit eiusmod" description="Ea cupidatat eu sunt commodo">
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
```
<Canvas>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
</Canvas>
## hideLabel
`hideLabel` proppen kan brukes for å vise label + description for bare skjermlesere.
Vi har satt `label` som required siden man vil bruke den i sammenheng med denne proppen for bedre UU.
```jsx
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
hideLabel
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
```
<Canvas>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
hideLabel
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
</Canvas>
## Sizing
```jsx
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
hideLabel
size="small"
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
```
<Canvas>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
hideLabel
size="small"
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
</Canvas>
## Errors
```jsx
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
hideLabel
error="Error message"
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
```
<Canvas>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
hideLabel
error="Error message"
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
</Canvas>
## ClearButton
Man kan bruke `SearchField.Clear` for å håndtere clearing av value i `<input />`. For å bruke denne
kreves det da at `SearchField.Input` er controlled slik at man styrer state selv slik som eksemplet under.
Hvis man bare ønsker at den bare skal vises når bruker har begynt å skrive, anbefaler vi bare å rendre
den når inputvalue ikke er empty.
```jsx
<SearchField label="Mollit eiusmod" description="Ea cupidatat eu sunt commodo">
<SearchField.Input value={value} onChange={(e) => setValue(e.target.value)} />
{!!value && (
<SearchField.Clear onClick={() => setValue("")}>
<Close /> Tøm
</SearchField.Clear>
)}
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
```
<Canvas>
<div>
<Example />
<Example size="small" />
</div>
</Canvas>
@@ -1,199 +0,0 @@
import React, { useState } from "react";
import { Meta } from "@storybook/react/types-6-0";
import { Close, Search } from "@navikt/ds-icons";
import { SearchField } from "../index";
import { Fieldset } from "../..";
export default {
title: "ds-react/form/search-field",
component: SearchField,
} as Meta;
export const All = () => {
const [show, setShow] = useState(false);
return (
<>
<h1>SearchField</h1>
<SearchField
label="Mollit eiusmod"
description={<div>Ea cupidatat eu sunt commodo</div>}
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
>
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
<SearchField.Input />
</SearchField>
<h1>SearchField w clearsearch</h1>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
>
<SearchField.Input />
<SearchField.Clear>
<Close />
</SearchField.Clear>
<SearchField.Button onClick={() => setShow(!show)}>
<Search /> Søk
</SearchField.Button>
</SearchField>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
>
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
<SearchField.Clear>
<Close /> Tøm
</SearchField.Clear>
<SearchField.Input />
</SearchField>
<h2>Hidelabel</h2>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
hideLabel
>
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
<SearchField.Input />
</SearchField>
<h2>SearchField small</h2>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
size="small"
>
<SearchField.Button>
<Search />
<span className="navds-sr-only">Søk</span>
</SearchField.Button>
<SearchField.Input />
</SearchField>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
size="small"
>
<SearchField.Input />
<SearchField.Button>
<Search />
<span className="navds-sr-only">Søk</span>
</SearchField.Button>
</SearchField>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
size="small"
>
<SearchField.Button>
<Search />
<span className="navds-sr-only">Søk</span>
</SearchField.Button>
<SearchField.Clear>
<Close /> Tøm
</SearchField.Clear>
<SearchField.Input />
</SearchField>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
size="small"
>
<SearchField.Input />
<SearchField.Clear>
<Close /> Tøm
</SearchField.Clear>
<SearchField.Button>
<Search />
<span className="navds-sr-only">Søk</span>
</SearchField.Button>
</SearchField>
<h2>SearchField w error</h2>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
error="Errormsg"
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
error="Errormsg"
>
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
<SearchField.Input />
</SearchField>
<h2>SearchField in Fieldset</h2>
<Fieldset legend="Filter" error="Fieldset-error-msg">
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
error="Errormsg"
hideLabel
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
hideLabel
>
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
<SearchField.Input />
</SearchField>
</Fieldset>
<h2>Disabled </h2>
<SearchField
disabled
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
<Fieldset legend="Filter" disabled>
<SearchField
label="Mollit eiusmod"
description="Ea cupidatat eu sunt commodo"
error="Errormsg"
>
<SearchField.Input />
<SearchField.Button>
<Search /> Søk
</SearchField.Button>
</SearchField>
</Fieldset>
</>
);
};
@@ -0,0 +1,233 @@
import { Close } from "@navikt/ds-icons";
import cl from "classnames";
import React, {
forwardRef,
InputHTMLAttributes,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import mergeRefs from "react-merge-refs";
import { BodyShort, Label, omit, useEventListener } from "../..";
import { FormFieldProps } from "../useFormField";
import SearchButton, { SearchButtonType } from "./SearchButton";
import { useSearch } from "./useSearch";
export type SearchClearEvent =
| {
trigger: "Click";
event: React.MouseEvent<HTMLButtonElement, MouseEvent>;
}
| { trigger: "Escape"; event: React.KeyboardEvent<HTMLDivElement> };
export interface SearchProps
extends Omit<FormFieldProps, "error" | "errorId">,
Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "onChange"> {
children?: React.ReactNode;
/**
* Search label
* @info Will be hidden by default, is required for accessibility reasons.
*/
label: React.ReactNode;
/**
* Shows label and description for screenreaders-only
* @default true
*/
hideLabel?: boolean;
/**
* Callback for value-change in input
*/
onChange?: (value: string) => void;
/**
* Callback for <Search.Button/> click or onSubmit in form
*/
onSearch?: (value: string | number | readonly string[]) => void;
/**
* Callback for click on clear-button or Escape keydown
*/
onClear?: (e: SearchClearEvent) => void;
/**
* aria-label on clear button
* @default "Tøm"
*/
clearButtonLabel?: string;
/**
* If false, removes clear-button option from input.
* @default true
*/
clearButton?: boolean;
/**
* Changes button-variant
* @default "tertiary"
*/
variant?: "tertiary" | "primary";
}
interface SearchComponent
extends React.ForwardRefExoticComponent<
SearchProps & React.RefAttributes<HTMLDivElement>
> {
Button: SearchButtonType;
}
export interface SearchContextProps {
disabled?: boolean;
size: "medium" | "small";
variant?: "tertiary" | "primary";
onSearch?: () => void;
}
export const SearchContext = React.createContext<SearchContextProps | null>(
null
);
const Search = forwardRef<HTMLInputElement, SearchProps>((props, ref) => {
const { inputProps, size = "medium", inputDescriptionId } = useSearch(
props,
"searchfield"
);
const {
className,
hideLabel = true,
label,
description,
value,
clearButtonLabel,
onClear,
clearButton = true,
children,
onSearch,
variant = "tertiary",
...rest
} = props;
const searchRef = useRef<HTMLInputElement | null>(null);
const mergedRef = mergeRefs([searchRef, ref]);
const [wrapperRef, setWrapperRef] = useState<HTMLDivElement | null>(null);
const [controlledValue, setControlledValue] = useState(value ?? "");
const handleChange = useCallback(
(v: string) => {
searchRef.current && value === undefined && setControlledValue(v);
props?.onChange?.(v);
},
[props, value]
);
const handleClear = useCallback(
(event: SearchClearEvent) => {
onClear?.(event);
handleChange("");
if (searchRef.current && value === undefined) {
searchRef.current.value = "";
}
searchRef.current && searchRef.current?.focus?.();
},
[handleChange, onClear, value]
);
useEventListener(
"keydown",
useCallback(
(e) => {
if (e.key === "Escape") {
e.preventDefault();
handleClear({ trigger: "Escape", event: e });
}
},
[handleClear]
),
wrapperRef
);
useEffect(() => {
value !== undefined && setControlledValue(value);
}, [value]);
return (
<div
ref={setWrapperRef}
className={cl(
className,
"navds-form-field",
`navds-form-field--${size}`,
"navds-search",
{
"navds-search--disabled": !!inputProps.disabled,
}
)}
>
<Label
htmlFor={inputProps.id}
size={size}
as="label"
className={cl("navds-text-field__label", {
"navds-sr-only": hideLabel,
})}
>
{label}
</Label>
{!!description && (
<BodyShort
as="div"
className={cl("navds-text-field__description", {
"navds-sr-only": hideLabel,
})}
id={inputDescriptionId}
size={size}
>
{description}
</BodyShort>
)}
<div className="navds-search__wrapper">
<div className="navds-search__wrapper-inner">
<input
ref={mergedRef}
{...omit(rest, ["size"])}
{...inputProps}
{...(props.value !== undefined && { value: props.value })}
onChange={(e) => handleChange(e.target.value)}
type="search"
role="searchbox"
className={cl(
className,
"navds-search__input",
"navds-text-field__input",
"navds-body-short",
`navds-body-${size ?? "medium"}`
)}
/>
{controlledValue && clearButton && (
<button
type="button"
onClick={(e) => handleClear({ trigger: "Click", event: e })}
className="navds-search__button-clear"
>
<span className="navds-sr-only">
{clearButtonLabel ? clearButtonLabel : "Tøm"}
</span>
<Close aria-hidden />
</button>
)}
</div>
<SearchContext.Provider
value={{
size,
disabled: inputProps.disabled,
variant,
onSearch: () => onSearch?.(controlledValue),
}}
>
{children ? children : <SearchButton />}
</SearchContext.Provider>
</div>
</div>
);
}) as SearchComponent;
Search.Button = SearchButton;
export default Search;
@@ -0,0 +1,51 @@
import { Search } from "@navikt/ds-icons";
import cl from "classnames";
import React, { forwardRef, useContext } from "react";
import { Button, ButtonProps } from "../..";
import { SearchContext } from "./Search";
export interface SearchButtonProps
extends Omit<ButtonProps, "size" | "children" | "variant"> {
/**
* Text set before <Search/> icon
*/
children?: React.ReactNode;
}
export type SearchButtonType = React.ForwardRefExoticComponent<
SearchButtonProps & React.RefAttributes<HTMLButtonElement>
>;
const SearchButton: SearchButtonType = forwardRef(
({ className, children, disabled, onClick, ...rest }, ref) => {
const context = useContext(SearchContext);
if (context === null) {
console.warn("<Search.Button> has to be wrapped in <Search />");
return null;
}
const { size, variant, onSearch } = context;
return (
<Button
type="submit"
{...rest}
ref={ref}
size={size}
variant={variant}
className={cl("navds-search__button-search", className)}
disabled={context?.disabled ?? disabled}
onClick={(e) => {
onSearch?.();
onClick?.(e);
}}
>
<Search aria-hidden />
{children ? children : <span className="navds-sr-only">Søk</span>}
</Button>
);
}
);
export default SearchButton;
@@ -0,0 +1 @@
export { default as Search, SearchProps, SearchClearEvent } from "./Search";
@@ -0,0 +1,94 @@
import { Meta } from "@storybook/react/types-6-0";
import React, { useState } from "react";
import { Search } from "../index";
export default {
title: "ds-react/form/search",
component: Search,
} as Meta;
export const All = () => {
const [value, setValue] = useState("");
return (
<div
style={{
display: "flex",
alignItems: "center",
flexDirection: "column",
maxWidth: 400,
}}
>
<h1>Search</h1>
<Search label="Søk alle sider om X og Y" onSearch={console.log}></Search>
<h2>Primary search</h2>
<Search
label="Søk alle sider om X og Y"
onSearch={console.log}
variant="primary"
></Search>
<h2>Search small</h2>
<Search
label="Søk alle sider om X og Y"
description="Beskrivelse av søket"
size="small"
>
<Search.Button />
</Search>
<br />
<Search
label="Søk alle sider om X og Y"
description="Beskrivelse av søket"
size="small"
hideLabel
variant="primary"
>
<Search.Button />
</Search>
<h2>Med knappe-tekst</h2>
<Search label="Søk alle sider om X og Y" hideLabel={false}>
<Search.Button>Søk</Search.Button>
</Search>
<Search
label="Søk alle sider om X og Y"
hideLabel={false}
variant="primary"
>
<Search.Button>Søk</Search.Button>
</Search>
<h2>Hidelabel false</h2>
<Search label="Søk alle sider om X og Y" hideLabel={false}>
<Search.Button />
</Search>
<h2>Controlled state </h2>
<Search
value={value}
label="Søk alle sider om X og Y"
description="Beskrivelse av søket"
onChange={(e) => setValue(e)}
onClear={() => setValue("")}
>
<Search.Button />
</Search>
<h2>No clear button</h2>
<Search
hideLabel
label="Søk alle sider om X og Y"
description="Beskrivelse av søket"
clearButton={false}
>
<Search.Button />
</Search>
</div>
);
};
export const UUDemo = () => {
return (
<Search label="Søk på nav.no" onSearch={console.log} placeholder="Søk">
<Search.Button />
</Search>
);
};
@@ -0,0 +1,31 @@
import cl from "classnames";
import { useContext } from "react";
import { useId } from "../../index";
import { FieldsetContext } from "../index";
import { FormFieldProps } from "../useFormField";
export const useSearch = (props: FormFieldProps, prefix: string) => {
const { size } = props;
const fieldset = useContext(FieldsetContext);
const genId = useId();
const id = props.id ?? `${prefix}-${genId}`;
const inputDescriptionId = `${prefix}-description-${genId}`;
const disabled = fieldset?.disabled || props.disabled;
return {
inputDescriptionId,
size: size ?? fieldset?.size ?? "medium",
inputProps: {
id,
"aria-describedby":
cl(props["aria-describedby"], {
[inputDescriptionId]: !!props?.description,
}) || undefined,
disabled,
},
};
};
+11 -22
View File
@@ -1,6 +1,6 @@
import { Helptext as HelpTextIcon } from "@navikt/ds-icons";
import cl from "classnames";
import React, { forwardRef, useEffect, useRef, useState } from "react";
import React, { forwardRef, useRef, useState } from "react";
import mergeRefs from "react-merge-refs";
import { Popover, PopoverProps } from "..";
@@ -21,53 +21,42 @@ const HelpText = forwardRef<HTMLButtonElement, HelpTextProps>(
placement = "top",
strategy = "absolute",
title = "hjelp",
onClick,
...rest
},
ref
) => {
const buttonRef = useRef<HTMLButtonElement | null>(null);
const mergedRef = mergeRefs([buttonRef, ref]);
const [popoverRef, setPopoverRef] = useState<HTMLDivElement | null>(null);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false);
useEffect(() => {
open && popoverRef?.focus?.();
}, [open, popoverRef]);
const handleClick = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
setOpen((x) => !x);
rest.onClick && rest.onClick(e);
};
return (
<div className="navds-help-text" ref={wrapperRef}>
<div className="navds-help-text">
<button
{...rest}
ref={mergedRef}
onClick={(e) => handleClick(e)}
onClick={(e) => {
setOpen((x) => !x);
onClick?.(e);
}}
className={cl(className, "navds-help-text__button")}
type="button"
aria-expanded={open}
aria-haspopup="dialog"
title={title}
>
<HelpTextIcon className="navds-help-text__icon" />
<span className="navds-sr-only">{title}</span>
<HelpTextIcon className="navds-help-text__icon" aria-hidden/>
</button>
<Popover
ref={setPopoverRef}
onClose={() => setOpen(false)}
className="navds-help-text__popover"
open={open}
role="tooltip"
anchorEl={buttonRef.current}
placement={placement}
strategy={strategy}
>
<Popover.Content>{children}</Popover.Content>
<Popover.Content className="navds-body-short">
{children}
</Popover.Content>
</Popover>
</div>
);
@@ -0,0 +1,28 @@
import React from "react";
import { TextField, HelpText } from "..";
export default {
title: "ds-react/help-text",
component: HelpText,
};
export const All = () => {
return (
<>
<h1>HelpText</h1>
<HelpText title="show tooltip">
Id ullamco excepteur elit fugiat labore.
</HelpText>
<TextField
label={
<div style={{ display: "flex", gap: 8 }}>
Text field label
<HelpText title="show tooltip">
Id ullamco excepteur elit fugiat labore.
</HelpText>
</div>
}
></TextField>
</>
);
};
@@ -1,35 +0,0 @@
import { Meta, Canvas } from "@storybook/addon-docs";
import { HelpText } from "../../index";
import { Success } from "@navikt/ds-react";
<Meta title="ds-react/help-text/intro" />
# Hvordan ta i bruk HelpText
```jsx
<HelpText>Exercitation esse minim esse mollit ex magna.</HelpText>
```
<Canvas>
<div style={{ padding: "5rem" }}>
<HelpText>Exercitation esse minim esse mollit ex magna.</HelpText>
</div>
</Canvas>
# Egen title
Husk å endre på "title" slik at skjermlesere får tydeligere informasjon
```jsx
<HelpText title="vis hjelpetekst angående søknad">
Exercitation esse minim esse mollit ex magna.
</HelpText>
```
<Canvas>
<div style={{ padding: "5rem" }}>
<HelpText title="vis hjelpetekst angående søknad">
Exercitation esse minim esse mollit ex magna.
</HelpText>
</div>
</Canvas>
@@ -1,32 +0,0 @@
import React from "react";
import { BodyLong } from "../../typography";
import { HelpText } from "../index";
export default {
title: "ds-react/help-text",
component: HelpText,
};
export const All = () => {
return (
<>
<h1>HelpText</h1>
<BodyLong style={{ display: "flex" }}>
Quis tempor incididunt sint sit veniam ad dolore cillum ut.
<HelpText title="show tooltip">
Id ullamco excepteur elit fugiat labore.
</HelpText>
</BodyLong>
<BodyLong>
Incididunt pariatur laborum dolor sint qui eiusmod exercitation non
cupidatat amet.
</BodyLong>
<h2>HelpText defaultOpen</h2>
<HelpText title="show tooltip">
Id ullamco excepteur elit fugiat labore.
</HelpText>
</>
);
};
+1
View File
@@ -13,6 +13,7 @@ export * from "./modal";
export * from "./pagination";
export * from "./panel";
export * from "./popover";
export * from "./read-more";
export * from "./speech-bubble";
export * from "./step-indicator";
export * from "./tag";
+9 -9
View File
@@ -19,15 +19,15 @@ interface MenuComponent
const Menu = forwardRef<HTMLElement, MenuProps>(
({ children, className, ...rest }, ref) => (
<nav
{...rest}
role="navigation"
ref={ref}
className={cl("navds-menu", className)}
>
<MenuItems>{children}</MenuItems>
</nav>
)
<nav
{...rest}
role="navigation"
ref={ref}
className={cl("navds-menu", className)}
>
<MenuItems>{children}</MenuItems>
</nav>
)
) as MenuComponent;
Menu.Collapse = Collapse;
+11
View File
@@ -33,9 +33,18 @@ export interface ModalProps {
* @default true
*/
closeButton?: boolean;
/**
* Callback for getting parent element modal will attach to
*/
parentSelector?(): HTMLElement;
"aria-labelledby"?: string;
"aria-describedby"?: string;
"aria-modal"?: boolean;
/**
* Sets aria-label on modal
* @warning This should be set if not using 'aria-labelledby' or 'aria-describedby'
*/
"aria-label"?: string;
}
interface ModalComponent
@@ -62,6 +71,7 @@ const Modal = forwardRef<ReactModal, ModalProps>(
"aria-describedby": ariaDescribedBy,
"aria-labelledby": ariaLabelledBy,
"aria-modal": ariaModal,
"aria-label": contentLabel,
...rest
},
ref
@@ -92,6 +102,7 @@ const Modal = forwardRef<ReactModal, ModalProps>(
labelledby: ariaLabelledBy,
modal: ariaModal,
}}
contentLabel={contentLabel}
>
{children}
{closeButton && (
@@ -16,9 +16,13 @@ export const All = () => {
return (
<>
<button onClick={() => setOpen(true)}>Open modal</button>
<Modal open={open} onClose={() => setOpen(false)}>
<Modal
open={open}
onClose={() => setOpen(false)}
aria-labelledby="header123"
>
<Modal.Content>
<h1>Header</h1>
<h1 id="header123">Header</h1>
<h2>subheader</h2>
<p>Cupidatat irure ipsum veniam ad in esse.</p>
<p>Cillum tempor pariatur amet ut laborum Lorem enim enim.</p>
@@ -32,9 +36,10 @@ export const All = () => {
shouldCloseOnOverlayClick={false}
open={openTwo}
onClose={() => setOpenTwo(false)}
aria-labelledby="header123"
>
<Modal.Content>
<h1>Header</h1>
<h1 id="header123">Header</h1>
<h2>subheader</h2>
<p>
Cupidatat irure ipsum veniam ad in esse. Voluptate do nulla amet
@@ -54,10 +59,9 @@ export const All = () => {
open={openThree}
closeButton={false}
onClose={() => setOpenThree(false)}
aria-label="Min modal"
>
<Modal.Content>
<h1>Header</h1>
<h2>subheader</h2>
<p>Cupidatat irure ipsum veniam ad in esse.</p>
<p>Cillum tempor pariatur amet ut laborum Lorem enim enim.</p>
</Modal.Content>
@@ -1,32 +0,0 @@
import { Meta, Canvas } from "@storybook/addon-docs";
import { Modal } from "../../index";
<Meta title="ds-react/modal/intro" />
# Hvordan ta i bruk Modal
NOTE: For at modalen skal mountes riktig må man kjøre
`Modal.setAppElement("#root");` før man bruke modalen, da i eks useEffect/useLayoutEffect.
`#root` kan da her erstattes med andre elementer, men root er relativt vanlig.
Dette er nødvendig for at skjermlesere ikke skal lese opp innholdet feil i open/closed state
Modalen styres med `open`-proppen som triggrer modal-staten til å endre. Når bruker ønsker å lukke modalen
vil `onClose`-triggre og det er opp til systemet å håndtere det.
```jsx
<Modal open={open} onClose={() => setOpen(false)}>
<Modal.Content>
<h2>Heading</h2>
<p>
Cupidatat irure ipsum veniam ad in esse. Voluptate do nulla amet
laboriselit sit dolor aliquip velit esse. Excepteur sint non minim nulla
excepteur labore non magna eu.
</p>
</Modal.Content>
</Modal>
```
## shouldCloseOnOverlayClick
Proppen `shouldCloseOnOverlayClick` toggler om `onClose` skal triggres
når bruker trykker utenfor modalen (overlayet). Er default: true .
@@ -0,0 +1,90 @@
import React, { forwardRef, useState } from "react";
import cl from "classnames";
import { Collapse, UnmountClosed } from "react-collapse";
import { Expand } from "@navikt/ds-icons";
import { BodyLong } from "../typography";
export interface ReadMoreProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* Content inside read more
*/
children: React.ReactNode;
/**
* Read more header content
*/
header: React.ReactNode;
/**
* Opens component if 'true', closes if 'false'
* Using this props removes automatic control of open-state
*/
open?: boolean;
/**
* Defaults the accordion to opened state
* @default false
*/
defaultOpen?: boolean;
/**
* Removes content-element from dom when closed
* @default false
*/
renderContentWhenClosed?: boolean;
}
const ReadMore = forwardRef<HTMLButtonElement, ReadMoreProps>(
(
{
className,
header,
renderContentWhenClosed = false,
children,
open,
defaultOpen = false,
onClick,
...rest
},
ref
) => {
const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);
const CollapseComponent = renderContentWhenClosed
? Collapse
: UnmountClosed;
const isOpened = open ?? internalOpen;
return (
<>
<button
{...rest}
className={cl(
"navds-read-more",
"navds-body-short",
"navds-body-short--small",
className,
{
"navds-read-more--open": isOpened,
}
)}
onClick={(e) => {
if (open === undefined) {
setInternalOpen((isOpen) => !isOpen);
}
onClick?.(e);
}}
aria-expanded={isOpened}
ref={ref}
>
<Expand className="navds-read-more__expand-icon" aria-hidden />
<span>{header}</span>
</button>
<CollapseComponent isOpened={isOpened}>
<div className="navds-read-more__content">
<BodyLong size="small">{children}</BodyLong>
</div>
</CollapseComponent>
</>
);
}
);
export default ReadMore;
@@ -0,0 +1,2 @@
export { default as ReadMore } from "./ReadMore";
export * from "./ReadMore";
@@ -0,0 +1,60 @@
import React, { useState } from "react";
import { ReadMore } from ".";
import { Link } from "..";
export default {
title: "ds-react/read-more",
component: ReadMore,
};
export const All = () => {
const [open, setOpen] = useState(false);
return (
<>
<h1>Read more</h1>
<h2>Controlled</h2>
<ReadMore
open={open}
onClick={() => setOpen(!open)}
header="ReadMore header text"
>
Magna aliquip aliquip fugiat nostrud nostrud velit pariatur veniam
officia laboris voluptate officia pariatur.Lorem est ex anim velit
occaecat nisi qui nostrud sit consectetur consectetur officia nostrud
ullamco. Est ex duis proident nostrud elit qui laborum anim minim eu
eiusmod. Veniam in nostrud sunt tempor velit incididunt sint ex dolor
qui velit id eu.{" "}
<Link href="example.com">Deserunt magna sunt velit in</Link>. Est
exercitation id cillum qui do. Minim adipisicing nostrud commodo
proident occaecat aliquip nulla anim proident reprehenderit. Magna ipsum
officia veniam cupidatat duis veniam dolore reprehenderit mollit
velit.Ut consequat commodo minim occaecat id pariatur. Nisi enim tempor
laborum commodo. Tempor sit quis nostrud eu cupidatat sunt commodo
reprehenderit irure deserunt eiusmod ipsum. Exercitation quis commodo
cillum eiusmod eiusmod. Do laborum qui proident commodo adipisicing
eiusmod id.
</ReadMore>
<h2>Un-controlled</h2>
<ReadMore header="ReadMore header text">
Magna aliquip aliquip fugiat nostrud nostrud velit pariatur veniam
officia laboris voluptate officia pariatur.Lorem est ex anim velit
occaecat nisi qui nostrud sit consectetur consectetur officia nostrud
ullamco. Est ex duis proident nostrud elit qui laborum anim minim eu
eiusmod. Veniam in nostrud sunt tempor velit incididunt sint ex dolor
qui velit id eu. Deserunt magna sunt velit in. Est exercitation id
cillum qui do. Minim adipisicing nostrud commodo proident occaecat
aliquip nulla anim proident reprehenderit. Magna ipsum officia veniam
cupidatat duis veniam dolore reprehenderit mollit velit.Ut consequat
commodo minim occaecat id pariatur. Nisi enim tempor laborum commodo.
Tempor sit quis nostrud eu cupidatat sunt commodo reprehenderit irure
deserunt eiusmod ipsum. Exercitation quis commodo cillum eiusmod
eiusmod. Do laborum qui proident commodo adipisicing eiusmod id.
</ReadMore>
<h2>Default open</h2>
<ReadMore header="ReadMore header text" defaultOpen>
Body
</ReadMore>
</>
);
};
@@ -0,0 +1,70 @@
import React, { forwardRef, useContext } from "react";
import { Down, Up } from "@navikt/ds-icons";
import { TableContext } from "..";
import HeaderCell, { HeaderCellProps } from "./HeaderCell";
interface ColumnHeaderProps extends HeaderCellProps {
/**
* Key to sort by
*/
sortKey?: string;
/**
* Column is sortable
* @default false
*/
sortable?: boolean;
}
export interface ColumnHeaderType
extends React.ForwardRefExoticComponent<
ColumnHeaderProps & React.RefAttributes<HTMLTableCellElement>
> {}
const ColumnHeader: ColumnHeaderType = forwardRef(
({ className, children, sortable = false, sortKey, ...rest }, ref) => {
const context = useContext(TableContext);
if (sortable && !sortKey) {
console.warn("ColumnHeader with `sortable=true` must have a sortKey.");
}
return (
<HeaderCell
scope="col"
ref={ref}
className={className}
aria-sort={
sortable
? context?.sort?.orderBy === sortKey
? context?.sort?.direction
: "none"
: undefined
}
{...rest}
>
{sortable ? (
<button
className="navds-table__sort-button"
onClick={
sortable && sortKey
? () => context?.onSortChange?.(sortKey)
: undefined
}
>
{children}
{context?.sort?.orderBy === sortKey &&
context?.sort?.direction === "descending" ? (
<Down aria-label="sorter synkende" />
) : (
<Up aria-label="sorter stigende" />
)}
</button>
) : (
children
)}
</HeaderCell>
);
}
);
export default ColumnHeader;
+11 -3
View File
@@ -3,7 +3,13 @@ import cl from "classnames";
import { BodyShort } from "..";
import { TableContext } from ".";
interface DataCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {}
interface DataCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
/**
* Content alignment
* @default "left"
*/
align?: "left" | "center" | "right";
}
export interface DataCellType
extends React.ForwardRefExoticComponent<
@@ -11,14 +17,16 @@ export interface DataCellType
> {}
const DataCell: DataCellType = forwardRef(
({ className, children = "", ...rest }, ref) => {
({ className, children = "", align, ...rest }, ref) => {
const context = useContext(TableContext);
return (
<BodyShort
as="td"
ref={ref}
className={cl("navds-table__data-cell", className)}
className={cl("navds-table__data-cell", className, {
[`navds-table__data-cell--align-${align}`]: align,
})}
size={context?.size}
{...rest}
>
+11 -3
View File
@@ -2,8 +2,14 @@ import React, { forwardRef, useContext } from "react";
import cl from "classnames";
import { Label, TableContext } from "..";
interface HeaderCellProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
export interface HeaderCellProps
extends React.ThHTMLAttributes<HTMLTableCellElement> {
scope?: string;
/**
* Content alignment
* @default "left"
*/
align?: "left" | "center" | "right";
}
export interface HeaderCellType
@@ -12,14 +18,16 @@ export interface HeaderCellType
> {}
const HeaderCell: HeaderCellType = forwardRef(
({ className, children, ...rest }, ref) => {
({ className, children, align, ...rest }, ref) => {
const context = useContext(TableContext);
return (
<Label
as="th"
ref={ref}
className={cl("navds-table__header-cell", className)}
className={cl("navds-table__header-cell", className, {
[`navds-table__header-cell--align-${align}`]: align,
})}
size={context?.size}
{...rest}
>
+30 -2
View File
@@ -3,9 +3,15 @@ import cl from "classnames";
import Header, { HeaderType } from "./Header";
import Body, { BodyType } from "./Body";
import Row, { RowType } from "./Row";
import ColumnHeader, { ColumnHeaderType } from "./ColumnHeader";
import HeaderCell, { HeaderCellType } from "./HeaderCell";
import DataCell, { DataCellType } from "./DataCell";
export interface SortState {
orderBy: string;
direction: "ascending" | "descending";
}
export interface TableProps
extends React.TableHTMLAttributes<HTMLTableElement> {
/**
@@ -18,6 +24,14 @@ export interface TableProps
* @default false
*/
zebraStripes?: boolean;
/**
* Sort state
*/
sort?: SortState;
/**
* Callback whens sort state changes
*/
onSortChange?: (sortKey?: string) => void;
}
export interface TableType
@@ -29,17 +43,30 @@ export interface TableType
Row: RowType;
DataCell: DataCellType;
HeaderCell: HeaderCellType;
ColumnHeader: ColumnHeaderType;
}
export interface TableContextProps {
size: "medium" | "small";
onSortChange?: (sortKey: string) => void;
sort?: SortState;
}
export const TableContext = createContext<TableContextProps | null>(null);
const Table = forwardRef(
({ className, zebraStripes = false, size = "medium", ...rest }, ref) => (
<TableContext.Provider value={{ size }}>
(
{
className,
zebraStripes = false,
size = "medium",
onSortChange,
sort,
...rest
},
ref
) => (
<TableContext.Provider value={{ size, onSortChange, sort }}>
<table
{...rest}
ref={ref}
@@ -54,6 +81,7 @@ const Table = forwardRef(
Table.Header = Header;
Table.Body = Body;
Table.Row = Row;
Table.ColumnHeader = ColumnHeader;
Table.HeaderCell = HeaderCell;
Table.DataCell = DataCell;
@@ -0,0 +1,822 @@
[
{
"name": "Luke Skywalker",
"height": 172,
"mass": 77,
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": 19,
"gender": "male"
},
{
"name": "C-3PO",
"height": 167,
"mass": 75,
"hair_color": "n/a",
"skin_color": "gold",
"eye_color": "yellow",
"birth_year": 112,
"gender": "n/a"
},
{
"name": "R2-D2",
"height": 96,
"mass": 32,
"hair_color": "n/a",
"skin_color": "white, blue",
"eye_color": "red",
"birth_year": 33,
"gender": "n/a"
},
{
"name": "Darth Vader",
"height": 202,
"mass": 136,
"hair_color": "none",
"skin_color": "white",
"eye_color": "yellow",
"birth_year": 41.9,
"gender": "male"
},
{
"name": "Leia Organa",
"height": 150,
"mass": 49,
"hair_color": "brown",
"skin_color": "light",
"eye_color": "brown",
"birth_year": 19,
"gender": "female"
},
{
"name": "Owen Lars",
"height": 178,
"mass": 120,
"hair_color": "brown, grey",
"skin_color": "light",
"eye_color": "blue",
"birth_year": 52,
"gender": "male"
},
{
"name": "Beru Whitesun lars",
"height": 165,
"mass": 75,
"hair_color": "brown",
"skin_color": "light",
"eye_color": "blue",
"birth_year": 47,
"gender": "female"
},
{
"name": "R5-D4",
"height": 97,
"mass": 32,
"hair_color": "n/a",
"skin_color": "white, red",
"eye_color": "red",
"birth_year": null,
"gender": "n/a"
},
{
"name": "Biggs Darklighter",
"height": 183,
"mass": 84,
"hair_color": "black",
"skin_color": "light",
"eye_color": "brown",
"birth_year": 24,
"gender": "male"
},
{
"name": "Obi-Wan Kenobi",
"height": 182,
"mass": 77,
"hair_color": "auburn, white",
"skin_color": "fair",
"eye_color": "blue-gray",
"birth_year": 57,
"gender": "male"
},
{
"name": "Anakin Skywalker",
"height": 188,
"mass": 84,
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": 41.9,
"gender": "male"
},
{
"name": "Wilhuff Tarkin",
"height": 180,
"mass": null,
"hair_color": "auburn, grey",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": 64,
"gender": "male"
},
{
"name": "Chewbacca",
"height": 228,
"mass": 112,
"hair_color": "brown",
"skin_color": null,
"eye_color": "blue",
"birth_year": 200,
"gender": "male"
},
{
"name": "Han Solo",
"height": 180,
"mass": 80,
"hair_color": "brown",
"skin_color": "fair",
"eye_color": "brown",
"birth_year": 29,
"gender": "male"
},
{
"name": "Greedo",
"height": 173,
"mass": 74,
"hair_color": "n/a",
"skin_color": "green",
"eye_color": "black",
"birth_year": 44,
"gender": "male"
},
{
"name": "Jabba Desilijic Tiure",
"height": 175,
"mass": null,
"hair_color": "n/a",
"skin_color": "green-tan, brown",
"eye_color": "orange",
"birth_year": 600,
"gender": "hermaphrodite"
},
{
"name": "Wedge Antilles",
"height": 170,
"mass": 77,
"hair_color": "brown",
"skin_color": "fair",
"eye_color": "hazel",
"birth_year": 21,
"gender": "male"
},
{
"name": "Jek Tono Porkins",
"height": 180,
"mass": 110,
"hair_color": "brown",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": null,
"gender": "male"
},
{
"name": "Yoda",
"height": 66,
"mass": 17,
"hair_color": "white",
"skin_color": "green",
"eye_color": "brown",
"birth_year": 896,
"gender": "male"
},
{
"name": "Palpatine",
"height": 170,
"mass": 75,
"hair_color": "grey",
"skin_color": "pale",
"eye_color": "yellow",
"birth_year": 82,
"gender": "male"
},
{
"name": "Boba Fett",
"height": 183,
"mass": 78.2,
"hair_color": "black",
"skin_color": "fair",
"eye_color": "brown",
"birth_year": 31.5,
"gender": "male"
},
{
"name": "IG-88",
"height": 200,
"mass": 140,
"hair_color": "none",
"skin_color": "metal",
"eye_color": "red",
"birth_year": 15,
"gender": "none"
},
{
"name": "Bossk",
"height": 190,
"mass": 113,
"hair_color": "none",
"skin_color": "green",
"eye_color": "red",
"birth_year": 53,
"gender": "male"
},
{
"name": "Lando Calrissian",
"height": 177,
"mass": 79,
"hair_color": "black",
"skin_color": "dark",
"eye_color": "brown",
"birth_year": 31,
"gender": "male"
},
{
"name": "Lobot",
"height": 175,
"mass": 79,
"hair_color": "none",
"skin_color": "light",
"eye_color": "blue",
"birth_year": 37,
"gender": "male"
},
{
"name": "Ackbar",
"height": 180,
"mass": 83,
"hair_color": "none",
"skin_color": "brown mottle",
"eye_color": "orange",
"birth_year": 41,
"gender": "male"
},
{
"name": "Mon Mothma",
"height": 150,
"mass": null,
"hair_color": "auburn",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": 48,
"gender": "female"
},
{
"name": "Arvel Crynyd",
"height": null,
"mass": null,
"hair_color": "brown",
"skin_color": "fair",
"eye_color": "brown",
"birth_year": null,
"gender": "male"
},
{
"name": "Wicket Systri Warrick",
"height": 88,
"mass": 20,
"hair_color": "brown",
"skin_color": "brown",
"eye_color": "brown",
"birth_year": 8,
"gender": "male"
},
{
"name": "Nien Nunb",
"height": 160,
"mass": 68,
"hair_color": "none",
"skin_color": "grey",
"eye_color": "black",
"birth_year": null,
"gender": "male"
},
{
"name": "Qui-Gon Jinn",
"height": 193,
"mass": 89,
"hair_color": "brown",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": 92,
"gender": "male"
},
{
"name": "Nute Gunray",
"height": 191,
"mass": 90,
"hair_color": "none",
"skin_color": "mottled green",
"eye_color": "red",
"birth_year": null,
"gender": "male"
},
{
"name": "Finis Valorum",
"height": 170,
"mass": null,
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": 91,
"gender": "male"
},
{
"name": "Padmé Amidala",
"height": 185,
"mass": 45,
"hair_color": "brown",
"skin_color": "light",
"eye_color": "brown",
"birth_year": 46,
"gender": "female"
},
{
"name": "Jar Jar Binks",
"height": 196,
"mass": 66,
"hair_color": "none",
"skin_color": "orange",
"eye_color": "orange",
"birth_year": 52,
"gender": "male"
},
{
"name": "Roos Tarpals",
"height": 224,
"mass": 82,
"hair_color": "none",
"skin_color": "grey",
"eye_color": "orange",
"birth_year": null,
"gender": "male"
},
{
"name": "Rugor Nass",
"height": 206,
"mass": null,
"hair_color": "none",
"skin_color": "green",
"eye_color": "orange",
"birth_year": null,
"gender": "male"
},
{
"name": "Ric Olié",
"height": 183,
"mass": null,
"hair_color": "brown",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": null,
"gender": "male"
},
{
"name": "Watto",
"height": 137,
"mass": null,
"hair_color": "black",
"skin_color": "blue, grey",
"eye_color": "yellow",
"birth_year": null,
"gender": "male"
},
{
"name": "Sebulba",
"height": 112,
"mass": 40,
"hair_color": "none",
"skin_color": "grey, red",
"eye_color": "orange",
"birth_year": null,
"gender": "male"
},
{
"name": "Quarsh Panaka",
"height": 183,
"mass": null,
"hair_color": "black",
"skin_color": "dark",
"eye_color": "brown",
"birth_year": 62,
"gender": "male"
},
{
"name": "Shmi Skywalker",
"height": 163,
"mass": null,
"hair_color": "black",
"skin_color": "fair",
"eye_color": "brown",
"birth_year": 72,
"gender": "female"
},
{
"name": "Darth Maul",
"height": 175,
"mass": 80,
"hair_color": "none",
"skin_color": "red",
"eye_color": "yellow",
"birth_year": 54,
"gender": "male"
},
{
"name": "Bib Fortuna",
"height": 180,
"mass": null,
"hair_color": "none",
"skin_color": "pale",
"eye_color": "pink",
"birth_year": null,
"gender": "male"
},
{
"name": "Ayla Secura",
"height": 178,
"mass": 55,
"hair_color": "none",
"skin_color": "blue",
"eye_color": "hazel",
"birth_year": 48,
"gender": "female"
},
{
"name": "Ratts Tyerel",
"height": 79,
"mass": 15,
"hair_color": "none",
"skin_color": "grey, blue",
"eye_color": null,
"birth_year": null,
"gender": "male"
},
{
"name": "Dud Bolt",
"height": 94,
"mass": 45,
"hair_color": "none",
"skin_color": "blue, grey",
"eye_color": "yellow",
"birth_year": null,
"gender": "male"
},
{
"name": "Gasgano",
"height": 122,
"mass": null,
"hair_color": "none",
"skin_color": "white, blue",
"eye_color": "black",
"birth_year": null,
"gender": "male"
},
{
"name": "Ben Quadinaros",
"height": 163,
"mass": 65,
"hair_color": "none",
"skin_color": "grey, green, yellow",
"eye_color": "orange",
"birth_year": null,
"gender": "male"
},
{
"name": "Mace Windu",
"height": 188,
"mass": 84,
"hair_color": "none",
"skin_color": "dark",
"eye_color": "brown",
"birth_year": 72,
"gender": "male"
},
{
"name": "Ki-Adi-Mundi",
"height": 198,
"mass": 82,
"hair_color": "white",
"skin_color": "pale",
"eye_color": "yellow",
"birth_year": 92,
"gender": "male"
},
{
"name": "Kit Fisto",
"height": 196,
"mass": 87,
"hair_color": "none",
"skin_color": "green",
"eye_color": "black",
"birth_year": null,
"gender": "male"
},
{
"name": "Eeth Koth",
"height": 171,
"mass": null,
"hair_color": "black",
"skin_color": "brown",
"eye_color": "brown",
"birth_year": null,
"gender": "male"
},
{
"name": "Adi Gallia",
"height": 184,
"mass": 50,
"hair_color": "none",
"skin_color": "dark",
"eye_color": "blue",
"birth_year": null,
"gender": "female"
},
{
"name": "Saesee Tiin",
"height": 188,
"mass": null,
"hair_color": "none",
"skin_color": "pale",
"eye_color": "orange",
"birth_year": null,
"gender": "male"
},
{
"name": "Yarael Poof",
"height": 264,
"mass": null,
"hair_color": "none",
"skin_color": "white",
"eye_color": "yellow",
"birth_year": null,
"gender": "male"
},
{
"name": "Plo Koon",
"height": 188,
"mass": 80,
"hair_color": "none",
"skin_color": "orange",
"eye_color": "black",
"birth_year": 22,
"gender": "male"
},
{
"name": "Mas Amedda",
"height": 196,
"mass": null,
"hair_color": "none",
"skin_color": "blue",
"eye_color": "blue",
"birth_year": null,
"gender": "male"
},
{
"name": "Gregar Typho",
"height": 185,
"mass": 85,
"hair_color": "black",
"skin_color": "dark",
"eye_color": "brown",
"birth_year": null,
"gender": "male"
},
{
"name": "Cordé",
"height": 157,
"mass": null,
"hair_color": "brown",
"skin_color": "light",
"eye_color": "brown",
"birth_year": null,
"gender": "female"
},
{
"name": "Cliegg Lars",
"height": 183,
"mass": null,
"hair_color": "brown",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": 82,
"gender": "male"
},
{
"name": "Poggle the Lesser",
"height": 183,
"mass": 80,
"hair_color": "none",
"skin_color": "green",
"eye_color": "yellow",
"birth_year": null,
"gender": "male"
},
{
"name": "Luminara Unduli",
"height": 170,
"mass": 56.2,
"hair_color": "black",
"skin_color": "yellow",
"eye_color": "blue",
"birth_year": 58,
"gender": "female"
},
{
"name": "Barriss Offee",
"height": 166,
"mass": 50,
"hair_color": "black",
"skin_color": "yellow",
"eye_color": "blue",
"birth_year": 40,
"gender": "female"
},
{
"name": "Dormé",
"height": 165,
"mass": null,
"hair_color": "brown",
"skin_color": "light",
"eye_color": "brown",
"birth_year": null,
"gender": "female"
},
{
"name": "Dooku",
"height": 193,
"mass": 80,
"hair_color": "white",
"skin_color": "fair",
"eye_color": "brown",
"birth_year": 102,
"gender": "male"
},
{
"name": "Bail Prestor Organa",
"height": 191,
"mass": null,
"hair_color": "black",
"skin_color": "tan",
"eye_color": "brown",
"birth_year": 67,
"gender": "male"
},
{
"name": "Jango Fett",
"height": 183,
"mass": 79,
"hair_color": "black",
"skin_color": "tan",
"eye_color": "brown",
"birth_year": 66,
"gender": "male"
},
{
"name": "Zam Wesell",
"height": 168,
"mass": 55,
"hair_color": "blonde",
"skin_color": "fair, green, yellow",
"eye_color": "yellow",
"birth_year": null,
"gender": "female"
},
{
"name": "Dexter Jettster",
"height": 198,
"mass": 102,
"hair_color": "none",
"skin_color": "brown",
"eye_color": "yellow",
"birth_year": null,
"gender": "male"
},
{
"name": "Lama Su",
"height": 229,
"mass": 88,
"hair_color": "none",
"skin_color": "grey",
"eye_color": "black",
"birth_year": null,
"gender": "male"
},
{
"name": "Taun We",
"height": 213,
"mass": null,
"hair_color": "none",
"skin_color": "grey",
"eye_color": "black",
"birth_year": null,
"gender": "female"
},
{
"name": "Jocasta Nu",
"height": 167,
"mass": null,
"hair_color": "white",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": null,
"gender": "female"
},
{
"name": "R4-P17",
"height": 96,
"mass": null,
"hair_color": "none",
"skin_color": "silver, red",
"eye_color": "red, blue",
"birth_year": null,
"gender": "female"
},
{
"name": "Wat Tambor",
"height": 193,
"mass": 48,
"hair_color": "none",
"skin_color": "green, grey",
"eye_color": null,
"birth_year": null,
"gender": "male"
},
{
"name": "San Hill",
"height": 191,
"mass": null,
"hair_color": "none",
"skin_color": "grey",
"eye_color": "gold",
"birth_year": null,
"gender": "male"
},
{
"name": "Shaak Ti",
"height": 178,
"mass": 57,
"hair_color": "none",
"skin_color": "red, blue, white",
"eye_color": "black",
"birth_year": null,
"gender": "female"
},
{
"name": "Grievous",
"height": 216,
"mass": 159,
"hair_color": "none",
"skin_color": "brown, white",
"eye_color": "green, yellow",
"birth_year": null,
"gender": "male"
},
{
"name": "Tarfful",
"height": 234,
"mass": 136,
"hair_color": "brown",
"skin_color": "brown",
"eye_color": "blue",
"birth_year": null,
"gender": "male"
},
{
"name": "Raymus Antilles",
"height": 188,
"mass": 79,
"hair_color": "brown",
"skin_color": "light",
"eye_color": "brown",
"birth_year": null,
"gender": "male"
},
{
"name": "Sly Moore",
"height": 178,
"mass": 48,
"hair_color": "none",
"skin_color": "pale",
"eye_color": "white",
"birth_year": null,
"gender": "female"
},
{
"name": "Tion Medon",
"height": 206,
"mass": 80,
"hair_color": "none",
"skin_color": "grey",
"eye_color": "black",
"birth_year": null,
"gender": "male"
}
]
@@ -0,0 +1,169 @@
import React, { useState } from "react";
import { rest } from "msw";
import useSWR from "swr";
import people from "./people.json";
import { Table } from "../index";
import { Loader, Pagination, SortState } from "../..";
export default {
title: "ds-react/table",
component: Table,
parameters: {
chromatic: { delay: 500 },
},
};
const rowsPerPage = 10;
const queryString = (obj) =>
Object.keys(obj)
.filter((key) => obj[key] !== undefined)
.map((key) => key + "=" + obj[key])
.join("&");
export const Async = () => {
const [page, setPage] = useState(1);
const [sort, setSort] = useState<SortState>();
const { data } = useSWR(
`/people?${queryString({
page,
sort: sort ? `${sort.orderBy}:${sort.direction}` : undefined,
})}`,
(url) => fetch(url).then((res) => res.json())
);
if (!data) {
return <Loader size="2xlarge" />;
}
const { results: people, count } = data;
const columns = [
{ key: "name", name: "Name", width: 154 },
{ key: "height", name: "Height", width: 108 },
{ key: "mass", name: "Mass", width: 95 },
{
key: "birth_year",
name: "Birth year",
value: (person) =>
person.birth_year !== null ? `${person.birth_year}BBY` : undefined,
width: 133,
},
{ key: "eye_color", name: "Eye color", width: 127 },
{ key: "gender", name: "Gender", width: 113, sortable: false },
{ key: "hair_color", name: "Hair color", width: 132 },
{ key: "skin_color", name: "Skin color", width: 133 },
];
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 16,
}}
>
<div style={{ overflow: "auto" }}>
<Table
style={{ width: "initial" }}
sort={sort}
onSortChange={(sortKey) =>
setSort(
sort?.orderBy === sortKey && sort.direction === "descending"
? undefined
: {
orderBy: sortKey,
direction:
sort?.direction === "ascending"
? "descending"
: "ascending",
}
)
}
>
<Table.Header>
<Table.Row>
{columns.map(({ key, name, width, sortable = true }) => (
<Table.ColumnHeader
style={{ width, minWidth: width, maxWidth: width }}
key={key}
sortable={sortable}
sortKey={key}
>
{name}
</Table.ColumnHeader>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{people.map((person) => (
<Table.Row key={person.name}>
{columns.map(({ key, width, value }) => (
<Table.DataCell
style={{
width,
minWidth: width,
maxWidth: width,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
}}
title={person[key]}
key={key}
>
{value ? value(person) : person[key]}
</Table.DataCell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
</div>
<Pagination
page={page}
onPageChange={setPage}
count={Math.ceil(count / rowsPerPage)}
/>
</div>
);
};
Async.parameters = {
msw: {
handlers: [
rest.get("/people", (req, res, ctx) => {
const comparator = (a, b, orderBy) => {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
};
const page = Number(req.url.searchParams.get("page"));
const sort = req.url.searchParams.get("sort");
return res(
//ctx.delay(),
ctx.json({
count: people.length,
results: people
.slice()
.sort((a, b) => {
if (sort) {
const [orderBy, direction] = sort.split(":");
return direction === "ascending"
? comparator(b, a, orderBy)
: comparator(a, b, orderBy);
}
return 1;
})
.slice((page - 1) * rowsPerPage, page * rowsPerPage),
})
);
}),
],
},
};
@@ -0,0 +1,376 @@
import React, { useState } from "react";
import { capitalize } from "@material-ui/core";
import { Table } from "../";
import { Button } from "../..";
import { SortState } from "../..";
export default {
title: "ds-react/table",
component: Table,
};
interface Column {
key: string;
name?: string;
sortable?: boolean;
render?: (value: any) => React.ReactElement<any, any> | null;
value?: (oppgave: any) => any;
width?: number;
align?: "left" | "center" | "right";
}
export const Hot = () => {
const [sort, setSort] = useState<SortState>();
const columns: Column[] = [
{
key: "eier",
name: "Eier",
render: (value) =>
value ?? (
<Button
size="xsmall"
style={{ marginTop: -2, marginBottom: -2, marginLeft: -6 }}
variant="tertiary"
>
Start saken
</Button>
),
value: (oppgave) => oppgave.saksbehandler?.navn,
width: 152,
},
{
key: "status",
name: "Status",
value: (oppgave) =>
({
ALLE: "Alle",
VEDTAK_FATTET: "Innvilget",
AVVENTER_SAKSBEHANDLER: "Mottatt",
SENDT_GOSYS: "Sendt GOSYS",
TILDELT_SAKSBEHANDLER: "Under behandling",
}[oppgave.status]),
width: 154,
},
{
key: "område",
name: "Område",
value: (oppgave) =>
capitalize(oppgave.personinformasjon.funksjonsnedsettelse[0]),
width: 152,
},
{
key: "søknadOm",
name: "Søknad om",
width: 192,
},
{
key: "hjelpemiddelbruker",
name: "Hjelpemiddelbruker",
value: (oppgave) =>
`${oppgave.personinformasjon.etternavn}, ${oppgave.personinformasjon.fornavn}`,
width: 188,
},
{
key: "fødselsnr",
name: "Fødselsnr.",
value: (oppgave) =>
`${oppgave.personinformasjon.fnr.slice(
0,
6
)} ${oppgave.personinformasjon.fnr.slice(6)}`,
width: 124,
align: "right",
},
{
key: "bosted",
name: "Bosted",
value: (oppgave) => oppgave.personinformasjon.bosted,
width: 144,
},
{
key: "formidlerNavn",
name: "Formidler",
width: 164,
},
{
key: "mottattDato",
name: "Mottatt dato",
width: 124,
},
{
key: "menu",
sortable: false,
render: (value) => (
<span style={{ display: "flex", marginBlock: -2 }}></span>
),
value: (oppgave) => oppgave.saksbehandler,
},
];
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 16,
}}
>
<div style={{ overflow: "auto" }}>
<Table
size="small"
style={{ width: "initial" }}
sort={sort}
onSortChange={(sortKey) =>
setSort({
orderBy: sortKey,
direction:
sort?.direction === "ascending" ? "descending" : "ascending",
})
}
>
<Table.Header>
<Table.Row>
{columns.map(({ key, name, width, sortable = true, align }) => (
<Table.ColumnHeader
style={{
width,
minWidth: width,
maxWidth: width,
}}
key={key}
sortable={sortable}
sortKey={key}
align={align}
>
<div
style={{
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
}}
>
{name}
</div>
</Table.ColumnHeader>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{data
.slice()
.sort((a, b) => {
if (sort) {
const comparator = (a, b, orderBy) => {
const lookup = (oppgave) => {
const column = columns.find(({ key }) => key === orderBy);
return column.value
? column.value(oppgave)
: oppgave[orderBy];
};
if (lookup(b) < lookup(a) || lookup(b) === undefined) {
return -1;
}
if (lookup(b) > lookup(a)) {
return 1;
}
return 0;
};
return sort.direction === "ascending"
? comparator(b, a, sort.orderBy)
: comparator(a, b, sort.orderBy);
}
return 1;
})
.map((oppgave) => (
<Table.Row key={oppgave.saksid}>
{columns.map(
({
key,
value,
width,
render = (value) => value,
align,
}) => (
<Table.DataCell
style={{
width,
minWidth: width,
maxWidth: width,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
}}
title={oppgave[key]}
key={key}
align={align}
>
{render(value ? value(oppgave) : oppgave[key])}
</Table.DataCell>
)
)}
</Table.Row>
))}
</Table.Body>
</Table>
</div>
</div>
);
};
const data = [
{
saksid: "111111",
søknadOm:
"Søknad om: Toalettforhøyer; kalender-/minnesystem; støttehåndtak; tidsur; komfyr-/elvakt",
mottattDato: "2021-09-19T13:55:45",
formidlerNavn: "Kari Johansen",
personinformasjon: {
fnr: "19044238651",
funksjonsnedsettelse: ["bevegelse"],
fornavn: "Petter Andreas",
etternavn: "Hansen",
poststed: "Trondheim",
bosted: "TRONDHEIM",
},
saksbehandler: null,
status: "AVVENTER_SAKSBEHANDLER",
enhet: [
{
enhetsnummer: "6969",
enhetsnavn: "Enhetsnavn",
},
],
},
{
saksid: "222222",
søknadOm: "Hjelpemidler",
mottattDato: "2021-09-24T13:55:45",
formidlerNavn: "Kari Johansen",
personinformasjon: {
fnr: "13044238651",
funksjonsnedsettelse: ["bevegelse"],
fornavn: "Mia Cathrine",
etternavn: "Svendsen",
poststed: "Trondheim",
bosted: "TRONDHEIM",
},
saksbehandler: null,
status: "AVVENTER_SAKSBEHANDLER",
enhet: [
{
enhetsnummer: "6969",
enhetsnavn: "Enhetsnavn",
},
],
},
{
saksid: "5878444",
søknadOm: "Hjelpemidler",
mottattDato: "2021-09-20T13:55:45",
formidlerNavn: "Kari Johansen",
personinformasjon: {
fnr: "130441386551",
funksjonsnedsettelse: ["bevegelse"],
fornavn: "Per Bjørkli",
etternavn: "Hansen",
poststed: "Trondheim",
bosted: "TRONDHEIM",
},
saksbehandler: {
epost: "a.b@nav.no",
objectId: "23ea7485-1324-4b25-a763-6969",
navn: "Geir Markusen",
},
status: "SENDT_GOSYS",
enhet: [
{
enhetsnummer: "6969",
enhetsnavn: "Enhetsnavn",
},
],
},
{
saksid: "1234567",
søknadOm: "Hjelpemidler",
mottattDato: "2021-06-25T13:55:45",
formidlerNavn: "Kari Johansen",
personinformasjon: {
fnr: "06115559891",
funksjonsnedsettelse: ["kognisjon"],
fornavn: "Iver",
etternavn: "Slettan",
poststed: "Lade",
bosted: "TRONDHEIM",
},
saksbehandler: {
epost: "a.b@nav.no",
objectId: "23ea7485-1324-4b25-a763-assdfdfa",
navn: "Silje Saksbehandler",
},
status: "TILDELT_SAKSBEHANDLER",
enhet: [
{
enhetsnummer: "6969",
enhetsnavn: "Enhetsnavn",
},
],
},
{
saksid: "999999",
søknadOm: "Hjelpemidler",
mottattDato: "2021-06-25T16:55:45",
formidlerNavn: "Kari Johansen",
personinformasjon: {
fnr: "16120101181",
funksjonsnedsettelse: ["bevegelse"],
fornavn: "Sondre",
etternavn: "Hansen",
poststed: "Lade",
bosted: "TRONDHEIM",
},
saksbehandler: {
epost: "a.b@nav.no",
objectId: "23ea7485-1324-4b25-a763-6969",
navn: "Geir Markusen",
},
status: "TILDELT_SAKSBEHANDLER",
enhet: [
{
enhetsnummer: "6969",
enhetsnavn: "Enhetsnavn",
},
],
},
{
saksid: "888888",
søknadOm: "Hjelpemidler",
mottattDato: "2021-09-19T13:55:45",
formidlerNavn: "Kari Johansen",
personinformasjon: {
fnr: "19044238651",
funksjonsnedsettelse: ["bevegelse"],
fornavn: "Geir",
etternavn: "Dalby",
poststed: "Trondheim",
bosted: "TRONDHEIM",
},
saksbehandler: {
epost: "a.b@nav.no",
objectId: "23ea7485-1324-4b25-a763-assdfdfa",
navn: "Silje Saksbehandler",
},
status: "VEDTAK_FATTET",
enhet: [
{
enhetsnummer: "6969",
enhetsnavn: "Enhetsnavn",
},
],
},
];
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Table } from "../index";
import { Alert, Checkbox, Link } from "@navikt/ds-react";
import { Table } from "../";
import { Alert, Button, Checkbox, Link } from "../..";
export default {
title: "ds-react/table",
@@ -12,6 +12,7 @@ export const All = () => {
<Table {...props}>
<Table.Header>
<Table.Row>
{props.button && <Table.HeaderCell>Action</Table.HeaderCell>}
<Table.HeaderCell>ID</Table.HeaderCell>
<Table.HeaderCell>Fornavn</Table.HeaderCell>
<Table.HeaderCell>Etternavn</Table.HeaderCell>
@@ -20,18 +21,48 @@ export const All = () => {
</Table.Header>
<Table.Body>
<Table.Row>
{props.button && (
<Table.DataCell
style={{
paddingTop: 6,
paddingBottom: 6,
}}
>
<Button size="xsmall">Click me!</Button>
</Table.DataCell>
)}
<Table.HeaderCell>1</Table.HeaderCell>
<Table.DataCell>Jean-Luc</Table.DataCell>
<Table.DataCell>Picard</Table.DataCell>
<Table.DataCell>Kaptein</Table.DataCell>
</Table.Row>
<Table.Row>
{props.button && (
<Table.DataCell
style={{
paddingTop: 6,
paddingBottom: 6,
}}
>
<Button size="xsmall">Click me!</Button>
</Table.DataCell>
)}
<Table.HeaderCell>2</Table.HeaderCell>
<Table.DataCell>William</Table.DataCell>
<Table.DataCell>Riker</Table.DataCell>
<Table.DataCell>Kommandør</Table.DataCell>
</Table.Row>
<Table.Row>
{props.button && (
<Table.DataCell
style={{
paddingTop: 6,
paddingBottom: 6,
}}
>
<Button size="xsmall">Click me!</Button>
</Table.DataCell>
)}
<Table.HeaderCell>3</Table.HeaderCell>
<Table.DataCell>Geordi</Table.DataCell>
<Table.DataCell>La Forge</Table.DataCell>
@@ -48,6 +79,8 @@ export const All = () => {
<TableComponent zebraStripes />
<h2>Small Table</h2>
<TableComponent size="small" />
<h2>Small Table with buttons</h2>
<TableComponent size="small" button />
<h2>Table with divs</h2>
<Alert variant="warning">
Obs! Hvis man skal bygge tabeller uten å bruke vanlig {"<tabell> "}
@@ -51,6 +51,17 @@ const Items = (icon?: boolean, both?: boolean) => (
</>
);
export const UUDemo = () => {
const [activeValue, setActiveValue] = useState("ulest");
return (
<ToggleGroup value={activeValue} onChange={setActiveValue}>
<ToggleGroup.Item value="ulest">Ulest</ToggleGroup.Item>
<ToggleGroup.Item value="lest">Leste</ToggleGroup.Item>
<ToggleGroup.Item value="sendt">Sendte</ToggleGroup.Item>
</ToggleGroup>
);
};
export const All = () => {
const [activeValue, setActiveValue] = useState("first");
@@ -120,7 +120,7 @@ const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
`navds-toggle-group--${size}`
)}
{...(describeBy && { "aria-describedby": describeBy })}
role="toolbar"
role="radiogroup"
type="single"
>
{children}
+33
View File
@@ -1,3 +1,5 @@
import { useEffect } from "react";
export * from "./OverridableComponent";
export * from "./useId";
@@ -11,3 +13,34 @@ export const omit = (obj: object, props: string[]) =>
}),
{}
);
export interface ListenerT {
addEventListener(
name: string,
handler: (event?: any) => void,
...args: any[]
);
removeEventListener(
name: string,
handler: (event?: any) => void,
...args: any[]
);
}
/* https://github.com/streamich/react-use/blob/master/src/useEvent.ts */
export const useEventListener = <T extends ListenerT>(
name: Parameters<ListenerT["addEventListener"]>[0],
handler: Parameters<ListenerT["addEventListener"]>[1],
target: null | T | Window = window
): void => {
useEffect(() => {
if (!target) {
return;
}
target?.addEventListener(name, handler);
return () => {
target?.addEventListener(name, handler);
};
}, [name, handler, target]);
};
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react-internal",
"version": "0.11.16",
"version": "0.12.6",
"private": false,
"description": "NAV internal designsystem react components",
"author": "NAV Designsystem team",
@@ -33,8 +33,8 @@
"watch": "tsc --watch -p tsconfig.esm.json"
},
"dependencies": {
"@navikt/ds-icons": "^0.8.4",
"@navikt/ds-react": "^0.16.16",
"@navikt/ds-icons": "^0.8.5",
"@navikt/ds-react": "^0.17.6",
"@popperjs/core": "^2.10.1",
"classnames": "^2.3.1",
"copy-to-clipboard": "^3.3.1",
@@ -83,7 +83,7 @@ const CopyToClipboard = forwardRef<HTMLButtonElement, CopyToClipboardProps>(
arrow={false}
className="navdsi-copy-to-clipboard__popover"
>
<BodyShort size={size} as="span">
<BodyShort size={size === "medium" ? size : "small"} as="span">
{popoverText}
</BodyShort>
</Popover>
+7 -7
View File
@@ -1,18 +1,18 @@
<h1 align="center">
<img src="https://avatars.githubusercontent.com/u/11848947?s=200&v=4" />
<br/>Designsystemet
<br/>Aksel
</h1>
<div align="center">
<p>
Monorepo for alle felleskomponenter i NAVs frontend-rammeverk.
Monorepo for Frontend-komponenter til Aksel.
</p>
<p>
<a href="https://github.com/navikt/nav-frontend-moduler/pulls">
<img src="https://img.shields.io/badge/PRs-welcome-green.svg" />
</a>
<img src="https://github.com/navikt/nav-frontend-moduler/actions/workflows/build-publish.yml/badge.svg" />
<img src="https://github.com/navikt/nav-frontend-moduler/actions/workflows/chromatic.yml/badge.svg" />
<img src="https://github.com/navikt/nav-frontend-moduler/actions/workflows/chromatic.yml/badge.svg" />
</p>
<img src="https://img.shields.io/endpoint?style=for-the-badge&url=https://runkit.io/fezvrasta/combined-npm-downloads/1.0.0?packages=@navikt/ds-react,@navikt/ds-css,@navikt/ds-icons,@navikt/ds-tokens,@navikt/ds-react-internal,@navikt/ds-css-internal,nav-frontend-alertstriper-style,nav-frontend-alertstriper,nav-frontend-chevron-style,nav-frontend-chevron,nav-frontend-core,nav-frontend-ekspanderbartpanel-style,nav-frontend-ekspanderbartpanel,nav-frontend-etiketter-style,nav-frontend-etiketter,nav-frontend-fullbreddeknapp-style,nav-frontend-grid-style,nav-frontend-grid,nav-frontend-hjelpetekst-style,nav-frontend-hjelpetekst,nav-frontend-ikoner-assets,nav-frontend-ikonknapper,nav-frontend-js-utils,nav-frontend-knapper-style,nav-frontend-knapper,nav-frontend-lenkepanel-style,nav-frontend-lenkepanel,nav-frontend-lenker-style,nav-frontend-lenker,nav-frontend-lesmerpanel-style,nav-frontend-lesmerpanel,nav-frontend-lukknapp-style,nav-frontend-lukknapp,nav-frontend-modal-style,nav-frontend-modal,nav-frontend-paneler-style,nav-frontend-paneler,nav-frontend-popover-style,nav-frontend-popover,nav-frontend-skjema-style,nav-frontend-skjema,nav-frontend-snakkeboble-style,nav-frontend-snakkeboble,nav-frontend-spinner-style,nav-frontend-spinner,nav-frontend-stegindikator-style,nav-frontend-stegindikator,nav-frontend-tabell-style,nav-frontend-tabs-style,nav-frontend-tabs,nav-frontend-tekstomrade,nav-frontend-toggle-style,nav-frontend-toggle,nav-frontend-typografi-style,nav-frontend-typografi,nav-frontend-veileder-style,nav-frontend-veileder,nav-frontend-veilederpanel-style,nav-frontend-veilederpanel" />
<h4><a href="https://design.nav.no">Nettside</a> | <a href="https://github.com/navikt/nav-frontend-moduler/blob/master/CONTRIBUTING.md">Bidra</a> | <a href="https://github.com/navikt/nav-frontend-moduler/issues">Bug-rapport</a>
@@ -26,8 +26,8 @@ Repoet inneholder følgende:
- [Storybook](https://5f801fb2aea7820022de2936-rognyzqcpw.chromatic.com/) for komponenter.
- [Gammel dokumentasjon](https://old-design-nav.vercel.app) for komponenter.
## Dokumentasjon
## Dokumentasjon
[Dokumentasjon side forvaltes i dette repoet](https://github.com/navikt/verktoykassen)
## Endringslogg
@@ -41,9 +41,9 @@ Gå til [CONTRIBUTING.md](https://github.com/navikt/nav-frontend-moduler/blob/ma
## Lisenser
Gå til [LICENSE](https://github.com/navikt/nav-frontend-moduler/blob/master/LICENCE)
## Contributors
<a href="https://github.com/navikt/nav-frontend-moduler/graphs/contributors">
<img src="https://contrib.rocks/image?repo=navikt/nav-frontend-moduler" />
</a>
+4 -2
View File
@@ -10,8 +10,8 @@
"storybook": "start-storybook -p 6006",
"build:packages": "yarn build:tokens && yarn run --silent lerna run build",
"build:storybook": "build-storybook",
"chromatic": "npx chromatic --project-token x3xqdfgkujg --build-script-name build:storybook",
"build:tokens": "yarn workspace @navikt/ds-tokens build",
"chromatic": "npx chromatic --project-token x3xqdfgkujg",
"lerna:watch": "yarn run lerna run watch --stream",
"update:icons": "yarn workspace @navikt/ds-icons update",
"update:tokens": "yarn workspace @navikt/ds-tokens update",
@@ -103,6 +103,8 @@
"typescript": "^4.3.5",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"msw": "^0.36.5",
"msw-storybook-addon": "^1.6.0"
}
}
+2278 -2135
View File
File diff suppressed because it is too large. Load diff