Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd1c128aac | ||
|
|
f1a6d768fc | ||
|
|
c47967589a | ||
|
|
51654edbfb | ||
|
|
5c3683a89e | ||
|
|
9d9bf898e9 | ||
|
|
e8e530b764 | ||
|
|
d2d08f58ce | ||
|
|
f75abaff4b | ||
|
|
7e8899df34 | ||
|
|
3926b24350 | ||
|
|
f1c2c718e5 | ||
|
|
ddcc6b8bd7 | ||
|
|
6f5fee9628 | ||
|
|
ad191179ab | ||
|
|
9c86becab0 | ||
|
|
200d420723 | ||
|
|
44ab348ecf | ||
|
|
e631b894bf | ||
|
|
fda0f5b3c5 | ||
|
|
8e4e08d51c | ||
|
|
5095184abf | ||
|
|
8ef07f0807 | ||
|
|
fb670a9809 | ||
|
|
b4377a228a | ||
|
|
58273eccb4 | ||
|
|
7c9b21495f | ||
|
|
b53842633e | ||
|
|
8d07378f1d | ||
|
|
f32c50fa88 | ||
|
|
5aa54c7aaf | ||
|
|
a771948d99 | ||
|
|
537cdd3def | ||
|
|
e9cd4f2e2b | ||
|
|
064bf66c0f | ||
|
|
a8b66dfec9 | ||
|
|
8f99813590 | ||
|
|
21b078a9a4 | ||
|
|
e89d3b27b2 | ||
|
|
ddd2fd2e08 | ||
|
|
a7c326aa43 | ||
|
|
0b89387bae | ||
|
|
8a58d75efb | ||
|
|
bf18ffcb0c | ||
|
|
ad3c88d542 | ||
|
|
eb90447bb7 | ||
|
|
cb429868c1 | ||
|
|
e09f478a09 | ||
|
|
00259c629b | ||
|
|
6124812ba8 | ||
|
|
5f6bd160d6 | ||
|
|
b876ffb607 | ||
|
|
6886cf7a67 | ||
|
|
a9e58e7bb2 | ||
|
|
861803f5ae | ||
|
|
5d383b754f | ||
|
|
a8dc377870 | ||
|
|
ead0378644 | ||
|
|
10a6bc18c4 | ||
|
|
68d4b60f0e | ||
|
|
106e37bc54 |
No files matched your search
@@ -33,4 +33,5 @@ module.exports = {
|
||||
core: {
|
||||
builder: "webpack5",
|
||||
},
|
||||
staticDirs: ["./public"],
|
||||
};
|
||||
@@ -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].*" },
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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 +11,4 @@
|
||||
@import "switch.css";
|
||||
@import "text-field.css";
|
||||
@import "textarea.css";
|
||||
@import "search-field.css";
|
||||
@import "search.css";
|
||||
@@ -168,9 +168,15 @@
|
||||
|
||||
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus)
|
||||
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus)
|
||||
+ .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--navds-radio-checkbox-color-shadow-hover);
|
||||
}
|
||||
|
||||
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:not(:disabled):not(:checked)
|
||||
+ .navds-radio__label::before {
|
||||
background-color: var(--navds-radio-checkbox-color-background-hover);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
: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;
|
||||
}
|
||||
|
||||
.navds-search__wrapper-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@charset "UTF-8";
|
||||
@import "baseline/baseline.css";
|
||||
@import "typography.css";
|
||||
@import "grid.css";
|
||||
@@ -13,8 +14,10 @@
|
||||
@import "loader.css";
|
||||
@import "menu.css";
|
||||
@import "modal.css";
|
||||
@import "pagination.css";
|
||||
@import "popover.css";
|
||||
@import "tag.css";
|
||||
@import "toggle-group.css";
|
||||
@import "panel.css";
|
||||
@import "link-panel.css";
|
||||
@import "speech-bubble.css";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "0.15.2",
|
||||
"version": "0.16.1",
|
||||
"private": false,
|
||||
"description": "Css for NAV Designsystem components",
|
||||
"author": "NAV Designsystem team",
|
||||
@@ -21,7 +21,7 @@
|
||||
"watch": "postcss --watch --use postcss-import -o dist/index.css index.css"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^0.8.1",
|
||||
"@navikt/ds-tokens": "^0.8.4",
|
||||
"normalize.css": "^8.0.1",
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-cli": "^8.3.1",
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
:root {
|
||||
--navds-pagination-color-background-hover: var(
|
||||
--navds-semantic-color-canvas-background
|
||||
);
|
||||
--navds-pagination-color-background-active: var(
|
||||
--navds-semantic-color-interaction-primary-selected
|
||||
);
|
||||
--navds-pagination-color-text-active: var(
|
||||
--navds-semantic-color-text-inverted
|
||||
);
|
||||
--navds-pagination-color-background-selected: var(
|
||||
--navds-semantic-color-interaction-primary
|
||||
);
|
||||
--navds-pagination-color-text-selected: var(
|
||||
--navds-semantic-color-text-inverted
|
||||
);
|
||||
}
|
||||
|
||||
.navds-pagination {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navds-pagination__list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: var(--navds-spacing-3);
|
||||
}
|
||||
|
||||
.navds-pagination__ellipsis {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--navds-spacing-8);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.navds-pagination--small .navds-pagination__ellipsis {
|
||||
width: var(--navds-spacing-6);
|
||||
}
|
||||
|
||||
.navds-pagination__item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 2px;
|
||||
padding: var(--navds-spacing-1) var(--navds-spacing-3);
|
||||
}
|
||||
|
||||
.navds-pagination--small .navds-pagination__item {
|
||||
padding: 2px var(--navds-spacing-2);
|
||||
}
|
||||
|
||||
.navds-pagination__item:hover {
|
||||
background-color: var(--navds-pagination-color-background-hover);
|
||||
}
|
||||
|
||||
.navds-pagination__item:active {
|
||||
background-color: var(--navds-pagination-color-background-active);
|
||||
color: var(--navds-pagination-color-text-active);
|
||||
}
|
||||
|
||||
.navds-pagination__item:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--navds-semantic-color-focus),
|
||||
inset 0 0 0 3px white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.navds-pagination__item[aria-current="true"] {
|
||||
background-color: var(--navds-pagination-color-background-selected);
|
||||
color: var(--navds-pagination-color-text-selected);
|
||||
font-weight: var(--navds-font-weight-bold);
|
||||
}
|
||||
|
||||
.navds-pagination__prev-next {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
text-decoration: underline;
|
||||
color: var(--navds-semantic-color-link);
|
||||
align-items: center;
|
||||
gap: var(--navds-spacing-1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.navds-pagination__prev-next:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 2px var(--navds-semantic-color-focus);
|
||||
}
|
||||
|
||||
.navds-pagination__prev-next:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.navds-pagination__prev-next-icon {
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
.navds-pagination--small .navds-pagination__prev-next-icon {
|
||||
margin: var(--navds-spacing-1);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.navds-pagination__next-text {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.navds-pagination__prev-text {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
:root {
|
||||
--navds-toggle-group-color-background: var(
|
||||
--navds-semantic-color-component-background-light
|
||||
);
|
||||
--navds-toggle-group-color-text: var(--navds-semantic-color-text);
|
||||
--navds-toggle-group-color-border: var(--navds-semantic-color-divider);
|
||||
--navds-toggle-group-color-background-hover: var(
|
||||
--navds-semantic-color-interaction-primary-hover-subtle
|
||||
);
|
||||
--navds-toggle-group-color-text-hover: var(
|
||||
--navds-semantic-color-interaction-primary
|
||||
);
|
||||
--navds-toggle-group-color-background-pressed: var(
|
||||
--navds-semantic-color-interaction-primary-selected
|
||||
);
|
||||
--navds-toggle-group-color-text-pressed: var(
|
||||
--navds-semantic-color-text-inverted
|
||||
);
|
||||
}
|
||||
|
||||
.navds-toggle-group {
|
||||
border-radius: 7px;
|
||||
background-color: var(--navds-toggle-group-color-background);
|
||||
box-shadow: inset 0 0 0 1px var(--navds-toggle-group-color-border);
|
||||
padding: calc(var(--navds-spacing-1) + 1px);
|
||||
gap: var(--navds-spacing-1);
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 1fr;
|
||||
}
|
||||
|
||||
.navds-toggle-group__button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--navds-spacing-3);
|
||||
min-height: 48px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: var(--navds-toggle-group-color-background);
|
||||
color: var(--navds-toggle-group-color-text);
|
||||
border-radius: 2px;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.navds-toggle-group__button:hover {
|
||||
background-color: var(--navds-toggle-group-color-background-hover);
|
||||
color: var(--navds-toggle-group-color-text-hover);
|
||||
}
|
||||
|
||||
.navds-toggle-group__button:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px var(--navds-toggle-group-color-background),
|
||||
0 0 0 4px var(--navds-semantic-color-focus);
|
||||
}
|
||||
|
||||
.navds-toggle-group__button:focus:hover[aria-pressed="false"] {
|
||||
box-shadow: 0 0 0 1px var(--navds-toggle-group-color-background-hover),
|
||||
0 0 0 4px var(--navds-semantic-color-focus);
|
||||
}
|
||||
|
||||
.navds-toggle-group__button-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--navds-spacing-2);
|
||||
}
|
||||
|
||||
.navds-toggle-group__button-inner > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.navds-toggle-group--small > .navds-toggle-group__button {
|
||||
padding: 0.375rem;
|
||||
min-height: 32px;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.navds-toggle-group__button > .navds-toggle-group__button-inner > svg {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navds-toggle-group--small
|
||||
> .navds-toggle-group__button
|
||||
> .navds-toggle-group__button-inner
|
||||
> svg {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.navds-toggle-group__label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@@ -744,13 +744,13 @@
|
||||
"pageName": "Status"
|
||||
},
|
||||
{
|
||||
"name": "EU",
|
||||
"name": "Eu",
|
||||
"description": "",
|
||||
"created_at": "2022-02-08T11:45:47.104Z",
|
||||
"pageName": "International"
|
||||
},
|
||||
{
|
||||
"name": "EUFilled",
|
||||
"name": "EuFilled",
|
||||
"description": "",
|
||||
"created_at": "2022-02-08T11:45:47.109Z",
|
||||
"pageName": "International"
|
||||
@@ -1019,6 +1019,30 @@
|
||||
"created_at": "2021-01-28T14:00:38.520Z",
|
||||
"pageName": "Fyrstikkalleen"
|
||||
},
|
||||
{
|
||||
"name": "FullscreenEnter",
|
||||
"description": "",
|
||||
"created_at": "2022-02-10T15:01:12.614Z",
|
||||
"pageName": "Interface"
|
||||
},
|
||||
{
|
||||
"name": "FullscreenEnterFilled",
|
||||
"description": "",
|
||||
"created_at": "2022-02-10T15:01:12.604Z",
|
||||
"pageName": "Interface"
|
||||
},
|
||||
{
|
||||
"name": "FullscreenExit",
|
||||
"description": "",
|
||||
"created_at": "2022-02-10T15:01:12.597Z",
|
||||
"pageName": "Interface"
|
||||
},
|
||||
{
|
||||
"name": "FullscreenExitFilled",
|
||||
"description": "",
|
||||
"created_at": "2022-02-10T15:01:12.608Z",
|
||||
"pageName": "Interface"
|
||||
},
|
||||
{
|
||||
"name": "Glass",
|
||||
"description": "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-icons",
|
||||
"version": "0.8.2",
|
||||
"version": "0.8.4",
|
||||
"private": false,
|
||||
"description": "NAVs egne ikoner i SVG og React format",
|
||||
"repository": {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 2C0 0.89543 0.895431 0 2 0H22C23.1046 0 24 0.895431 24 2V22C24 23.1046 23.1046 24 22 24H2C0.89543 24 0 23.1046 0 22V2Z" fill="#262626"/>
|
||||
<path d="M11.6942 1.58536C11.7541 1.4011 12.0147 1.40111 12.0746 1.58536L12.4898 2.86313C12.5166 2.94553 12.5934 3.00132 12.68 3.00132H14.0233C14.2171 3.00132 14.2976 3.24925 14.1409 3.36313L13.054 4.15276C12.9839 4.20369 12.9546 4.29397 12.9814 4.37637L13.3966 5.65405C13.4564 5.83831 13.2455 5.99154 13.0888 5.87766L12.002 5.08798C11.9319 5.03705 11.8369 5.03705 11.7668 5.08798L10.6799 5.87768C10.5232 5.99156 10.3123 5.83833 10.3721 5.65407L10.7873 4.37637C10.8141 4.29397 10.7848 4.20369 10.7147 4.15276L9.62781 3.36313C9.47107 3.24925 9.55162 3.00132 9.74537 3.00132H11.0888C11.1754 3.00132 11.2522 2.94553 11.279 2.86313L11.6942 1.58536Z" fill="white"/>
|
||||
<path d="M4.43501 4.70412C4.34705 4.5315 4.53137 4.34717 4.704 4.43512L5.92459 5.05696C6.00178 5.09628 6.09553 5.08144 6.15679 5.02017L7.12523 4.05178C7.26224 3.91479 7.49451 4.03314 7.46419 4.2245L7.24987 5.57721C7.23631 5.66279 7.2794 5.74737 7.35661 5.7867L8.57708 6.40856C8.74971 6.49651 8.70893 6.75399 8.51757 6.7843L7.16475 6.99857C7.07918 7.01213 7.01206 7.07925 6.99851 7.16482L6.78423 8.51771C6.75392 8.70907 6.49644 8.74985 6.40849 8.57722L5.78664 7.35676C5.7473 7.27955 5.66272 7.23646 5.57714 7.25002L4.22446 7.46433C4.0331 7.49465 3.91474 7.26238 4.05174 7.12537L5.02017 6.15695C5.08144 6.09568 5.09629 6.00193 5.05695 5.92472L4.43501 4.70412Z" fill="white"/>
|
||||
<path d="M1.58536 12.2163C1.40111 12.1564 1.40111 11.8957 1.58536 11.8358L2.9661 11.3872C3.0485 11.3604 3.10429 11.2836 3.10429 11.197V9.74534C3.10429 9.55159 3.35223 9.47104 3.4661 9.6278L4.31929 10.8023C4.37021 10.8724 4.46049 10.9017 4.5429 10.8749L5.9236 10.4263C6.10787 10.3664 6.26109 10.5773 6.1472 10.7341L5.29384 11.9085C5.24291 11.9786 5.24291 12.0735 5.29384 12.1436L6.14722 13.3181C6.26111 13.4748 6.10789 13.6857 5.92363 13.6258L4.5429 13.1772C4.46049 13.1505 4.37022 13.1798 4.31929 13.2499L3.4661 14.4243C3.35222 14.5811 3.10429 14.5005 3.10429 14.3068V12.8551C3.10429 12.7685 3.0485 12.6917 2.9661 12.6649L1.58536 12.2163Z" fill="white"/>
|
||||
<path d="M4.70399 19.3918C4.53137 19.4798 4.34704 19.2954 4.435 19.1228L5.0569 17.9022C5.09623 17.825 5.08139 17.7312 5.02012 17.67L4.05174 16.7015C3.91474 16.5645 4.0331 16.3323 4.22446 16.3626L5.57723 16.577C5.66282 16.5905 5.74741 16.5474 5.78674 16.4702L6.40847 15.2498C6.49642 15.0771 6.7539 15.1179 6.78422 15.3093L6.99855 16.6621C7.01211 16.7477 7.07922 16.8148 7.1648 16.8283L8.51763 17.0426C8.70898 17.0729 8.74976 17.3304 8.57715 17.4184L7.3568 18.0402C7.2796 18.0796 7.23652 18.1641 7.25007 18.2497L7.4643 19.6025C7.4946 19.7939 7.26233 19.9122 7.12534 19.7752L6.15683 18.8067C6.09556 18.7454 6.00182 18.7306 5.92461 18.7699L4.70399 19.3918Z" fill="white"/>
|
||||
<path d="M12.0746 22.1451C12.0147 22.3293 11.754 22.3293 11.6942 22.1451L11.2789 20.8673C11.2521 20.7849 11.1753 20.7291 11.0887 20.7291H9.74539C9.55164 20.7291 9.47109 20.4812 9.62784 20.3673L10.7147 19.5777C10.7848 19.5268 10.8142 19.4365 10.7874 19.3541L10.3722 18.0765C10.3123 17.8923 10.5232 17.739 10.6799 17.8529L11.7668 18.6425C11.8369 18.6934 11.9319 18.6934 12.0019 18.6425L13.0888 17.8529C13.2456 17.739 13.4564 17.8923 13.3966 18.0765L12.9814 19.3541C12.9546 19.4365 12.9839 19.5268 13.054 19.5777L14.1409 20.3673C14.2976 20.4812 14.2171 20.7291 14.0233 20.7291H12.6799C12.5933 20.7291 12.5165 20.7849 12.4897 20.8673L12.0746 22.1451Z" fill="white"/>
|
||||
<path d="M19.3918 19.1229C19.4798 19.2955 19.2955 19.4798 19.1228 19.3919L17.9023 18.77C17.8251 18.7307 17.7314 18.7456 17.6701 18.8068L16.7015 19.7753C16.5645 19.9123 16.3323 19.794 16.3626 19.6026L16.5769 18.2497C16.5905 18.1641 16.5474 18.0795 16.4702 18.0402L15.2498 17.4184C15.0772 17.3304 15.1179 17.073 15.3093 17.0427L16.6621 16.8284C16.7477 16.8148 16.8148 16.7477 16.8284 16.6621L17.0426 15.3093C17.0729 15.1179 17.3304 15.0771 17.4184 15.2498L18.0402 16.4702C18.0795 16.5475 18.1641 16.5906 18.2497 16.577L19.6024 16.3626C19.7938 16.3323 19.9122 16.5646 19.7752 16.7016L18.8067 17.6702C18.7454 17.7314 18.7305 17.8252 18.7699 17.9024L19.3918 19.1229Z" fill="white"/>
|
||||
<path d="M22.4146 11.8359C22.5988 11.8958 22.5988 12.1565 22.4146 12.2164L21.0338 12.665C20.9514 12.6918 20.8956 12.7686 20.8956 12.8552V14.3069C20.8956 14.5007 20.6477 14.5812 20.5338 14.4245L19.6805 13.25C19.6296 13.1798 19.5393 13.1505 19.4569 13.1773L18.0763 13.6259C17.8921 13.6858 17.7388 13.4749 17.8527 13.3182L18.706 12.1437C18.757 12.0736 18.757 11.9787 18.706 11.9086L17.8527 10.7341C17.7388 10.5773 17.8921 10.3664 18.0763 10.4263L19.457 10.875C19.5394 10.9017 19.6297 10.8724 19.6806 10.8023L20.5338 9.6278C20.6477 9.47104 20.8956 9.55159 20.8956 9.74535V11.1971C20.8956 11.2838 20.9514 11.3605 21.0338 11.3873L22.4146 11.8359Z" fill="white"/>
|
||||
<path d="M19.1228 4.43517C19.2954 4.34721 19.4797 4.53153 19.3918 4.70416L18.7699 5.92475C18.7306 6.00195 18.7454 6.0957 18.8067 6.15697L19.7751 7.1254C19.9121 7.26241 19.7938 7.49468 19.6024 7.46436L18.2496 7.24999C18.164 7.23642 18.0794 7.27952 18.0401 7.35674L17.4183 8.57714C17.3304 8.74978 17.0729 8.70901 17.0426 8.51764L16.8283 7.16486C16.8148 7.07928 16.7477 7.01217 16.6621 6.99861L15.3093 6.78427C15.1179 6.75395 15.0771 6.49649 15.2498 6.40853L16.4701 5.78673C16.5473 5.74739 16.5904 5.66281 16.5769 5.57722L16.3626 4.22452C16.3322 4.03317 16.5645 3.91481 16.7015 4.05179L17.6701 5.02027C17.7314 5.08153 17.8251 5.09637 17.9023 5.05704L19.1228 4.43517Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 2C0 0.89543 0.895431 0 2 0H22C23.1046 0 24 0.895431 24 2V22C24 23.1046 23.1046 24 22 24H2C0.89543 24 0 23.1046 0 22V2ZM11.6942 1.58536C11.754 1.4011 12.0147 1.40111 12.0746 1.58536L12.4898 2.86313C12.5166 2.94553 12.5934 3.00132 12.68 3.00132H14.0233C14.2171 3.00132 14.2976 3.24925 14.1409 3.36313L13.054 4.15276C12.9839 4.20369 12.9546 4.29397 12.9814 4.37637L13.3966 5.65405C13.4564 5.83831 13.2455 5.99154 13.0888 5.87766L12.0019 5.08798C11.9318 5.03705 11.8369 5.03705 11.7668 5.08798L10.6799 5.87768C10.5231 5.99156 10.3123 5.83833 10.3721 5.65407L10.7873 4.37637C10.8141 4.29397 10.7848 4.20369 10.7147 4.15276L9.6278 3.36313C9.47105 3.24925 9.55161 3.00132 9.74535 3.00132H11.0888C11.1754 3.00132 11.2522 2.94553 11.279 2.86313L11.6942 1.58536ZM4.704 4.43512C4.53138 4.34717 4.34706 4.5315 4.43502 4.70413L5.05696 5.92473C5.0963 6.00193 5.08145 6.09568 5.02018 6.15695L4.05175 7.12538C3.91475 7.26238 4.03311 7.49465 4.22447 7.46433L5.57715 7.25002C5.66273 7.23646 5.74731 7.27955 5.78664 7.35676L6.40849 8.57722C6.49645 8.74985 6.75392 8.70907 6.78423 8.51772L6.99851 7.16483C7.01207 7.07925 7.07919 7.01213 7.16476 6.99858L8.51758 6.7843C8.70894 6.75399 8.74972 6.49652 8.57709 6.40856L7.35662 5.78671C7.27941 5.74737 7.23632 5.66279 7.24988 5.57721L7.4642 4.22451C7.49452 4.03315 7.26225 3.91479 7.12524 4.05178L6.1568 5.02018C6.09553 5.08144 6.00179 5.09629 5.92459 5.05696L4.704 4.43512ZM1.58536 12.2163C1.40111 12.1564 1.40111 11.8957 1.58536 11.8358L2.9661 11.3872C3.0485 11.3604 3.10429 11.2836 3.10429 11.197V9.74534C3.10429 9.55159 3.35223 9.47104 3.4661 9.6278L4.31929 10.8023C4.37021 10.8724 4.46049 10.9017 4.5429 10.8749L5.9236 10.4263C6.10787 10.3664 6.26109 10.5773 6.1472 10.7341L5.29384 11.9085C5.24291 11.9786 5.24291 12.0735 5.29384 12.1436L6.14722 13.3181C6.26111 13.4748 6.10789 13.6857 5.92363 13.6258L4.5429 13.1772C4.46049 13.1505 4.37022 13.1798 4.31929 13.2499L3.4661 14.4243C3.35222 14.5811 3.10429 14.5005 3.10429 14.3068V12.8551C3.10429 12.7685 3.0485 12.6917 2.9661 12.6649L1.58536 12.2163ZM4.43501 19.1228C4.34705 19.2954 4.53138 19.4798 4.704 19.3918L5.92462 18.7699C6.00182 18.7306 6.09557 18.7454 6.15684 18.8067L7.12535 19.7752C7.26234 19.9122 7.49461 19.7938 7.46431 19.6025L7.25008 18.2497C7.23652 18.1641 7.27961 18.0796 7.35681 18.0402L8.57716 17.4184C8.74977 17.3304 8.70899 17.0729 8.51764 17.0426L7.1648 16.8283C7.07923 16.8148 7.01212 16.7477 6.99856 16.6621L6.78423 15.3093C6.75391 15.1179 6.49643 15.0771 6.40848 15.2498L5.78675 16.4702C5.74742 16.5474 5.66283 16.5905 5.57724 16.577L4.22447 16.3626C4.03311 16.3323 3.91475 16.5645 4.05174 16.7015L5.02013 17.67C5.0814 17.7312 5.09624 17.825 5.05691 17.9022L4.43501 19.1228ZM12.0746 22.1451C12.0147 22.3293 11.754 22.3293 11.6942 22.1451L11.2789 20.8673C11.2521 20.7849 11.1753 20.7291 11.0887 20.7291H9.74537C9.55163 20.7291 9.47107 20.4812 9.62782 20.3673L10.7147 19.5777C10.7848 19.5268 10.8142 19.4365 10.7874 19.3541L10.3722 18.0765C10.3123 17.8923 10.5232 17.739 10.6799 17.8529L11.7668 18.6425C11.8369 18.6934 11.9318 18.6934 12.0019 18.6425L13.0888 17.8529C13.2455 17.739 13.4564 17.8923 13.3965 18.0765L12.9814 19.3541C12.9546 19.4365 12.9839 19.5268 13.054 19.5777L14.1409 20.3673C14.2976 20.4812 14.2171 20.7291 14.0233 20.7291H12.6799C12.5933 20.7291 12.5165 20.7849 12.4897 20.8673L12.0746 22.1451ZM19.1228 19.3919C19.2954 19.4798 19.4798 19.2955 19.3918 19.1229L18.7699 17.9024C18.7305 17.8252 18.7454 17.7314 18.8066 17.6702L19.7752 16.7016C19.9121 16.5646 19.7938 16.3323 19.6024 16.3626L18.2497 16.577C18.1641 16.5905 18.0795 16.5475 18.0402 16.4702L17.4184 15.2498C17.3304 15.0771 17.0729 15.1179 17.0426 15.3093L16.8283 16.6621C16.8148 16.7477 16.7477 16.8148 16.6621 16.8284L15.3093 17.0426C15.1179 17.073 15.0772 17.3304 15.2498 17.4184L16.4702 18.0402C16.5474 18.0795 16.5905 18.1641 16.5769 18.2497L16.3626 19.6026C16.3322 19.794 16.5645 19.9123 16.7015 19.7753L17.6701 18.8068C17.7313 18.7456 17.8251 18.7307 17.9023 18.77L19.1228 19.3919ZM22.4146 11.8359C22.5988 11.8958 22.5988 12.1565 22.4146 12.2164L21.0338 12.665C20.9514 12.6918 20.8956 12.7686 20.8956 12.8552V14.3069C20.8956 14.5007 20.6477 14.5812 20.5338 14.4245L19.6805 13.25C19.6296 13.1798 19.5393 13.1505 19.4569 13.1773L18.0763 13.6259C17.8921 13.6858 17.7389 13.4749 17.8527 13.3182L18.706 12.1437C18.757 12.0736 18.757 11.9787 18.706 11.9086L17.8527 10.7341C17.7388 10.5773 17.8921 10.3664 18.0763 10.4263L19.457 10.875C19.5394 10.9017 19.6297 10.8724 19.6806 10.8023L20.5338 9.6278C20.6477 9.47104 20.8956 9.55159 20.8956 9.74534V11.1971C20.8956 11.2838 20.9514 11.3605 21.0338 11.3873L22.4146 11.8359ZM19.3918 4.70416C19.4797 4.53153 19.2954 4.34721 19.1228 4.43517L17.9023 5.05703C17.8251 5.09637 17.7314 5.08152 17.6701 5.02027L16.7015 4.05179C16.5645 3.9148 16.3322 4.03316 16.3625 4.22452L16.5769 5.57722C16.5904 5.6628 16.5473 5.74738 16.4701 5.78672L15.2497 6.40853C15.0771 6.49648 15.1179 6.75395 15.3092 6.78427L16.6621 6.9986C16.7476 7.01216Line truncated
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -1,4 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 23V10C2 9.44772 2.44772 9 3 9H11.5556C12.1078 9 12.5556 9.44772 12.5556 10V12H21C21.5523 12 22 12.4477 22 13V23C22 23.5523 21.5523 24 21 24H3C2.44772 24 2 23.5523 2 23Z" fill="#262626"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 0H20V10H14V9C14 7.89543 13.1046 7 12 7H4V0ZM8 5H16V3H8V5Z" fill="#262626"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 0H4V7H12C13.1046 7 14 7.89543 14 9V10H20V0ZM16 5H8V3H16V5ZM2 10V23C2 23.5523 2.44772 24 3 24H21C21.5523 24 22 23.5523 22 23V13C22 12.4477 21.5523 12 21 12H12.5556V10C12.5556 9.44772 12.1078 9 11.5556 9H3C2.44772 9 2 9.44772 2 10Z" fill="#262626"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 430 B After Width: | Height: | Size: 404 B |
@@ -1,4 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 1V6H18" stroke="#262626" stroke-width="2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4142 0H2V22H11V20H4V2H13.5858L17 5.41421V16H19V4.58579L14.4142 0ZM22.2689 14.3177L16.0317 21.0004L13.7593 18.3492L12.2407 19.6508L15.9683 23.9996L23.7311 15.6823L22.2689 14.3177Z" fill="#262626"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 0H14.4142L19 4.58579V16H17V7H12V2H4V20H11V22H2V0ZM14 2.41421L16.5858 5H14V2.41421ZM22.2689 14.3177L16.0317 21.0004L13.7593 18.3492L12.2407 19.6508L15.9683 23.9996L23.7311 15.6823L22.2689 14.3177Z" fill="#262626"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 410 B After Width: | Height: | Size: 369 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5857 5L12.2928 10.2929L13.707 11.7071L19 6.41412V11H21V3H13V5L17.5857 5ZM5 13V17.5859L10.293 12.2929L11.7072 13.7071L6.4143 19H11V21H3V13H5Z" fill="#262626"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 0C0.447715 0 0 0.447716 0 1V23C0 23.5523 0.447716 24 1 24H23C23.5523 24 24 23.5523 24 23V1C24 0.447715 23.5523 0 23 0H1ZM12.2928 10.2929L17.5857 5L13 5V3H21V11H19V6.41412L13.707 11.7071L12.2928 10.2929ZM5 17.5859V13H3V21H11V19H6.4143L11.7072 13.7071L10.293 12.2929L5 17.5859Z" fill="#262626"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 449 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.414 8.99999L21.7068 3.7071L20.2926 2.29288L14.9999 7.58561V2.99999H12.9999V11H20.9999V8.99999H16.414ZM7.58562 15L2.29272 20.2929L3.70694 21.7071L8.99991 16.4141L8.99991 21H10.9999L10.9999 13L2.99991 13L2.99991 15L7.58562 15Z" fill="#262626"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 399 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 0C0.447715 0 0 0.447716 0 1V23C0 23.5523 0.447716 24 1 24H23C23.5523 24 24 23.5523 24 23V1C24 0.447715 23.5523 0 23 0H1ZM21.7068 3.70709L16.414 8.99998H20.9999V11H12.9999V2.99998H14.9999V7.5856L20.2926 2.29288L21.7068 3.70709ZM2.29272 20.2929L7.58562 15L2.99991 15L2.99991 13L10.9999 13L10.9999 21H8.99991V16.4141L3.70694 21.7071L2.29272 20.2929Z" fill="#262626"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 520 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "0.16.2",
|
||||
"version": "0.17.2",
|
||||
"private": false,
|
||||
"description": "NAV designsystem react components",
|
||||
"author": "NAV Designsystem team",
|
||||
@@ -35,8 +35,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@navikt/ds-icons": "^0.8.2",
|
||||
"@navikt/ds-icons": "^0.8.4",
|
||||
"@popperjs/core": "^2.10.1",
|
||||
"@radix-ui/react-toggle-group": "0.1.5",
|
||||
"classnames": "^2.2.6",
|
||||
"react-collapse": "^5.1.0",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
@@ -58,6 +59,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": {
|
||||
|
||||
@@ -36,13 +36,13 @@ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
const Icon = ({ variant, ...props }) => {
|
||||
switch (variant) {
|
||||
case "error":
|
||||
return <ErrorFilled {...props} />;
|
||||
return <ErrorFilled title="Feil" {...props} />;
|
||||
case "warning":
|
||||
return <WarningFilled {...props} />;
|
||||
return <WarningFilled title="Advarsel" {...props} />;
|
||||
case "info":
|
||||
return <InformationFilled {...props} />;
|
||||
return <InformationFilled title="Informasjon" {...props} />;
|
||||
case "success":
|
||||
return <SuccessFilled {...props} />;
|
||||
return <SuccessFilled title="Suksess" {...props} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -76,11 +76,7 @@ const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
||||
{ "navds-alert--full-width": fullWidth, "navds-alert--inline": inline }
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
title={`${variant}-ikon`}
|
||||
variant={variant}
|
||||
className="navds-alert__icon"
|
||||
/>
|
||||
<Icon variant={variant} className="navds-alert__icon" />
|
||||
<BodyLong as="div" size={size} className="navds-alert__wrapper">
|
||||
{children}
|
||||
</BodyLong>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5,6 +5,9 @@ import { Success } from "@navikt/ds-icons";
|
||||
export default {
|
||||
title: "ds-react/button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
chromatic: { delay: 300 },
|
||||
},
|
||||
};
|
||||
|
||||
const Section = ({ children }) => (
|
||||
@@ -78,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) => (
|
||||
|
||||
@@ -6,7 +6,7 @@ import { BodyShort, Detail, omit } from "../..";
|
||||
|
||||
export interface CheckboxProps
|
||||
extends Omit<FormFieldProps, "errorId">,
|
||||
Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
||||
Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "value"> {
|
||||
/**
|
||||
* Checkbox has error
|
||||
* @default false
|
||||
@@ -23,7 +23,7 @@ export interface CheckboxProps
|
||||
/**
|
||||
* The value of the HTML element.
|
||||
*/
|
||||
value?: string;
|
||||
value?: any;
|
||||
/**
|
||||
* Specify whether the Checkbox is in an indeterminate state
|
||||
* @default false
|
||||
@@ -55,6 +55,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
|
||||
"error",
|
||||
"description",
|
||||
"hideLabel",
|
||||
"indeterminate",
|
||||
])}
|
||||
{...inputProps}
|
||||
type="checkbox"
|
||||
|
||||
@@ -3,9 +3,9 @@ import cl from "classnames";
|
||||
import { Fieldset, FieldsetProps, FieldsetContext } from "..";
|
||||
|
||||
export interface CheckboxGroupState {
|
||||
readonly defaultValue?: readonly string[];
|
||||
readonly value?: readonly string[];
|
||||
toggleValue(value: string): void;
|
||||
readonly defaultValue?: readonly any[];
|
||||
readonly value?: readonly any[];
|
||||
toggleValue(value: any): void;
|
||||
}
|
||||
|
||||
export const CheckboxGroupContext = createContext<CheckboxGroupState | null>(
|
||||
@@ -13,7 +13,10 @@ export const CheckboxGroupContext = createContext<CheckboxGroupState | null>(
|
||||
);
|
||||
|
||||
export interface CheckboxGroupProps
|
||||
extends Omit<FieldsetProps, "onChange" | "errorPropagation"> {
|
||||
extends Omit<
|
||||
FieldsetProps,
|
||||
"onChange" | "errorPropagation" | "defaultValue"
|
||||
> {
|
||||
/**
|
||||
* Checkboxes
|
||||
*/
|
||||
@@ -21,15 +24,15 @@ export interface CheckboxGroupProps
|
||||
/**
|
||||
* Controlled state for group
|
||||
*/
|
||||
value?: string[];
|
||||
value?: any[];
|
||||
/**
|
||||
* Default checked checkboxes on render
|
||||
*/
|
||||
defaultValue?: string[];
|
||||
defaultValue?: any[];
|
||||
/**
|
||||
* Returns current checked checkboxes in group
|
||||
*/
|
||||
onChange?: (value: string[]) => void;
|
||||
onChange?: (value: any[]) => void;
|
||||
}
|
||||
|
||||
const CheckboxGroup = forwardRef<HTMLFieldSetElement, CheckboxGroupProps>(
|
||||
@@ -39,10 +42,10 @@ const CheckboxGroup = forwardRef<HTMLFieldSetElement, CheckboxGroupProps>(
|
||||
) => {
|
||||
const fieldset = useContext(FieldsetContext);
|
||||
|
||||
const [state, setState] = useState<string[]>([]);
|
||||
const [state, setState] = useState<any[]>(defaultValue ?? []);
|
||||
|
||||
const handleChange = (v: string) => {
|
||||
const newValue = value ? value : state;
|
||||
const toggleValue = (v: any) => {
|
||||
const newValue = value ?? state;
|
||||
const newState = newValue.includes(v)
|
||||
? newValue.filter((x) => x !== v)
|
||||
: [...newValue, v];
|
||||
@@ -65,7 +68,7 @@ const CheckboxGroup = forwardRef<HTMLFieldSetElement, CheckboxGroupProps>(
|
||||
value={{
|
||||
value,
|
||||
defaultValue,
|
||||
toggleValue: (value: string) => handleChange(value),
|
||||
toggleValue,
|
||||
}}
|
||||
>
|
||||
<div className="navds-checkboxes">{children}</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { Checkbox, CheckboxGroup } from "../../index";
|
||||
import { Meta } from "@storybook/react/types-6-0";
|
||||
import { CheckboxGroupProps } from "..";
|
||||
export default {
|
||||
title: "ds-react/form/checkbox",
|
||||
component: Checkbox,
|
||||
@@ -37,13 +38,15 @@ export const Indeterminate = () => {
|
||||
};
|
||||
|
||||
export const All = () => {
|
||||
const Checkboxes = (props) => (
|
||||
const Checkboxes = (
|
||||
props: Omit<CheckboxGroupProps, "legend" | "description" | "children">
|
||||
) => (
|
||||
<CheckboxGroup
|
||||
legend="Mollit eiusmod"
|
||||
description="Exercitation do labore"
|
||||
{...props}
|
||||
>
|
||||
<Checkbox value="Apple">Apple</Checkbox>
|
||||
<Checkbox value={1}>Apple</Checkbox>
|
||||
<Checkbox value="Orange" description="Laborum ad">
|
||||
Orange
|
||||
</Checkbox>
|
||||
@@ -60,7 +63,7 @@ export const All = () => {
|
||||
<h2>Single checkbox</h2>
|
||||
<Checkbox value="Apple">Apple</Checkbox>
|
||||
<h3>Desription</h3>
|
||||
<Checkbox value="Apple" description="Laborum ad" defaultChecked>
|
||||
<Checkbox value={1} description="Laborum ad" defaultChecked>
|
||||
Apple
|
||||
</Checkbox>
|
||||
<h3>Error</h3>
|
||||
@@ -98,7 +101,7 @@ export const All = () => {
|
||||
<h3>Small + error</h3>
|
||||
<Checkboxes size="small" error="Dette er en feilmelding" />
|
||||
<h3>Default value</h3>
|
||||
<Checkboxes defaultValue={["Orange", "Melon"]} />
|
||||
<Checkboxes defaultValue={[1, "Melon"]} />
|
||||
<h3>Disabled</h3>
|
||||
<Checkboxes disabled />
|
||||
</>
|
||||
|
||||
@@ -33,14 +33,14 @@ const useCheckbox = ({ children, ...props }: CheckboxProps) => {
|
||||
inputProps: {
|
||||
...inputProps,
|
||||
checked: checkboxGroup?.value
|
||||
? checkboxGroup.value.includes(props.value as string)
|
||||
? checkboxGroup.value.includes(props.value)
|
||||
: props.checked,
|
||||
defaultChecked: checkboxGroup?.defaultValue
|
||||
? checkboxGroup.defaultValue.includes(props.value as string)
|
||||
? checkboxGroup.defaultValue.includes(props.value)
|
||||
: props.defaultChecked,
|
||||
onChange: (e) => {
|
||||
props.onChange && props.onChange(e);
|
||||
checkboxGroup && checkboxGroup.toggleValue(props.value as string);
|
||||
checkboxGroup && checkboxGroup.toggleValue(props.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRadio } from "./useRadio";
|
||||
|
||||
export interface RadioProps
|
||||
extends Omit<FormFieldProps, "error" | "errorId">,
|
||||
Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
||||
Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "value"> {
|
||||
/**
|
||||
* Label for radio
|
||||
*/
|
||||
@@ -14,7 +14,7 @@ export interface RadioProps
|
||||
/**
|
||||
* The value of the HTML element
|
||||
*/
|
||||
value: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
const Radio = forwardRef<HTMLInputElement, RadioProps>((props, ref) => {
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useId } from "../..";
|
||||
|
||||
export interface RadioGroupContextProps {
|
||||
name: string;
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
defaultValue?: any;
|
||||
value?: any;
|
||||
onChange: (value: any) => void;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ export const RadioGroupContext = React.createContext<RadioGroupContextProps | nu
|
||||
);
|
||||
|
||||
export interface RadioGroupProps
|
||||
extends Omit<FieldsetProps, "onChange" | "errorPropagation"> {
|
||||
extends Omit<
|
||||
FieldsetProps,
|
||||
"onChange" | "errorPropagation" | "defaultValue"
|
||||
> {
|
||||
/**
|
||||
* Collection of <Radio>-elements
|
||||
*/
|
||||
@@ -28,15 +31,15 @@ export interface RadioGroupProps
|
||||
/**
|
||||
* Default checked radiobutton
|
||||
*/
|
||||
defaultValue?: string;
|
||||
defaultValue?: any;
|
||||
/**
|
||||
* Controlled state for Radiobutton
|
||||
*/
|
||||
value?: string;
|
||||
value?: any;
|
||||
/**
|
||||
* Returns current checked radiobutton in group
|
||||
*/
|
||||
onChange?: (value: string) => void;
|
||||
onChange?: (value: any) => void;
|
||||
/**
|
||||
* Tells Fieldset if group is required
|
||||
*/
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import React from "react";
|
||||
import { Meta } from "@storybook/react/types-6-0";
|
||||
import { RadioGroup, Radio } from "../index";
|
||||
import { RadioGroupProps } from "../RadioGroup";
|
||||
export default {
|
||||
title: "ds-react/form/radio",
|
||||
component: RadioGroup,
|
||||
} as Meta;
|
||||
|
||||
export const All = () => {
|
||||
const Radios = (props) => (
|
||||
const Radios = (props: Omit<RadioGroupProps, "legend" | "children">) => (
|
||||
<RadioGroup
|
||||
legend="Mollit eiusmod"
|
||||
description={<div>"Exercitation do labore"</div>}
|
||||
{...props}
|
||||
>
|
||||
<Radio value="Apple">Apple</Radio>
|
||||
<Radio value="Orange" description="Laborum ad">
|
||||
Orange
|
||||
</Radio>
|
||||
<Radio value="Orange" description={<div>Laborum ad</div>}>
|
||||
<Radio value={1}>Apple</Radio>
|
||||
<Radio value={false} description={<div>Laborum ad</div>}>
|
||||
Orange
|
||||
</Radio>
|
||||
<Radio value="Melon">Melon</Radio>
|
||||
@@ -35,13 +33,13 @@ export const All = () => {
|
||||
<h2>Small + error</h2>
|
||||
<Radios size="small" error="Dette er en feilmelding" />
|
||||
<h2>Default value</h2>
|
||||
<Radios defaultValue="Orange" />
|
||||
<Radios defaultValue={1} />
|
||||
<h2>Disabled</h2>
|
||||
<RadioGroup legend="Mollit eiusmod" description="Exercitation do labore">
|
||||
<Radio value="Apple" disabled>
|
||||
Apple
|
||||
</Radio>
|
||||
<Radio value="Orange" description="Laborum ad" disabled>
|
||||
<Radio value={false} description="Laborum ad" disabled>
|
||||
Orange
|
||||
</Radio>
|
||||
<Radio value="Melon">Melon</Radio>
|
||||
|
||||
@@ -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,236 @@
|
||||
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";
|
||||
}
|
||||
|
||||
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<HTMLFormElement | 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 (
|
||||
<form
|
||||
role="search"
|
||||
ref={setWrapperRef}
|
||||
className={cl(
|
||||
className,
|
||||
"navds-form-field",
|
||||
`navds-form-field--${size}`,
|
||||
"navds-search",
|
||||
{
|
||||
"navds-search--disabled": !!inputProps.disabled,
|
||||
}
|
||||
)}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onSearch?.(controlledValue);
|
||||
}}
|
||||
>
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
{children ? children : <SearchButton />}
|
||||
</SearchContext.Provider>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}) as SearchComponent;
|
||||
|
||||
Search.Button = SearchButton;
|
||||
|
||||
export default Search;
|
||||
@@ -0,0 +1,48 @@
|
||||
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 } = 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) => 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,91 @@
|
||||
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={{ maxWidth: 300 }}>
|
||||
<h1>Search</h1>
|
||||
<div>
|
||||
<Search
|
||||
label="Søk alle sider om X og Y"
|
||||
onSearch={console.log}
|
||||
></Search>
|
||||
<br />
|
||||
<Search
|
||||
label="Søk alle sider om X og Y"
|
||||
onSearch={console.log}
|
||||
variant="primary"
|
||||
></Search>
|
||||
</div>
|
||||
|
||||
<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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -10,11 +10,13 @@ export * from "./link-panel";
|
||||
export * from "./loader";
|
||||
export * from "./menu";
|
||||
export * from "./modal";
|
||||
export * from "./pagination";
|
||||
export * from "./panel";
|
||||
export * from "./popover";
|
||||
export * from "./speech-bubble";
|
||||
export * from "./step-indicator";
|
||||
export * from "./tag";
|
||||
export * from "./toggle-group";
|
||||
export * from "./table";
|
||||
export * from "./typography";
|
||||
export * from "./util";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,6 +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
|
||||
@@ -56,6 +68,10 @@ const Modal = forwardRef<ReactModal, ModalProps>(
|
||||
className,
|
||||
shouldCloseOnOverlayClick = true,
|
||||
closeButton = true,
|
||||
"aria-describedby": ariaDescribedBy,
|
||||
"aria-labelledby": ariaLabelledBy,
|
||||
"aria-modal": ariaModal,
|
||||
"aria-label": contentLabel,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
@@ -81,6 +97,12 @@ const Modal = forwardRef<ReactModal, ModalProps>(
|
||||
overlayClassName="navds-modal__overlay"
|
||||
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
|
||||
onRequestClose={(e) => onModalCloseRequest(e)}
|
||||
aria={{
|
||||
describedby: ariaDescribedBy,
|
||||
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,187 @@
|
||||
import React from "react";
|
||||
import cl from "classnames";
|
||||
import { Back, Next } from "@navikt/ds-icons";
|
||||
import { BodyShort } from "..";
|
||||
|
||||
interface PaginationProps extends React.HTMLAttributes<HTMLElement> {
|
||||
/**
|
||||
* Current page
|
||||
*/
|
||||
page: number;
|
||||
/**
|
||||
* Number of always visible pages before and after the current page.
|
||||
* @default 1
|
||||
*/
|
||||
siblingCount?: number;
|
||||
/**
|
||||
* Number of always visible pages at the beginning and end.
|
||||
* @default 1
|
||||
*/
|
||||
boundaryCount?: number;
|
||||
/**
|
||||
* Callback when current page changes
|
||||
*/
|
||||
onPageChange: (page: number) => void;
|
||||
/**
|
||||
* Total number of pages
|
||||
*/
|
||||
count: number;
|
||||
/**
|
||||
* Changes padding, height and font-size
|
||||
* @default "medium"
|
||||
*/
|
||||
size?: "medium" | "small";
|
||||
/**
|
||||
* Display text alongside "previous" and "next" icons
|
||||
* @default false
|
||||
*/
|
||||
prevNextTexts?: boolean;
|
||||
}
|
||||
|
||||
export const getSteps = ({
|
||||
page,
|
||||
count,
|
||||
boundaryCount = 1,
|
||||
siblingCount = 1,
|
||||
}) => {
|
||||
const range = (start: number, end: number) =>
|
||||
Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||
|
||||
if (count <= (boundaryCount + siblingCount) * 2 + 3) return range(1, count);
|
||||
|
||||
const startPages = range(1, boundaryCount);
|
||||
const endPages = range(count - boundaryCount + 1, count);
|
||||
|
||||
const siblingsStart = Math.max(
|
||||
Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1),
|
||||
boundaryCount + 2
|
||||
);
|
||||
const siblingsEnd = siblingsStart + siblingCount * 2;
|
||||
|
||||
return [
|
||||
...startPages,
|
||||
siblingsStart - (startPages[startPages.length - 1] ?? 0) === 2
|
||||
? siblingsStart - 1
|
||||
: "ellipsis",
|
||||
...range(siblingsStart, siblingsEnd),
|
||||
(endPages[0] ?? count + 1) - siblingsEnd === 2
|
||||
? siblingsEnd + 1
|
||||
: "ellipsis",
|
||||
...endPages,
|
||||
];
|
||||
};
|
||||
|
||||
const Pagination = ({
|
||||
page,
|
||||
onPageChange,
|
||||
count,
|
||||
boundaryCount = 1,
|
||||
siblingCount = 1,
|
||||
className,
|
||||
size = "medium",
|
||||
prevNextTexts = false,
|
||||
}: PaginationProps) => {
|
||||
if (page < 1) {
|
||||
console.error("page cannot be less than 1");
|
||||
return null;
|
||||
}
|
||||
if (count < 1) {
|
||||
console.error("count cannot be less than 1");
|
||||
return null;
|
||||
}
|
||||
if (boundaryCount < 0) {
|
||||
console.error("boundaryCount cannot be less than 0");
|
||||
return null;
|
||||
}
|
||||
if (siblingCount < 0) {
|
||||
console.error("siblingCount cannot be less than 0");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={cl("navds-pagination", `navds-pagination--${size}`, className)}
|
||||
>
|
||||
{prevNextTexts && page !== 1 && (
|
||||
<button
|
||||
className="navds-pagination__prev-next"
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
>
|
||||
<Back
|
||||
className="navds-pagination__prev-next-icon"
|
||||
role="presentation"
|
||||
/>
|
||||
<BodyShort size={size} className="navds-pagination__prev-text">
|
||||
Forrige
|
||||
</BodyShort>
|
||||
</button>
|
||||
)}
|
||||
<ul className="navds-pagination__list">
|
||||
{!prevNextTexts && page !== 1 && (
|
||||
<li>
|
||||
<button
|
||||
className="navds-pagination__prev-next"
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
>
|
||||
<Back
|
||||
className="navds-pagination__prev-next-icon"
|
||||
title="Forrige"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
{getSteps({ page, count, siblingCount, boundaryCount }).map(
|
||||
(step, i) => {
|
||||
const n = Number(step);
|
||||
return isNaN(n) ? (
|
||||
<li className="navds-pagination__ellipsis" key={`${step}${i}`}>
|
||||
<BodyShort size={size}>...</BodyShort>
|
||||
</li>
|
||||
) : (
|
||||
<li key={step}>
|
||||
<BodyShort
|
||||
size={size}
|
||||
as="button"
|
||||
className="navds-pagination__item"
|
||||
onClick={() => onPageChange(n)}
|
||||
aria-current={page === n ? true : undefined}
|
||||
>
|
||||
{n}
|
||||
</BodyShort>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
)}
|
||||
{!prevNextTexts && page !== count && (
|
||||
<li>
|
||||
<button
|
||||
className="navds-pagination__prev-next"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
>
|
||||
<Next
|
||||
className="navds-pagination__prev-next-icon"
|
||||
title="Neste"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{prevNextTexts && page !== count && (
|
||||
<button
|
||||
className="navds-pagination__prev-next"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
>
|
||||
<BodyShort size={size} className="navds-pagination__next-text">
|
||||
Neste
|
||||
</BodyShort>
|
||||
<Next
|
||||
className="navds-pagination__prev-next-icon"
|
||||
role="presentation"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Pagination } from "./Pagination";
|
||||
export * from "./Pagination";
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { useState } from "react";
|
||||
import Pagination from "./Pagination";
|
||||
|
||||
export default {
|
||||
title: "ds-react/pagination",
|
||||
component: Pagination,
|
||||
};
|
||||
|
||||
export const All = (props) => {
|
||||
const [page, setPage] = useState(props.page);
|
||||
return (
|
||||
<div>
|
||||
<h2>Pagination</h2>
|
||||
<Pagination {...props} page={page} onPageChange={setPage} />
|
||||
|
||||
<h2>Small</h2>
|
||||
<Pagination size="small" {...props} page={page} onPageChange={setPage} />
|
||||
|
||||
<h2>prevNextTexts</h2>
|
||||
<Pagination prevNextTexts {...props} page={page} onPageChange={setPage} />
|
||||
<h3>prevNextTexts small</h3>
|
||||
<Pagination
|
||||
prevNextTexts
|
||||
size="small"
|
||||
{...props}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
All.args = {
|
||||
page: 1,
|
||||
count: 8,
|
||||
siblingCount: 1,
|
||||
boundaryCount: 1,
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import faker from "faker";
|
||||
import { getSteps } from "./Pagination";
|
||||
|
||||
describe("getSteps", () => {
|
||||
it("lists all pages when count is <= 7", () => {
|
||||
const count = faker.datatype.number({ min: 1, max: 7 });
|
||||
expect(getSteps({ page: 1, count })).toEqual(
|
||||
Array.from({ length: count }, (_, i) => i + 1)
|
||||
);
|
||||
});
|
||||
|
||||
it("has an end ellipsis when count >= 8", () => {
|
||||
const count = faker.datatype.number({ min: 8 });
|
||||
const page = faker.datatype.number({ min: 1, max: 4 });
|
||||
expect(
|
||||
getSteps({
|
||||
page,
|
||||
count,
|
||||
})
|
||||
).toEqual([1, 2, 3, 4, 5, "ellipsis", count]);
|
||||
});
|
||||
|
||||
it("has a start ellipsis when count - page >= 3", () => {
|
||||
const count = faker.datatype.number({ min: 8 });
|
||||
const page = faker.datatype.number({ min: count - 3, max: count });
|
||||
expect(
|
||||
getSteps({
|
||||
page,
|
||||
count,
|
||||
})
|
||||
).toEqual([
|
||||
1,
|
||||
"ellipsis",
|
||||
count - 4,
|
||||
count - 3,
|
||||
count - 2,
|
||||
count - 1,
|
||||
count,
|
||||
]);
|
||||
});
|
||||
|
||||
it("has start & end ellipsis when count is high", () => {
|
||||
const count = faker.datatype.number({ min: 9 });
|
||||
const page = faker.datatype.number({ min: 5, max: count - 4 });
|
||||
expect(
|
||||
getSteps({
|
||||
page,
|
||||
count,
|
||||
})
|
||||
).toEqual([1, "ellipsis", page - 1, page, page + 1, "ellipsis", count]);
|
||||
});
|
||||
|
||||
it("can have a reduced siblingCount", () => {
|
||||
const count = faker.datatype.number({ min: 7 });
|
||||
const page = faker.datatype.number({ min: 4, max: count - 3 });
|
||||
expect(
|
||||
getSteps({
|
||||
page,
|
||||
count,
|
||||
siblingCount: 0,
|
||||
})
|
||||
).toEqual([1, "ellipsis", page, "ellipsis", count]);
|
||||
});
|
||||
|
||||
it("can have an increased siblingCount", () => {
|
||||
const count = faker.datatype.number({ min: 11 });
|
||||
const page = faker.datatype.number({ min: 6, max: count - 5 });
|
||||
expect(
|
||||
getSteps({
|
||||
page,
|
||||
count,
|
||||
siblingCount: 2,
|
||||
})
|
||||
).toEqual([
|
||||
1,
|
||||
"ellipsis",
|
||||
page - 2,
|
||||
page - 1,
|
||||
page,
|
||||
page + 1,
|
||||
page + 2,
|
||||
"ellipsis",
|
||||
count,
|
||||
]);
|
||||
});
|
||||
|
||||
it("can have an reduced boundaryCount", () => {
|
||||
const count = faker.datatype.number({ min: 7 });
|
||||
const page = faker.datatype.number({ min: 4, max: count - 3 });
|
||||
expect(
|
||||
getSteps({
|
||||
page,
|
||||
count,
|
||||
boundaryCount: 0,
|
||||
})
|
||||
).toEqual(["ellipsis", page - 1, page, page + 1, "ellipsis"]);
|
||||
});
|
||||
|
||||
it("can have an increased boundaryCount", () => {
|
||||
const count = faker.datatype.number({ min: 11 });
|
||||
const page = faker.datatype.number({ min: 6, max: count - 5 });
|
||||
expect(
|
||||
getSteps({
|
||||
page,
|
||||
count,
|
||||
boundaryCount: 2,
|
||||
})
|
||||
).toEqual([
|
||||
1,
|
||||
2,
|
||||
"ellipsis",
|
||||
page - 1,
|
||||
page,
|
||||
page + 1,
|
||||
"ellipsis",
|
||||
count - 1,
|
||||
count,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -3,7 +3,13 @@ import cl from "classnames";
|
||||
import { BodyShort } from "..";
|
||||
import { TableContext } from ".";
|
||||
|
||||
interface DataCellProps extends React.HTMLAttributes<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}
|
||||
>
|
||||
|
||||
@@ -2,8 +2,14 @@ import React, { forwardRef, useContext } from "react";
|
||||
import cl from "classnames";
|
||||
import { Label, TableContext } from "..";
|
||||
|
||||
interface HeaderCellProps extends React.HTMLAttributes<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}
|
||||
>
|
||||
|
||||
@@ -3,10 +3,17 @@ 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 TableProps extends React.HTMLAttributes<HTMLTableElement> {
|
||||
export interface SortState {
|
||||
orderBy: string;
|
||||
direction: "ascending" | "descending";
|
||||
}
|
||||
|
||||
export interface TableProps
|
||||
extends React.TableHTMLAttributes<HTMLTableElement> {
|
||||
/**
|
||||
* Changes padding
|
||||
* @default "medium"
|
||||
@@ -17,6 +24,14 @@ export interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
|
||||
* @default false
|
||||
*/
|
||||
zebraStripes?: boolean;
|
||||
/**
|
||||
* Sort state
|
||||
*/
|
||||
sort?: SortState;
|
||||
/**
|
||||
* Callback whens sort state changes
|
||||
*/
|
||||
onSortChange?: (sortKey?: string) => void;
|
||||
}
|
||||
|
||||
export interface TableType
|
||||
@@ -28,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}
|
||||
@@ -53,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> "}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import React, { useState } from "react";
|
||||
import { Attachment, Hamburger, Star, System } from "@navikt/ds-icons";
|
||||
import { ToggleGroup } from "../index";
|
||||
import { Meta } from "@storybook/react/types-6-0";
|
||||
export default {
|
||||
title: "ds-react/toggle-group",
|
||||
component: ToggleGroup,
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Items = (icon?: boolean, both?: boolean) => (
|
||||
<>
|
||||
<ToggleGroup.Item value="first">
|
||||
{both ? (
|
||||
<>
|
||||
<Hamburger /> First
|
||||
</>
|
||||
) : (
|
||||
<>{icon ? <Hamburger /> : "First"}</>
|
||||
)}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="second">
|
||||
{both ? (
|
||||
<>
|
||||
<Star /> Second more txt
|
||||
</>
|
||||
) : (
|
||||
<>{icon ? <Star /> : "Second more txt"}</>
|
||||
)}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="third">
|
||||
{both ? (
|
||||
<>
|
||||
<Attachment /> Thrid
|
||||
</>
|
||||
) : (
|
||||
<>{icon ? <Attachment /> : "Third"}</>
|
||||
)}
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="fourth">
|
||||
{both ? (
|
||||
<>
|
||||
<System /> Fourth
|
||||
</>
|
||||
) : (
|
||||
<>{icon ? <System /> : "Fourth"}</>
|
||||
)}
|
||||
</ToggleGroup.Item>
|
||||
</>
|
||||
);
|
||||
|
||||
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");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>ToggleGroup</h2>
|
||||
<h3>{activeValue}</h3>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
<ToggleGroup value={activeValue} onChange={setActiveValue}>
|
||||
{Items()}
|
||||
</ToggleGroup>
|
||||
<ToggleGroup value={activeValue} onChange={setActiveValue}>
|
||||
{Items(true)}
|
||||
</ToggleGroup>
|
||||
<ToggleGroup value={activeValue} onChange={setActiveValue}>
|
||||
{Items(true, true)}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<h2>ToggleGroup Small</h2>
|
||||
<h3>{activeValue}</h3>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
<ToggleGroup size="small" value={activeValue} onChange={setActiveValue}>
|
||||
{Items()}
|
||||
</ToggleGroup>
|
||||
<ToggleGroup size="small" value={activeValue} onChange={setActiveValue}>
|
||||
{Items(true)}
|
||||
</ToggleGroup>
|
||||
<ToggleGroup size="small" value={activeValue} onChange={setActiveValue}>
|
||||
{Items(true, true)}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<h2>ToggleGroup label</h2>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
<ToggleGroup
|
||||
label="Label msg"
|
||||
value={activeValue}
|
||||
onChange={setActiveValue}
|
||||
>
|
||||
{Items()}
|
||||
</ToggleGroup>
|
||||
<ToggleGroup
|
||||
label="Label msg"
|
||||
value={activeValue}
|
||||
onChange={setActiveValue}
|
||||
aria-describedby="demo-id"
|
||||
>
|
||||
{Items()}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
<h2>Uncontrolled</h2>
|
||||
<ToggleGroup onChange={setActiveValue} defaultValue="second">
|
||||
{Items()}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import cl from "classnames";
|
||||
import React, {
|
||||
createContext,
|
||||
forwardRef,
|
||||
HTMLAttributes,
|
||||
useState,
|
||||
} from "react";
|
||||
import ToggleItem, { ToggleItemType } from "./ToggleItem";
|
||||
import * as RadixToggleGroup from "@radix-ui/react-toggle-group";
|
||||
import { Label, useId } from "..";
|
||||
|
||||
export interface ToggleGroupProps
|
||||
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "dir"> {
|
||||
/**
|
||||
* Toggles.Item elements
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* Changes padding and font-size
|
||||
* @default "medium"
|
||||
*/
|
||||
size?: "medium" | "small";
|
||||
/**
|
||||
* Controlled selected value
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* If not controlled, a default-value needs to be set
|
||||
*/
|
||||
defaultValue?: string;
|
||||
/**
|
||||
* Returns elements that wants to be active
|
||||
*/
|
||||
onChange: (value: string) => void;
|
||||
/**
|
||||
* Label describing ToggleGroup
|
||||
*/
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ToggleGroupComponent
|
||||
extends React.ForwardRefExoticComponent<
|
||||
ToggleGroupProps & React.RefAttributes<HTMLDivElement>
|
||||
> {
|
||||
Item: ToggleItemType;
|
||||
}
|
||||
|
||||
interface ToggleContextProps {
|
||||
size: "medium" | "small";
|
||||
}
|
||||
|
||||
export const ToggleGroupContext = createContext<ToggleContextProps | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
onChange,
|
||||
size = "medium",
|
||||
label,
|
||||
value,
|
||||
defaultValue,
|
||||
id,
|
||||
"aria-describedby": desc,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [groupValue, setGroupValue] = useState(defaultValue);
|
||||
const labelId = `toggle-group-label-${useId()}`;
|
||||
|
||||
const handleValueChange = (v: string) => {
|
||||
if (v !== "") {
|
||||
setGroupValue(v);
|
||||
onChange?.(v);
|
||||
}
|
||||
};
|
||||
|
||||
if (!value && !defaultValue) {
|
||||
console.error("ToggleGroup without value/defaultvalue is not allowed");
|
||||
}
|
||||
|
||||
const describeBy = cl({
|
||||
[desc ?? ""]: !!desc,
|
||||
[labelId]: !!label,
|
||||
});
|
||||
|
||||
if (!value && !defaultValue) {
|
||||
console.error("ToggleGroup needs either a value or defaultValue");
|
||||
}
|
||||
|
||||
return (
|
||||
<ToggleGroupContext.Provider
|
||||
value={{
|
||||
size,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{label && (
|
||||
<Label
|
||||
size={size}
|
||||
className="navds-toggle-group__label"
|
||||
id={labelId}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<RadixToggleGroup.Root
|
||||
{...rest}
|
||||
onValueChange={handleValueChange}
|
||||
value={value ?? groupValue}
|
||||
defaultValue={defaultValue}
|
||||
ref={ref}
|
||||
className={cl(
|
||||
"navds-toggle-group",
|
||||
className,
|
||||
`navds-toggle-group--${size}`
|
||||
)}
|
||||
{...(describeBy && { "aria-describedby": describeBy })}
|
||||
role="radiogroup"
|
||||
type="single"
|
||||
>
|
||||
{children}
|
||||
</RadixToggleGroup.Root>
|
||||
</div>
|
||||
</ToggleGroupContext.Provider>
|
||||
);
|
||||
}
|
||||
) as ToggleGroupComponent;
|
||||
|
||||
ToggleGroup.Item = ToggleItem;
|
||||
|
||||
export default ToggleGroup;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { forwardRef, useContext } from "react";
|
||||
import cl from "classnames";
|
||||
import { Label } from "..";
|
||||
import * as RadixToggleGroup from "@radix-ui/react-toggle-group";
|
||||
import { ToggleGroupContext } from "./ToggleGroup";
|
||||
|
||||
export interface ToggleItemProps
|
||||
extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
/**
|
||||
* Content
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* Value for state-handling
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ToggleItemType = React.ForwardRefExoticComponent<
|
||||
ToggleItemProps & React.RefAttributes<HTMLButtonElement>
|
||||
>;
|
||||
|
||||
const ToggleItem = forwardRef<HTMLButtonElement, ToggleItemProps>(
|
||||
({ className, children, ...rest }, ref) => {
|
||||
const context = useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<RadixToggleGroup.Item
|
||||
{...rest}
|
||||
ref={ref}
|
||||
className={cl("navds-toggle-group__button", className)}
|
||||
>
|
||||
<Label
|
||||
as="span"
|
||||
className="navds-toggle-group__button-inner"
|
||||
size={context?.size}
|
||||
>
|
||||
{children}
|
||||
</Label>
|
||||
</RadixToggleGroup.Item>
|
||||
);
|
||||
}
|
||||
) as ToggleItemType;
|
||||
|
||||
export default ToggleItem;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as ToggleGroup } from "./ToggleGroup";
|
||||
export { ToggleGroupProps } from "./ToggleGroup";
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tailwind",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.6",
|
||||
"private": false,
|
||||
"description": "Auto-generated tailwind config based on @navikt/ds-tokens",
|
||||
"author": "NAV Designsystem team",
|
||||
@@ -23,7 +23,7 @@
|
||||
"directory": "@navikt/core/tailwind"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^0.8.1",
|
||||
"@navikt/ds-tokens": "^0.8.4",
|
||||
"@types/jest": "^27.0.1",
|
||||
"jest": "^27.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-ignore
|
||||
import * as TokensBuild from "@navikt/ds-tokens/dist/tokens";
|
||||
import { writeFileSync } from "fs";
|
||||
import { kebabCase } from "lodash";
|
||||
import kebabCase from "./kebabCase";
|
||||
import Reducer from "./reducer";
|
||||
|
||||
const tokens = Object.entries(TokensBuild).reduce(
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
const _ = require("lodash");
|
||||
|
||||
const kebabCase = (string) =>
|
||||
_.kebabCase(string).replace(/(^|-)(\d+)-(x[ls])/g, "$1$2$3");
|
||||
|
||||
export default kebabCase;
|
||||
@@ -0,0 +1,26 @@
|
||||
import kebabCase from "../kebabCase";
|
||||
|
||||
describe("kebabCase", () => {
|
||||
[
|
||||
"foo bar",
|
||||
"Foo bar",
|
||||
"foo Bar",
|
||||
"Foo Bar",
|
||||
"FOO BAR",
|
||||
"fooBar",
|
||||
"--foo-bar--",
|
||||
"__foo_bar__",
|
||||
].forEach((string) =>
|
||||
test(`${string} is converted to foo-bar`, () => {
|
||||
expect(kebabCase(string)).toBe("foo-bar");
|
||||
})
|
||||
);
|
||||
|
||||
test(`spacing 3xl is converted to spacing-3xl`, () => {
|
||||
expect(kebabCase("spacing 3xl")).toBe("spacing-3xl");
|
||||
});
|
||||
|
||||
test(`3xs is converted to 3xs`, () => {
|
||||
expect(kebabCase("3xs")).toBe("3xs");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tokens",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.4",
|
||||
"private": false,
|
||||
"description": "Auto-generated design-tokens for NAV design-systems framework",
|
||||
"author": "NAV Designsystem team",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"navds": {
|
||||
"border": {
|
||||
"radius": {
|
||||
"small": { "value": "2px" },
|
||||
"medium": { "value": "4px" },
|
||||
"large": { "value": "8px" },
|
||||
"xlarge": { "value": "12px" },
|
||||
"full": { "value": "9999px" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,19 +39,6 @@ module.exports = {
|
||||
regular: { value: "400" },
|
||||
},
|
||||
},
|
||||
shadow: {
|
||||
focus: { value: "0 0 0 3px {navds.semantic.color.focus.@.value}" },
|
||||
"focus-inverted": {
|
||||
value: "0 0 0 3px {navds.semantic.color.focus.inverted.value}",
|
||||
},
|
||||
popover: {
|
||||
value: "0 2px 4px 0 {navds.global.color.gray.200.value}",
|
||||
},
|
||||
card: {
|
||||
value:
|
||||
"0 1px 3px 0 rgba(38,38,38,0.2),0 2px 1px 0 rgba(38,38,38,0.12),0 1px 1px 0 rgba(38,38,38,0.14)",
|
||||
},
|
||||
},
|
||||
"z-index": {
|
||||
modal: { value: 2000 },
|
||||
popover: { value: 1000 },
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"navds": {
|
||||
"shadow": {
|
||||
"xsmall": {
|
||||
"value": "0 1px 2px 0 rgba(0, 0, 0, 0.5)"
|
||||
},
|
||||
"small": {
|
||||
"value": "0 1px 2px -1px rgba(0, 0, 0, 0.05), 0 1px 3px 0 rgba(0, 0, 0, 0.1)"
|
||||
},
|
||||
"medium": {
|
||||
"value": "0 2px 4px -2px rgba(0, 0, 0, 0.05), 0 4px 6px -1px rgba(0, 0, 0, 0.1)"
|
||||
},
|
||||
"large": {
|
||||
"value": "0 4px 6px -4px rgba(0, 0, 0, 0.05), 0 10px 15px -3px rgba(0, 0, 0, 0.1)"
|
||||
},
|
||||
"xlarge": {
|
||||
"value": "0 8px 10px -6px rgba(0, 0, 0, 0.05), 0 25px 50px -5px rgba(0, 0, 0, 0.1)"
|
||||
},
|
||||
"focus": {
|
||||
"value": "0 0 0 3px {navds.semantic.color.focus.@.value}"
|
||||
},
|
||||
"focus-inverted": {
|
||||
"value": "0 0 0 3px {navds.semantic.color.focus.inverted.value}"
|
||||
},
|
||||
"popover": {
|
||||
"value": "0 2px 4px 0 {navds.global.color.gray.200.value}"
|
||||
},
|
||||
"card": {
|
||||
"value": "0 1px 3px 0 rgba(38,38,38,0.2),0 2px 1px 0 rgba(38,38,38,0.12),0 1px 1px 0 rgba(38,38,38,0.14)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react-internal",
|
||||
"version": "0.11.2",
|
||||
"version": "0.12.2",
|
||||
"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.2",
|
||||
"@navikt/ds-react": "^0.16.2",
|
||||
"@navikt/ds-icons": "^0.8.4",
|
||||
"@navikt/ds-react": "^0.17.2",
|
||||
"@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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||