├── LICENSE
├── CHANGELOG.md
├── types
├── keycoder.d.ts
└── react-relative-portal.d.ts
├── public
└── .gitignore
├── .npmignore
├── .gitattributes
├── .gitignore
├── stories
├── data
│ ├── CAMPAIGNS.ts
│ ├── SCOPES.ts
│ ├── OPPORTUNITIES.ts
│ ├── CASES.ts
│ └── COMPANIES.ts
├── util.tsx
├── Tree.stories.tsx
├── BreadCrumbs.stories.tsx
├── Toggle.stories.tsx
├── SalesPath.stories.tsx
├── Pill.stories.tsx
├── Badge.stories.tsx
├── Grid.stories.tsx
├── MediaObject.stories.tsx
├── Spinner.stories.tsx
├── Datepicker.stories.tsx
├── Textarea.stories.tsx
├── Radio.stories.tsx
├── DateInput.stories.tsx
├── DropdownMenu.stories.tsx
├── Select.stories.tsx
├── Checkbox.stories.tsx
├── ButtonGroup.stories.tsx
├── Table.stories.tsx
├── Icon.stories.tsx
├── Form.stories.tsx
├── Input.stories.tsx
├── Notification.stories.tsx
├── Button.stories.tsx
├── Tabs.stories.tsx
└── Picklist.stories.tsx
├── .prettierrc.js
├── src
└── scripts
│ ├── common.ts
│ ├── typeUtils.ts
│ ├── Container.tsx
│ ├── Badge.tsx
│ ├── Form.tsx
│ ├── MediaObject.tsx
│ ├── ComponentSettings.tsx
│ ├── ButtonGroup.tsx
│ ├── BreadCrumbs.tsx
│ ├── Text.tsx
│ ├── TooltipContent.tsx
│ ├── hooks.ts
│ ├── Spinner.tsx
│ ├── index.ts
│ ├── Radio.tsx
│ ├── Tree.tsx
│ ├── util.ts
│ ├── Toggle.tsx
│ ├── Checkbox.tsx
│ ├── Pill.tsx
│ ├── FieldSet.tsx
│ ├── Textarea.tsx
│ ├── Select.tsx
│ ├── Notification.tsx
│ ├── RadioGroup.tsx
│ ├── FormElement.tsx
│ ├── Popover.tsx
│ ├── Modal.tsx
│ ├── CheckboxGroup.tsx
│ ├── TreeNode.tsx
│ ├── Button.tsx
│ ├── Grid.tsx
│ ├── SalesPath.tsx
│ └── Input.tsx
├── tsconfig.types.json
├── .storybook
├── wrapSLDS.js
├── main.js
├── preview-head.html
└── preview.js
├── .editorconfig
├── babel.config.js
├── regconfig.json
├── .eslintrc.js
├── README.md
├── .circleci
└── config.yml
├── package.json
└── tsconfig.json
/LICENSE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/types/keycoder.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'keycoder';
2 |
--------------------------------------------------------------------------------
/public/.gitignore:
--------------------------------------------------------------------------------
1 | *.html
2 | *.ico
3 | assets
4 | static
5 |
--------------------------------------------------------------------------------
/types/react-relative-portal.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-relative-portal';
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | build
2 | coverage
3 | docs
4 | examples
5 | icon-builder
6 | test
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log*
3 | lib/*
4 | module/*
5 | .idea
6 |
7 | coverage
8 | .reg
9 |
10 | .env
11 | .vscode
--------------------------------------------------------------------------------
/stories/data/CAMPAIGNS.ts:
--------------------------------------------------------------------------------
1 | export default `
2 | Online Seminar
3 | Event
4 | Survey
5 | `
6 | .split('\n')
7 | .filter((n) => n);
8 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | jsxSingleQuote: true,
4 | arrowParens: 'always',
5 | trailingComma: 'es5',
6 | };
7 |
--------------------------------------------------------------------------------
/stories/data/SCOPES.ts:
--------------------------------------------------------------------------------
1 | export default `
2 | Account
3 | Campaign
4 | Case
5 | Contract
6 | Opportunity
7 | Solution
8 | `
9 | .split('\n')
10 | .filter((n) => n);
11 |
--------------------------------------------------------------------------------
/stories/data/OPPORTUNITIES.ts:
--------------------------------------------------------------------------------
1 | export default `
2 | New License
3 | Professional Service
4 | Additional License
5 | Hardware Renewal
6 | `
7 | .split('\n')
8 | .filter((n) => n);
9 |
--------------------------------------------------------------------------------
/src/scripts/common.ts:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | /**
4 | *
5 | */
6 | export function createFC
(componentFn: FC
, statics: T): FC
& T {
7 | return Object.assign(componentFn, statics);
8 | }
9 |
--------------------------------------------------------------------------------
/stories/data/CASES.ts:
--------------------------------------------------------------------------------
1 | export default new Array(1001)
2 | .join('_')
3 | .split('')
4 | .map((a, i) => {
5 | const padded = new Array(5).join('0') + i;
6 | return padded.substring(padded.length - 5);
7 | });
8 |
--------------------------------------------------------------------------------
/src/scripts/typeUtils.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
2 | export type Bivariant any> = {
3 | bivarianceHack(...args: Parameters): ReturnType;
4 | }['bivarianceHack'];
5 |
--------------------------------------------------------------------------------
/tsconfig.types.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "emitDeclarationOnly": true,
6 | "rootDir": "src"
7 | },
8 | "include": [
9 | "src/**/*"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.storybook/wrapSLDS.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentSettings } from '../src/scripts';
3 |
4 | export default function wrapSLDS(options) {
5 | return story => (
6 |
7 | {story()}
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | insert_final_newline = true
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | typescript: {
5 | reactDocgen: "react-docgen-typescript-plugin"
6 | },
7 | stories: ['../stories/**/*.stories.tsx'],
8 | addons: [
9 | '@storybook/addon-docs',
10 | '@storybook/addon-actions',
11 | '@storybook/addon-controls',
12 | 'storycap',
13 | ],
14 | };
15 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/env',
5 | process.env.BUILD_TARGET === 'module'
6 | ? {
7 | modules: false,
8 | }
9 | : {},
10 | ],
11 | '@babel/react',
12 | '@babel/typescript',
13 | ],
14 | plugins: [
15 | '@babel/plugin-transform-runtime',
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/regconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "core": {
3 | "workingDir": ".reg",
4 | "actualDir": "images",
5 | "thresholdRate": 0.001,
6 | "addIgnore": true,
7 | "ximgdiff": {
8 | "invocationType": "client"
9 | }
10 | },
11 | "plugins": {
12 | "reg-keygen-git-hash-plugin": true,
13 | "reg-notify-github-plugin": {
14 | "clientId": "$REG_NOTIF_CLIENT_ID"
15 | },
16 | "reg-publish-s3-plugin": {
17 | "bucketName": "$S3_BUCKET_NAME"
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
12 |
17 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import 'core-js/stable';
2 | import { withScreenshot } from 'storycap';
3 | import wrapSLDS from './wrapSLDS';
4 |
5 | let assetRoot;
6 | if (typeof location !== 'undefined' && location.hostname === 'mashmatrix.github.io') {
7 | assetRoot = '//mashmatrix.github.io/react-lightning-design-system/assets';
8 | }
9 |
10 | const withSLDS = wrapSLDS({ assetRoot });
11 |
12 | export const decorators = [
13 | withScreenshot,
14 | withSLDS,
15 | ];
16 |
17 | export const parameters = {
18 | // Global parameter is optional.
19 | screenshot: {
20 | // Put global screenshot parameters(e.g. viewport)
21 | },
22 | };
--------------------------------------------------------------------------------
/src/scripts/Container.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, HTMLAttributes } from 'react';
2 | import classnames from 'classnames';
3 |
4 | /**
5 | *
6 | */
7 | export type ContainerProps = {
8 | size: 'small' | 'medium' | 'large';
9 | align: 'left' | 'center' | 'right';
10 | } & HTMLAttributes;
11 |
12 | /**
13 | *
14 | */
15 | export const Container: FC = ({
16 | className,
17 | size,
18 | align,
19 | children,
20 | ...props
21 | }) => {
22 | const ctClassNames = classnames(
23 | className,
24 | `slds-container_${size || 'fluid'}`,
25 | align ? `slds-container_${align}` : null
26 | );
27 | return (
28 |
29 | {children}
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/scripts/Badge.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, HTMLAttributes } from 'react';
2 | import classnames from 'classnames';
3 |
4 | /**
5 | *
6 | */
7 | export type BadgeProps = {
8 | type?: 'inverse' | 'lightest' | 'success' | 'warning' | 'error';
9 | label?: string;
10 | } & HTMLAttributes;
11 |
12 | /**
13 | *
14 | */
15 | export const Badge: FC = ({ type, label, ...props }) => {
16 | const typeClassName = /^(inverse|lightest)$/.test(type ?? '')
17 | ? `slds-badge_${type}`
18 | : null;
19 | const themeClassName = /^(success|warning|error)$/.test(type ?? '')
20 | ? `slds-theme_${type}`
21 | : null;
22 | const badgeClassNames = classnames(
23 | 'slds-badge',
24 | typeClassName,
25 | themeClassName
26 | );
27 | return (
28 |
29 | {label || props.children}
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/scripts/Form.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, HTMLAttributes } from 'react';
2 | import classnames from 'classnames';
3 | import { FormElement } from './FormElement';
4 |
5 | /**
6 | *
7 | */
8 | export type FormProps = {
9 | type?: 'stacked' | 'horizontal' | 'inline' | 'compound';
10 | } & HTMLAttributes;
11 |
12 | /**
13 | *
14 | */
15 | export const Form: FC = (props) => {
16 | const { className, type = 'stacked', children, ...rprops } = props;
17 | const formClassNames = classnames(className, `slds-form_${type}`);
18 | return (
19 |
20 | {React.Children.map(children, (child) => {
21 | if (
22 | React.isValidElement(child) &&
23 | !(child.type as unknown as { isFormElement?: boolean }).isFormElement
24 | ) {
25 | return {child};
26 | }
27 | return child;
28 | })}
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/scripts/MediaObject.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, HTMLAttributes, ReactNode } from 'react';
2 | import classnames from 'classnames';
3 |
4 | /**
5 | *
6 | */
7 | export type MediaObjectProps = {
8 | figureLeft?: ReactNode;
9 | figureRight?: ReactNode;
10 | centered?: boolean;
11 | children?: ReactNode;
12 | } & HTMLAttributes;
13 |
14 | /**
15 | *
16 | */
17 | export const MediaObject: FC = (props) => {
18 | const { className, figureLeft, figureRight, centered, children, ...rprops } =
19 | props;
20 | const mediaClassNames = classnames(
21 | 'slds-media',
22 | { 'slds-media_center': centered },
23 | className
24 | );
25 | return (
26 |
27 | {figureLeft ? (
28 |
{figureLeft}
29 | ) : undefined}
30 |
{children}
31 | {figureRight ? (
32 |
33 | {figureRight}
34 |
35 | ) : undefined}
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/scripts/ComponentSettings.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, FC, ReactNode } from 'react';
2 |
3 | export type ComponentSettingsProps = {
4 | assetRoot?: string;
5 | portalClassName?: string;
6 | portalStyle?: object;
7 | getActiveElement?: () => HTMLElement | null;
8 | children?: ReactNode;
9 | };
10 |
11 | function getDocumentActiveElement() {
12 | return document.activeElement as HTMLElement | null;
13 | }
14 |
15 | export const ComponentSettingsContext = createContext<
16 | ComponentSettingsProps &
17 | Required>
18 | >({ getActiveElement: getDocumentActiveElement });
19 |
20 | /**
21 | *
22 | */
23 | export const ComponentSettings: FC = (props) => {
24 | const {
25 | assetRoot,
26 | portalClassName,
27 | portalStyle,
28 | getActiveElement = getDocumentActiveElement,
29 | children,
30 | } = props;
31 | return (
32 |
35 | {children}
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/scripts/ButtonGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { Children, HTMLAttributes, FC, createContext } from 'react';
2 | import classnames from 'classnames';
3 |
4 | /**
5 | *
6 | */
7 | export type ButtonGroupProps = HTMLAttributes;
8 |
9 | /**
10 | *
11 | */
12 | export const ButtonGroupContext = createContext<{
13 | grouped: true;
14 | isFirstInGroup: boolean;
15 | isLastInGroup: boolean;
16 | } | null>(null);
17 |
18 | /**
19 | *
20 | */
21 | export const ButtonGroup: FC = (props) => {
22 | const { className, children, ...rprops } = props;
23 | const btnGrpClassNames = classnames(className, 'slds-button-group');
24 | const cnt = React.Children.count(children);
25 | return (
26 |
27 | {Children.map(children, (child, index) => (
28 |
35 | {child}
36 |
37 | ))}
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/scripts/BreadCrumbs.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, HTMLAttributes } from 'react';
2 | import classnames from 'classnames';
3 |
4 | /**
5 | *
6 | */
7 | export type CrumbProps = HTMLAttributes & {
8 | href?: string;
9 | };
10 |
11 | /**
12 | *
13 | */
14 | export const Crumb: FC = ({
15 | className,
16 | href,
17 | children,
18 | ...props
19 | }) => {
20 | const text = children;
21 | const cClassName = classnames('slds-breadcrumb__item', className);
22 |
23 | return (
24 |
25 | {text}
26 |
27 | );
28 | };
29 |
30 | /**
31 | *
32 | */
33 | export type BreadCrumbsProps = HTMLAttributes;
34 |
35 | /**
36 | *
37 | */
38 | export const BreadCrumbs: FC = ({
39 | className,
40 | children,
41 | ...props
42 | }) => {
43 | const oClassName = classnames(
44 | 'slds-breadcrumb',
45 | 'slds-list_horizontal',
46 | 'slds-wrap',
47 | className
48 | );
49 |
50 | return (
51 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/scripts/Text.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, ReactHTML, HTMLAttributes } from 'react';
2 | import classnames from 'classnames';
3 |
4 | /**
5 | *
6 | */
7 | export type TextProps = {
8 | tag?: keyof ReactHTML;
9 | category?: 'body' | 'heading' | 'title';
10 | type?: 'small' | 'regular' | 'medium' | 'large' | 'caps';
11 | align?: 'left' | 'center' | 'right';
12 | truncate?: boolean;
13 | section?: boolean;
14 | } & HTMLAttributes;
15 |
16 | /**
17 | *
18 | */
19 | export const Text: FC = ({
20 | tag,
21 | category,
22 | type,
23 | align,
24 | truncate,
25 | section,
26 | children,
27 | className,
28 | ...props
29 | }) => {
30 | const textClassNames = classnames(
31 | type && category ? `slds-text-${category}_${type}` : undefined,
32 | category === 'title' && !type ? `slds-text-${category}` : undefined,
33 | align ? `slds-text-align_${align}` : undefined,
34 | {
35 | 'slds-truncate': truncate,
36 | 'slds-section-title_divider': section,
37 | },
38 | className
39 | );
40 | const Tag = tag || 'p';
41 | return (
42 |
43 | {children}
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/scripts/TooltipContent.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | ReactNode,
3 | useRef,
4 | useState,
5 | FocusEvent,
6 | useCallback,
7 | } from 'react';
8 | import { Button } from './Button';
9 | import { Popover } from './Popover';
10 |
11 | /**
12 | *
13 | */
14 | export const TooltipContent = (props: {
15 | children: ReactNode;
16 | icon?: string;
17 | }) => {
18 | const { children, icon = 'info' } = props;
19 | const [isHideTooltip, setIsHideTooltip] = useState(true);
20 | const popoverRef = useRef(null);
21 | const tooltipToggle = useCallback(() => {
22 | setIsHideTooltip((hidden) => !hidden);
23 | }, []);
24 | const onBlur = useCallback((e: FocusEvent) => {
25 | if (!popoverRef.current?.contains(e.relatedTarget)) {
26 | setIsHideTooltip(true);
27 | }
28 | }, []);
29 | return (
30 |
31 |
32 |
40 | {children}
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/stories/util.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { DecoratorFn } from '@storybook/react';
3 | import { ButtonType } from '../src/scripts';
4 |
5 | /**
6 | *
7 | */
8 | export function containerDecorator(style: CSSProperties): DecoratorFn {
9 | const decorator: DecoratorFn = (story) => {story()}
;
10 | return decorator;
11 | }
12 |
13 | /**
14 | *
15 | */
16 | export function buildContainerDecorator(
17 | builderFunc: (args: Props) => CSSProperties | null | undefined
18 | ): DecoratorFn {
19 | const decorator: DecoratorFn = (story, ctx) => {
20 | ctx.args;
21 | const style = builderFunc(ctx.args as Props);
22 | return style ? {story()}
: story();
23 | };
24 | return decorator;
25 | }
26 |
27 | /**
28 | *
29 | */
30 | export const buttonBgDecorator = buildContainerDecorator<{
31 | type?: ButtonType;
32 | }>((args) => {
33 | const type = args.type;
34 | return type === 'inverse' ||
35 | type === 'icon-inverse' ||
36 | type === 'icon-border-inverse'
37 | ? {
38 | backgroundColor: '#16325c',
39 | padding: 4,
40 | }
41 | : type === 'icon-border-filled'
42 | ? {
43 | backgroundColor: '#cccccc',
44 | padding: 4,
45 | }
46 | : null;
47 | });
48 |
--------------------------------------------------------------------------------
/stories/data/COMPANIES.ts:
--------------------------------------------------------------------------------
1 | export default `
2 | Apple Inc.
3 | Alphabet Inc.
4 | Microsoft Corporation
5 | Facebook, Inc.
6 | Intel Corporation
7 | Oracle Corporation
8 | Cisco Systems, Inc.
9 | International Business Machines Corporation
10 | QUALCOMM Incorporated
11 | Texas Instruments Incorporated
12 | Salesforce.com Inc
13 | EMC Corporation
14 | Adobe Systems Incorporated
15 | Automatic Data Processing, Inc.
16 | Cognizant Technology Solutions Corporation
17 | Broadcom Corporation
18 | Illinois Tool Works Inc.
19 | Yahoo! Inc.
20 | LinkedIn Corporation
21 | Activision Blizzard, Inc
22 | Intuit Inc.
23 | Vmware, Inc.
24 | Fiserv, Inc.
25 | Applied Materials, Inc.
26 | Cerner Corporation
27 | Electronic Arts Inc.
28 | HP Inc.
29 | Omnicom Group Inc.
30 | NVIDIA Corporation
31 | Analog Devices, Inc.
32 | SanDisk Corporation
33 | Micron Technology, Inc.
34 | Red Hat, Inc.
35 | Palo Alto Networks, Inc.
36 | Workday, Inc.
37 | Symantec Corporation
38 | Western Digital Corporation
39 | Skyworks Solutions, Inc.
40 | ServiceNow, Inc.
41 | Autodesk, Inc.
42 | Verisk Analytics, Inc.
43 | CA Inc.
44 | Lam Research Corporation
45 | Motorola Solutions, Inc.
46 | Xilinx, Inc.
47 | TripAdvisor, Inc.
48 | Citrix Systems, Inc.
49 | Juniper Networks, Inc.
50 | Maxim Integrated Products, Inc.
51 | `
52 | .split('\n')
53 | .filter((n) => n);
54 |
--------------------------------------------------------------------------------
/src/scripts/hooks.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Ref,
3 | useCallback,
4 | useLayoutEffect,
5 | useMemo,
6 | useRef,
7 | useState,
8 | } from 'react';
9 | import mergeRefs from 'react-merge-refs';
10 |
11 | /**
12 | *
13 | */
14 | export function useControlledValue(value: T | undefined, defaultValue: T) {
15 | const initValue = typeof value !== 'undefined' ? value : defaultValue;
16 | const [stateValue, setStateValue] = useState(initValue);
17 | return [typeof value !== 'undefined' ? value : stateValue, setStateValue] as [
18 | T,
19 | typeof setStateValue,
20 | ];
21 | }
22 |
23 | /**
24 | *
25 | */
26 | export function useEventCallback(
27 | callback: (...args: A) => R
28 | ) {
29 | const ref = useRef(() => {
30 | throw new Error('Should not call function in render');
31 | });
32 | useLayoutEffect(() => {
33 | ref.current = callback;
34 | });
35 | return useCallback((...args: A) => ref.current(...args), []);
36 | }
37 |
38 | /**
39 | *
40 | */
41 | export function useMergeRefs(refs: Array[ | undefined>) {
42 | const mrefs: Ref[] = [];
43 | for (const ref of refs) {
44 | if (ref != null) {
45 | mrefs.push(ref);
46 | }
47 | }
48 | // eslint-disable-next-line react-hooks/exhaustive-deps
49 | return useMemo(() => mergeRefs(mrefs), [...mrefs]);
50 | }
51 |
--------------------------------------------------------------------------------
/stories/Tree.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tree, TreeNode } from '../src/scripts';
3 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
4 |
5 | /**
6 | *
7 | */
8 | const meta: ComponentMeta = {
9 | title: 'Tree',
10 | component: Tree,
11 | subcomponents: { TreeNode },
12 | argTypes: {
13 | onNodeClick: { action: 'nodeClick' },
14 | onNodeToggle: { action: 'nodeToggle' },
15 | onNodeLabelClick: { action: 'nodeLabelClick' },
16 | },
17 | };
18 | export default meta;
19 |
20 | /**
21 | *
22 | */
23 | export const ControlledWithKnobs: ComponentStoryObj = {
24 | render: (args) => (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | ),
37 | name: 'Controlled with knobs',
38 | args: {
39 | label: 'Tree Example #1',
40 | },
41 | parameters: {
42 | docs: {
43 | description: {
44 | story: 'Tree / TreeNode controlled with knobs',
45 | },
46 | },
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/scripts/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, HTMLAttributes } from 'react';
2 | import classnames from 'classnames';
3 |
4 | /**
5 | *
6 | */
7 | export type SpinnerSize = 'x-small' | 'small' | 'medium' | 'large';
8 | export type SpinnerType = 'brand' | 'inverse';
9 | export type SpinnerLayout = 'inline';
10 | export type SpinnerProps = {
11 | container?: boolean;
12 | size?: SpinnerSize;
13 | type?: SpinnerType;
14 | layout?: SpinnerLayout;
15 | } & HTMLAttributes;
16 |
17 | /**
18 | *
19 | */
20 | export const Spinner: FC = (props) => {
21 | const {
22 | className,
23 | container = true,
24 | size = 'small',
25 | type,
26 | layout,
27 | ...rprops
28 | } = props;
29 | const spinnerClassNames = classnames(
30 | className,
31 | 'slds-spinner',
32 | `slds-spinner_${size}`,
33 | type ? `slds-spinner_${type}` : null,
34 | layout ? `slds-spinner_${layout}` : null
35 | );
36 | const spinner = (
37 | ]
38 |
Loading
39 |
40 |
41 |
42 | );
43 | return container ? (
44 | {spinner}
45 | ) : (
46 | spinner
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/scripts/index.ts:
--------------------------------------------------------------------------------
1 | // TODO: revert
2 | // changed
3 | // because of https://github.com/gaearon/react-hot-loader/issues/158
4 | export { default as util } from './util';
5 |
6 | export * from './Badge';
7 | export * from './BreadCrumbs';
8 | export * from './Button';
9 | export * from './ButtonGroup';
10 | export * from './Container';
11 | export * from './Checkbox';
12 | export * from './CheckboxGroup';
13 | export * from './Datepicker';
14 | export * from './DateInput';
15 | export * from './Select';
16 | export * from './DropdownMenu';
17 | export * from './DropdownButton';
18 | export * from './Icon';
19 | export * from './MediaObject';
20 | export * from './Radio';
21 | export * from './RadioGroup';
22 | export * from './Form';
23 | export * from './FormElement';
24 | export * from './FieldSet';
25 | export * from './Input';
26 | export * from './Picklist';
27 | export * from './Lookup';
28 | export * from './Grid';
29 | export * from './Pill';
30 | export * from './Spinner';
31 | export * from './Text';
32 | export * from './Textarea';
33 | export * from './Toggle';
34 | export * from './Modal';
35 | export * from './Notification';
36 | export * from './PageHeader';
37 | export * from './Tree';
38 | export * from './TreeNode';
39 | export * from './SalesPath';
40 | export * from './Popover';
41 | export * from './Tabs';
42 | export * from './Table';
43 | export * from './ComponentSettings';
44 |
--------------------------------------------------------------------------------
/stories/BreadCrumbs.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps } from 'react';
2 | import { BreadCrumbs, Crumb } from '../src/scripts';
3 | import { Meta, StoryObj } from '@storybook/react';
4 |
5 | type StoryProps = ComponentProps & {
6 | crumb1: ComponentProps;
7 | crumb1_onClick: ComponentProps['onClick'];
8 | } & {
9 | crumb2: ComponentProps;
10 | crumb2_onClick: ComponentProps['onClick'];
11 | };
12 |
13 | /**
14 | *
15 | */
16 | const meta: Meta = {
17 | title: 'BreadCrumbs',
18 | component: BreadCrumbs,
19 | argTypes: {
20 | crumb1_onClick: { action: 'crumb1_click' },
21 | crumb2_onClick: { action: 'crumb2_click' },
22 | },
23 | };
24 | export default meta;
25 |
26 | /**
27 | *
28 | */
29 | export const Default: StoryObj = {
30 | render: ({ crumb1, crumb2, crumb1_onClick, crumb2_onClick, ...args }) => (
31 |
32 |
33 |
34 |
35 | ),
36 | args: {
37 | crumb1: {
38 | children: 'Parent Entity',
39 | },
40 | crumb2: {
41 | children: 'Parent Record Name',
42 | },
43 | },
44 | parameters: {
45 | docs: {
46 | storyDescription: 'Default BreadCrumbs',
47 | },
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'parser': '@typescript-eslint/parser',
3 | 'extends': [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
7 | 'plugin:react/recommended',
8 | 'prettier',
9 | ],
10 | 'plugins': [
11 | '@typescript-eslint',
12 | 'jsx-a11y',
13 | 'prettier',
14 | 'react-hooks',
15 | ],
16 | 'parserOptions': {
17 | 'sourceType': 'module',
18 | 'project': './tsconfig.json',
19 | },
20 | 'env': {
21 | 'browser': true
22 | },
23 | 'rules': {
24 | // temp disabled - start
25 | '@typescript-eslint/no-explicit-any': 1,
26 | '@typescript-eslint/no-unsafe-assignment': 1,
27 | '@typescript-eslint/no-unsafe-argument': 1,
28 | '@typescript-eslint/no-unsafe-member-access': 1,
29 | '@typescript-eslint/no-unsafe-call': 1,
30 | '@typescript-eslint/no-unsafe-return': 1,
31 | '@typescript-eslint/ban-ts-comment': 1,
32 | '@typescript-eslint/ban-types': 1,
33 | '@typescript-eslint/restrict-template-expressions': 1,
34 | '@typescript-eslint/unbound-method': 1,
35 | '@typescript-eslint/no-non-null-assertion': 1,
36 | '@typescript-eslint/restrict-plus-operands': 1,
37 | '@typescript-eslint/no-empty-function': 1,
38 | 'react/prop-types': 0,
39 | 'react/display-name': 0,
40 | 'react/no-deprecated': 0,
41 | // tmp disabled - end
42 | 'prettier/prettier': 2,
43 | 'react-hooks/rules-of-hooks': 2,
44 | 'react-hooks/exhaustive-deps': 2,
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/stories/Toggle.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Toggle } from '../src/scripts';
2 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
3 |
4 | /**
5 | *
6 | */
7 | const meta: ComponentMeta = {
8 | title: 'Toggle',
9 | component: Toggle,
10 | argTypes: {
11 | onChange: { action: 'change' },
12 | onValueChange: { action: 'valueChange' },
13 | },
14 | };
15 | export default meta;
16 |
17 | /**
18 | *
19 | */
20 | export const ControlledWithKnobs: ComponentStoryObj = {
21 | name: 'Controlled with knobs',
22 | args: {
23 | label: 'Toggle Label',
24 | },
25 | parameters: {
26 | docs: {
27 | description: {
28 | story: 'Toggle controlled with knobs',
29 | },
30 | },
31 | },
32 | };
33 |
34 | /**
35 | *
36 | */
37 | export const Default: ComponentStoryObj = {
38 | args: {},
39 | parameters: {
40 | docs: {
41 | description: {
42 | story: 'Toggle control',
43 | },
44 | },
45 | },
46 | };
47 |
48 | /**
49 | *
50 | */
51 | export const Checked: ComponentStoryObj = {
52 | args: {
53 | checked: true,
54 | },
55 | parameters: {
56 | docs: {
57 | description: {
58 | story: 'Toggle control with checked status',
59 | },
60 | },
61 | },
62 | };
63 |
64 | /**
65 | *
66 | */
67 | export const Disabled: ComponentStoryObj = {
68 | args: {
69 | disabled: true,
70 | },
71 | parameters: {
72 | docs: {
73 | description: {
74 | story: 'Toggle control with disabled status',
75 | },
76 | },
77 | },
78 | };
79 |
--------------------------------------------------------------------------------
/stories/SalesPath.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SalesPath } from '../src/scripts';
3 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
4 |
5 | /**
6 | *
7 | */
8 | const meta: ComponentMeta = {
9 | title: 'SalesPath',
10 | component: SalesPath,
11 | subcomponents: { SalesPathItem: SalesPath.PathItem },
12 | argTypes: {
13 | onSelect: { action: 'select' },
14 | },
15 | };
16 | export default meta;
17 |
18 | /**
19 | *
20 | */
21 | export const ControlledWithKnobs: ComponentStoryObj = {
22 | render: (args) => (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ),
31 | name: 'Controlled with knobs',
32 | args: {
33 | activeKey: '1',
34 | },
35 | argTypes: {
36 | activeKey: {
37 | control: {
38 | type: 'select',
39 | options: ['1', '2', '3', '4', '5'],
40 | },
41 | },
42 | },
43 | parameters: {
44 | docs: {
45 | description: {
46 | story: 'Sales Path controlled with knobs',
47 | },
48 | },
49 | },
50 | };
51 |
52 | /**
53 | *
54 | */
55 | export const Default: ComponentStoryObj = {
56 | ...ControlledWithKnobs,
57 | name: 'Default',
58 | args: {
59 | activeKey: '3',
60 | },
61 | parameters: {
62 | docs: {
63 | description: {
64 | story: 'Sales Path',
65 | },
66 | },
67 | },
68 | };
69 |
--------------------------------------------------------------------------------
/src/scripts/Radio.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, InputHTMLAttributes, Ref, useId, useContext } from 'react';
2 | import classnames from 'classnames';
3 | import { RadioGroupContext, RadioValueType } from './RadioGroup';
4 | import { useEventCallback } from './hooks';
5 |
6 | /**
7 | *
8 | */
9 | export type RadioProps = {
10 | label?: string;
11 | name?: string;
12 | value?: RadioValueType;
13 | inputRef?: Ref;
14 | } & Omit, 'value'>;
15 |
16 | /**
17 | *
18 | */
19 | export const Radio: FC = ({
20 | id: id_,
21 | className,
22 | label,
23 | name,
24 | value,
25 | inputRef,
26 | onChange: onChange_,
27 | children,
28 | ...props
29 | }) => {
30 | const {
31 | name: grpName,
32 | error,
33 | errorId,
34 | onValueChange,
35 | } = useContext(RadioGroupContext);
36 |
37 | const prefix = useId();
38 | const id = id_ ?? `${prefix}-id`;
39 |
40 | const onChange = useEventCallback(
41 | (e: React.ChangeEvent) => {
42 | onChange_?.(e);
43 | if (value != null) {
44 | onValueChange?.(value);
45 | }
46 | }
47 | );
48 | const radioClassNames = classnames(className, 'slds-radio');
49 | return (
50 |
51 |
61 |
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/src/scripts/Tree.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes, createContext, FC, useMemo } from 'react';
2 | import classnames from 'classnames';
3 | import { TreeNodeProps } from './TreeNode';
4 |
5 | /**
6 | *
7 | */
8 | export const TreeContext = createContext<{
9 | toggleOnNodeClick?: boolean;
10 | onNodeClick?: (e: React.MouseEvent, props: TreeNodeProps) => void;
11 | onNodeLabelClick?: (e: React.MouseEvent, props: TreeNodeProps) => void;
12 | onNodeToggle?: (e: React.MouseEvent, props: TreeNodeProps) => void;
13 | }>({});
14 |
15 | /**
16 | *
17 | */
18 | export type TreeProps = {
19 | label?: string;
20 | toggleOnNodeClick?: boolean;
21 | onNodeClick?: (e: React.MouseEvent, props: TreeNodeProps) => void;
22 | onNodeLabelClick?: (e: React.MouseEvent, props: TreeNodeProps) => void;
23 | onNodeToggle?: (e: React.MouseEvent, props: TreeNodeProps) => void;
24 | } & HTMLAttributes;
25 |
26 | /**
27 | *
28 | */
29 | export const Tree: FC = (props) => {
30 | const {
31 | className,
32 | label,
33 | children,
34 | toggleOnNodeClick,
35 | onNodeClick,
36 | onNodeLabelClick,
37 | onNodeToggle,
38 | ...rprops
39 | } = props;
40 | const treeClassNames = classnames(className, 'slds-tree_container');
41 | const ctx = useMemo(
42 | () => ({
43 | toggleOnNodeClick,
44 | onNodeClick,
45 | onNodeLabelClick,
46 | onNodeToggle,
47 | }),
48 | [toggleOnNodeClick, onNodeClick, onNodeLabelClick, onNodeToggle]
49 | );
50 | return (
51 |
52 | {label ?
{label}
: null}
53 |
56 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/scripts/util.ts:
--------------------------------------------------------------------------------
1 | import { updateScroll } from 'react-relative-portal';
2 |
3 | export const getToday =
4 | process.env.NODE_ENV === 'test'
5 | ? () => '2022-05-18'
6 | : () => new Date().toISOString().substring(0, 10);
7 |
8 | let assetRoot = '/assets';
9 |
10 | export function setAssetRoot(path: string) {
11 | assetRoot = path;
12 | }
13 |
14 | export function getAssetRoot() {
15 | return assetRoot;
16 | }
17 |
18 | export function registerStyle(styleName: string, rules: string[][]) {
19 | const styleId = `react-slds-cssfix-${styleName}`;
20 | if (document.getElementById(styleId)) {
21 | return;
22 | }
23 | const style = document.createElement('style');
24 | style.id = styleId;
25 | const styleText = rules
26 | .map((ruleSet) => {
27 | const declaration = ruleSet.pop();
28 | let selectors = ruleSet;
29 | selectors = selectors.concat(selectors.map((s) => `.slds ${s}`));
30 | return `${selectors.join(', ')} ${declaration}`;
31 | })
32 | .join('\n');
33 | style.appendChild(document.createTextNode(styleText));
34 | document.documentElement.appendChild(style);
35 | }
36 |
37 | export function isElInChildren(rootEl: any, targetEl: any) {
38 | /* eslint-disable no-param-reassign */
39 | while (targetEl && targetEl !== rootEl) {
40 | targetEl = targetEl.parentNode;
41 | }
42 |
43 | return !!targetEl;
44 | }
45 |
46 | export function offset(el: HTMLElement) {
47 | const rect = el.getBoundingClientRect();
48 |
49 | return {
50 | top: rect.top + document.body.scrollTop,
51 | left: rect.left + document.body.scrollLeft,
52 | };
53 | }
54 |
55 | export function cleanProps(props: object, propTypes: object) {
56 | const newProps = props;
57 | Object.keys(propTypes).forEach((key) => {
58 | // @ts-ignore
59 | delete newProps[key];
60 | });
61 | return newProps;
62 | }
63 |
64 | export default {
65 | setAssetRoot,
66 | getAssetRoot,
67 | registerStyle,
68 | isElInChildren,
69 | offset,
70 | cleanProps,
71 | updateScroll,
72 | };
73 |
--------------------------------------------------------------------------------
/stories/Pill.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Pill } from '../src/scripts';
3 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
4 |
5 | /**
6 | *
7 | */
8 | const meta: ComponentMeta = {
9 | title: 'Pill',
10 | component: Pill,
11 | argTypes: {
12 | onClick: { action: 'click' },
13 | onRemove: { action: 'remove' },
14 | },
15 | };
16 | export default meta;
17 |
18 | /**
19 | *
20 | */
21 | export const ControlledWithKnobs: ComponentStoryObj = {
22 | name: 'Controlled with knobs',
23 | args: {
24 | label: 'Pill Label',
25 | title: 'Full Label of the Pill',
26 | },
27 | parameters: {
28 | info: 'Pill controlled with knobs',
29 | },
30 | };
31 |
32 | /**
33 | *
34 | */
35 | export const WithIcon: ComponentStoryObj = {
36 | name: 'with icon',
37 | args: {
38 | label: 'Pill Label',
39 | icon: {
40 | category: 'standard',
41 | icon: 'account',
42 | },
43 | },
44 | parameters: {
45 | docs: {
46 | description: {
47 | story: 'Pill with icon',
48 | },
49 | },
50 | },
51 | };
52 |
53 | /**
54 | *
55 | */
56 | export const Disabled: ComponentStoryObj = {
57 | name: 'disabled',
58 | args: {
59 | label: 'Pill Label',
60 | disabled: true,
61 | },
62 | parameters: {
63 | docs: {
64 | description: {
65 | story: 'Pill with disabled status',
66 | },
67 | },
68 | },
69 | };
70 |
71 | /**
72 | *
73 | */
74 | export const Truncate: ComponentStoryObj = {
75 | name: 'truncate',
76 | args: {
77 | label: 'Pill Label that is longer than the area that contains it',
78 | truncate: true,
79 | },
80 | decorators: [
81 | (story) => (
82 |
85 | ),
86 | ],
87 | parameters: {
88 | docs: {
89 | description: {
90 | story: 'Pill with truncated label',
91 | },
92 | },
93 | },
94 | };
95 |
--------------------------------------------------------------------------------
/src/scripts/Toggle.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, InputHTMLAttributes, Ref } from 'react';
2 | import classnames from 'classnames';
3 | import { FormElement, FormElementProps } from './FormElement';
4 | import { useEventCallback } from './hooks';
5 | import { createFC } from './common';
6 |
7 | /**
8 | *
9 | */
10 | export type ToggleProps = {
11 | label?: string;
12 | required?: boolean;
13 | error?: FormElementProps['error'];
14 | cols?: number;
15 | name?: string;
16 | elementRef?: Ref;
17 | inputRef?: Ref;
18 | onValueChange?: (checked: boolean) => void;
19 | } & InputHTMLAttributes;
20 |
21 | /**
22 | *
23 | */
24 | export const Toggle = createFC(
25 | (props) => {
26 | const {
27 | id,
28 | className,
29 | label,
30 | required,
31 | error,
32 | cols,
33 | elementRef,
34 | inputRef,
35 | onChange: onChange_,
36 | onValueChange,
37 | ...rprops
38 | } = props;
39 | const onChange = useEventCallback((e: ChangeEvent) => {
40 | onChange_?.(e);
41 | onValueChange?.(e.target.checked);
42 | });
43 | const toggleClassNames = classnames(
44 | className,
45 | 'slds-checkbox_toggle slds-grid'
46 | );
47 | const toggle = (
48 |
62 | );
63 | const formElemProps = {
64 | controlId: id,
65 | label,
66 | required,
67 | error,
68 | cols,
69 | elementRef,
70 | };
71 | return {toggle};
72 | },
73 | { isFormElement: true }
74 | );
75 |
--------------------------------------------------------------------------------
/src/scripts/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | FC,
3 | InputHTMLAttributes,
4 | Ref,
5 | useId,
6 | useContext,
7 | ReactNode,
8 | } from 'react';
9 | import classnames from 'classnames';
10 | import { FormElement } from './FormElement';
11 | import { CheckboxGroupContext, CheckboxValueType } from './CheckboxGroup';
12 |
13 | /**
14 | *
15 | */
16 | export type CheckboxProps = {
17 | label?: string;
18 | required?: boolean;
19 | cols?: number;
20 | name?: string;
21 | value?: CheckboxValueType;
22 | checked?: boolean;
23 | defaultChecked?: boolean;
24 | tooltip?: ReactNode;
25 | tooltipIcon?: string;
26 | elementRef?: Ref;
27 | inputRef?: Ref;
28 | } & InputHTMLAttributes;
29 |
30 | /**
31 | *
32 | */
33 | export const Checkbox: FC = (props) => {
34 | const {
35 | type, // eslint-disable-line @typescript-eslint/no-unused-vars
36 | id: id_,
37 | className,
38 | label,
39 | required,
40 | cols,
41 | tooltip,
42 | tooltipIcon,
43 | elementRef,
44 | inputRef,
45 | children,
46 | ...rprops
47 | } = props;
48 |
49 | const prefix = useId();
50 | const id = id_ ?? `${prefix}-id`;
51 |
52 | const { grouped, error, errorId } = useContext(CheckboxGroupContext);
53 | const formElemProps = {
54 | required,
55 | error,
56 | errorId,
57 | cols,
58 | tooltip,
59 | tooltipIcon,
60 | elementRef,
61 | };
62 | const checkClassNames = classnames(className, 'slds-checkbox');
63 | const check = (
64 |
65 |
72 |
76 |
77 | );
78 | return grouped ? (
79 | check
80 | ) : (
81 | {check}
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/stories/Badge.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from '../src/scripts';
2 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
3 |
4 | /**
5 | *
6 | */
7 | const meta: ComponentMeta = {
8 | title: 'Badge',
9 | component: Badge,
10 | argTypes: {
11 | onClick: { action: 'click' },
12 | },
13 | };
14 | export default meta;
15 |
16 | /**
17 | *
18 | */
19 | export const Default: ComponentStoryObj = {
20 | args: {
21 | children: 'Badge Label',
22 | },
23 | parameters: {
24 | docs: {
25 | storyDescription: 'Default badge',
26 | },
27 | },
28 | };
29 |
30 | /**
31 | *
32 | */
33 | export const Inverse: ComponentStoryObj = {
34 | args: {
35 | type: 'inverse',
36 | children: 'Badge Label',
37 | },
38 | parameters: {
39 | docs: {
40 | storyDescription: 'Badge with type: inverse',
41 | },
42 | },
43 | };
44 |
45 | /**
46 | *
47 | */
48 | export const Lightest: ComponentStoryObj = {
49 | args: {
50 | type: 'lightest',
51 | children: 'Badge Label',
52 | },
53 | parameters: {
54 | docs: {
55 | storyDescription: 'Badge with type: lightest',
56 | },
57 | },
58 | };
59 |
60 | /**
61 | *
62 | */
63 | export const Success: ComponentStoryObj = {
64 | args: {
65 | type: 'success',
66 | children: 'Badge Label',
67 | },
68 | parameters: {
69 | docs: {
70 | storyDescription: 'Badge with type: success',
71 | },
72 | },
73 | };
74 |
75 | /**
76 | *
77 | */
78 | export const Warning: ComponentStoryObj = {
79 | args: {
80 | type: 'warning',
81 | children: 'Badge Label',
82 | },
83 | parameters: {
84 | docs: {
85 | storyDescription: 'Badge with type: warning',
86 | },
87 | },
88 | };
89 |
90 | /**
91 | *
92 | */
93 | export const Error: ComponentStoryObj = {
94 | args: {
95 | type: 'error',
96 | children: 'Badge Label',
97 | },
98 | parameters: {
99 | docs: {
100 | storyDescription: 'Badge with type: error',
101 | },
102 | },
103 | };
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [React Lightning Design System](https://mashmatrix.github.io/react-lightning-design-system/)
2 | [](https://travis-ci.org/mashmatrix/react-lightning-design-system)
3 |
4 | [Salesforce Lightning Design System](http://www.lightningdesignsystem.com/) components built with React.
5 |
6 | See the [demo](https://mashmatrix.github.io/react-lightning-design-system/).
7 |
8 |
9 | ## Install
10 |
11 | ```
12 | $ npm install react-lightning-design-system
13 | ```
14 |
15 | ## Example
16 |
17 | ```javascript
18 | import React from 'react';
19 | import ReactDOM from 'react-dom';
20 | import { Button } from 'react-lightning-design-system';
21 |
22 | function click() { alert('Clicked'); }
23 |
24 | ReactDOM.render(
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | , document.body);
34 | ```
35 |
36 | See more examples in [examples](https://github.com/mashmatrix/react-lightning-design-system/tree/master/stories) directory.
37 |
38 |
39 | ## Running example stories locally
40 |
41 | This repo ships with a react storybook based story scripts.
42 | To run stories and get component examples, follow these steps:
43 |
44 | 1. run ```npm install```
45 | 2. run ```npm run storybook```
46 | 3. Find the stories running on [localhost:9001](http://localhost:9001).
47 |
48 | ## Snapshot testing in react storybook
49 |
50 | This repo ships with story snapshots to examine differences in rendering as a result of changes to source code.
51 |
52 | To identify render differences run ```npm run test:storyshots```. If all changes are intentional run ```npm run test:storyshots -- -u```. To learn about other run options including *interactive mode*, read
53 | [Snapshot Testing in React Storybook](https://voice.kadira.io/snapshot-testing-in-react-storybook-43b3b71cec4f)
54 |
--------------------------------------------------------------------------------
/src/scripts/Pill.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | HTMLAttributes,
3 | MouseEvent,
4 | KeyboardEvent,
5 | Ref,
6 | FC,
7 | } from 'react';
8 | import classnames from 'classnames';
9 | import { Icon, IconCategory } from './Icon';
10 | import { Button } from './Button';
11 | import { useEventCallback } from './hooks';
12 |
13 | /**
14 | *
15 | */
16 | export type PillProps = {
17 | label?: string;
18 | title?: string;
19 | truncate?: boolean;
20 | disabled?: boolean;
21 | icon?: {
22 | category?: IconCategory;
23 | icon?: string;
24 | };
25 | pillRef?: Ref;
26 | onRemove?: () => void;
27 | } & HTMLAttributes;
28 |
29 | /**
30 | *
31 | */
32 | export const Pill: FC = (props) => {
33 | const {
34 | icon,
35 | disabled,
36 | label,
37 | title,
38 | truncate,
39 | className,
40 | pillRef,
41 | onClick,
42 | onRemove,
43 | } = props;
44 | const onPillRemove = useEventCallback(
45 | (e: MouseEvent | KeyboardEvent) => {
46 | e.preventDefault();
47 | e.stopPropagation();
48 | onRemove?.();
49 | }
50 | );
51 |
52 | const onKeyDown = useEventCallback((e: KeyboardEvent) => {
53 | if (e.keyCode === 8 || e.keyCode === 46) {
54 | // Bacspace / DEL
55 | onPillRemove(e);
56 | }
57 | });
58 |
59 | const pillClassNames = classnames(
60 | 'slds-pill',
61 | { 'slds-pill_link': !disabled },
62 | { 'slds-truncate': truncate },
63 | className
64 | );
65 | return (
66 |
72 | {icon && icon.icon ? (
73 |
74 |
75 |
76 | ) : undefined}
77 | {disabled ? (
78 |
79 | {label}
80 |
81 | ) : (
82 |
83 | {label}
84 |
85 | )}
86 |
95 |
96 | );
97 | };
98 |
--------------------------------------------------------------------------------
/src/scripts/FieldSet.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | HTMLAttributes,
4 | ReactNode,
5 | useMemo,
6 | } from 'react';
7 | import classnames from 'classnames';
8 | import { FormElement } from './FormElement';
9 | import { createFC } from './common';
10 |
11 | /**
12 | *
13 | */
14 | export const FieldSetColumnContext = createContext<{
15 | isFieldSetColumn?: boolean;
16 | totalCols?: number;
17 | }>({});
18 |
19 | /**
20 | *
21 | */
22 | type FieldSetRowProps = {
23 | className?: string;
24 | cols?: number;
25 | children?: ReactNode;
26 | };
27 |
28 | /**
29 | *
30 | */
31 | export const FieldSetRow = createFC<
32 | FieldSetRowProps,
33 | { isFormElement: boolean }
34 | >(
35 | (props) => {
36 | const { className, cols, children } = props;
37 | const totalCols = cols || React.Children.count(children);
38 | const ctx = useMemo(
39 | () => ({ isFieldSetColumn: true, totalCols }),
40 | [totalCols]
41 | );
42 | const rowClassNames = classnames(className, 'slds-form-element__row');
43 | return (
44 |
45 |
46 | {React.Children.map(children, (child) => {
47 | if (
48 | React.isValidElement(child) &&
49 | !(child.type as unknown as { isFormElement?: boolean })
50 | .isFormElement
51 | ) {
52 | return {child};
53 | }
54 | return child;
55 | })}
56 |
57 |
58 | );
59 | },
60 | { isFormElement: true }
61 | );
62 |
63 | /**
64 | *
65 | */
66 | export type FieldSetProps = {
67 | label?: string;
68 | } & HTMLAttributes;
69 |
70 | /**
71 | *
72 | */
73 | export const FieldSet = createFC<
74 | FieldSetProps,
75 | { isFormElement: boolean; Row: typeof FieldSetRow }
76 | >(
77 | ({ className, label, children, ...props }) => {
78 | const fsClassNames = classnames(
79 | className,
80 | 'slds-form-element',
81 | 'slds-form-element_compound'
82 | );
83 | const legendClassNames = classnames(
84 | 'slds-form-element__legend',
85 | 'slds-form-element__label'
86 | );
87 | return (
88 |
92 | );
93 | },
94 | { isFormElement: true, Row: FieldSetRow }
95 | );
96 |
--------------------------------------------------------------------------------
/stories/Grid.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, ReactNode } from 'react';
2 | import { Grid, Row, Col } from '../src/scripts';
3 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
4 |
5 | /**
6 | *
7 | */
8 | const Box: FC<{ children?: ReactNode }> = ({ children }) => {
9 | const styles = {
10 | padding: '12px',
11 | backgroundColor: '#33f',
12 | color: '#fff',
13 | border: '1px solid #aaf',
14 | };
15 | return {children}
;
16 | };
17 |
18 | /**
19 | *
20 | */
21 | const meta: ComponentMeta = {
22 | title: 'Grid',
23 | component: Grid,
24 | };
25 | export default meta;
26 |
27 | /**
28 | *
29 | */
30 | export const Weighted: ComponentStoryObj = {
31 | render: (args) => (
32 |
33 |
34 |
35 | A: w=1
36 |
37 |
38 | B: w=2
39 |
40 |
41 | C: w=1
42 |
43 |
44 |
45 | ),
46 | parameters: {
47 | docs: {
48 | storyDescription: 'columns with weighted width',
49 | },
50 | },
51 | };
52 |
53 | /**
54 | *
55 | */
56 | export const EquallyWeighted: ComponentStoryObj = {
57 | render: (args) => (
58 |
59 |
60 |
61 | A
62 |
63 |
64 | B
65 |
66 |
67 | C
68 |
69 |
70 |
71 | ),
72 | parameters: {
73 | docs: {
74 | storyDescription: 'columns with equally weighted',
75 | },
76 | },
77 | };
78 |
79 | /**
80 | *
81 | */
82 | export const WeightedNoFlex: ComponentStoryObj = {
83 | render: (args) => (
84 |
85 |
86 |
87 | A: w=1
88 |
89 |
90 | B: w=1
91 |
92 |
93 | C: w=2
94 |
95 |
96 | D: w=3
97 |
98 |
99 | E: w=3
100 |
101 |
102 |
103 | ),
104 | name: 'Weighted, no-flex',
105 | parameters: {
106 | docs: {
107 | storyDescription: 'columns with weighted, flex is disabled',
108 | },
109 | },
110 | };
111 |
--------------------------------------------------------------------------------
/src/scripts/Textarea.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useId,
3 | ChangeEvent,
4 | ReactNode,
5 | Ref,
6 | TextareaHTMLAttributes,
7 | useContext,
8 | useRef,
9 | } from 'react';
10 | import classnames from 'classnames';
11 | import { Text } from './Text';
12 | import { FormElement, FormElementProps } from './FormElement';
13 | import { FieldSetColumnContext } from './FieldSet';
14 | import { useEventCallback } from './hooks';
15 | import { createFC } from './common';
16 |
17 | /**
18 | *
19 | */
20 | export type TextareaProps = {
21 | label?: string;
22 | required?: boolean;
23 | error?: FormElementProps['error'];
24 | cols?: number;
25 | tooltip?: ReactNode;
26 | tooltipIcon?: string;
27 | elementRef?: Ref;
28 | textareaRef?: Ref;
29 | onValueChange?: (value: string, prevValue?: string) => void;
30 | readOnly?: boolean;
31 | htmlReadOnly?: boolean;
32 | } & TextareaHTMLAttributes;
33 |
34 | /**
35 | *
36 | */
37 | export const Textarea = createFC(
38 | (props) => {
39 | const {
40 | id,
41 | className,
42 | label,
43 | required,
44 | error,
45 | cols,
46 | tooltip,
47 | tooltipIcon,
48 | elementRef,
49 | textareaRef,
50 | onChange: onChange_,
51 | onValueChange,
52 | readOnly,
53 | htmlReadOnly,
54 | ...rprops
55 | } = props;
56 | const prevValueRef = useRef();
57 | const onChange = useEventCallback((e: ChangeEvent) => {
58 | onChange_?.(e);
59 | onValueChange?.(e.target.value, prevValueRef.current);
60 | prevValueRef.current = e.target.value;
61 | });
62 | const { isFieldSetColumn } = useContext(FieldSetColumnContext);
63 | const errorId = useId();
64 | const taClassNames = classnames(className, 'slds-textarea');
65 | const textareaElem = readOnly ? (
66 |
72 | {rprops.value}
73 |
74 | ) : (
75 |
84 | );
85 | if (isFieldSetColumn || label || required || error || cols) {
86 | const formElemProps = {
87 | controlId: id,
88 | label,
89 | required,
90 | error,
91 | errorId,
92 | cols,
93 | tooltip,
94 | tooltipIcon,
95 | elementRef,
96 | readOnly,
97 | };
98 | return {textareaElem};
99 | }
100 | return textareaElem;
101 | },
102 | { isFormElement: true }
103 | );
104 |
--------------------------------------------------------------------------------
/src/scripts/Select.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | SelectHTMLAttributes,
3 | OptionHTMLAttributes,
4 | Ref,
5 | useContext,
6 | useRef,
7 | ChangeEvent,
8 | FC,
9 | ReactNode,
10 | } from 'react';
11 | import classnames from 'classnames';
12 | import { FormElement, FormElementProps } from './FormElement';
13 | import { FieldSetColumnContext } from './FieldSet';
14 | import { useEventCallback } from './hooks';
15 | import { createFC } from './common';
16 |
17 | /**
18 | *
19 | */
20 | export type SelectProps = {
21 | label?: string;
22 | required?: boolean;
23 | cols?: number;
24 | error?: FormElementProps['error'];
25 | tooltip?: ReactNode;
26 | tooltipIcon?: string;
27 | elementRef?: Ref;
28 | selectRef?: Ref;
29 | onValueChange?: (value: string, prevValue?: string) => void;
30 | } & SelectHTMLAttributes;
31 |
32 | /**
33 | *
34 | */
35 | export const Select = createFC(
36 | (props) => {
37 | const {
38 | id,
39 | className,
40 | label,
41 | required,
42 | error,
43 | cols,
44 | tooltip,
45 | tooltipIcon,
46 | elementRef,
47 | selectRef,
48 | children,
49 | onChange: onChange_,
50 | onValueChange,
51 | ...rprops
52 | } = props;
53 | const { isFieldSetColumn } = useContext(FieldSetColumnContext);
54 | const prevValueRef = useRef();
55 | const onChange = useEventCallback((e: ChangeEvent) => {
56 | onChange_?.(e);
57 | onValueChange?.(e.target.value, prevValueRef.current);
58 | prevValueRef.current = e.target.value;
59 | });
60 | const selectClassNames = classnames(className, 'slds-select');
61 | const selectElem = (
62 |
71 | );
72 | const selectElemWithContainer = rprops.multiple ? (
73 | selectElem
74 | ) : (
75 | {selectElem}
76 | );
77 | if (isFieldSetColumn || label || required || error || cols) {
78 | const formElemProps = {
79 | controlId: id,
80 | label,
81 | required,
82 | error,
83 | cols,
84 | tooltip,
85 | tooltipIcon,
86 | elementRef,
87 | };
88 | return (
89 | {selectElemWithContainer}
90 | );
91 | }
92 | return selectElemWithContainer;
93 | },
94 | { isFormElement: true }
95 | );
96 |
97 | /**
98 | *
99 | */
100 | export type OptionProps = OptionHTMLAttributes;
101 |
102 | /**
103 | *
104 | */
105 | export const Option: FC = (props) => {
106 | const { label, children, ...rprops } = props;
107 | return ;
108 | };
109 |
--------------------------------------------------------------------------------
/stories/MediaObject.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
2 | import React from 'react';
3 | import { MediaObject, util } from '../src/scripts';
4 |
5 | /**
6 | *
7 | */
8 | const image1 = (
9 |
16 | );
17 | const image2 = (
18 |
25 | );
26 | const mediaText = `Sit nulla est ex deserunt exercitation anim occaecat.
27 | Nostrud ullamco deserunt aute id consequat veniam incididunt duis in sint irure nisi.
28 | Mollit officia cillum Lorem ullamco minim nostrud elit officia tempor esse quis.`;
29 | const mediaContent = {mediaText}
;
30 |
31 | /**
32 | *
33 | */
34 | const meta: ComponentMeta = {
35 | title: 'MediaObject',
36 | component: MediaObject,
37 | };
38 | export default meta;
39 |
40 | /**
41 | *
42 | */
43 | export const Figure: ComponentStoryObj = {
44 | args: {
45 | figureLeft: image1,
46 | children: mediaContent,
47 | },
48 | parameters: {
49 | info: 'Media Object with figure in left',
50 | },
51 | };
52 |
53 | /**
54 | *
55 | */
56 | export const FigureCenter: ComponentStoryObj = {
57 | name: 'Figure (Center)',
58 | args: {
59 | figureLeft: image1,
60 | centered: true,
61 | children: mediaContent,
62 | },
63 | parameters: {
64 | info: 'Vertically centered Media Object with figure in left',
65 | },
66 | };
67 |
68 | /**
69 | *
70 | */
71 | export const FigureReverse: ComponentStoryObj = {
72 | name: 'Figure - Reverse',
73 | args: {
74 | figureRight: image2,
75 | children: mediaContent,
76 | },
77 | parameters: {
78 | info: 'Media Object with figure in right',
79 | },
80 | };
81 |
82 | /**
83 | *
84 | */
85 | export const FigureReverseCenter: ComponentStoryObj = {
86 | name: 'Figure - Reverse (Center)',
87 | args: {
88 | figureRight: image2,
89 | centered: true,
90 | children: mediaContent,
91 | },
92 | parameters: {
93 | info: 'Vertically centered Media Object with figure in right',
94 | },
95 | };
96 |
97 | /**
98 | *
99 | */
100 | export const FigureBothSide: ComponentStoryObj = {
101 | name: 'Figure - Both Side',
102 | args: {
103 | figureLeft: image1,
104 | figureRight: image2,
105 | children: mediaContent,
106 | },
107 | parameters: {
108 | info: 'Media Object with figure in left and right',
109 | },
110 | };
111 |
112 | /**
113 | *
114 | */
115 | export const FigureBothSideCenter: ComponentStoryObj = {
116 | name: 'Figure - Both Side (Center)',
117 | args: {
118 | figureLeft: image1,
119 | figureRight: image2,
120 | centered: true,
121 | children: mediaContent,
122 | },
123 | parameters: {
124 | info: 'Vertically centered Media Object with figure in left and right',
125 | },
126 | };
127 |
--------------------------------------------------------------------------------
/stories/Spinner.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spinner } from '../src/scripts/Spinner';
3 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
4 | import { buildContainerDecorator } from './util';
5 |
6 | /**
7 | *
8 | */
9 | const containerStyle = {
10 | position: 'relative' as const,
11 | width: 100,
12 | height: 100,
13 | display: 'inline-block',
14 | };
15 | const inverseContainerStyle = Object.assign({}, containerStyle, {
16 | background: '#16325C',
17 | });
18 |
19 | /**
20 | *
21 | */
22 | const meta: ComponentMeta = {
23 | title: 'Spinner',
24 | component: Spinner,
25 | };
26 | export default meta;
27 |
28 | /**
29 | *
30 | */
31 | export const ControlledWithKnobs: ComponentStoryObj = {
32 | name: 'Controlled with knobs',
33 | decorators: [
34 | buildContainerDecorator<{ type?: unknown }>(({ type }) =>
35 | type === 'inverse' ? inverseContainerStyle : containerStyle
36 | ),
37 | ],
38 | parameters: {
39 | docs: {
40 | description: {
41 | story: 'Spinner with knobs',
42 | },
43 | },
44 | },
45 | };
46 |
47 | /**
48 | *
49 | */
50 | export const Default: ComponentStoryObj = {
51 | render: (args) => (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | ),
67 | parameters: {
68 | docs: {
69 | description: {
70 | story:
71 | 'Default spinner with different sizes (x-small, small, medium, large)',
72 | },
73 | },
74 | },
75 | };
76 |
77 | /**
78 | *
79 | */
80 | export const Brand: ComponentStoryObj = {
81 | ...Default,
82 | args: {
83 | type: 'brand',
84 | },
85 | parameters: {
86 | docs: {
87 | description: {
88 | story:
89 | 'Brand spinner with different sizes (x-small, small, medium, large)',
90 | },
91 | },
92 | },
93 | };
94 |
95 | /**
96 | *
97 | */
98 | export const Inverse: ComponentStoryObj = {
99 | render: (args) => (
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | ),
115 | args: {
116 | type: 'inverse',
117 | },
118 | parameters: {
119 | docs: {
120 | description: {
121 | story:
122 | 'Inverse spinner with different sizes (x-small, small, medium, large)',
123 | },
124 | },
125 | },
126 | };
127 |
--------------------------------------------------------------------------------
/src/scripts/Notification.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEvent, HTMLAttributes, EventHandler, FC } from 'react';
2 | import classnames from 'classnames';
3 | import { Button } from './Button';
4 | import { Icon, IconSize } from './Icon';
5 |
6 | const NOTIFICATION_TYPES = ['alert', 'toast'] as const;
7 |
8 | const NOTIFICATION_LEVELS = ['info', 'success', 'warning', 'error'] as const;
9 |
10 | export type NotificationType = (typeof NOTIFICATION_TYPES)[number];
11 | export type NotificationLevel = (typeof NOTIFICATION_LEVELS)[number];
12 |
13 | export type NotificationProps = {
14 | type?: NotificationType;
15 | level?: NotificationLevel;
16 | alt?: string;
17 | icon?: string;
18 | iconSize?: IconSize;
19 | onClose?: EventHandler>;
20 | } & HTMLAttributes;
21 |
22 | export const Notification: FC = (props) => {
23 | const {
24 | className,
25 | type,
26 | level,
27 | alt,
28 | icon,
29 | iconSize = 'small',
30 | onClose,
31 | children,
32 | ...pprops
33 | } = props;
34 | const typeClassName =
35 | type && NOTIFICATION_TYPES.indexOf(type) >= 0
36 | ? `slds-notify_${type}`
37 | : null;
38 | const levelClassName =
39 | level && NOTIFICATION_LEVELS.indexOf(level) >= 0
40 | ? `slds-theme_${level}`
41 | : null;
42 | const alertClassNames = classnames(
43 | className,
44 | 'slds-notify',
45 | typeClassName,
46 | levelClassName,
47 | {
48 | [`slds-alert_${level}`]: type === 'alert' && level && level !== 'info',
49 | }
50 | );
51 |
52 | const iconEl = icon ? (
53 |
63 | ) : undefined;
64 |
65 | return (
66 |
67 | {alt ?
{alt} : undefined}
68 | {onClose ? (
69 |
70 |
77 |
78 | ) : undefined}
79 | {iconEl}
80 | {type === 'toast' ? (
81 |
82 |
{children}
83 |
84 | ) : (
85 |
{children}
86 | )}
87 |
88 | );
89 | };
90 |
91 | export type AlertProps = Omit;
92 | export const Alert: FC = (props) => (
93 |
94 | );
95 |
96 | export type ToastProps = Omit;
97 | export const Toast: FC = (props) => (
98 |
99 | );
100 |
--------------------------------------------------------------------------------
/stories/Datepicker.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import dayjs from 'dayjs';
3 | import { Datepicker, Button } from '../src/scripts';
4 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
5 | import { containerDecorator } from './util';
6 |
7 | /**
8 | *
9 | */
10 | const TodayButtonExtensionRenderer = (props: {
11 | onSelect?: (date: string) => void;
12 | }) => {
13 | const { onSelect } = props;
14 | const today = dayjs().format('YYYY-MM-DD');
15 | const onSelectToday = useCallback(() => {
16 | onSelect?.(today);
17 | }, [onSelect, today]);
18 | return (
19 |
25 |
28 |
29 | );
30 | };
31 |
32 | const datepickerDecorator = containerDecorator({
33 | padding: 8,
34 | width: 350,
35 | borderRadius: 4,
36 | boxShadow: '0 0 4px gray',
37 | });
38 |
39 | /**
40 | *
41 | */
42 | const meta: ComponentMeta = {
43 | title: 'Datepicker',
44 | component: Datepicker,
45 | argTypes: {
46 | onSelect: { action: 'select' },
47 | onClose: { action: 'close' },
48 | onBlur: { action: 'blur' },
49 | },
50 | };
51 | export default meta;
52 |
53 | /**
54 | *
55 | */
56 | export const ControlledWithKnobs: ComponentStoryObj = {
57 | name: 'Controlled with knobs',
58 | args: {},
59 | decorators: [datepickerDecorator],
60 | parameters: {
61 | docs: {
62 | storyDescription: 'DateInput controlled with knobs',
63 | },
64 | },
65 | };
66 |
67 | /**
68 | *
69 | */
70 | export const Default: ComponentStoryObj = {
71 | args: {
72 | selectedDate: '2016-04-13',
73 | },
74 | decorators: [datepickerDecorator],
75 | parameters: {
76 | docs: {
77 | storyDescription: 'Default date input control',
78 | },
79 | },
80 | };
81 |
82 | /**
83 | *
84 | */
85 | export const WithMinDate: ComponentStoryObj = {
86 | name: 'With min date',
87 | args: {
88 | selectedDate: '2016-04-13',
89 | minDate: '2016-04-05',
90 | },
91 | decorators: [datepickerDecorator],
92 | parameters: {
93 | docs: {
94 | storyDescription: 'Date input with min date',
95 | },
96 | },
97 | };
98 |
99 | /**
100 | *
101 | */
102 | export const WithMaxDate: ComponentStoryObj = {
103 | name: 'With max date',
104 | args: {
105 | selectedDate: '2016-04-13',
106 | maxDate: '2016-04-20',
107 | },
108 | decorators: [datepickerDecorator],
109 | parameters: {
110 | docs: {
111 | storyDescription: 'Date input with max date',
112 | },
113 | },
114 | };
115 |
116 | /**
117 | *
118 | */
119 | export const ExtensionRendering: ComponentStoryObj = {
120 | args: {
121 | selectedDate: '2016-04-13',
122 | extensionRenderer: TodayButtonExtensionRenderer,
123 | },
124 | decorators: [datepickerDecorator],
125 | parameters: {
126 | docs: {
127 | storyDescription: 'Specify extension component in datepicker content',
128 | },
129 | },
130 | };
131 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | defaults: &defaults
4 | docker:
5 | - image: cimg/node:20.12.1-browsers
6 | working_directory: ~/project
7 |
8 | orbs:
9 | browser-tools: circleci/browser-tools@1.4.8
10 |
11 | jobs:
12 | install:
13 | <<: *defaults
14 | steps:
15 | - checkout
16 | - restore_cache:
17 | keys:
18 | - yarn-lock-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
19 | - yarn-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
20 | - run:
21 | name: Environemnt Variable Check
22 | command: |
23 | if [ -z "$AWS_ACCESS_KEY_ID" ]; then
24 | echo "No AWS_ACCESS_KEY_ID is set! Failing..."
25 | exit 1;
26 | else
27 | echo "Credentials are available."
28 | fi
29 | - run:
30 | name: Install Dependencies
31 | command: yarn install
32 | - save_cache:
33 | key: yarn-lock-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
34 | paths:
35 | - node_modules
36 | - save_cache:
37 | key: yarn-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
38 | paths:
39 | - /usr/local/share/.cache/yarn
40 | - persist_to_workspace:
41 | root: ~/
42 | paths:
43 | - project/*
44 |
45 | analyze_static:
46 | <<: *defaults
47 | steps:
48 | - attach_workspace:
49 | at: ~/
50 | - run:
51 | name: Dump Common Ancestor Commit of "master" Branch
52 | command: |
53 | git merge-base master HEAD > common-ancestor-commit.txt
54 | - restore_cache:
55 | keys:
56 | - tsc-output--{{ .Environment.CACHE_VERSION }}--{{ .Branch }}
57 | - tsc-output--{{ .Environment.CACHE_VERSION }}--master--{{ checksum "common-ancestor-commit.txt" }}
58 | - tsc-output--{{ .Environment.CACHE_VERSION }}--master
59 | - run:
60 | name: Lint Source
61 | command: |
62 | yarn lint
63 | - run:
64 | name: Type Check Source
65 | command: |
66 | yarn type-check
67 | - save_cache:
68 | key: tsc-output--{{ .Environment.CACHE_VERSION }}--{{ .Branch }}
69 | paths:
70 | - /tmp/react-lightning-design-system/tsc-output
71 | - save_cache:
72 | key: tsc-output--{{ .Environment.CACHE_VERSION }}--{{ .Branch }}--{{ checksum "common-ancestor-commit.txt" }}
73 | paths:
74 | - /tmp/react-lightning-design-system/tsc-output
75 |
76 | test:
77 | <<: *defaults
78 | steps:
79 | - attach_workspace:
80 | at: ~/
81 | - run:
82 | name: Test
83 | command: |
84 | yarn test
85 |
86 | test_visual:
87 | <<: *defaults
88 | steps:
89 | - browser-tools/install-chrome
90 | - browser-tools/install-chromedriver
91 | - attach_workspace:
92 | at: ~/
93 | - run:
94 | name: Test Visual Regression
95 | command: |
96 | yarn test:visual
97 |
98 | workflows:
99 | version: 2
100 | build_test_deploy:
101 | jobs:
102 | - install
103 | - analyze_static:
104 | requires:
105 | - install
106 | - test:
107 | requires:
108 | - install
109 | - test_visual:
110 | requires:
111 | - install
112 |
--------------------------------------------------------------------------------
/stories/Textarea.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Textarea } from '../src/scripts';
2 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
3 |
4 | /**
5 | *
6 | */
7 | const meta: ComponentMeta = {
8 | title: 'Textarea',
9 | component: Textarea,
10 | argTypes: {
11 | onChange: { action: 'change' },
12 | onValueChange: { aciton: 'valueChange' },
13 | onBlur: { action: 'blur' },
14 | },
15 | };
16 | export default meta;
17 |
18 | /**
19 | *
20 | */
21 | export const ControlledWithKnobs: ComponentStoryObj = {
22 | name: 'Controlled with knobs',
23 | args: {
24 | label: 'Textarea Label',
25 | },
26 | parameters: {
27 | docs: {
28 | description: {
29 | story: 'Textarea controlled with knobs',
30 | },
31 | },
32 | },
33 | };
34 |
35 | /**
36 | *
37 | */
38 | export const Default: ComponentStoryObj = {
39 | args: {
40 | label: 'Textarea Label',
41 | placeholder: 'Placeholder',
42 | },
43 | parameters: {
44 | docs: {
45 | description: {
46 | story: 'Default Textarea control',
47 | },
48 | },
49 | },
50 | };
51 |
52 | /**
53 | *
54 | */
55 | export const Required: ComponentStoryObj = {
56 | args: {
57 | label: 'Textarea Label',
58 | placeholder: 'Placeholder',
59 | required: true,
60 | },
61 | parameters: {
62 | docs: {
63 | description: {
64 | story: 'Textarea control with required attribute',
65 | },
66 | },
67 | },
68 | };
69 |
70 | /**
71 | *
72 | */
73 | export const Error: ComponentStoryObj = {
74 | args: {
75 | label: 'Textarea Label',
76 | placeholder: 'Placeholder',
77 | required: true,
78 | error: 'This field is required',
79 | },
80 | parameters: {
81 | docs: {
82 | description: {
83 | story: 'Textarea control with error message',
84 | },
85 | },
86 | },
87 | };
88 |
89 | /**
90 | *
91 | */
92 | export const Disabled: ComponentStoryObj = {
93 | args: {
94 | label: 'Textarea Label',
95 | placeholder: 'Placeholder',
96 | disabled: true,
97 | },
98 | parameters: {
99 | docs: {
100 | description: {
101 | story: 'Textarea control with disabled status',
102 | },
103 | },
104 | },
105 | };
106 |
107 | /**
108 | *
109 | */
110 | export const ReadOnly: ComponentStoryObj = {
111 | name: 'Read only',
112 | args: {
113 | label: 'Textarea Label',
114 | value: 'Read Only',
115 | readOnly: true,
116 | },
117 | parameters: {
118 | docs: {
119 | description: {
120 | story: 'Textarea control with readOnly status',
121 | },
122 | },
123 | },
124 | };
125 |
126 | /**
127 | *
128 | */
129 | export const ReadOnlyHtml: ComponentStoryObj = {
130 | name: 'Read only (HTML)',
131 | args: {
132 | label: 'Input Label',
133 | value: 'Textarea Only',
134 | htmlReadOnly: true,
135 | },
136 | parameters: {
137 | docs: {
138 | description: {
139 | story:
140 | 'Textarea control with readOnly status (passsed to HTML <input> element)',
141 | },
142 | },
143 | },
144 | };
145 |
146 | /**
147 | *
148 | */
149 | export const WithTooltip: ComponentStoryObj = {
150 | name: 'With tooltip',
151 | args: {
152 | label: 'Textarea Label',
153 | tooltip: 'Tooltip Text',
154 | },
155 | parameters: {
156 | docs: {
157 | description: {
158 | story: 'Textarea control with tooltip',
159 | },
160 | },
161 | },
162 | };
163 |
--------------------------------------------------------------------------------
/stories/Radio.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps } from 'react';
2 | import { RadioGroup, Radio } from '../src/scripts';
3 | import { Meta, StoryObj } from '@storybook/react';
4 |
5 | /**
6 | *
7 | */
8 | type StoryProps = ComponentProps & {
9 | radiogroup1?: ComponentProps;
10 | radiogroup2?: ComponentProps;
11 | radio1?: ComponentProps;
12 | radio2?: ComponentProps;
13 | };
14 |
15 | /**
16 | *
17 | */
18 | const meta: Meta = {
19 | title: 'Radio',
20 | component: RadioGroup,
21 | subcomponents: { Radio },
22 | argTypes: {
23 | onChange: { action: 'change' },
24 | },
25 | };
26 | export default meta;
27 |
28 | /**
29 | *
30 | */
31 | export const ControlledWithKnobs: StoryObj = {
32 | render: ({ radio1, radio2, ...args }) => (
33 |
34 |
35 |
36 |
37 | ),
38 | name: 'Controlled with knobs',
39 | args: {
40 | radio1: {
41 | label: 'Radio Label One',
42 | value: '1',
43 | },
44 | radio2: {
45 | label: 'Radio Label Two',
46 | value: '2',
47 | },
48 | label: 'Radio Group Label',
49 | },
50 | parameters: {
51 | docs: {
52 | description: {
53 | story: 'Radio Group controlled with knobs',
54 | },
55 | },
56 | },
57 | };
58 |
59 | /**
60 | *
61 | */
62 | export const Default: StoryObj = {
63 | render: (args) => (
64 |
65 |
66 |
67 |
68 | ),
69 | args: {
70 | label: 'Radio Group Label',
71 | },
72 | parameters: {
73 | docs: {
74 | description: {
75 | story: 'Default Radio Group control',
76 | },
77 | },
78 | },
79 | };
80 |
81 | /**
82 | *
83 | */
84 | export const Required: StoryObj = {
85 | ...Default,
86 | args: {
87 | label: 'Radio Group Label',
88 | required: true,
89 | },
90 | parameters: {
91 | docs: {
92 | description: {
93 | story: 'Radio Group control with required attribute',
94 | },
95 | },
96 | },
97 | };
98 |
99 | /**
100 | *
101 | */
102 | export const Error: StoryObj = {
103 | ...Default,
104 | args: {
105 | label: 'Radio Group Label',
106 | required: true,
107 | error: 'This field is required',
108 | },
109 | parameters: {
110 | docs: {
111 | description: {
112 | story: 'Radio Group control with error message',
113 | },
114 | },
115 | },
116 | };
117 |
118 | /**
119 | *
120 | */
121 | export const Disabled: StoryObj = {
122 | render: (args) => (
123 |
124 |
125 |
126 |
127 | ),
128 | args: {
129 | label: 'Radio Group Label',
130 | },
131 | parameters: {
132 | docs: {
133 | description: {
134 | story: 'Radio Group control with disabled status',
135 | },
136 | },
137 | },
138 | };
139 |
140 | /**
141 | *
142 | */
143 | export const WithTooltip: StoryObj = {
144 | render: (args) => (
145 |
146 |
147 |
148 |
149 | ),
150 | args: {
151 | label: 'Radio Group Label',
152 | tooltip: 'Tooltip Text',
153 | },
154 | parameters: {
155 | docs: {
156 | description: {
157 | story: 'Radio Group control with tooltip',
158 | },
159 | },
160 | },
161 | };
162 |
--------------------------------------------------------------------------------
/src/scripts/RadioGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useId,
3 | HTMLAttributes,
4 | Ref,
5 | createContext,
6 | useContext,
7 | useMemo,
8 | ReactNode,
9 | } from 'react';
10 | import classnames from 'classnames';
11 | import { FieldSetColumnContext } from './FieldSet';
12 | import { TooltipContent } from './TooltipContent';
13 | import { FormElementProps } from './FormElement';
14 | import { createFC } from './common';
15 | import { Bivariant } from './typeUtils';
16 |
17 | /**
18 | *
19 | */
20 | export type RadioValueType = string | number;
21 |
22 | /**
23 | *
24 | */
25 | export const RadioGroupContext = createContext<{
26 | name?: string;
27 | error?: FormElementProps['error'];
28 | errorId?: string;
29 | onValueChange?: Bivariant<(value: RadioValueType) => void>;
30 | }>({});
31 |
32 | /**
33 | *
34 | */
35 | export type RadioGroupProps = {
36 | label?: string;
37 | required?: boolean;
38 | error?: boolean | string | { message: string };
39 | name?: string;
40 | cols?: number;
41 | tooltip?: ReactNode;
42 | tooltipIcon?: string;
43 | elementRef?: Ref;
44 | onValueChange?: Bivariant<(value: RadioValueType) => void>;
45 | } & HTMLAttributes;
46 |
47 | /**
48 | *
49 | */
50 | export const RadioGroup = createFC(
51 | (props) => {
52 | const {
53 | className,
54 | label,
55 | required,
56 | error,
57 | cols,
58 | style,
59 | children,
60 | name,
61 | tooltip,
62 | tooltipIcon,
63 | elementRef,
64 | onValueChange,
65 | ...rprops
66 | } = props;
67 | const { totalCols } = useContext(FieldSetColumnContext);
68 | const grpClassNames = classnames(
69 | className,
70 | 'slds-form-element',
71 | {
72 | 'slds-has-error': error,
73 | 'slds-is-required': required,
74 | },
75 | typeof totalCols === 'number'
76 | ? `slds-size_${cols || 1}-of-${totalCols}`
77 | : null
78 | );
79 | const grpStyles =
80 | typeof totalCols === 'number'
81 | ? { display: 'inline-block', ...style }
82 | : style;
83 | const errorMessage = error
84 | ? typeof error === 'string'
85 | ? error
86 | : typeof error === 'object'
87 | ? error.message
88 | : undefined
89 | : undefined;
90 |
91 | const errorId = useId();
92 | const grpCtx = useMemo(
93 | () => ({ name, error, errorId, onValueChange }),
94 | [name, error, errorId, onValueChange]
95 | );
96 |
97 | return (
98 |
133 | );
134 | },
135 | { isFormElement: true }
136 | );
137 |
--------------------------------------------------------------------------------
/src/scripts/FormElement.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | Ref,
3 | useContext,
4 | useMemo,
5 | ReactNode,
6 | CSSProperties,
7 | useRef,
8 | useCallback,
9 | } from 'react';
10 | import classnames from 'classnames';
11 | import { FieldSetColumnContext } from './FieldSet';
12 | import { createFC } from './common';
13 | import { TooltipContent } from './TooltipContent';
14 |
15 | /**
16 | *
17 | */
18 | export type FormElementProps = {
19 | id?: string;
20 | className?: string;
21 | controlId?: string;
22 | label?: string;
23 | required?: boolean;
24 | error?: boolean | string | { message: string };
25 | errorId?: string;
26 | readOnly?: boolean;
27 | cols?: number;
28 | dropdown?: JSX.Element;
29 | elementRef?: Ref;
30 | style?: CSSProperties;
31 | children?: ReactNode;
32 | tooltip?: ReactNode;
33 | tooltipIcon?: string;
34 | };
35 |
36 | /**
37 | *
38 | */
39 | export const FormElement = createFC<
40 | FormElementProps,
41 | { isFormElement: boolean }
42 | >(
43 | (props) => {
44 | const {
45 | id,
46 | className,
47 | controlId,
48 | cols = 1,
49 | elementRef,
50 | label,
51 | required,
52 | error,
53 | errorId,
54 | dropdown,
55 | children,
56 | readOnly,
57 | tooltip,
58 | tooltipIcon,
59 | } = props;
60 |
61 | const controlElRef = useRef(null);
62 |
63 | const { totalCols } = useContext(FieldSetColumnContext);
64 |
65 | const errorMessage = error
66 | ? typeof error === 'string'
67 | ? error
68 | : typeof error === 'object'
69 | ? error.message
70 | : undefined
71 | : undefined;
72 |
73 | const formElementClassNames = classnames(
74 | 'slds-form-element',
75 | readOnly ? 'slds-form-element_readonly' : null,
76 | error ? 'slds-has-error' : null,
77 | typeof totalCols === 'number'
78 | ? `slds-size_${cols}-of-${totalCols}`
79 | : null,
80 | className
81 | );
82 |
83 | const onClickLabel = useCallback(() => {
84 | if (controlElRef.current) {
85 | const inputEl = controlElRef.current.querySelector(
86 | 'input,select,button'
87 | );
88 | inputEl?.focus();
89 | }
90 | }, []);
91 |
92 | const emptyCtx = useMemo(() => ({}), []);
93 |
94 | const LabelTag = readOnly ? 'span' : 'label';
95 |
96 | return (
97 |
98 |
99 | {label ? (
100 |
106 | {required ? (
107 |
112 | *
113 |
114 | ) : undefined}
115 | {label}
116 |
117 | ) : null}
118 | {tooltip ? (
119 |
{tooltip}
120 | ) : null}
121 |
122 | {readOnly ? (
123 |
{children}
124 | ) : (
125 | children
126 | )}
127 | {dropdown}
128 | {errorMessage ? (
129 |
133 | {errorMessage}
134 |
135 | ) : undefined}
136 |
137 |
138 |
139 | );
140 | },
141 | { isFormElement: true }
142 | );
143 |
--------------------------------------------------------------------------------
/src/scripts/Popover.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | HTMLAttributes,
3 | CSSProperties,
4 | FC,
5 | ReactNode,
6 | forwardRef,
7 | useId,
8 | useEffect,
9 | } from 'react';
10 | import classnames from 'classnames';
11 | import {
12 | AutoAlign,
13 | AutoAlignInjectedProps,
14 | RectangleAlignment,
15 | } from './AutoAlign';
16 | import { registerStyle } from './util';
17 |
18 | /**
19 | *
20 | */
21 | function useInitComponentStyle() {
22 | useEffect(() => {
23 | registerStyle('popover', [
24 | ['.react-slds-popover.slds-popover_tooltip a', '{ color: white; }'],
25 | ]);
26 | }, []);
27 | }
28 |
29 | /**
30 | *
31 | */
32 | export const PopoverHeader: FC<{ children?: ReactNode }> = (props) => (
33 | {props.children}
34 | );
35 |
36 | /**
37 | *
38 | */
39 | export type PopoverBodyProps = React.HTMLAttributes;
40 |
41 | export const PopoverBody: FC = (props) => (
42 |
43 | {props.children}
44 |
45 | );
46 |
47 | /**
48 | *
49 | */
50 | export type PopoverPosition =
51 | | 'top'
52 | | 'top-left'
53 | | 'top-right'
54 | | 'bottom'
55 | | 'bottom-left'
56 | | 'bottom-right'
57 | | 'left'
58 | | 'left-top'
59 | | 'left-bottom'
60 | | 'right'
61 | | 'right-top'
62 | | 'right-bottom';
63 |
64 | export type PopoverTheme = 'info' | 'success' | 'warning' | 'error';
65 |
66 | export type PopoverProps = {
67 | position?: PopoverPosition;
68 | hidden?: boolean;
69 | theme?: PopoverTheme;
70 | tooltip?: boolean;
71 | bodyStyle?: CSSProperties;
72 | offsetX?: number;
73 | offsetY?: number;
74 | } & HTMLAttributes;
75 |
76 | /**
77 | *
78 | */
79 | export const PopoverInner = forwardRef<
80 | HTMLElement,
81 | PopoverProps & AutoAlignInjectedProps
82 | >((props, ref) => {
83 | const {
84 | children,
85 | alignment,
86 | hidden,
87 | theme,
88 | tooltip,
89 | style,
90 | bodyStyle,
91 | ...rprops
92 | } = props;
93 | const nubbinPosition = alignment.join('-');
94 | const [firstAlign, secondAlign] = alignment;
95 | const popoverClassNames = classnames(
96 | 'react-slds-popover',
97 | 'slds-popover',
98 | {
99 | 'slds-hide': hidden,
100 | 'slds-popover_tooltip': tooltip,
101 | },
102 | `slds-nubbin_${nubbinPosition}`,
103 | `slds-m-${firstAlign}_small`,
104 | theme ? `slds-theme_${theme}` : undefined
105 | );
106 | const rootStyle: typeof style = {
107 | ...style,
108 | position: 'absolute',
109 | [firstAlign]: 0,
110 | ...(secondAlign ? { [secondAlign]: 0 } : {}),
111 | ...(tooltip ? { width: 'max-content' } : {}),
112 | transform:
113 | secondAlign === undefined
114 | ? firstAlign === 'top' || firstAlign === 'bottom'
115 | ? 'translateX(-50%)'
116 | : firstAlign === 'left' || firstAlign === 'right'
117 | ? 'translateY(-50%)'
118 | : undefined
119 | : undefined,
120 | };
121 | const bodyId = useId();
122 | return (
123 |
131 |
132 | {children}
133 |
134 |
135 | );
136 | });
137 |
138 | /**
139 | *
140 | */
141 | export const Popover = forwardRef(
142 | ({ position, offsetX = 0, offsetY = 0, ...props }, ref) => {
143 | useInitComponentStyle();
144 |
145 | const alignment: RectangleAlignment | undefined = position?.split('-') as
146 | | RectangleAlignment
147 | | undefined;
148 | return (
149 |
156 | {(injectedProps) => (
157 |
158 | )}
159 |
160 | );
161 | }
162 | );
163 |
--------------------------------------------------------------------------------
/src/scripts/Modal.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes, CSSProperties, FC, ReactNode } from 'react';
2 | import classnames from 'classnames';
3 | import { Button } from './Button';
4 | import { Text } from './Text';
5 | import { useEventCallback } from './hooks';
6 |
7 | /**
8 | *
9 | */
10 | export type ModalHeaderProps = {
11 | className?: string;
12 | title?: string;
13 | tagline?: string;
14 | };
15 |
16 | /**
17 | *
18 | */
19 | export const ModalHeader: FC = (props) => {
20 | const { className, title, tagline, ...rprops } = props;
21 | const hdClassNames = classnames(className, 'slds-modal__header');
22 | return (
23 |
24 |
25 | {title}
26 |
27 | {tagline ?
{tagline}
: null}
28 |
29 | );
30 | };
31 |
32 | /**
33 | *
34 | */
35 | export type ModalContentProps = {
36 | className?: string;
37 | children?: ReactNode;
38 | };
39 |
40 | /**
41 | *
42 | */
43 | export const ModalContent: FC = ({
44 | className,
45 | children,
46 | ...props
47 | }) => {
48 | const ctClassNames = classnames(className, 'slds-modal__content');
49 | return (
50 |
51 | {children}
52 |
53 | );
54 | };
55 |
56 | /**
57 | *
58 | */
59 | export type ModalFooterProps = {
60 | className?: string;
61 | directional?: boolean;
62 | children?: ReactNode;
63 | };
64 |
65 | /**
66 | *
67 | */
68 | export const ModalFooter: FC = ({
69 | className,
70 | directional,
71 | children,
72 | ...props
73 | }) => {
74 | const ftClassNames = classnames(className, 'slds-modal__footer', {
75 | 'slds-modal__footer_directional': directional,
76 | });
77 | return (
78 |
79 | {children}
80 |
81 | );
82 | };
83 |
84 | /**
85 | *
86 | */
87 | export type ModalSize = 'large';
88 |
89 | export type ModalProps = {
90 | size?: ModalSize;
91 | opened?: boolean;
92 | containerStyle?: CSSProperties;
93 | onHide?: () => void;
94 | closeButton?: boolean;
95 | onClose?: () => void;
96 | } & HTMLAttributes;
97 |
98 | /**
99 | *
100 | */
101 | const Modal_: FC = (props) => {
102 | const {
103 | className,
104 | opened,
105 | children,
106 | size,
107 | containerStyle,
108 | onHide,
109 | closeButton,
110 | onClose: onClose_,
111 | ...rprops
112 | } = props;
113 | const modalClassNames = classnames(className, 'slds-modal', {
114 | 'slds-fade-in-open': opened,
115 | 'slds-modal_large': size === 'large',
116 | });
117 | const backdropClassNames = classnames(className, 'slds-backdrop', {
118 | 'slds-backdrop_open': opened,
119 | });
120 | const onClose = useEventCallback(() => {
121 | onClose_?.();
122 | onHide?.();
123 | });
124 | return (
125 | <>
126 |
134 |
135 | {closeButton ? (
136 |
145 | ) : null}
146 | {children}
147 |
148 |
149 |
150 | >
151 | );
152 | };
153 |
154 | type ModalType = FC & {
155 | Header: typeof ModalHeader;
156 | Content: typeof ModalContent;
157 | Footer: typeof ModalFooter;
158 | };
159 |
160 | (Modal_ as unknown as ModalType).Header = ModalHeader;
161 | (Modal_ as unknown as ModalType).Content = ModalContent;
162 | (Modal_ as unknown as ModalType).Footer = ModalFooter;
163 |
164 | export const Modal: ModalType = Modal_ as ModalType;
165 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-lightning-design-system",
3 | "version": "6.0.0-beta.12",
4 | "description": "Salesforce Lightning Design System components built with React",
5 | "main": "lib/scripts/index.js",
6 | "module": "module/scripts/index.js",
7 | "types": "lib/scripts/index.d.ts",
8 | "keywords": [
9 | "react",
10 | "react-component",
11 | "salesforce",
12 | "lightning",
13 | "lightning design system",
14 | "slds",
15 | "tab",
16 | "form",
17 | "datepicker",
18 | "modal"
19 | ],
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/mashmatrix/react-lightning-design-system.git"
23 | },
24 | "scripts": {
25 | "storybook": "start-storybook -s ./node_modules/@salesforce-ux/design-system -p 9001 -c .storybook",
26 | "test": "echo \"no test to run\"",
27 | "pretest:visual": "storycap --serverCmd \"NODE_ENV=test npm run storybook -- --ci\" http://localhost:9001 -o images --serverTimeout 3600000 --captureTimeout 10000 --delay 2000",
28 | "test:visual": "reg-suit -v run",
29 | "type-check": "tsc --noEmit",
30 | "type-check:watch": "npm run type-check -- --watch",
31 | "format": "run-p format:src format:stories",
32 | "format:src": "npm run lint:src -- --fix",
33 | "format:stories": "npm run lint:stories -- --fix",
34 | "lint": "run-p lint:src lint:stories",
35 | "lint:src": "eslint --ext .ts,.tsx src/scripts/**",
36 | "lint:stories": "eslint --ext .ts,.tsx stories/**",
37 | "build": "run-p build:lib build:module build:types",
38 | "build:lib": "babel -d lib/ src/ --extensions \".ts,.tsx\" --source-maps true",
39 | "build:module": "BUILD_TARGET=module babel -d module/ src/ --extensions \".ts,.tsx\" --source-maps true",
40 | "build:types": "tsc -p tsconfig.types.json",
41 | "build:assets": "cp -r node_modules/@salesforce-ux/design-system/assets public",
42 | "build:storybook": "build-storybook -o public",
43 | "deploy": "run-s build:storybook build:assets deploy:ghpage",
44 | "deploy:ghpage": "gh-pages -d public"
45 | },
46 | "files": [
47 | "lib",
48 | "module",
49 | "src"
50 | ],
51 | "author": "Shinichi Tomita ",
52 | "license": "MIT",
53 | "dependencies": {
54 | "@babel/runtime": "^7.17.9",
55 | "classnames": "^2.3.1",
56 | "dayjs": "^1.11.2",
57 | "keycoder": "^1.1.1",
58 | "react-merge-refs": "^1.1.0",
59 | "react-relative-portal": "github:stomita/react-relative-portal#dist",
60 | "svg4everybody": "^2.1.9"
61 | },
62 | "devDependencies": {
63 | "@babel/cli": "^7.23.0",
64 | "@babel/core": "^7.23.3",
65 | "@babel/plugin-transform-runtime": "^7.23.3",
66 | "@babel/preset-env": "^7.23.3",
67 | "@babel/preset-react": "^7.23.3",
68 | "@babel/preset-typescript": "^7.23.3",
69 | "@salesforce-ux/design-system": "^2.22.2",
70 | "@storybook/addon-actions": "^6.5.16",
71 | "@storybook/addon-controls": "^6.5.16",
72 | "@storybook/addon-docs": "^6.5.16",
73 | "@storybook/addon-storyshots": "^6.5.16",
74 | "@storybook/react": "^6.5.16",
75 | "@storybook/theming": "^6.5.16",
76 | "@types/classnames": "^2.2.7",
77 | "@types/react": "^18.2.37",
78 | "@types/react-dom": "^18.2.15",
79 | "@types/svg4everybody": "^2.1.1",
80 | "@typescript-eslint/eslint-plugin": "^6.10.0",
81 | "@typescript-eslint/parser": "^6.10.0",
82 | "babel-loader": "^9.1.3",
83 | "eslint": "^8.53.0",
84 | "eslint-config-prettier": "^9.0.0",
85 | "eslint-plugin-jsx-a11y": "^6.8.0",
86 | "eslint-plugin-prettier": "^5.0.1",
87 | "eslint-plugin-react": "^7.33.2",
88 | "eslint-plugin-react-hooks": "^4.6.0",
89 | "gh-pages": "^0.12.0",
90 | "npm-run-all": "^4.0.1",
91 | "prettier": "^3.0.3",
92 | "react": "^18.2.0",
93 | "react-docgen-typescript-plugin": "^1.0.5",
94 | "react-dom": "^18.2.0",
95 | "reg-keygen-git-hash-plugin": "^0.11.1",
96 | "reg-notify-github-plugin": "^0.11.1",
97 | "reg-publish-s3-plugin": "^0.11.0",
98 | "reg-suit": "^0.11.1",
99 | "storycap": "^3.1.7",
100 | "typescript": "^5.2.2"
101 | },
102 | "peerDependencies": {
103 | "@types/react": "^18.0.0",
104 | "react": "^18.0.0",
105 | "react-dom": "^18.0.0"
106 | },
107 | "resolutions": {
108 | "@types/react": "^18.2.37"
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/stories/DateInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DateInput } from '../src/scripts';
2 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
3 |
4 | /**
5 | *
6 | */
7 | const meta: ComponentMeta = {
8 | title: 'DateInput',
9 | component: DateInput,
10 | argTypes: {
11 | onBlur: { action: 'blur' },
12 | onValueChange: { action: 'valueChange' },
13 | onComplete: { action: 'complete' },
14 | },
15 | parameters: {
16 | docs: {
17 | inlineStories: false,
18 | iframeHeight: 400,
19 | },
20 | },
21 | };
22 | export default meta;
23 |
24 | /**
25 | *
26 | */
27 | export const ControlledWithKnobs: ComponentStoryObj = {
28 | name: 'Controlled with knobs',
29 | args: {
30 | label: 'Date Input Label',
31 | },
32 | parameters: {
33 | docs: {
34 | description: {
35 | story: 'DateInput controlled with knobs',
36 | },
37 | },
38 | },
39 | };
40 |
41 | /**
42 | *
43 | */
44 | export const Default: ComponentStoryObj = {
45 | args: {
46 | label: 'Date Input Label',
47 | value: '2016-04-13',
48 | defaultOpened: true,
49 | },
50 | parameters: {
51 | docs: {
52 | description: {
53 | story: 'Default date input control',
54 | },
55 | },
56 | },
57 | };
58 |
59 | /**
60 | *
61 | */
62 | export const Required: ComponentStoryObj = {
63 | args: {
64 | label: 'Date Input Label',
65 | value: '2016-04-13',
66 | defaultOpened: true,
67 | required: true,
68 | },
69 | parameters: {
70 | docs: {
71 | description: {
72 | story: 'Date input control with required attribute',
73 | },
74 | },
75 | },
76 | };
77 |
78 | /**
79 | *
80 | */
81 | export const Error: ComponentStoryObj = {
82 | args: {
83 | label: 'Date Input Label',
84 | value: '2016-04-13',
85 | defaultOpened: true,
86 | required: true,
87 | error: 'This field is required',
88 | },
89 | parameters: {
90 | docs: {
91 | description: {
92 | story: 'Date input control with error message',
93 | },
94 | },
95 | },
96 | };
97 |
98 | /**
99 | *
100 | */
101 | export const Disabled: ComponentStoryObj = {
102 | args: {
103 | label: 'Date Input Label',
104 | value: '2016-04-13',
105 | defaultOpened: true,
106 | disabled: true,
107 | },
108 | parameters: {
109 | docs: {
110 | description: {
111 | story: 'Date input control with disabled status',
112 | },
113 | },
114 | },
115 | };
116 |
117 | /**
118 | *
119 | */
120 | export const WithDateFormat: ComponentStoryObj = {
121 | name: 'With date format',
122 | args: {
123 | label: 'Date Input Label',
124 | value: '2016-04-13',
125 | defaultOpened: true,
126 | dateFormat: 'YYYY.MM.DD',
127 | },
128 | parameters: {
129 | docs: {
130 | description: {
131 | story: 'Date input control with date format specified (YYYY.MM.DD)',
132 | },
133 | },
134 | },
135 | };
136 |
137 | /**
138 | *
139 | */
140 | export const WithMinMaxDate: ComponentStoryObj = {
141 | name: 'With min/max date',
142 | args: {
143 | label: 'Date Input Label',
144 | value: '2016-04-13',
145 | defaultOpened: true,
146 | minDate: '2016-04-10',
147 | maxDate: '2016-04-19',
148 | },
149 | parameters: {
150 | docs: {
151 | description: {
152 | story: 'Date input control with minimum date boundary',
153 | },
154 | },
155 | },
156 | };
157 |
158 | /**
159 | *
160 | */
161 | export const IncludeTimeData: ComponentStoryObj = {
162 | name: 'Include time data',
163 | args: {
164 | label: 'Date Input Label',
165 | value: '2016-04-13',
166 | defaultOpened: true,
167 | dateFormat: 'YYYY/MM/DD HH:mm:ss',
168 | includeTime: true,
169 | },
170 | parameters: {
171 | docs: {
172 | description: {
173 | story: 'Date input control with time information',
174 | },
175 | },
176 | },
177 | };
178 |
179 | /**
180 | *
181 | */
182 | export const WithTooltip: ComponentStoryObj = {
183 | name: 'With tooltip',
184 | args: {
185 | label: 'Date Input Label',
186 | tooltip: 'Tooltip Text',
187 | },
188 | parameters: {
189 | docs: {
190 | description: {
191 | story: 'Date input control with tooltip',
192 | },
193 | },
194 | },
195 | };
196 |
--------------------------------------------------------------------------------
/src/scripts/CheckboxGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useId,
3 | createContext,
4 | FieldsetHTMLAttributes,
5 | Ref,
6 | useContext,
7 | useMemo,
8 | useRef,
9 | ReactNode,
10 | } from 'react';
11 | import classnames from 'classnames';
12 | import { FormElementProps } from './FormElement';
13 | import { FieldSetColumnContext } from './FieldSet';
14 | import { TooltipContent } from './TooltipContent';
15 | import { useEventCallback } from './hooks';
16 | import { createFC } from './common';
17 | import { Bivariant } from './typeUtils';
18 |
19 | /**
20 | *
21 | */
22 | export type CheckboxValueType = string | number;
23 |
24 | /**
25 | *
26 | */
27 | export const CheckboxGroupContext = createContext<{
28 | grouped?: boolean;
29 | error?: FormElementProps['error'];
30 | errorId?: string;
31 | }>({});
32 |
33 | /**
34 | *
35 | */
36 | export type CheckboxGroupProps = {
37 | label?: string;
38 | required?: boolean;
39 | error?: FormElementProps['error'];
40 | name?: string;
41 | cols?: number;
42 | tooltip?: ReactNode;
43 | tooltipIcon?: string;
44 | elementRef?: Ref;
45 | onValueChange?: Bivariant<(values: CheckboxValueType[]) => void>;
46 | } & FieldsetHTMLAttributes;
47 |
48 | /**
49 | *
50 | */
51 | export const CheckboxGroup = createFC<
52 | CheckboxGroupProps,
53 | { isFormElement: boolean }
54 | >(
55 | (props) => {
56 | const {
57 | className,
58 | label,
59 | cols,
60 | style,
61 | required,
62 | error,
63 | tooltip,
64 | tooltipIcon,
65 | elementRef,
66 | onValueChange,
67 | onChange: onChange_,
68 | children,
69 | ...rprops
70 | } = props;
71 | const { totalCols } = useContext(FieldSetColumnContext);
72 | const controlElRef = useRef(null);
73 |
74 | const onChange = useEventCallback(
75 | (e: React.FormEvent) => {
76 | if (onValueChange) {
77 | const checkboxes =
78 | controlElRef.current?.querySelectorAll(
79 | 'input[type=checkbox]'
80 | );
81 | if (!checkboxes) {
82 | return;
83 | }
84 | const values = [...checkboxes]
85 | .filter((checkbox) => checkbox.checked)
86 | .map((checkbox) => checkbox.value);
87 | onValueChange?.(values);
88 | }
89 | onChange_?.(e);
90 | }
91 | );
92 |
93 | const grpClassNames = classnames(
94 | className,
95 | 'slds-form-element',
96 | {
97 | 'slds-has-error': error,
98 | 'slds-is-required': required,
99 | },
100 | typeof totalCols === 'number'
101 | ? `slds-size_${cols || 1}-of-${totalCols}`
102 | : null
103 | );
104 | const grpStyles =
105 | typeof totalCols === 'number'
106 | ? { display: 'inline-block', ...style }
107 | : style;
108 | const errorMessage = error
109 | ? typeof error === 'string'
110 | ? error
111 | : typeof error === 'object'
112 | ? error.message
113 | : undefined
114 | : undefined;
115 |
116 | const errorId = useId();
117 | const grpCtx = useMemo(
118 | () => ({ grouped: true, error, errorId }),
119 | [error, errorId]
120 | );
121 |
122 | return (
123 |
158 | );
159 | },
160 | { isFormElement: true }
161 | );
162 |
--------------------------------------------------------------------------------
/stories/DropdownMenu.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps } from 'react';
2 | import { DropdownMenu, DropdownMenuItem } from '../src/scripts/DropdownMenu';
3 | import { Meta, StoryObj } from '@storybook/react';
4 |
5 | /**
6 | *
7 | */
8 | type StoryProps = ComponentProps & {
9 | menuItems?: Array>;
10 | };
11 |
12 | /**
13 | *
14 | */
15 | const meta: Meta = {
16 | title: 'DropdownMenu',
17 | component: DropdownMenu,
18 | argTypes: {
19 | onMenuSelect: { action: 'menuSelect' },
20 | onMenuClose: { action: 'menuClose' },
21 | },
22 | };
23 | export default meta;
24 |
25 | /**
26 | *
27 | */
28 | export const ControlledWithKnobs: StoryObj = {
29 | render: ({ menuItems = [], ...args }) => (
30 |
31 |
32 |
33 |
34 | {menuItems[3] ? : undefined}
35 |
36 | ),
37 | name: 'Controlled with knobs',
38 | args: {
39 | menuItems: [
40 | {
41 | eventKey: '1',
42 | children: 'Menu Item One',
43 | },
44 | {
45 | eventKey: '2',
46 | children: 'Menu Item One',
47 | },
48 | {
49 | eventKey: '3',
50 | children: 'Menu Item One',
51 | },
52 | ],
53 | },
54 | parameters: {
55 | docs: {
56 | storyDescription: 'Dropdown menu controlled with knobs',
57 | },
58 | },
59 | };
60 |
61 | /**
62 | *
63 | */
64 | export const Default: StoryObj = {
65 | render: ({ menuItems = [], ...args }) => (
66 |
67 |
68 |
69 |
70 | {menuItems[3] ? : undefined}
71 |
72 | ),
73 | args: {
74 | menuItems: [
75 | {
76 | eventKey: 1,
77 | children: 'Menu Item One',
78 | },
79 | {
80 | eventKey: 2,
81 | disabled: true,
82 | children: 'Menu Item Two',
83 | },
84 | {
85 | eventKey: 3,
86 | children: 'Menu Item Three',
87 | },
88 | {
89 | eventKey: 4,
90 | divider: 'top',
91 | children: 'Menu Item Four',
92 | },
93 | ],
94 | },
95 | parameters: {
96 | docs: {
97 | storyDescription: 'Dropdown menu',
98 | },
99 | },
100 | };
101 |
102 | /**
103 | *
104 | */
105 | export const WithSubmenu: StoryObj = {
106 | ...Default,
107 | args: {
108 | menuItems: [
109 | {
110 | eventKey: 1,
111 | children: 'Menu Item One',
112 | },
113 | {
114 | eventKey: 2,
115 | disabled: true,
116 | children: 'Menu Item Two',
117 | },
118 | {
119 | eventKey: 3,
120 | children: 'Menu Item Three',
121 | submenuItems: [
122 | {
123 | eventKey: 31,
124 | key: 31,
125 | label: 'Menu Item Three - One',
126 | submenuItems: [
127 | {
128 | eventKey: 311,
129 | key: 311,
130 | label: 'Menu Item Three - One - One',
131 | },
132 | {
133 | eventKey: 312,
134 | key: 312,
135 | label: 'Menu Item Three - One - Two',
136 | },
137 | {
138 | eventKey: 313,
139 | key: 313,
140 | label: 'Menu Item Three - One - Three',
141 | },
142 | ],
143 | },
144 | {
145 | eventKey: 32,
146 | key: 32,
147 | label: 'Menu Item Three - Two',
148 | submenuItems: [
149 | {
150 | eventKey: 321,
151 | key: 321,
152 | label: 'Menu Item Three - Two - One',
153 | },
154 | {
155 | eventKey: 322,
156 | key: 322,
157 | label: 'Menu Item Three - Two - Two',
158 | },
159 | {
160 | eventKey: 323,
161 | key: 323,
162 | label: 'Menu Item Three - Two - Three',
163 | },
164 | ],
165 | },
166 | ],
167 | },
168 | {
169 | eventKey: 4,
170 | divider: 'top',
171 | children: 'Menu Item Four',
172 | },
173 | ],
174 | },
175 | parameters: {
176 | docs: {
177 | storyDescription: 'Dropdown menu with Submenu',
178 | },
179 | },
180 | };
181 |
--------------------------------------------------------------------------------
/stories/Select.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Select, Option } from '../src/scripts';
3 | import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
4 |
5 | /**
6 | *
7 | */
8 | const meta: ComponentMeta = {
9 | title: 'Select',
10 | component: Select,
11 | subcomponents: { Option },
12 | argTypes: {
13 | onChange: { action: 'change' },
14 | onValueChange: { action: 'valueChange' },
15 | onBlur: { action: 'blur' },
16 | },
17 | };
18 | export default meta;
19 |
20 | /**
21 | *
22 | */
23 | export const ControlledWithKnobs: ComponentStoryObj = {
24 | render: (args) => (
25 |
30 | ),
31 | name: 'Controlled with knobs',
32 | args: {
33 | label: 'Select Label',
34 | },
35 | parameters: {
36 | docs: {
37 | description: {
38 | story: 'Select controlled with knobs',
39 | },
40 | },
41 | },
42 | };
43 |
44 | /**
45 | *
46 | */
47 | export const Default: ComponentStoryObj = {
48 | ...ControlledWithKnobs,
49 | name: 'Default',
50 | args: {},
51 | parameters: {
52 | docs: {
53 | description: {
54 | story: 'Default Select control',
55 | },
56 | },
57 | },
58 | };
59 |
60 | /**
61 | *
62 | */
63 | export const Required: ComponentStoryObj = {
64 | ...Default,
65 | name: 'Required',
66 | args: {
67 | label: 'Select Label',
68 | required: true,
69 | },
70 | parameters: {
71 | docs: {
72 | description: {
73 | story: 'Select control with required attribute',
74 | },
75 | },
76 | },
77 | };
78 |
79 | /**
80 | *
81 | */
82 | export const Error: ComponentStoryObj = {
83 | ...Default,
84 | name: 'Error',
85 | args: {
86 | label: 'Select Label',
87 | required: true,
88 | error: 'This field is required',
89 | },
90 | parameters: {
91 | docs: {
92 | description: {
93 | story: 'Select control with error message',
94 | },
95 | },
96 | },
97 | };
98 |
99 | /**
100 | *
101 | */
102 | export const Disabled: ComponentStoryObj = {
103 | ...Default,
104 | name: 'Disabled',
105 | args: {
106 | label: 'Select Label',
107 | disabled: true,
108 | },
109 | parameters: {
110 | docs: {
111 | description: {
112 | story: 'Select control with disabled status',
113 | },
114 | },
115 | },
116 | };
117 |
118 | /**
119 | *
120 | */
121 | export const MultipleDefault: ComponentStoryObj = {
122 | ...Default,
123 | name: 'Multiple - Default',
124 | args: {
125 | label: 'Select Label',
126 | value: ['2', '3'],
127 | multiple: true,
128 | },
129 | parameters: {
130 | docs: {
131 | description: {
132 | story: 'Multiple Select control',
133 | },
134 | },
135 | },
136 | };
137 |
138 | /**
139 | *
140 | */
141 | export const MultipleRequired: ComponentStoryObj = {
142 | ...Default,
143 | name: 'Multiple - Required',
144 | args: {
145 | label: 'Select Label',
146 | required: true,
147 | multiple: true,
148 | },
149 | parameters: {
150 | docs: {
151 | description: {
152 | story: 'Multiple Select control with required attribute',
153 | },
154 | },
155 | },
156 | };
157 |
158 | /**
159 | *
160 | */
161 | export const MultipleError: ComponentStoryObj = {
162 | ...Default,
163 | name: 'Multiple - Error',
164 | args: {
165 | label: 'Select Label',
166 | required: true,
167 | error: 'This field is required',
168 | multiple: true,
169 | },
170 | parameters: {
171 | docs: {
172 | description: {
173 | story: 'Multiple Select control with error message',
174 | },
175 | },
176 | },
177 | };
178 |
179 | /**
180 | *
181 | */
182 | export const MultipleDisabled: ComponentStoryObj = {
183 | ...Default,
184 | name: 'Multiple - Disabled',
185 | args: {
186 | label: 'Select Label',
187 | disabled: true,
188 | value: ['2', '3'],
189 | multiple: true,
190 | },
191 | parameters: {
192 | docs: {
193 | description: {
194 | story: 'Multiple Select control with disabled status',
195 | },
196 | },
197 | },
198 | };
199 |
200 | /**
201 | *
202 | */
203 | export const WithTooltip: ComponentStoryObj = {
204 | ...Default,
205 | name: 'With tooltip',
206 | args: {
207 | label: 'Select Label',
208 | tooltip: 'Tooltip Text',
209 | },
210 | parameters: {
211 | docs: {
212 | description: {
213 | story: 'Select control with tooltip',
214 | },
215 | },
216 | },
217 | };
218 |
--------------------------------------------------------------------------------
/stories/Checkbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps } from 'react';
2 | import { CheckboxGroup, Checkbox } from '../src/scripts';
3 | import { Meta, StoryObj } from '@storybook/react';
4 |
5 | /**
6 | *
7 | */
8 | type StoryProps = ComponentProps & {
9 | checkbox1: ComponentProps;
10 | } & {
11 | checkbox2: ComponentProps;
12 | };
13 |
14 | /**
15 | *
16 | */
17 | const meta: Meta = {
18 | title: 'Checkbox',
19 | component: CheckboxGroup,
20 | argTypes: {
21 | onValueChange: { action: 'valueChange' },
22 | },
23 | };
24 | export default meta;
25 |
26 | /**
27 | *
28 | */
29 | export const ControlledWithKnobs: StoryObj = {
30 | render: ({ checkbox1, checkbox2, ...args }) => (
31 |
32 |
33 |
34 |
35 | ),
36 | name: 'Controlled with knobs',
37 | args: {
38 | label: 'Checkbox Group Label',
39 | checkbox1: {
40 | label: 'Checkbox Label One',
41 | value: '1',
42 | disabled: false,
43 | checked: false,
44 | },
45 | checkbox2: {
46 | label: 'Checkbox Label Two',
47 | value: '2',
48 | disabled: false,
49 | checked: false,
50 | },
51 | },
52 | parameters: {
53 | docs: {
54 | storyDescription: 'Checkbox controlled with knobs',
55 | },
56 | },
57 | };
58 |
59 | /**
60 | *
61 | */
62 | export const Default: StoryObj = {
63 | render: ({ checkbox1, checkbox2, ...args }) => (
64 |
65 |
66 |
67 |
68 | ),
69 | args: {
70 | label: 'Checkbox Group Label',
71 | checkbox1: {
72 | label: 'Checkbox Label One',
73 | value: '1',
74 | checked: true,
75 | },
76 | checkbox2: {
77 | label: 'Checkbox Label Two',
78 | value: '2',
79 | checked: false,
80 | },
81 | },
82 | parameters: {
83 | docs: {
84 | storyDescription: 'Checkbox Textarea control',
85 | },
86 | },
87 | };
88 |
89 | /**
90 | *
91 | */
92 | export const Required: StoryObj = {
93 | render: ({ checkbox1, checkbox2, ...args }) => (
94 |
95 |
96 |
97 |
98 | ),
99 | args: {
100 | label: 'Checkbox Group Label',
101 | required: true,
102 | checkbox1: {
103 | label: 'Checkbox Label One',
104 | value: '1',
105 | checked: true,
106 | },
107 | checkbox2: {
108 | label: 'Checkbox Label Two',
109 | value: '2',
110 | },
111 | },
112 | parameters: {
113 | docs: {
114 | storyDescription: 'Checkbox control with required attribute',
115 | },
116 | },
117 | };
118 |
119 | /**
120 | *
121 | */
122 | export const Error: StoryObj = {
123 | render: ({ checkbox1, checkbox2, ...args }) => (
124 |
125 |
126 |
127 |
128 | ),
129 | args: {
130 | label: 'Checkbox Group Label',
131 | required: true,
132 | error: 'This field is required',
133 | checkbox1: {
134 | label: 'Checkbox Label One',
135 | value: '1',
136 | checked: true,
137 | },
138 | checkbox2: {
139 | label: 'Checkbox Label Two',
140 | value: '2',
141 | },
142 | },
143 | parameters: {
144 | docs: {
145 | storyDescription: 'Checkbox control with error message',
146 | },
147 | },
148 | };
149 |
150 | /**
151 | *
152 | */
153 | export const Disabled: StoryObj = {
154 | render: ({ checkbox1, checkbox2, ...args }) => (
155 |
156 |
157 |
158 |
159 | ),
160 | args: {
161 | label: 'Checkbox Group Label',
162 | checkbox1: {
163 | label: 'Checkbox Label One',
164 | value: '1',
165 | disabled: true,
166 | },
167 | checkbox2: {
168 | label: 'Checkbox Label Two',
169 | value: '2',
170 | disabled: true,
171 | },
172 | },
173 | parameters: {
174 | docs: {
175 | storyDescription: 'Checkbox control with disabled status',
176 | },
177 | },
178 | };
179 |
180 | /**
181 | *
182 | */
183 | export const WithTooltip: StoryObj = {
184 | render: ({ checkbox1, checkbox2, ...args }) => (
185 |
186 |
187 |
188 |
189 | ),
190 | name: 'With tooltip',
191 | args: {
192 | label: 'Checkbox Group Label',
193 | tooltip: 'Tooltip Text',
194 | checkbox1: {
195 | label: 'Checkbox Label One',
196 | value: '1',
197 | },
198 | checkbox2: {
199 | label: 'Checkbox Label Two',
200 | value: '2',
201 | },
202 | },
203 | parameters: {
204 | docs: {
205 | storyDescription: 'Checkbox group with tooltip',
206 | },
207 | },
208 | };
209 |
--------------------------------------------------------------------------------
/stories/ButtonGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps } from 'react';
2 | import { ButtonGroup, Button, DropdownButton, MenuItem } from '../src/scripts';
3 | import { Meta, StoryObj } from '@storybook/react';
4 | import { containerDecorator } from './util';
5 |
6 | /**
7 | *
8 | */
9 | type StoryProps = ComponentProps & {
10 | button1_onClick: ComponentProps['onClick'];
11 | button2_onClick: ComponentProps['onClick'];
12 | button3_onClick: ComponentProps['onClick'];
13 | menu_onMenuSelect: ComponentProps['onMenuSelect'];
14 | };
15 |
16 | /**
17 | *
18 | */
19 | const darkBgDecorator = containerDecorator({
20 | backgroundColor: '#16325c',
21 | padding: 4,
22 | });
23 |
24 | /**
25 | *
26 | */
27 | const meta: Meta = {
28 | title: 'ButtonGroup',
29 | component: ButtonGroup,
30 | argTypes: {
31 | button1_onClick: { action: 'button1_click' },
32 | button2_onClick: { action: 'button2_click' },
33 | button3_onClick: { action: 'button3_click' },
34 | menu_onMenuSelect: { action: 'menuSelect' },
35 | },
36 | };
37 | export default meta;
38 |
39 | /**
40 | *
41 | */
42 | export const Default: StoryObj = {
43 | render: ({ button1_onClick, button2_onClick, button3_onClick, ...args }) => (
44 |
45 |
48 |
51 |
59 |
60 | ),
61 | parameters: {
62 | docs: {
63 | storyDescription: 'Default grouped buttons',
64 | },
65 | },
66 | };
67 |
68 | /**
69 | *
70 | */
71 | export const DefaultDisabled: StoryObj = {
72 | render: ({ button1_onClick, button2_onClick, button3_onClick, ...args }) => (
73 |
74 |
77 |
80 |
89 |
90 | ),
91 | parameters: {
92 | docs: {
93 | storyDescription: 'Grouped buttons with disabled button',
94 | },
95 | },
96 | };
97 |
98 | /**
99 | *
100 | */
101 | export const More: StoryObj = {
102 | render: ({
103 | button1_onClick,
104 | button2_onClick,
105 | button3_onClick,
106 | menu_onMenuSelect,
107 | ...args
108 | }) => (
109 |
110 |
113 |
116 |
124 |
129 |
130 |
131 |
132 |
133 |
134 | ),
135 | parameters: {
136 | docs: {
137 | storyDescription: 'Grouped buttons with dropdown button in right',
138 | },
139 | },
140 | };
141 |
142 | /**
143 | *
144 | */
145 | export const Inverse: StoryObj = {
146 | render: ({
147 | button1_onClick,
148 | button2_onClick,
149 | button3_onClick,
150 | menu_onMenuSelect,
151 | ...args
152 | }) => (
153 |
154 |
157 |
160 |
168 |
173 |
174 |
175 |
176 |
177 |
178 | ),
179 | decorators: [darkBgDecorator],
180 | parameters: {
181 | docs: {
182 | storyDescription: 'Grouped buttons with inversed color',
183 | },
184 | },
185 | };
186 |
--------------------------------------------------------------------------------
/stories/Table.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps } from 'react';
2 | import {
3 | Table,
4 | TableHeader,
5 | TableHeaderColumn,
6 | TableBody,
7 | TableRow,
8 | TableRowColumn,
9 | TableRowColumnActions,
10 | DropdownButton,
11 | MenuItem,
12 | } from '../src/scripts';
13 | import { Meta, StoryObj } from '@storybook/react';
14 |
15 | /**
16 | *
17 | */
18 | type StoryProps = ComponentProps & {
19 | hasActions?: boolean;
20 | };
21 |
22 | /**
23 | *
24 | */
25 | const headerNames =
26 | 'Opportunity Name,Account Name,Close Date,Stage,Confidence,Amount,Contact'.split(
27 | ','
28 | );
29 | const records = new Array(6)
30 | .join('_')
31 | .split('')
32 | .map((_, i) => [
33 | `Cloudhub ${i + 1}`,
34 | 'Cloudhub',
35 | '4/14/2015',
36 | 'Prospecting',
37 | '20%',
38 | '$25k',
39 | 'jrogers@cloudhub.com',
40 | ]);
41 |
42 | /**
43 | *
44 | */
45 | const meta: Meta = {
46 | title: 'Table',
47 | component: Table,
48 | subcomponents: {
49 | TableHeader,
50 | TableHeaderColumn,
51 | TableBody,
52 | TableRow,
53 | TableRowColumn,
54 | TableRowColumnActions,
55 | },
56 | };
57 | export default meta;
58 |
59 | /**
60 | *
61 | */
62 | export const ControlledWithKnobs: StoryObj = {
63 | render: ({ hasActions, ...args }) => (
64 |
65 |
66 |
67 | {headerNames.map((name, i) => (
68 |
72 | {name}
73 |
74 | ))}
75 |
76 |
77 |
78 | {records.map((record) => (
79 |
80 | {hasActions ? (
81 |
82 |
83 |
84 |
85 |
86 |
87 | ) : undefined}
88 | {headerNames.map((name, i) => (
89 | {record[i]}
90 | ))}
91 |
92 | ))}
93 |
94 |
95 | ),
96 | name: 'Controlled with knobs',
97 | args: {
98 | hasActions: true,
99 | },
100 | argTypes: {
101 | hasActions: { type: 'boolean' },
102 | },
103 | parameters: {
104 | info: 'Table controlled with knobs',
105 | },
106 | };
107 |
108 | /**
109 | *
110 | */
111 | export const Default: StoryObj = {
112 | ...ControlledWithKnobs,
113 | name: 'Default',
114 | args: {
115 | bordered: true,
116 | },
117 | parameters: {
118 | docs: {
119 | description: {
120 | story: 'Default Table component',
121 | },
122 | },
123 | },
124 | };
125 |
126 | /**
127 | *
128 | */
129 | export const WithStripedRow: StoryObj = {
130 | ...Default,
131 | name: 'With Striped Row',
132 | args: {
133 | bordered: true,
134 | striped: true,
135 | },
136 | parameters: {
137 | docs: {
138 | description: {
139 | story: 'Table component with striped row',
140 | },
141 | },
142 | },
143 | };
144 |
145 | /**
146 | *
147 | */
148 | export const WithNoRowBorder: StoryObj = {
149 | ...Default,
150 | name: 'With No Row Border',
151 | args: {},
152 | parameters: {
153 | docs: {
154 | description: {
155 | story: 'Table component with no row borders',
156 | },
157 | },
158 | },
159 | };
160 |
161 | /**
162 | *
163 | */
164 | export const WithNoRowHover: StoryObj = {
165 | ...Default,
166 | name: 'With No Row Hover',
167 | args: {
168 | bordered: true,
169 | noRowHover: true,
170 | },
171 | parameters: {
172 | docs: {
173 | description: {
174 | story: 'Table component with row hovering highlight is disabled',
175 | },
176 | },
177 | },
178 | };
179 |
180 | /**
181 | *
182 | */
183 | export const WithVerticalBorders: StoryObj = {
184 | ...Default,
185 | name: 'With Vertical Borders',
186 | args: {
187 | bordered: true,
188 | verticalBorders: true,
189 | },
190 | parameters: {
191 | docs: {
192 | description: {
193 | story: 'Table component with vertical borders enabled',
194 | },
195 | },
196 | },
197 | };
198 |
199 | /**
200 | *
201 | */
202 | export const WithFixedLayout: StoryObj = {
203 | ...Default,
204 | name: 'With Fixed Layout',
205 | args: {
206 | bordered: true,
207 | fixedLayout: true,
208 | },
209 | parameters: {
210 | docs: {
211 | description: {
212 | story: 'Table component with fixed layout',
213 | },
214 | },
215 | },
216 | };
217 |
218 | /**
219 | *
220 | */
221 | export const WithSortEnabled: StoryObj = {
222 | ...Default,
223 | name: 'With Sort Enabled',
224 | args: {
225 | bordered: true,
226 | sortable: true,
227 | },
228 | parameters: {
229 | docs: {
230 | description: {
231 | story:
232 | 'Table component with sort feature enabled ("Account Name" column is disabled)',
233 | },
234 | },
235 | },
236 | };
237 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": { "*": ["types/*"] },
5 | /* Basic Options */
6 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
7 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
8 | // "lib": [], /* Specify library files to be included in the compilation. */
9 | // "allowJs": true, /* Allow javascript files to be compiled. */
10 | // "checkJs": true, /* Report errors in .js files. */
11 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
14 | // "sourceMap": true, /* Generates corresponding '.map' file. */
15 | // "outFile": "./", /* Concatenate and emit output to single file. */
16 | "outDir": "lib", /* Redirect output structure to the directory. */
17 | // "composite": true, /* Enable project compilation */
18 | // "removeComments": true, /* Do not emit comments to output. */
19 | // "noEmit": true, /* Do not emit outputs. */
20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
23 |
24 | /* Strict Type-Checking Options */
25 | "strict": true, /* Enable all strict type-checking options. */
26 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
27 | // "strictNullChecks": true, /* Enable strict null checks. */
28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
29 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
33 |
34 | /* Additional Checks */
35 | // "noUnusedLocals": true, /* Report errors on unused locals. */
36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
39 |
40 | /* Module Resolution Options */
41 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
45 | // "typeRoots": [], /* List of folders to include type definitions from. */
46 | // "types": [], /* Type declaration files to be included in compilation. */
47 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
48 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
50 |
51 | /* Source Map Options */
52 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
53 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
56 |
57 | /* Experimental Options */
58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
60 | },
61 | "include": [
62 | "src/**/*",
63 | "stories/**/*",
64 | "test/**/*"
65 | ]
66 | }
67 |
--------------------------------------------------------------------------------
/src/scripts/TreeNode.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | ComponentType,
3 | createContext,
4 | FC,
5 | useContext,
6 | ReactNode,
7 | } from 'react';
8 | import classnames from 'classnames';
9 | import { Button } from './Button';
10 | import { Spinner } from './Spinner';
11 | import { useControlledValue, useEventCallback } from './hooks';
12 | import { TreeContext } from './Tree';
13 |
14 | /**
15 | *
16 | */
17 | const TreeNodeLevelContext = createContext(1);
18 |
19 | /**
20 | *
21 | */
22 | export type TreeNodeProps = {
23 | className?: string;
24 | label?: string | JSX.Element;
25 | defaultOpened?: boolean;
26 | opened?: boolean;
27 | selected?: boolean;
28 | leaf?: boolean;
29 | loading?: boolean;
30 | level?: number;
31 | disabled?: boolean;
32 | children?: ReactNode;
33 | onClick?: (e: React.MouseEvent) => void;
34 | onLabelClick?: (e: React.MouseEvent) => void;
35 | onToggle?: (e: React.MouseEvent) => void;
36 | itemRender?: ComponentType;
37 | };
38 |
39 | /**
40 | *
41 | */
42 | const TreeNodeItem: FC = (props) => {
43 | const {
44 | className,
45 | label,
46 | icon = 'chevronright',
47 | loading,
48 | selected,
49 | leaf,
50 | opened,
51 | level = 0,
52 | children,
53 | itemRender: ItemRender,
54 | onClick,
55 | onToggle,
56 | onLabelClick,
57 | ...rprops
58 | } = props;
59 | const itmClassNames = classnames(className, 'slds-tree__item', {
60 | 'slds-is-open': opened,
61 | 'slds-is-selected': selected,
62 | });
63 | const spinnerClassNames = classnames(
64 | 'react-slds-spinner',
65 | 'slds-m-right_x-small'
66 | );
67 | const loadingPositionStyle = {
68 | left: `${level}rem`,
69 | };
70 | return (
71 |
108 | );
109 | };
110 |
111 | /**
112 | *
113 | */
114 | export const TreeNode: FC = (props) => {
115 | const {
116 | defaultOpened,
117 | opened: opened_,
118 | leaf,
119 | selected,
120 | disabled,
121 | children,
122 | onClick: onClick_,
123 | onToggle: onToggle_,
124 | onLabelClick: onLabelClick_,
125 | ...rprops
126 | } = props;
127 | const { toggleOnNodeClick, onNodeClick, onNodeLabelClick, onNodeToggle } =
128 | useContext(TreeContext);
129 | const level = useContext(TreeNodeLevelContext);
130 | const [opened, setOpened] = useControlledValue(
131 | opened_,
132 | defaultOpened ?? false
133 | );
134 |
135 | const onToggle = useEventCallback((e: React.MouseEvent) => {
136 | onToggle_?.(e);
137 | onNodeToggle?.(e, props);
138 | setOpened((opened) => !opened);
139 | });
140 |
141 | const onLabelClick = useEventCallback((e: React.MouseEvent) => {
142 | onLabelClick_?.(e);
143 | onNodeLabelClick?.(e, props);
144 | });
145 |
146 | const onClick = useEventCallback((e: React.MouseEvent) => {
147 | onClick_?.(e);
148 | onNodeClick?.(e, props);
149 | if (toggleOnNodeClick) {
150 | onToggle(e);
151 | }
152 | });
153 |
154 | const grpClassNames = classnames('slds-tree__group', {
155 | 'slds-nested': !leaf,
156 | 'is-expanded': opened,
157 | 'slds-show': opened,
158 | 'slds-hide': !opened,
159 | });
160 | const labelText =
161 | typeof rprops.label === 'string' ? rprops.label : 'Tree Branch';
162 | const ariaLabel = !leaf ? labelText : undefined;
163 | return (
164 |
172 |
186 | {!leaf ? (
187 |
188 |
189 | {children}
190 |
191 |
192 | ) : undefined}
193 |
194 | );
195 | };
196 |
--------------------------------------------------------------------------------
/src/scripts/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, ReactNode, ButtonHTMLAttributes, Ref, useRef } from 'react';
2 | import classnames from 'classnames';
3 | import { SvgIcon, IconCategory } from './Icon';
4 | import { Spinner } from './Spinner';
5 | import { useEventCallback, useMergeRefs } from './hooks';
6 |
7 | export type ButtonType =
8 | | 'neutral'
9 | | 'brand'
10 | | 'outline-brand'
11 | | 'destructive'
12 | | 'text-destructive'
13 | | 'success'
14 | | 'inverse'
15 | | 'icon'
16 | | 'icon-container'
17 | | 'icon-inverse'
18 | | 'icon-more'
19 | | 'icon-border'
20 | | 'icon-border-filled'
21 | | 'icon-border-inverse';
22 |
23 | const ICON_SIZES = ['x-small', 'small', 'medium', 'large'] as const;
24 | const ICON_ALIGNS = ['left', 'right'] as const;
25 |
26 | export type ButtonSize = 'x-small' | 'small' | 'medium' | 'large';
27 | export type ButtonIconSize = (typeof ICON_SIZES)[number];
28 | export type ButtonIconAlign = (typeof ICON_ALIGNS)[number];
29 | export type ButtonIconMoreSize = 'x-small' | 'small' | 'medium' | 'large';
30 |
31 | /**
32 | *
33 | */
34 | export type ButtonIconProps = {
35 | className?: string;
36 | category?: IconCategory;
37 | icon: string;
38 | align?: ButtonIconAlign;
39 | size?: ButtonIconSize;
40 | inverse?: boolean;
41 | style?: object;
42 | };
43 |
44 | /**
45 | *
46 | */
47 | export const ButtonIcon: FC = ({
48 | icon,
49 | category = 'utility',
50 | align,
51 | size,
52 | className,
53 | style,
54 | ...props
55 | }) => {
56 | const alignClassName =
57 | align && ICON_ALIGNS.indexOf(align) >= 0
58 | ? `slds-button__icon_${align}`
59 | : null;
60 | const sizeClassName =
61 | size && ICON_SIZES.indexOf(size) >= 0 ? `slds-button__icon_${size}` : null;
62 | const iconClassNames = classnames(
63 | 'slds-button__icon',
64 | alignClassName,
65 | sizeClassName,
66 | className
67 | );
68 |
69 | if (icon.indexOf(':') > 0) {
70 | [category, icon] = icon.split(':') as [IconCategory, string];
71 | }
72 |
73 | return (
74 |
82 | );
83 | };
84 |
85 | /**
86 | *
87 | */
88 | export type ButtonProps = {
89 | label?: ReactNode;
90 | alt?: string;
91 | type?: ButtonType;
92 | size?: ButtonSize;
93 | htmlType?: 'button' | 'submit' | 'reset';
94 | selected?: boolean;
95 | inverse?: boolean;
96 | loading?: boolean;
97 | icon?: string;
98 | iconSize?: ButtonIconSize;
99 | iconAlign?: ButtonIconAlign;
100 | iconMore?: string;
101 | iconMoreSize?: ButtonIconMoreSize;
102 | buttonRef?: Ref;
103 | } & Omit, 'type'>;
104 |
105 | /**
106 | *
107 | */
108 | export const Button: FC = (props) => {
109 | const {
110 | className,
111 | type,
112 | size,
113 | icon,
114 | iconAlign,
115 | iconMore,
116 | selected,
117 | alt,
118 | label,
119 | loading,
120 | iconSize,
121 | inverse: inverse_,
122 | htmlType = 'button',
123 | children,
124 | buttonRef: buttonRef_,
125 | iconMoreSize: iconMoreSize_,
126 | onClick: onClick_,
127 | tabIndex,
128 | ...rprops
129 | } = props;
130 |
131 | const adjoining = icon && (iconAlign === 'right' || !(label || children));
132 | const iconMoreSize = iconMoreSize_ || adjoining ? 'x-small' : 'small';
133 | const inverse = inverse_ || /-?inverse$/.test(type || '');
134 | const buttonElRef = useRef(null);
135 | const buttonRef = useMergeRefs([buttonElRef, buttonRef_]);
136 |
137 | const onClick = useEventCallback((e: React.MouseEvent) => {
138 | if (buttonElRef.current !== null) {
139 | // Safari, FF to trigger focus event on click
140 | buttonElRef.current.focus();
141 | }
142 | onClick_?.(e);
143 | });
144 |
145 | const content = children || label;
146 | const isIconOnly = type && /^icon-/.test(type) && icon && !content;
147 |
148 | const typeClassName = type ? `slds-button_${type}` : null;
149 | const btnClassNames = classnames(className, 'slds-button', typeClassName, {
150 | 'slds-is-selected': selected,
151 | ['slds-button_icon']: /^icon-/.test(type ?? ''),
152 | [`slds-button_icon-${size ?? ''}`]:
153 | /^(x-small|small)$/.test(size ?? '') && /^icon-/.test(type ?? ''),
154 | });
155 |
156 | return (
157 |
191 | );
192 | };
193 |
--------------------------------------------------------------------------------
/src/scripts/Grid.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | ReactHTML,
3 | HTMLAttributes,
4 | createContext,
5 | useContext,
6 | FC,
7 | useMemo,
8 | } from 'react';
9 | import classnames from 'classnames';
10 |
11 | /**
12 | *
13 | */
14 | export type GridProps = {
15 | tag?: keyof ReactHTML;
16 | frame?: boolean;
17 | vertical?: boolean;
18 | } & HTMLAttributes;
19 |
20 | /**
21 | *
22 | */
23 | export const Grid: FC = ({
24 | className,
25 | frame,
26 | vertical = true,
27 | children,
28 | tag,
29 | ...props
30 | }) => {
31 | const gridClassNames = classnames(
32 | className,
33 | 'slds-grid',
34 | vertical ? 'slds-grid_vertical' : null,
35 | frame ? 'slds-grid_frame' : null
36 | );
37 | const Tag = tag || 'div';
38 | return (
39 |
40 | {children}
41 |
42 | );
43 | };
44 |
45 | /**
46 | *
47 | */
48 | const GridRowContext = createContext<{
49 | totalCols?: number;
50 | totalColsSmall?: number;
51 | totalColsMedium?: number;
52 | totalColsLarge?: number;
53 | }>({});
54 |
55 | /**
56 | *
57 | */
58 | function adjustCols(colNum: number, large?: boolean) {
59 | if (colNum > 6) {
60 | return large ? 12 : 6;
61 | }
62 | return colNum;
63 | }
64 |
65 | /**
66 | *
67 | */
68 | export type ColProps = {
69 | padded?: boolean | 'medium' | 'large';
70 | align?: 'top' | 'middle' | 'bottom';
71 | noFlex?: boolean;
72 | order?: number;
73 | orderSmall?: number;
74 | orderMedium?: number;
75 | orderLarge?: number;
76 | cols?: number;
77 | colsSmall?: number;
78 | colsMedium?: number;
79 | colsLarge?: number;
80 | } & HTMLAttributes;
81 |
82 | /**
83 | *
84 | */
85 | export const Col: FC = (props) => {
86 | const {
87 | className,
88 | padded,
89 | align,
90 | noFlex,
91 | order,
92 | orderSmall,
93 | orderMedium,
94 | orderLarge,
95 | cols,
96 | colsSmall,
97 | colsMedium,
98 | colsLarge,
99 | children,
100 | ...pprops
101 | } = props;
102 | const { totalCols, totalColsSmall, totalColsMedium, totalColsLarge } =
103 | useContext(GridRowContext);
104 | const rowClassNames = classnames(
105 | className,
106 | padded
107 | ? `slds-col_padded${
108 | typeof padded === 'string' && /^(medium|large)$/.test(padded)
109 | ? `-${padded}`
110 | : ''
111 | }`
112 | : 'slds-col',
113 | align ? `slds-align-${align}` : null,
114 | noFlex ? 'slds-no-flex' : null,
115 | order ? `slds-order_${order}` : null,
116 | orderSmall ? `slds-small-order_${orderSmall}` : null,
117 | orderMedium ? `slds-medium-order_${orderMedium}` : null,
118 | orderLarge ? `slds-large-order_${orderLarge}` : null,
119 | cols && totalCols
120 | ? `slds-size_${cols}-of-${adjustCols(totalCols, true)}`
121 | : null,
122 | colsSmall && totalColsSmall
123 | ? `slds-small-size_${colsSmall}-of-${adjustCols(totalColsSmall)}`
124 | : null,
125 | colsMedium && totalColsMedium
126 | ? `slds-medium-size_${colsMedium}-of-${adjustCols(totalColsMedium)}`
127 | : null,
128 | colsLarge && totalColsLarge
129 | ? `slds-large-size_${colsLarge}-of-${adjustCols(totalColsLarge, true)}`
130 | : null
131 | );
132 | return (
133 |
134 | {children}
135 |
136 | );
137 | };
138 |
139 | export type RowProps = {
140 | align?: 'center' | 'space' | 'spread';
141 | nowrap?: boolean;
142 | nowrapSmall?: boolean;
143 | nowrapMedium?: boolean;
144 | nowrapLarge?: boolean;
145 | pullPadded?: boolean;
146 | cols?: number;
147 | colsSmall?: number;
148 | colsMedium?: number;
149 | colsLarge?: number;
150 | } & HTMLAttributes;
151 |
152 | /**
153 | *
154 | */
155 | export const Row: FC = (props) => {
156 | const {
157 | className,
158 | align,
159 | nowrap,
160 | nowrapSmall,
161 | nowrapMedium,
162 | nowrapLarge,
163 | cols,
164 | colsSmall,
165 | colsMedium,
166 | colsLarge,
167 | pullPadded,
168 | children,
169 | ...rprops
170 | } = props;
171 | const rowClassNames = classnames(
172 | className,
173 | 'slds-grid',
174 | align ? `slds-grid_align-${align}` : null,
175 | nowrap ? 'slds-nowrap' : 'slds-wrap',
176 | nowrapSmall ? 'slds-nowrap_small' : null,
177 | nowrapMedium ? 'slds-nowrap_medium' : null,
178 | nowrapLarge ? 'slds-nowrap_large' : null,
179 | pullPadded ? 'slds-grid_pull-padded' : null
180 | );
181 | const totalCols =
182 | cols ||
183 | (() => {
184 | let cnt = 0;
185 | React.Children.forEach(children, (child) => {
186 | if (!React.isValidElement(child)) return;
187 | cnt += (child.props as { cols?: number }).cols || 1;
188 | });
189 | return cnt;
190 | })();
191 | const gridRowCtx = useMemo(
192 | () => ({
193 | totalCols,
194 | totalColsSmall: colsSmall || totalCols,
195 | totalColsMedium: colsMedium || totalCols,
196 | totalColsLarge: colsLarge || totalCols,
197 | }),
198 | [totalCols, colsSmall, colsMedium, colsLarge]
199 | );
200 | return (
201 |
202 |
203 | {React.Children.map(children, (child) => {
204 | if (!React.isValidElement(child) || child.type !== Col) {
205 | return
{child};
206 | }
207 | return child;
208 | })}
209 |
210 |
211 | );
212 | };
213 |
--------------------------------------------------------------------------------
/stories/Icon.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps, FC, ReactNode } from 'react';
2 | import { Icon } from '../src/scripts/Icon';
3 | import { Meta, StoryObj } from '@storybook/react';
4 |
5 | /**
6 | *
7 | */
8 | const ListEntry: FC<{ title: string; children?: ReactNode }> = ({
9 | children,
10 | title,
11 | }) => (
12 |
22 |
23 | {children}
24 | {title}
25 |
26 |
27 | );
28 |
29 | type StoryProps = ComponentProps & {
30 | xxsmall_onClick?: ComponentProps['onClick'];
31 | xsmall_onClick?: ComponentProps['onClick'];
32 | small_onClick?: ComponentProps['onClick'];
33 | medium_onClick?: ComponentProps['onClick'];
34 | large_onClick?: ComponentProps['onClick'];
35 | icons?: ComponentProps[];
36 | };
37 |
38 | /**
39 | *
40 | */
41 | const meta: Meta = {
42 | title: 'Icon',
43 | component: Icon,
44 | argTypes: {
45 | onClick: { action: 'click' },
46 | },
47 | };
48 | export default meta;
49 |
50 | /**
51 | *
52 | */
53 | export const ControlledWithKnobs: StoryObj> = {
54 | name: 'Controlled with knobs',
55 | args: {
56 | category: 'standard',
57 | size: 'medium',
58 | icon: 'account',
59 | },
60 | parameters: {
61 | docs: {
62 | storyDescription: 'Icon controlled with knobs',
63 | },
64 | },
65 | };
66 |
67 | /**
68 | *
69 | */
70 | export const CurrentColor: StoryObj> = {
71 | render: ({ color, ...args }) => (
72 |
73 |