mirror of
https://github.com/navikt/aksel.git
synced 2026-10-15 02:02:02 +00:00
Compare commits
48
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 |
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";
|
||||
@@ -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.3",
|
||||
"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;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "0.16.7",
|
||||
"version": "0.17.2",
|
||||
"private": false,
|
||||
"description": "NAV designsystem react components",
|
||||
"author": "NAV Designsystem team",
|
||||
@@ -37,6 +37,7 @@
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@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": {
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface ButtonProps
|
||||
* Changes padding, height and font-size
|
||||
* @default "medium"
|
||||
*/
|
||||
size?: "medium" | "small";
|
||||
size?: "medium" | "small" | "xsmall";
|
||||
/**
|
||||
* Prevent the user from interacting with the button: it cannot be pressed or focused.
|
||||
* @note Avoid using if possible for accessibility purposes
|
||||
@@ -43,6 +43,7 @@ const Button: OverridableComponent<ButtonProps, HTMLButtonElement> = forwardRef(
|
||||
size = "medium",
|
||||
loading = false,
|
||||
disabled,
|
||||
style,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
@@ -76,13 +77,16 @@ const Button: OverridableComponent<ButtonProps, HTMLButtonElement> = forwardRef(
|
||||
"navds-button--loading": widthOverride,
|
||||
}
|
||||
)}
|
||||
style={{ width: widthOverride }}
|
||||
style={{
|
||||
...style,
|
||||
width: widthOverride,
|
||||
}}
|
||||
disabled={disabled ?? widthOverride ? true : undefined}
|
||||
>
|
||||
<BodyShort
|
||||
as="span"
|
||||
className="navds-button__inner"
|
||||
size={size}
|
||||
size={size === "medium" ? "medium" : "small"}
|
||||
aria-live="polite"
|
||||
>
|
||||
{widthOverride ? <Loader size={size} /> : children}
|
||||
|
||||
@@ -81,6 +81,14 @@ export const All = () => {
|
||||
</Button>
|
||||
))}
|
||||
</Section>
|
||||
<h2>Xsmall</h2>
|
||||
<Section>
|
||||
{variants.map((variant) => (
|
||||
<Button key={variant} variant={variant} size="xsmall">
|
||||
{varSwitch[variant]}
|
||||
</Button>
|
||||
))}
|
||||
</Section>
|
||||
<h2>Button w/icon</h2>
|
||||
<Section>
|
||||
{variants.map((variant) => (
|
||||
|
||||
@@ -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,9 +33,18 @@ export interface ModalProps {
|
||||
* @default true
|
||||
*/
|
||||
closeButton?: boolean;
|
||||
/**
|
||||
* Callback for getting parent element modal will attach to
|
||||
*/
|
||||
parentSelector?(): HTMLElement;
|
||||
"aria-labelledby"?: string;
|
||||
"aria-describedby"?: string;
|
||||
"aria-modal"?: boolean;
|
||||
/**
|
||||
* Sets aria-label on modal
|
||||
* @warning This should be set if not using 'aria-labelledby' or 'aria-describedby'
|
||||
*/
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
interface ModalComponent
|
||||
@@ -62,6 +71,7 @@ const Modal = forwardRef<ReactModal, ModalProps>(
|
||||
"aria-describedby": ariaDescribedBy,
|
||||
"aria-labelledby": ariaLabelledBy,
|
||||
"aria-modal": ariaModal,
|
||||
"aria-label": contentLabel,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
@@ -92,6 +102,7 @@ const Modal = forwardRef<ReactModal, ModalProps>(
|
||||
labelledby: ariaLabelledBy,
|
||||
modal: ariaModal,
|
||||
}}
|
||||
contentLabel={contentLabel}
|
||||
>
|
||||
{children}
|
||||
{closeButton && (
|
||||
|
||||
+9
-5
@@ -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.TdHTMLAttributes<HTMLTableCellElement> {}
|
||||
interface DataCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
|
||||
/**
|
||||
* Content alignment
|
||||
* @default "left"
|
||||
*/
|
||||
align?: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
export interface DataCellType
|
||||
extends React.ForwardRefExoticComponent<
|
||||
@@ -11,14 +17,16 @@ export interface DataCellType
|
||||
> {}
|
||||
|
||||
const DataCell: DataCellType = forwardRef(
|
||||
({ className, children = "", ...rest }, ref) => {
|
||||
({ className, children = "", align, ...rest }, ref) => {
|
||||
const context = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<BodyShort
|
||||
as="td"
|
||||
ref={ref}
|
||||
className={cl("navds-table__data-cell", className)}
|
||||
className={cl("navds-table__data-cell", className, {
|
||||
[`navds-table__data-cell--align-${align}`]: align,
|
||||
})}
|
||||
size={context?.size}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -2,8 +2,14 @@ import React, { forwardRef, useContext } from "react";
|
||||
import cl from "classnames";
|
||||
import { Label, TableContext } from "..";
|
||||
|
||||
interface HeaderCellProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
|
||||
export interface HeaderCellProps
|
||||
extends React.ThHTMLAttributes<HTMLTableCellElement> {
|
||||
scope?: string;
|
||||
/**
|
||||
* Content alignment
|
||||
* @default "left"
|
||||
*/
|
||||
align?: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
export interface HeaderCellType
|
||||
@@ -12,14 +18,16 @@ export interface HeaderCellType
|
||||
> {}
|
||||
|
||||
const HeaderCell: HeaderCellType = forwardRef(
|
||||
({ className, children, ...rest }, ref) => {
|
||||
({ className, children, align, ...rest }, ref) => {
|
||||
const context = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<Label
|
||||
as="th"
|
||||
ref={ref}
|
||||
className={cl("navds-table__header-cell", className)}
|
||||
className={cl("navds-table__header-cell", className, {
|
||||
[`navds-table__header-cell--align-${align}`]: align,
|
||||
})}
|
||||
size={context?.size}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -3,9 +3,15 @@ import cl from "classnames";
|
||||
import Header, { HeaderType } from "./Header";
|
||||
import Body, { BodyType } from "./Body";
|
||||
import Row, { RowType } from "./Row";
|
||||
import ColumnHeader, { ColumnHeaderType } from "./ColumnHeader";
|
||||
import HeaderCell, { HeaderCellType } from "./HeaderCell";
|
||||
import DataCell, { DataCellType } from "./DataCell";
|
||||
|
||||
export interface SortState {
|
||||
orderBy: string;
|
||||
direction: "ascending" | "descending";
|
||||
}
|
||||
|
||||
export interface TableProps
|
||||
extends React.TableHTMLAttributes<HTMLTableElement> {
|
||||
/**
|
||||
@@ -18,6 +24,14 @@ export interface TableProps
|
||||
* @default false
|
||||
*/
|
||||
zebraStripes?: boolean;
|
||||
/**
|
||||
* Sort state
|
||||
*/
|
||||
sort?: SortState;
|
||||
/**
|
||||
* Callback whens sort state changes
|
||||
*/
|
||||
onSortChange?: (sortKey?: string) => void;
|
||||
}
|
||||
|
||||
export interface TableType
|
||||
@@ -29,17 +43,30 @@ export interface TableType
|
||||
Row: RowType;
|
||||
DataCell: DataCellType;
|
||||
HeaderCell: HeaderCellType;
|
||||
ColumnHeader: ColumnHeaderType;
|
||||
}
|
||||
|
||||
export interface TableContextProps {
|
||||
size: "medium" | "small";
|
||||
onSortChange?: (sortKey: string) => void;
|
||||
sort?: SortState;
|
||||
}
|
||||
|
||||
export const TableContext = createContext<TableContextProps | null>(null);
|
||||
|
||||
const Table = forwardRef(
|
||||
({ className, zebraStripes = false, size = "medium", ...rest }, ref) => (
|
||||
<TableContext.Provider value={{ size }}>
|
||||
(
|
||||
{
|
||||
className,
|
||||
zebraStripes = false,
|
||||
size = "medium",
|
||||
onSortChange,
|
||||
sort,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<TableContext.Provider value={{ size, onSortChange, sort }}>
|
||||
<table
|
||||
{...rest}
|
||||
ref={ref}
|
||||
@@ -54,6 +81,7 @@ const Table = forwardRef(
|
||||
Table.Header = Header;
|
||||
Table.Body = Body;
|
||||
Table.Row = Row;
|
||||
Table.ColumnHeader = ColumnHeader;
|
||||
Table.HeaderCell = HeaderCell;
|
||||
Table.DataCell = DataCell;
|
||||
|
||||
|
||||
@@ -0,0 +1,822 @@
|
||||
[
|
||||
{
|
||||
"name": "Luke Skywalker",
|
||||
"height": 172,
|
||||
"mass": 77,
|
||||
"hair_color": "blond",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 19,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "C-3PO",
|
||||
"height": 167,
|
||||
"mass": 75,
|
||||
"hair_color": "n/a",
|
||||
"skin_color": "gold",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": 112,
|
||||
"gender": "n/a"
|
||||
},
|
||||
{
|
||||
"name": "R2-D2",
|
||||
"height": 96,
|
||||
"mass": 32,
|
||||
"hair_color": "n/a",
|
||||
"skin_color": "white, blue",
|
||||
"eye_color": "red",
|
||||
"birth_year": 33,
|
||||
"gender": "n/a"
|
||||
},
|
||||
{
|
||||
"name": "Darth Vader",
|
||||
"height": 202,
|
||||
"mass": 136,
|
||||
"hair_color": "none",
|
||||
"skin_color": "white",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": 41.9,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Leia Organa",
|
||||
"height": 150,
|
||||
"mass": 49,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "light",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 19,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Owen Lars",
|
||||
"height": 178,
|
||||
"mass": 120,
|
||||
"hair_color": "brown, grey",
|
||||
"skin_color": "light",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 52,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Beru Whitesun lars",
|
||||
"height": 165,
|
||||
"mass": 75,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "light",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 47,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "R5-D4",
|
||||
"height": 97,
|
||||
"mass": 32,
|
||||
"hair_color": "n/a",
|
||||
"skin_color": "white, red",
|
||||
"eye_color": "red",
|
||||
"birth_year": null,
|
||||
"gender": "n/a"
|
||||
},
|
||||
{
|
||||
"name": "Biggs Darklighter",
|
||||
"height": 183,
|
||||
"mass": 84,
|
||||
"hair_color": "black",
|
||||
"skin_color": "light",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 24,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Obi-Wan Kenobi",
|
||||
"height": 182,
|
||||
"mass": 77,
|
||||
"hair_color": "auburn, white",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue-gray",
|
||||
"birth_year": 57,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Anakin Skywalker",
|
||||
"height": 188,
|
||||
"mass": 84,
|
||||
"hair_color": "blond",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 41.9,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Wilhuff Tarkin",
|
||||
"height": 180,
|
||||
"mass": null,
|
||||
"hair_color": "auburn, grey",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 64,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Chewbacca",
|
||||
"height": 228,
|
||||
"mass": 112,
|
||||
"hair_color": "brown",
|
||||
"skin_color": null,
|
||||
"eye_color": "blue",
|
||||
"birth_year": 200,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Han Solo",
|
||||
"height": 180,
|
||||
"mass": 80,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 29,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Greedo",
|
||||
"height": 173,
|
||||
"mass": 74,
|
||||
"hair_color": "n/a",
|
||||
"skin_color": "green",
|
||||
"eye_color": "black",
|
||||
"birth_year": 44,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Jabba Desilijic Tiure",
|
||||
"height": 175,
|
||||
"mass": null,
|
||||
"hair_color": "n/a",
|
||||
"skin_color": "green-tan, brown",
|
||||
"eye_color": "orange",
|
||||
"birth_year": 600,
|
||||
"gender": "hermaphrodite"
|
||||
},
|
||||
{
|
||||
"name": "Wedge Antilles",
|
||||
"height": 170,
|
||||
"mass": 77,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "hazel",
|
||||
"birth_year": 21,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Jek Tono Porkins",
|
||||
"height": 180,
|
||||
"mass": 110,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Yoda",
|
||||
"height": 66,
|
||||
"mass": 17,
|
||||
"hair_color": "white",
|
||||
"skin_color": "green",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 896,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Palpatine",
|
||||
"height": 170,
|
||||
"mass": 75,
|
||||
"hair_color": "grey",
|
||||
"skin_color": "pale",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": 82,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Boba Fett",
|
||||
"height": 183,
|
||||
"mass": 78.2,
|
||||
"hair_color": "black",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 31.5,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "IG-88",
|
||||
"height": 200,
|
||||
"mass": 140,
|
||||
"hair_color": "none",
|
||||
"skin_color": "metal",
|
||||
"eye_color": "red",
|
||||
"birth_year": 15,
|
||||
"gender": "none"
|
||||
},
|
||||
{
|
||||
"name": "Bossk",
|
||||
"height": 190,
|
||||
"mass": 113,
|
||||
"hair_color": "none",
|
||||
"skin_color": "green",
|
||||
"eye_color": "red",
|
||||
"birth_year": 53,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Lando Calrissian",
|
||||
"height": 177,
|
||||
"mass": 79,
|
||||
"hair_color": "black",
|
||||
"skin_color": "dark",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 31,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Lobot",
|
||||
"height": 175,
|
||||
"mass": 79,
|
||||
"hair_color": "none",
|
||||
"skin_color": "light",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 37,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Ackbar",
|
||||
"height": 180,
|
||||
"mass": 83,
|
||||
"hair_color": "none",
|
||||
"skin_color": "brown mottle",
|
||||
"eye_color": "orange",
|
||||
"birth_year": 41,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Mon Mothma",
|
||||
"height": 150,
|
||||
"mass": null,
|
||||
"hair_color": "auburn",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 48,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Arvel Crynyd",
|
||||
"height": null,
|
||||
"mass": null,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "brown",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Wicket Systri Warrick",
|
||||
"height": 88,
|
||||
"mass": 20,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "brown",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 8,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Nien Nunb",
|
||||
"height": 160,
|
||||
"mass": 68,
|
||||
"hair_color": "none",
|
||||
"skin_color": "grey",
|
||||
"eye_color": "black",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Qui-Gon Jinn",
|
||||
"height": 193,
|
||||
"mass": 89,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 92,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Nute Gunray",
|
||||
"height": 191,
|
||||
"mass": 90,
|
||||
"hair_color": "none",
|
||||
"skin_color": "mottled green",
|
||||
"eye_color": "red",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Finis Valorum",
|
||||
"height": 170,
|
||||
"mass": null,
|
||||
"hair_color": "blond",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 91,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Padmé Amidala",
|
||||
"height": 185,
|
||||
"mass": 45,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "light",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 46,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Jar Jar Binks",
|
||||
"height": 196,
|
||||
"mass": 66,
|
||||
"hair_color": "none",
|
||||
"skin_color": "orange",
|
||||
"eye_color": "orange",
|
||||
"birth_year": 52,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Roos Tarpals",
|
||||
"height": 224,
|
||||
"mass": 82,
|
||||
"hair_color": "none",
|
||||
"skin_color": "grey",
|
||||
"eye_color": "orange",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Rugor Nass",
|
||||
"height": 206,
|
||||
"mass": null,
|
||||
"hair_color": "none",
|
||||
"skin_color": "green",
|
||||
"eye_color": "orange",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Ric Olié",
|
||||
"height": 183,
|
||||
"mass": null,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Watto",
|
||||
"height": 137,
|
||||
"mass": null,
|
||||
"hair_color": "black",
|
||||
"skin_color": "blue, grey",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Sebulba",
|
||||
"height": 112,
|
||||
"mass": 40,
|
||||
"hair_color": "none",
|
||||
"skin_color": "grey, red",
|
||||
"eye_color": "orange",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Quarsh Panaka",
|
||||
"height": 183,
|
||||
"mass": null,
|
||||
"hair_color": "black",
|
||||
"skin_color": "dark",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 62,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Shmi Skywalker",
|
||||
"height": 163,
|
||||
"mass": null,
|
||||
"hair_color": "black",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 72,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Darth Maul",
|
||||
"height": 175,
|
||||
"mass": 80,
|
||||
"hair_color": "none",
|
||||
"skin_color": "red",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": 54,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Bib Fortuna",
|
||||
"height": 180,
|
||||
"mass": null,
|
||||
"hair_color": "none",
|
||||
"skin_color": "pale",
|
||||
"eye_color": "pink",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Ayla Secura",
|
||||
"height": 178,
|
||||
"mass": 55,
|
||||
"hair_color": "none",
|
||||
"skin_color": "blue",
|
||||
"eye_color": "hazel",
|
||||
"birth_year": 48,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Ratts Tyerel",
|
||||
"height": 79,
|
||||
"mass": 15,
|
||||
"hair_color": "none",
|
||||
"skin_color": "grey, blue",
|
||||
"eye_color": null,
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Dud Bolt",
|
||||
"height": 94,
|
||||
"mass": 45,
|
||||
"hair_color": "none",
|
||||
"skin_color": "blue, grey",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Gasgano",
|
||||
"height": 122,
|
||||
"mass": null,
|
||||
"hair_color": "none",
|
||||
"skin_color": "white, blue",
|
||||
"eye_color": "black",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Ben Quadinaros",
|
||||
"height": 163,
|
||||
"mass": 65,
|
||||
"hair_color": "none",
|
||||
"skin_color": "grey, green, yellow",
|
||||
"eye_color": "orange",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Mace Windu",
|
||||
"height": 188,
|
||||
"mass": 84,
|
||||
"hair_color": "none",
|
||||
"skin_color": "dark",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 72,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Ki-Adi-Mundi",
|
||||
"height": 198,
|
||||
"mass": 82,
|
||||
"hair_color": "white",
|
||||
"skin_color": "pale",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": 92,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Kit Fisto",
|
||||
"height": 196,
|
||||
"mass": 87,
|
||||
"hair_color": "none",
|
||||
"skin_color": "green",
|
||||
"eye_color": "black",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Eeth Koth",
|
||||
"height": 171,
|
||||
"mass": null,
|
||||
"hair_color": "black",
|
||||
"skin_color": "brown",
|
||||
"eye_color": "brown",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Adi Gallia",
|
||||
"height": 184,
|
||||
"mass": 50,
|
||||
"hair_color": "none",
|
||||
"skin_color": "dark",
|
||||
"eye_color": "blue",
|
||||
"birth_year": null,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Saesee Tiin",
|
||||
"height": 188,
|
||||
"mass": null,
|
||||
"hair_color": "none",
|
||||
"skin_color": "pale",
|
||||
"eye_color": "orange",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Yarael Poof",
|
||||
"height": 264,
|
||||
"mass": null,
|
||||
"hair_color": "none",
|
||||
"skin_color": "white",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Plo Koon",
|
||||
"height": 188,
|
||||
"mass": 80,
|
||||
"hair_color": "none",
|
||||
"skin_color": "orange",
|
||||
"eye_color": "black",
|
||||
"birth_year": 22,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Mas Amedda",
|
||||
"height": 196,
|
||||
"mass": null,
|
||||
"hair_color": "none",
|
||||
"skin_color": "blue",
|
||||
"eye_color": "blue",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Gregar Typho",
|
||||
"height": 185,
|
||||
"mass": 85,
|
||||
"hair_color": "black",
|
||||
"skin_color": "dark",
|
||||
"eye_color": "brown",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Cordé",
|
||||
"height": 157,
|
||||
"mass": null,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "light",
|
||||
"eye_color": "brown",
|
||||
"birth_year": null,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Cliegg Lars",
|
||||
"height": 183,
|
||||
"mass": null,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 82,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Poggle the Lesser",
|
||||
"height": 183,
|
||||
"mass": 80,
|
||||
"hair_color": "none",
|
||||
"skin_color": "green",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Luminara Unduli",
|
||||
"height": 170,
|
||||
"mass": 56.2,
|
||||
"hair_color": "black",
|
||||
"skin_color": "yellow",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 58,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Barriss Offee",
|
||||
"height": 166,
|
||||
"mass": 50,
|
||||
"hair_color": "black",
|
||||
"skin_color": "yellow",
|
||||
"eye_color": "blue",
|
||||
"birth_year": 40,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Dormé",
|
||||
"height": 165,
|
||||
"mass": null,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "light",
|
||||
"eye_color": "brown",
|
||||
"birth_year": null,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Dooku",
|
||||
"height": 193,
|
||||
"mass": 80,
|
||||
"hair_color": "white",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 102,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Bail Prestor Organa",
|
||||
"height": 191,
|
||||
"mass": null,
|
||||
"hair_color": "black",
|
||||
"skin_color": "tan",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 67,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Jango Fett",
|
||||
"height": 183,
|
||||
"mass": 79,
|
||||
"hair_color": "black",
|
||||
"skin_color": "tan",
|
||||
"eye_color": "brown",
|
||||
"birth_year": 66,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Zam Wesell",
|
||||
"height": 168,
|
||||
"mass": 55,
|
||||
"hair_color": "blonde",
|
||||
"skin_color": "fair, green, yellow",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": null,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Dexter Jettster",
|
||||
"height": 198,
|
||||
"mass": 102,
|
||||
"hair_color": "none",
|
||||
"skin_color": "brown",
|
||||
"eye_color": "yellow",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Lama Su",
|
||||
"height": 229,
|
||||
"mass": 88,
|
||||
"hair_color": "none",
|
||||
"skin_color": "grey",
|
||||
"eye_color": "black",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Taun We",
|
||||
"height": 213,
|
||||
"mass": null,
|
||||
"hair_color": "none",
|
||||
"skin_color": "grey",
|
||||
"eye_color": "black",
|
||||
"birth_year": null,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Jocasta Nu",
|
||||
"height": 167,
|
||||
"mass": null,
|
||||
"hair_color": "white",
|
||||
"skin_color": "fair",
|
||||
"eye_color": "blue",
|
||||
"birth_year": null,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "R4-P17",
|
||||
"height": 96,
|
||||
"mass": null,
|
||||
"hair_color": "none",
|
||||
"skin_color": "silver, red",
|
||||
"eye_color": "red, blue",
|
||||
"birth_year": null,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Wat Tambor",
|
||||
"height": 193,
|
||||
"mass": 48,
|
||||
"hair_color": "none",
|
||||
"skin_color": "green, grey",
|
||||
"eye_color": null,
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "San Hill",
|
||||
"height": 191,
|
||||
"mass": null,
|
||||
"hair_color": "none",
|
||||
"skin_color": "grey",
|
||||
"eye_color": "gold",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Shaak Ti",
|
||||
"height": 178,
|
||||
"mass": 57,
|
||||
"hair_color": "none",
|
||||
"skin_color": "red, blue, white",
|
||||
"eye_color": "black",
|
||||
"birth_year": null,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Grievous",
|
||||
"height": 216,
|
||||
"mass": 159,
|
||||
"hair_color": "none",
|
||||
"skin_color": "brown, white",
|
||||
"eye_color": "green, yellow",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Tarfful",
|
||||
"height": 234,
|
||||
"mass": 136,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "brown",
|
||||
"eye_color": "blue",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Raymus Antilles",
|
||||
"height": 188,
|
||||
"mass": 79,
|
||||
"hair_color": "brown",
|
||||
"skin_color": "light",
|
||||
"eye_color": "brown",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"name": "Sly Moore",
|
||||
"height": 178,
|
||||
"mass": 48,
|
||||
"hair_color": "none",
|
||||
"skin_color": "pale",
|
||||
"eye_color": "white",
|
||||
"birth_year": null,
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"name": "Tion Medon",
|
||||
"height": 206,
|
||||
"mass": 80,
|
||||
"hair_color": "none",
|
||||
"skin_color": "grey",
|
||||
"eye_color": "black",
|
||||
"birth_year": null,
|
||||
"gender": "male"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,169 @@
|
||||
import React, { useState } from "react";
|
||||
import { rest } from "msw";
|
||||
import useSWR from "swr";
|
||||
import people from "./people.json";
|
||||
import { Table } from "../index";
|
||||
import { Loader, Pagination, SortState } from "../..";
|
||||
|
||||
export default {
|
||||
title: "ds-react/table",
|
||||
component: Table,
|
||||
parameters: {
|
||||
chromatic: { delay: 500 },
|
||||
},
|
||||
};
|
||||
|
||||
const rowsPerPage = 10;
|
||||
|
||||
const queryString = (obj) =>
|
||||
Object.keys(obj)
|
||||
.filter((key) => obj[key] !== undefined)
|
||||
.map((key) => key + "=" + obj[key])
|
||||
.join("&");
|
||||
|
||||
export const Async = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [sort, setSort] = useState<SortState>();
|
||||
|
||||
const { data } = useSWR(
|
||||
`/people?${queryString({
|
||||
page,
|
||||
sort: sort ? `${sort.orderBy}:${sort.direction}` : undefined,
|
||||
})}`,
|
||||
(url) => fetch(url).then((res) => res.json())
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return <Loader size="2xlarge" />;
|
||||
}
|
||||
|
||||
const { results: people, count } = data;
|
||||
|
||||
const columns = [
|
||||
{ key: "name", name: "Name", width: 154 },
|
||||
{ key: "height", name: "Height", width: 108 },
|
||||
{ key: "mass", name: "Mass", width: 95 },
|
||||
{
|
||||
key: "birth_year",
|
||||
name: "Birth year",
|
||||
value: (person) =>
|
||||
person.birth_year !== null ? `${person.birth_year}BBY` : undefined,
|
||||
width: 133,
|
||||
},
|
||||
{ key: "eye_color", name: "Eye color", width: 127 },
|
||||
{ key: "gender", name: "Gender", width: 113, sortable: false },
|
||||
{ key: "hair_color", name: "Hair color", width: 132 },
|
||||
{ key: "skin_color", name: "Skin color", width: 133 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ overflow: "auto" }}>
|
||||
<Table
|
||||
style={{ width: "initial" }}
|
||||
sort={sort}
|
||||
onSortChange={(sortKey) =>
|
||||
setSort(
|
||||
sort?.orderBy === sortKey && sort.direction === "descending"
|
||||
? undefined
|
||||
: {
|
||||
orderBy: sortKey,
|
||||
direction:
|
||||
sort?.direction === "ascending"
|
||||
? "descending"
|
||||
: "ascending",
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{columns.map(({ key, name, width, sortable = true }) => (
|
||||
<Table.ColumnHeader
|
||||
style={{ width, minWidth: width, maxWidth: width }}
|
||||
key={key}
|
||||
sortable={sortable}
|
||||
sortKey={key}
|
||||
>
|
||||
{name}
|
||||
</Table.ColumnHeader>
|
||||
))}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{people.map((person) => (
|
||||
<Table.Row key={person.name}>
|
||||
{columns.map(({ key, width, value }) => (
|
||||
<Table.DataCell
|
||||
style={{
|
||||
width,
|
||||
minWidth: width,
|
||||
maxWidth: width,
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
title={person[key]}
|
||||
key={key}
|
||||
>
|
||||
{value ? value(person) : person[key]}
|
||||
</Table.DataCell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
<Pagination
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
count={Math.ceil(count / rowsPerPage)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Async.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.get("/people", (req, res, ctx) => {
|
||||
const comparator = (a, b, orderBy) => {
|
||||
if (b[orderBy] < a[orderBy]) {
|
||||
return -1;
|
||||
}
|
||||
if (b[orderBy] > a[orderBy]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const page = Number(req.url.searchParams.get("page"));
|
||||
const sort = req.url.searchParams.get("sort");
|
||||
return res(
|
||||
//ctx.delay(),
|
||||
ctx.json({
|
||||
count: people.length,
|
||||
results: people
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
if (sort) {
|
||||
const [orderBy, direction] = sort.split(":");
|
||||
return direction === "ascending"
|
||||
? comparator(b, a, orderBy)
|
||||
: comparator(a, b, orderBy);
|
||||
}
|
||||
return 1;
|
||||
})
|
||||
.slice((page - 1) * rowsPerPage, page * rowsPerPage),
|
||||
})
|
||||
);
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,376 @@
|
||||
import React, { useState } from "react";
|
||||
import { capitalize } from "@material-ui/core";
|
||||
import { Table } from "../";
|
||||
import { Button } from "../..";
|
||||
import { SortState } from "../..";
|
||||
|
||||
export default {
|
||||
title: "ds-react/table",
|
||||
component: Table,
|
||||
};
|
||||
|
||||
interface Column {
|
||||
key: string;
|
||||
name?: string;
|
||||
sortable?: boolean;
|
||||
render?: (value: any) => React.ReactElement<any, any> | null;
|
||||
value?: (oppgave: any) => any;
|
||||
width?: number;
|
||||
align?: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
export const Hot = () => {
|
||||
const [sort, setSort] = useState<SortState>();
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
key: "eier",
|
||||
name: "Eier",
|
||||
render: (value) =>
|
||||
value ?? (
|
||||
<Button
|
||||
size="xsmall"
|
||||
style={{ marginTop: -2, marginBottom: -2, marginLeft: -6 }}
|
||||
variant="tertiary"
|
||||
>
|
||||
Start saken
|
||||
</Button>
|
||||
),
|
||||
value: (oppgave) => oppgave.saksbehandler?.navn,
|
||||
width: 152,
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
name: "Status",
|
||||
value: (oppgave) =>
|
||||
({
|
||||
ALLE: "Alle",
|
||||
VEDTAK_FATTET: "Innvilget",
|
||||
AVVENTER_SAKSBEHANDLER: "Mottatt",
|
||||
SENDT_GOSYS: "Sendt GOSYS",
|
||||
TILDELT_SAKSBEHANDLER: "Under behandling",
|
||||
}[oppgave.status]),
|
||||
width: 154,
|
||||
},
|
||||
{
|
||||
key: "område",
|
||||
name: "Område",
|
||||
value: (oppgave) =>
|
||||
capitalize(oppgave.personinformasjon.funksjonsnedsettelse[0]),
|
||||
width: 152,
|
||||
},
|
||||
{
|
||||
key: "søknadOm",
|
||||
name: "Søknad om",
|
||||
width: 192,
|
||||
},
|
||||
{
|
||||
key: "hjelpemiddelbruker",
|
||||
name: "Hjelpemiddelbruker",
|
||||
value: (oppgave) =>
|
||||
`${oppgave.personinformasjon.etternavn}, ${oppgave.personinformasjon.fornavn}`,
|
||||
width: 188,
|
||||
},
|
||||
{
|
||||
key: "fødselsnr",
|
||||
name: "Fødselsnr.",
|
||||
value: (oppgave) =>
|
||||
`${oppgave.personinformasjon.fnr.slice(
|
||||
0,
|
||||
6
|
||||
)} ${oppgave.personinformasjon.fnr.slice(6)}`,
|
||||
width: 124,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
key: "bosted",
|
||||
name: "Bosted",
|
||||
value: (oppgave) => oppgave.personinformasjon.bosted,
|
||||
width: 144,
|
||||
},
|
||||
{
|
||||
key: "formidlerNavn",
|
||||
name: "Formidler",
|
||||
width: 164,
|
||||
},
|
||||
{
|
||||
key: "mottattDato",
|
||||
name: "Mottatt dato",
|
||||
width: 124,
|
||||
},
|
||||
{
|
||||
key: "menu",
|
||||
sortable: false,
|
||||
render: (value) => (
|
||||
<span style={{ display: "flex", marginBlock: -2 }}></span>
|
||||
),
|
||||
value: (oppgave) => oppgave.saksbehandler,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ overflow: "auto" }}>
|
||||
<Table
|
||||
size="small"
|
||||
style={{ width: "initial" }}
|
||||
sort={sort}
|
||||
onSortChange={(sortKey) =>
|
||||
setSort({
|
||||
orderBy: sortKey,
|
||||
direction:
|
||||
sort?.direction === "ascending" ? "descending" : "ascending",
|
||||
})
|
||||
}
|
||||
>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{columns.map(({ key, name, width, sortable = true, align }) => (
|
||||
<Table.ColumnHeader
|
||||
style={{
|
||||
width,
|
||||
minWidth: width,
|
||||
maxWidth: width,
|
||||
}}
|
||||
key={key}
|
||||
sortable={sortable}
|
||||
sortKey={key}
|
||||
align={align}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</Table.ColumnHeader>
|
||||
))}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{data
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
if (sort) {
|
||||
const comparator = (a, b, orderBy) => {
|
||||
const lookup = (oppgave) => {
|
||||
const column = columns.find(({ key }) => key === orderBy);
|
||||
|
||||
return column.value
|
||||
? column.value(oppgave)
|
||||
: oppgave[orderBy];
|
||||
};
|
||||
|
||||
if (lookup(b) < lookup(a) || lookup(b) === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (lookup(b) > lookup(a)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
return sort.direction === "ascending"
|
||||
? comparator(b, a, sort.orderBy)
|
||||
: comparator(a, b, sort.orderBy);
|
||||
}
|
||||
return 1;
|
||||
})
|
||||
.map((oppgave) => (
|
||||
<Table.Row key={oppgave.saksid}>
|
||||
{columns.map(
|
||||
({
|
||||
key,
|
||||
value,
|
||||
width,
|
||||
render = (value) => value,
|
||||
align,
|
||||
}) => (
|
||||
<Table.DataCell
|
||||
style={{
|
||||
width,
|
||||
minWidth: width,
|
||||
maxWidth: width,
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
title={oppgave[key]}
|
||||
key={key}
|
||||
align={align}
|
||||
>
|
||||
{render(value ? value(oppgave) : oppgave[key])}
|
||||
</Table.DataCell>
|
||||
)
|
||||
)}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const data = [
|
||||
{
|
||||
saksid: "111111",
|
||||
søknadOm:
|
||||
"Søknad om: Toalettforhøyer; kalender-/minnesystem; støttehåndtak; tidsur; komfyr-/elvakt",
|
||||
mottattDato: "2021-09-19T13:55:45",
|
||||
formidlerNavn: "Kari Johansen",
|
||||
personinformasjon: {
|
||||
fnr: "19044238651",
|
||||
funksjonsnedsettelse: ["bevegelse"],
|
||||
fornavn: "Petter Andreas",
|
||||
etternavn: "Hansen",
|
||||
poststed: "Trondheim",
|
||||
bosted: "TRONDHEIM",
|
||||
},
|
||||
saksbehandler: null,
|
||||
status: "AVVENTER_SAKSBEHANDLER",
|
||||
enhet: [
|
||||
{
|
||||
enhetsnummer: "6969",
|
||||
enhetsnavn: "Enhetsnavn",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
saksid: "222222",
|
||||
søknadOm: "Hjelpemidler",
|
||||
mottattDato: "2021-09-24T13:55:45",
|
||||
formidlerNavn: "Kari Johansen",
|
||||
personinformasjon: {
|
||||
fnr: "13044238651",
|
||||
funksjonsnedsettelse: ["bevegelse"],
|
||||
fornavn: "Mia Cathrine",
|
||||
etternavn: "Svendsen",
|
||||
poststed: "Trondheim",
|
||||
bosted: "TRONDHEIM",
|
||||
},
|
||||
saksbehandler: null,
|
||||
status: "AVVENTER_SAKSBEHANDLER",
|
||||
enhet: [
|
||||
{
|
||||
enhetsnummer: "6969",
|
||||
enhetsnavn: "Enhetsnavn",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
saksid: "5878444",
|
||||
søknadOm: "Hjelpemidler",
|
||||
mottattDato: "2021-09-20T13:55:45",
|
||||
formidlerNavn: "Kari Johansen",
|
||||
personinformasjon: {
|
||||
fnr: "130441386551",
|
||||
funksjonsnedsettelse: ["bevegelse"],
|
||||
fornavn: "Per Bjørkli",
|
||||
etternavn: "Hansen",
|
||||
poststed: "Trondheim",
|
||||
bosted: "TRONDHEIM",
|
||||
},
|
||||
saksbehandler: {
|
||||
epost: "a.b@nav.no",
|
||||
objectId: "23ea7485-1324-4b25-a763-6969",
|
||||
navn: "Geir Markusen",
|
||||
},
|
||||
status: "SENDT_GOSYS",
|
||||
enhet: [
|
||||
{
|
||||
enhetsnummer: "6969",
|
||||
enhetsnavn: "Enhetsnavn",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
saksid: "1234567",
|
||||
søknadOm: "Hjelpemidler",
|
||||
mottattDato: "2021-06-25T13:55:45",
|
||||
formidlerNavn: "Kari Johansen",
|
||||
personinformasjon: {
|
||||
fnr: "06115559891",
|
||||
funksjonsnedsettelse: ["kognisjon"],
|
||||
fornavn: "Iver",
|
||||
etternavn: "Slettan",
|
||||
poststed: "Lade",
|
||||
bosted: "TRONDHEIM",
|
||||
},
|
||||
saksbehandler: {
|
||||
epost: "a.b@nav.no",
|
||||
objectId: "23ea7485-1324-4b25-a763-assdfdfa",
|
||||
navn: "Silje Saksbehandler",
|
||||
},
|
||||
status: "TILDELT_SAKSBEHANDLER",
|
||||
enhet: [
|
||||
{
|
||||
enhetsnummer: "6969",
|
||||
enhetsnavn: "Enhetsnavn",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
saksid: "999999",
|
||||
søknadOm: "Hjelpemidler",
|
||||
mottattDato: "2021-06-25T16:55:45",
|
||||
formidlerNavn: "Kari Johansen",
|
||||
personinformasjon: {
|
||||
fnr: "16120101181",
|
||||
funksjonsnedsettelse: ["bevegelse"],
|
||||
fornavn: "Sondre",
|
||||
etternavn: "Hansen",
|
||||
poststed: "Lade",
|
||||
bosted: "TRONDHEIM",
|
||||
},
|
||||
saksbehandler: {
|
||||
epost: "a.b@nav.no",
|
||||
objectId: "23ea7485-1324-4b25-a763-6969",
|
||||
navn: "Geir Markusen",
|
||||
},
|
||||
status: "TILDELT_SAKSBEHANDLER",
|
||||
enhet: [
|
||||
{
|
||||
enhetsnummer: "6969",
|
||||
enhetsnavn: "Enhetsnavn",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
saksid: "888888",
|
||||
søknadOm: "Hjelpemidler",
|
||||
mottattDato: "2021-09-19T13:55:45",
|
||||
formidlerNavn: "Kari Johansen",
|
||||
personinformasjon: {
|
||||
fnr: "19044238651",
|
||||
funksjonsnedsettelse: ["bevegelse"],
|
||||
fornavn: "Geir",
|
||||
etternavn: "Dalby",
|
||||
poststed: "Trondheim",
|
||||
bosted: "TRONDHEIM",
|
||||
},
|
||||
saksbehandler: {
|
||||
epost: "a.b@nav.no",
|
||||
objectId: "23ea7485-1324-4b25-a763-assdfdfa",
|
||||
navn: "Silje Saksbehandler",
|
||||
},
|
||||
status: "VEDTAK_FATTET",
|
||||
enhet: [
|
||||
{
|
||||
enhetsnummer: "6969",
|
||||
enhetsnavn: "Enhetsnavn",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Table } from "../index";
|
||||
import { Alert, Checkbox, Link } from "@navikt/ds-react";
|
||||
import { Table } from "../";
|
||||
import { Alert, Button, Checkbox, Link } from "../..";
|
||||
|
||||
export default {
|
||||
title: "ds-react/table",
|
||||
@@ -12,6 +12,7 @@ export const All = () => {
|
||||
<Table {...props}>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{props.button && <Table.HeaderCell>Action</Table.HeaderCell>}
|
||||
<Table.HeaderCell>ID</Table.HeaderCell>
|
||||
<Table.HeaderCell>Fornavn</Table.HeaderCell>
|
||||
<Table.HeaderCell>Etternavn</Table.HeaderCell>
|
||||
@@ -20,18 +21,48 @@ export const All = () => {
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
{props.button && (
|
||||
<Table.DataCell
|
||||
style={{
|
||||
paddingTop: 6,
|
||||
paddingBottom: 6,
|
||||
}}
|
||||
>
|
||||
<Button size="xsmall">Click me!</Button>
|
||||
</Table.DataCell>
|
||||
)}
|
||||
<Table.HeaderCell>1</Table.HeaderCell>
|
||||
<Table.DataCell>Jean-Luc</Table.DataCell>
|
||||
<Table.DataCell>Picard</Table.DataCell>
|
||||
<Table.DataCell>Kaptein</Table.DataCell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
{props.button && (
|
||||
<Table.DataCell
|
||||
style={{
|
||||
paddingTop: 6,
|
||||
paddingBottom: 6,
|
||||
}}
|
||||
>
|
||||
<Button size="xsmall">Click me!</Button>
|
||||
</Table.DataCell>
|
||||
)}
|
||||
<Table.HeaderCell>2</Table.HeaderCell>
|
||||
<Table.DataCell>William</Table.DataCell>
|
||||
<Table.DataCell>Riker</Table.DataCell>
|
||||
<Table.DataCell>Kommandør</Table.DataCell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
{props.button && (
|
||||
<Table.DataCell
|
||||
style={{
|
||||
paddingTop: 6,
|
||||
paddingBottom: 6,
|
||||
}}
|
||||
>
|
||||
<Button size="xsmall">Click me!</Button>
|
||||
</Table.DataCell>
|
||||
)}
|
||||
<Table.HeaderCell>3</Table.HeaderCell>
|
||||
<Table.DataCell>Geordi</Table.DataCell>
|
||||
<Table.DataCell>La Forge</Table.DataCell>
|
||||
@@ -48,6 +79,8 @@ export const All = () => {
|
||||
<TableComponent zebraStripes />
|
||||
<h2>Small Table</h2>
|
||||
<TableComponent size="small" />
|
||||
<h2>Small Table with buttons</h2>
|
||||
<TableComponent size="small" button />
|
||||
<h2>Table with divs</h2>
|
||||
<Alert variant="warning">
|
||||
Obs! Hvis man skal bygge tabeller uten å bruke vanlig {"<tabell> "}
|
||||
|
||||
@@ -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.7",
|
||||
"version": "0.12.2",
|
||||
"private": false,
|
||||
"description": "NAV internal designsystem react components",
|
||||
"author": "NAV Designsystem team",
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@navikt/ds-icons": "^0.8.4",
|
||||
"@navikt/ds-react": "^0.16.7",
|
||||
"@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>
|
||||
+4
-2
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user