diff --git a/.eslintrc.js b/.eslintrc.js index 0bd37b6457..66e8dd2a4d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,6 +39,41 @@ const config = { 'import/no-extraneous-dependencies': 'error', }, }, + // TypeScript overrides for migrated components + { + files: ['**/*.ts', '**/*.tsx'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, + plugins: ['@typescript-eslint'], + extends: ['plugin:@typescript-eslint/recommended'], + settings: { + 'import/resolver': { + typescript: { + project: './tsconfig.json', + }, + }, + }, + rules: { + // Disable JS-only rules that conflict with TS + 'react/prop-types': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + // Keep consistent with existing code style + 'react/no-unknown-property': [ + 'error', + { ignore: ['jsx', 'global'] }, + ], + // Allow .js extension imports in TS files (Babel resolves .tsx -> .js) + 'import/extensions': 'off', + }, + }, ], } diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000000..ed2b117c18 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,114 @@ +# dhis2/ui — Incremental TypeScript Migration Strategy + +## Overview + +This document describes the approach for incrementally migrating `@dhis2/ui` components from JavaScript to TypeScript, one component at a time, without breaking the rest of the library. + +## Key Findings + +- **Build system**: `d2-app-scripts build` uses Babel with `@babel/preset-typescript` already included — it can compile `.ts`/`.tsx` out of the box. +- **styled-jsx**: Ships TypeScript definitions that augment React's `StyleHTMLAttributes` with `jsx` and `global` props. Works in `.tsx` files with the `"types": ["styled-jsx"]` tsconfig option. +- **Existing types**: Each component has hand-written `.d.ts` files in a `types/` directory. After migration, these can be auto-generated from source via `tsc --declaration`. +- **ESLint**: The project uses `@dhis2/cli-style` (ESLint 7.x). TypeScript linting works via `@typescript-eslint/parser` v6 + `@typescript-eslint/eslint-plugin` v6. +- **One quirk**: `d2-app-scripts build` doesn't rename `.tsx` → `.js` in build output. A `post-build-rename.js` script handles this. + +## How to Migrate a Component + +### 1. Add a `tsconfig.json` to the component + +```json +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} +``` + +### 2. Convert source files from `.js` → `.tsx` (or `.ts`) + +- Replace `PropTypes` with TypeScript interfaces +- Remove `prop-types` and `@dhis2/prop-types` imports +- Add explicit types for props, state, and function parameters +- Keep `styled-jsx` usage as-is (it works in `.tsx`) +- Keep import paths using `.js` extensions (Babel + Node resolve these to `.tsx`) +- Leave `.stories.js` and `.feature` test files as JavaScript + +### 3. Update `d2.config.js` entry point + +```js +module.exports = { + type: 'lib', + entryPoints: { + lib: 'src/index.ts', // was src/index.js + }, +} +``` + +### 4. Update `package.json` build script + +```json +"build": "d2-app-scripts build && node ../../scripts/post-build-rename.js" +``` + +### 5. Delete old `.js` source files + +Remove the original `.js` files that were converted (keep stories/features as JS). + +### 6. Run the feedback pipeline + +```bash +node scripts/ts-check.js # check one component +node scripts/ts-check.js --all # check all migrated components +node scripts/ts-check.js --fix # auto-fix lint + format +``` + +### 7. Verify the build + +```bash +cd components/ && yarn build +``` + +## Files Added/Modified + +| File | Purpose | +| --------------------------------- | ------------------------------------------------------ | +| `tsconfig.json` (root) | Base TypeScript config for the whole repo | +| `components//tsconfig.json` | Per-component TS config extending root | +| `.eslintrc.js` | Added TypeScript override block for `.ts`/`.tsx` files | +| `scripts/ts-check.js` | Unified feedback pipeline (tsc + eslint + prettier) | +| `scripts/post-build-rename.js` | Renames `.tsx`/`.ts` → `.js` in build output | + +## Dev Dependencies Added + +- `typescript` ~5.4.5 +- `@typescript-eslint/parser` ^6 +- `@typescript-eslint/eslint-plugin` ^6 +- `eslint-import-resolver-typescript` ^3 + +## Migration Order Recommendation + +Start with leaf components (no internal deps) and work up: + +1. **Simple leaf**: `divider`, `cover`, `center`, `required`, `help`, `legend`, `label` +2. **Small with sub-components**: `tag`, `chip`, `card`, `box`, `status-icon`, `user-avatar`, `logo` +3. **Medium**: `loader`, `notice-box`, `alert`, `tooltip`, `popover`, `popper` +4. **Complex**: `button`, `checkbox`, `radio`, `switch`, `input`, `text-area`, `file-input` +5. **Field wrappers**: `field` +6. **Composite**: `select`, `menu`, `tab`, `table`, `pagination`, `modal`, `calendar` +7. **Large/complex**: `header-bar`, `organisation-unit-tree`, `transfer`, `sharing-dialog`, `selector-bar` +8. **Collections**: `collections/ui`, `collections/forms` + +## Notes + +- Stories and E2E feature files can stay as `.js` — they don't need to be migrated immediately. +- The `types/index.d.ts` hand-written files can eventually be replaced by auto-generated declarations from `tsc --declaration`. +- The `import/extensions` ESLint rule is disabled for `.ts`/`.tsx` files because the codebase convention is to import with `.js` extensions (which Babel resolves to the actual `.tsx` files). diff --git a/collections/forms/i18n/en.pot b/collections/forms/i18n/en.pot index 6c0a78ad62..847f0647b4 100644 --- a/collections/forms/i18n/en.pot +++ b/collections/forms/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-01-12T11:22:32.560Z\n" -"PO-Revision-Date: 2026-01-12T11:22:32.562Z\n" +"POT-Creation-Date: 2026-04-13T21:27:29.445Z\n" +"PO-Revision-Date: 2026-04-13T21:27:29.445Z\n" msgid "Upload file" msgstr "Upload file" diff --git a/components/alert/d2.config.js b/components/alert/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/alert/d2.config.js +++ b/components/alert/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/alert/package.json b/components/alert/package.json index 5673984b6d..94e359c41c 100644 --- a/components/alert/package.json +++ b/components/alert/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/alert/src/alert-bar/action.js b/components/alert/src/alert-bar/action.js deleted file mode 100644 index 67e88051c2..0000000000 --- a/components/alert/src/alert-bar/action.js +++ /dev/null @@ -1,37 +0,0 @@ -import { spacers } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React, { Component } from 'react' - -class Action extends Component { - onClick = (event) => { - this.props.onClick(event) - this.props.hide(event) - } - - render() { - return ( - - {this.props.label} - - - ) - } -} - -Action.propTypes = { - dataTest: PropTypes.string.isRequired, - hide: PropTypes.func.isRequired, - label: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, -} - -export { Action } diff --git a/components/alert/src/alert-bar/action.tsx b/components/alert/src/alert-bar/action.tsx new file mode 100644 index 0000000000..7672431fe6 --- /dev/null +++ b/components/alert/src/alert-bar/action.tsx @@ -0,0 +1,36 @@ +import { spacers } from '@dhis2/ui-constants' +import React, { Component } from 'react' + +export interface ActionProps { + dataTest: string + hide: (event: React.MouseEvent) => void + label: string + onClick: (event: React.MouseEvent) => void +} + +class Action extends Component { + onClick = (event: React.MouseEvent) => { + this.props.onClick(event) + this.props.hide(event) + } + + render() { + return ( + + {this.props.label} + + + ) + } +} + +export { Action } diff --git a/components/alert/src/alert-bar/actions.js b/components/alert/src/alert-bar/actions.js deleted file mode 100644 index 830824a5fe..0000000000 --- a/components/alert/src/alert-bar/actions.js +++ /dev/null @@ -1,47 +0,0 @@ -import { arrayWithLength } from '@dhis2/prop-types' -import { spacers } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React from 'react' -import { Action } from './action.js' - -const Actions = ({ actions, hide, dataTest }) => { - if (!actions) { - return null - } - - return ( -
- {actions.map((action) => ( - - ))} - -
- ) -} - -const actionsPropType = arrayWithLength( - 0, - 2, - PropTypes.shape({ - label: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - }) -) - -Actions.propTypes = { - dataTest: PropTypes.string.isRequired, - hide: PropTypes.func.isRequired, - actions: actionsPropType, -} - -export { Actions, actionsPropType } diff --git a/components/alert/src/alert-bar/actions.tsx b/components/alert/src/alert-bar/actions.tsx new file mode 100644 index 0000000000..a291bb6301 --- /dev/null +++ b/components/alert/src/alert-bar/actions.tsx @@ -0,0 +1,41 @@ +import { spacers } from '@dhis2/ui-constants' +import React from 'react' +import { Action } from './action.tsx' + +export interface AlertBarAction { + label: string + onClick: (event: React.MouseEvent) => void +} + +interface ActionsProps { + dataTest: string + hide: (event: React.MouseEvent) => void + actions?: AlertBarAction[] +} + +const Actions = ({ actions, hide, dataTest }: ActionsProps) => { + if (!actions) { + return null + } + + return ( +
+ {actions.map((action) => ( + + ))} + +
+ ) +} + +export { Actions } diff --git a/components/alert/src/alert-bar/alert-bar.e2e.stories.js b/components/alert/src/alert-bar/alert-bar.e2e.stories.js index 1987690404..8e062c2d2c 100644 --- a/components/alert/src/alert-bar/alert-bar.e2e.stories.js +++ b/components/alert/src/alert-bar/alert-bar.e2e.stories.js @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { AlertBar } from './index.js' +import { AlertBar } from './index.ts' window.onHidden = window.Cypress && window.Cypress.cy.stub() diff --git a/components/alert/src/alert-bar/alert-bar.js b/components/alert/src/alert-bar/alert-bar.js deleted file mode 100644 index 4440e1996f..0000000000 --- a/components/alert/src/alert-bar/alert-bar.js +++ /dev/null @@ -1,172 +0,0 @@ -import { mutuallyExclusive } from '@dhis2/prop-types' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { useRef, useState, useEffect } from 'react' -import { Actions, actionsPropType } from './actions.js' -import styles, { ANIMATION_TIME } from './alert-bar.styles.js' -import { Dismiss } from './dismiss.js' -import { Icon, iconPropType } from './icon.js' -import { Message } from './message.js' - -const AlertBar = ({ - actions, - children, - className, - critical, - dataTest = 'dhis2-uicore-alertbar', - duration = 8000, - hidden, - icon = true, - permanent, - success, - warning, - onHidden, -}) => { - const [inViewport, setInViewport] = useState(!hidden) - const [inDOM, setInDOM] = useState(!hidden) - const showTimeout = useRef(null) - const displayTimeout = useRef(null) - const hideTimeout = useRef(null) - const displayStartTime = useRef(null) - const displayTimeRemaining = useRef(null) - const info = !critical && !success && !warning - const shouldAutoHide = !(permanent || warning || critical) - const show = () => { - setInDOM(true) - setInViewport(true) - } - const hide = () => { - setInDOM(true) - setInViewport(false) - } - const remove = () => { - setInDOM(false) - setInViewport(false) - onHidden && onHidden({}, null) - } - const clearAllTimeouts = () => { - clearTimeout(showTimeout.current) - clearTimeout(displayTimeout.current) - clearTimeout(hideTimeout.current) - } - const runHideAnimation = () => { - clearAllTimeouts() - hide() - hideTimeout.current = setTimeout(remove, ANIMATION_TIME) - } - const startDisplayTimeout = () => { - if (shouldAutoHide) { - clearAllTimeouts() - displayStartTime.current = Date.now() - displayTimeRemaining.current = duration - displayTimeout.current = setTimeout( - runHideAnimation, - displayTimeRemaining.current - ) - } - } - const runShowAnimation = () => { - clearAllTimeouts() - show() - showTimeout.current = setTimeout(startDisplayTimeout, ANIMATION_TIME) - } - const pauseDisplayTimeout = () => { - if (shouldAutoHide) { - clearAllTimeouts() - const elapsedTime = Date.now() - displayStartTime.current - displayTimeRemaining.current -= elapsedTime - } - } - const resumeDisplayTimeout = () => { - if (shouldAutoHide) { - clearAllTimeouts() - displayTimeout.current = setTimeout( - runHideAnimation, - displayTimeRemaining.current - ) - } - } - - useEffect(() => { - // Additional check on inDOM prevents the AlertBar from briefly showing - // when it is mounted with a hidden prop set to true - if (hidden && inDOM) { - runHideAnimation() - } - if (!hidden) { - runShowAnimation() - } - - return clearAllTimeouts - }, [hidden]) - - return !inDOM ? null : ( -
- - {children} - - - - -
- ) -} - -const alertTypePropType = mutuallyExclusive( - ['success', 'warning', 'critical'], - PropTypes.bool -) - -AlertBar.propTypes = { - /** An array of 0-2 action objects -`[{label: "Save", onClick: clickHandler}]`*/ - actions: actionsPropType, - /** The message string for the alert */ - children: PropTypes.string, - className: PropTypes.string, - /** Alert bars with `critical` will not autohide */ - critical: alertTypePropType, - dataTest: PropTypes.string, - /** How long you want the notification to display, in `ms`, when it's not permanent */ - duration: PropTypes.number, - /** AlertBar will be hidden on creation when this is set to true */ - hidden: PropTypes.bool, - /** - * A specific icon to override the default icon in the bar. - * If `false` is provided, no icon will be shown. - */ - icon: iconPropType, - /** When set, AlertBar will not autohide */ - permanent: PropTypes.bool, - success: alertTypePropType, - /** Alert bars with `warning` will not autohide */ - warning: alertTypePropType, - onHidden: PropTypes.func, -} - -export { AlertBar } diff --git a/components/alert/src/alert-bar/alert-bar.prod.stories.js b/components/alert/src/alert-bar/alert-bar.prod.stories.js index a79fc37e79..2ba9a119a8 100644 --- a/components/alert/src/alert-bar/alert-bar.prod.stories.js +++ b/components/alert/src/alert-bar/alert-bar.prod.stories.js @@ -1,6 +1,6 @@ import { IconFile16 } from '@dhis2/ui-icons' import React, { useState } from 'react' -import { AlertBar } from './index.js' +import { AlertBar } from './index.ts' const subtitle = ` A floating alert that informs the user about temporary information diff --git a/components/alert/src/alert-bar/alert-bar.styles.js b/components/alert/src/alert-bar/alert-bar.styles.ts similarity index 100% rename from components/alert/src/alert-bar/alert-bar.styles.js rename to components/alert/src/alert-bar/alert-bar.styles.ts diff --git a/components/alert/src/alert-bar/alert-bar.tsx b/components/alert/src/alert-bar/alert-bar.tsx new file mode 100644 index 0000000000..41414740d9 --- /dev/null +++ b/components/alert/src/alert-bar/alert-bar.tsx @@ -0,0 +1,167 @@ +import cx from 'classnames' +import React, { useRef, useState, useEffect } from 'react' +import { Actions } from './actions.tsx' +import type { AlertBarAction } from './actions.tsx' +import styles, { ANIMATION_TIME } from './alert-bar.styles.ts' +import { Dismiss } from './dismiss.tsx' +import { Icon } from './icon.tsx' +import { Message } from './message.tsx' + +export interface AlertBarProps { + /** An array of 0-2 action objects `[{label: "Save", onClick: clickHandler}]` */ + actions?: AlertBarAction[] + /** The message string for the alert */ + children?: string + className?: string + /** Alert bars with `critical` will not autohide */ + critical?: boolean + dataTest?: string + /** How long you want the notification to display, in `ms`, when it's not permanent */ + duration?: number + /** AlertBar will be hidden on creation when this is set to true */ + hidden?: boolean + /** + * A specific icon to override the default icon in the bar. + * If `false` is provided, no icon will be shown. + */ + icon?: boolean | React.ReactElement + /** When set, AlertBar will not autohide */ + permanent?: boolean + success?: boolean + /** Alert bars with `warning` will not autohide */ + warning?: boolean + onHidden?: (payload: Record, event: null) => void +} + +const AlertBar = ({ + actions, + children, + className, + critical, + dataTest = 'dhis2-uicore-alertbar', + duration = 8000, + hidden, + icon = true, + permanent, + success, + warning, + onHidden, +}: AlertBarProps) => { + const [inViewport, setInViewport] = useState(!hidden) + const [inDOM, setInDOM] = useState(!hidden) + const showTimeout = useRef | null>(null) + const displayTimeout = useRef | null>(null) + const hideTimeout = useRef | null>(null) + const displayStartTime = useRef(null) + const displayTimeRemaining = useRef(null) + const info = !critical && !success && !warning + const shouldAutoHide = !(permanent || warning || critical) + const show = () => { + setInDOM(true) + setInViewport(true) + } + const hide = () => { + setInDOM(true) + setInViewport(false) + } + const remove = () => { + setInDOM(false) + setInViewport(false) + onHidden && onHidden({}, null) + } + const clearAllTimeouts = () => { + clearTimeout(showTimeout.current as ReturnType) + clearTimeout(displayTimeout.current as ReturnType) + clearTimeout(hideTimeout.current as ReturnType) + } + const runHideAnimation = () => { + clearAllTimeouts() + hide() + hideTimeout.current = setTimeout(remove, ANIMATION_TIME) + } + const startDisplayTimeout = () => { + if (shouldAutoHide) { + clearAllTimeouts() + displayStartTime.current = Date.now() + displayTimeRemaining.current = duration + displayTimeout.current = setTimeout( + runHideAnimation, + displayTimeRemaining.current + ) + } + } + const runShowAnimation = () => { + clearAllTimeouts() + show() + showTimeout.current = setTimeout(startDisplayTimeout, ANIMATION_TIME) + } + const pauseDisplayTimeout = () => { + if (shouldAutoHide) { + clearAllTimeouts() + const elapsedTime = + Date.now() - (displayStartTime.current as number) + displayTimeRemaining.current = + (displayTimeRemaining.current as number) - elapsedTime + } + } + const resumeDisplayTimeout = () => { + if (shouldAutoHide) { + clearAllTimeouts() + displayTimeout.current = setTimeout( + runHideAnimation, + displayTimeRemaining.current as number + ) + } + } + + useEffect(() => { + // Additional check on inDOM prevents the AlertBar from briefly showing + // when it is mounted with a hidden prop set to true + if (hidden && inDOM) { + runHideAnimation() + } + if (!hidden) { + runShowAnimation() + } + + return clearAllTimeouts + }, [hidden]) + + return !inDOM ? null : ( +
+ + {children as string} + + + + +
+ ) +} + +export { AlertBar } diff --git a/components/alert/src/alert-bar/dismiss.js b/components/alert/src/alert-bar/dismiss.js deleted file mode 100644 index cc2c0592aa..0000000000 --- a/components/alert/src/alert-bar/dismiss.js +++ /dev/null @@ -1,39 +0,0 @@ -import { spacers } from '@dhis2/ui-constants' -import { IconCross24 } from '@dhis2/ui-icons' -import PropTypes from 'prop-types' -import React from 'react' - -const Dismiss = ({ onClick, dataTest }) => ( -
- - -
-) - -Dismiss.propTypes = { - dataTest: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, -} - -export { Dismiss } diff --git a/components/alert/src/alert-bar/dismiss.tsx b/components/alert/src/alert-bar/dismiss.tsx new file mode 100644 index 0000000000..2cd29cd0a2 --- /dev/null +++ b/components/alert/src/alert-bar/dismiss.tsx @@ -0,0 +1,38 @@ +import { spacers } from '@dhis2/ui-constants' +import { IconCross24 } from '@dhis2/ui-icons' +import React from 'react' + +interface DismissProps { + dataTest: string + onClick: (event: React.MouseEvent) => void +} + +const Dismiss = ({ onClick, dataTest }: DismissProps) => ( +
+ + +
+) + +export { Dismiss } diff --git a/components/alert/src/alert-bar/icon.js b/components/alert/src/alert-bar/icon.js deleted file mode 100644 index dec45757b7..0000000000 --- a/components/alert/src/alert-bar/icon.js +++ /dev/null @@ -1,78 +0,0 @@ -import { mutuallyExclusive } from '@dhis2/prop-types' -import { spacers, colors } from '@dhis2/ui-constants' -import { - IconErrorFilled24, - IconInfoFilled24, - IconWarningFilled24, - IconCheckmark24, -} from '@dhis2/ui-icons' -import PropTypes from 'prop-types' -import React from 'react' - -const StatusIcon = ({ error, warning, valid, info, defaultTo = null }) => { - if (error) { - return - } - if (warning) { - return - } - if (valid) { - return - } - if (info) { - return - } - - return defaultTo -} - -StatusIcon.propTypes = { - defaultTo: PropTypes.element, - error: PropTypes.bool, - info: PropTypes.bool, - valid: PropTypes.bool, - warning: PropTypes.bool, -} - -const Icon = ({ icon, success, warning, critical, info, dataTest }) => { - if (icon === false) { - return null - } - - return ( -
- {React.isValidElement(icon) ? ( - icon - ) : ( - - )} - -
- ) -} - -const iconPropType = PropTypes.oneOfType([PropTypes.bool, PropTypes.element]) -const alertStatePropType = mutuallyExclusive( - ['success', 'warning', 'critical', 'info'], - PropTypes.bool -) - -Icon.propTypes = { - dataTest: PropTypes.string.isRequired, - critical: alertStatePropType, - icon: iconPropType, - info: alertStatePropType, - success: alertStatePropType, - warning: alertStatePropType, -} - -export { Icon, iconPropType } diff --git a/components/alert/src/alert-bar/icon.tsx b/components/alert/src/alert-bar/icon.tsx new file mode 100644 index 0000000000..1ff5a37eea --- /dev/null +++ b/components/alert/src/alert-bar/icon.tsx @@ -0,0 +1,84 @@ +import { spacers, colors } from '@dhis2/ui-constants' +import { + IconErrorFilled24, + IconInfoFilled24, + IconWarningFilled24, + IconCheckmark24, +} from '@dhis2/ui-icons' +import React from 'react' + +interface StatusIconProps { + defaultTo?: React.ReactElement | null + error?: boolean + info?: boolean + valid?: boolean + warning?: boolean +} + +const StatusIcon = ({ + error, + warning, + valid, + info, + defaultTo = null, +}: StatusIconProps) => { + if (error) { + return + } + if (warning) { + return + } + if (valid) { + return + } + if (info) { + return + } + + return defaultTo +} + +interface IconProps { + dataTest: string + critical?: boolean + icon?: boolean | React.ReactElement + info?: boolean + success?: boolean + warning?: boolean +} + +const Icon = ({ + icon, + success, + warning, + critical, + info, + dataTest, +}: IconProps) => { + if (icon === false) { + return null + } + + return ( +
+ {React.isValidElement(icon) ? ( + icon + ) : ( + + )} + +
+ ) +} + +export { Icon } +export type { IconProps } diff --git a/components/alert/src/alert-bar/index.js b/components/alert/src/alert-bar/index.js deleted file mode 100644 index 685cf1eeba..0000000000 --- a/components/alert/src/alert-bar/index.js +++ /dev/null @@ -1 +0,0 @@ -export { AlertBar } from './alert-bar.js' diff --git a/components/alert/src/alert-bar/index.ts b/components/alert/src/alert-bar/index.ts new file mode 100644 index 0000000000..a72d36204d --- /dev/null +++ b/components/alert/src/alert-bar/index.ts @@ -0,0 +1,2 @@ +export { AlertBar } from './alert-bar.tsx' +export type { AlertBarProps } from './alert-bar.tsx' diff --git a/components/alert/src/alert-bar/message.js b/components/alert/src/alert-bar/message.js deleted file mode 100644 index 1e37e4af39..0000000000 --- a/components/alert/src/alert-bar/message.js +++ /dev/null @@ -1,19 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' - -const Message = ({ children }) => ( -
- {children} - -
-) - -Message.propTypes = { - children: PropTypes.string.isRequired, -} - -export { Message } diff --git a/components/alert/src/alert-bar/message.tsx b/components/alert/src/alert-bar/message.tsx new file mode 100644 index 0000000000..306d27c642 --- /dev/null +++ b/components/alert/src/alert-bar/message.tsx @@ -0,0 +1,18 @@ +import React from 'react' + +interface MessageProps { + children: string +} + +const Message = ({ children }: MessageProps) => ( +
+ {children} + +
+) + +export { Message } diff --git a/components/alert/src/alert-stack/alert-stack.e2e.stories.js b/components/alert/src/alert-stack/alert-stack.e2e.stories.js index 372df0d277..36852edf85 100644 --- a/components/alert/src/alert-stack/alert-stack.e2e.stories.js +++ b/components/alert/src/alert-stack/alert-stack.e2e.stories.js @@ -1,6 +1,6 @@ import React from 'react' -import { AlertBar } from '../alert-bar/index.js' -import { AlertStack } from './alert-stack.js' +import { AlertBar } from '../alert-bar/index.ts' +import { AlertStack } from './alert-stack.tsx' export default { title: 'AlertStack' } diff --git a/components/alert/src/alert-stack/alert-stack.js b/components/alert/src/alert-stack/alert-stack.js deleted file mode 100644 index 31ce8f4838..0000000000 --- a/components/alert/src/alert-stack/alert-stack.js +++ /dev/null @@ -1,44 +0,0 @@ -import { layers } from '@dhis2/ui-constants' -import { Portal } from '@dhis2-ui/portal' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React from 'react' - -export const AlertStack = ({ - className, - children, - dataTest = 'dhis2-uicore-alertstack', -}) => ( - -
- {children} - -
-
-) - -AlertStack.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - dataTest: PropTypes.string, -} diff --git a/components/alert/src/alert-stack/alert-stack.prod.stories.js b/components/alert/src/alert-stack/alert-stack.prod.stories.js index ec1a6189a5..f2ddb03530 100644 --- a/components/alert/src/alert-stack/alert-stack.prod.stories.js +++ b/components/alert/src/alert-stack/alert-stack.prod.stories.js @@ -1,6 +1,6 @@ import React, { useEffect } from 'react' -import { AlertBar } from '../alert-bar/index.js' -import { AlertStack } from './alert-stack.js' +import { AlertBar } from '../alert-bar/index.ts' +import { AlertStack } from './alert-stack.tsx' const description = ` A container for Alert Bars. diff --git a/components/alert/src/alert-stack/alert-stack.tsx b/components/alert/src/alert-stack/alert-stack.tsx new file mode 100644 index 0000000000..7155f942b3 --- /dev/null +++ b/components/alert/src/alert-stack/alert-stack.tsx @@ -0,0 +1,43 @@ +import { layers } from '@dhis2/ui-constants' +import { Portal } from '@dhis2-ui/portal' +import cx from 'classnames' +import React from 'react' + +export interface AlertStackProps { + children?: React.ReactNode + className?: string + dataTest?: string +} + +export const AlertStack = ({ + className, + children, + dataTest = 'dhis2-uicore-alertstack', +}: AlertStackProps) => ( + +
+ {children} + +
+
+) diff --git a/components/alert/src/alert-stack/index.js b/components/alert/src/alert-stack/index.js deleted file mode 100644 index 48f6ef2d0e..0000000000 --- a/components/alert/src/alert-stack/index.js +++ /dev/null @@ -1 +0,0 @@ -export { AlertStack } from './alert-stack.js' diff --git a/components/alert/src/alert-stack/index.ts b/components/alert/src/alert-stack/index.ts new file mode 100644 index 0000000000..8b69d1bf73 --- /dev/null +++ b/components/alert/src/alert-stack/index.ts @@ -0,0 +1,2 @@ +export { AlertStack } from './alert-stack.tsx' +export type { AlertStackProps } from './alert-stack.tsx' diff --git a/components/alert/src/index.js b/components/alert/src/index.js deleted file mode 100644 index 06d4fc7444..0000000000 --- a/components/alert/src/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { AlertBar } from './alert-bar/index.js' -export { AlertStack } from './alert-stack/index.js' diff --git a/components/alert/src/index.ts b/components/alert/src/index.ts new file mode 100644 index 0000000000..2ad931c356 --- /dev/null +++ b/components/alert/src/index.ts @@ -0,0 +1,5 @@ +export { AlertBar } from './alert-bar/index.ts' +export type { AlertBarProps } from './alert-bar/index.ts' +export type { AlertBarAction } from './alert-bar/actions.tsx' +export { AlertStack } from './alert-stack/index.ts' +export type { AlertStackProps } from './alert-stack/index.ts' diff --git a/components/alert/tsconfig.json b/components/alert/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/alert/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/box/d2.config.js b/components/box/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/box/d2.config.js +++ b/components/box/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/box/package.json b/components/box/package.json index 40f9dc2679..59c3c62e2e 100644 --- a/components/box/package.json +++ b/components/box/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/box/src/box.e2e.stories.js b/components/box/src/box.e2e.stories.js index 498db343ae..7d7cae4946 100644 --- a/components/box/src/box.e2e.stories.js +++ b/components/box/src/box.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Box } from './box.js' +import { Box } from './box.tsx' export default { title: 'Box' } export const WithChildren = () => I am a child diff --git a/components/box/src/box.js b/components/box/src/box.js deleted file mode 100644 index 0221e1462e..0000000000 --- a/components/box/src/box.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' - -export const Box = ({ - overflow, - height, - minHeight, - maxHeight, - width, - minWidth, - maxWidth, - marginTop, - children, - dataTest = 'dhis2-uicore-box', - className, -}) => ( -
- {children} - -
-) - -Box.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - dataTest: PropTypes.string, - height: PropTypes.string, - marginTop: PropTypes.string, - maxHeight: PropTypes.string, - maxWidth: PropTypes.string, - minHeight: PropTypes.string, - minWidth: PropTypes.string, - overflow: PropTypes.string, - width: PropTypes.string, -} diff --git a/components/box/src/box.prod.stories.js b/components/box/src/box.prod.stories.js index d1c9c78e18..49012fd37b 100644 --- a/components/box/src/box.prod.stories.js +++ b/components/box/src/box.prod.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Box } from './box.js' +import { Box } from './box.tsx' const description = ` A box for creating some layout on the page. diff --git a/components/box/src/box.tsx b/components/box/src/box.tsx new file mode 100644 index 0000000000..7f6e4df196 --- /dev/null +++ b/components/box/src/box.tsx @@ -0,0 +1,45 @@ +import React from 'react' + +export interface BoxProps { + children?: React.ReactNode + className?: string + dataTest?: string + height?: string + marginTop?: string + maxHeight?: string + maxWidth?: string + minHeight?: string + minWidth?: string + overflow?: string + width?: string +} + +export const Box = ({ + overflow, + height, + minHeight, + maxHeight, + width, + minWidth, + maxWidth, + marginTop, + children, + dataTest = 'dhis2-uicore-box', + className, +}: BoxProps) => ( +
+ {children} + +
+) diff --git a/components/box/src/index.js b/components/box/src/index.js deleted file mode 100644 index 6bb9722ea8..0000000000 --- a/components/box/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Box } from './box.js' diff --git a/components/box/src/index.ts b/components/box/src/index.ts new file mode 100644 index 0000000000..c1dd9dcdf8 --- /dev/null +++ b/components/box/src/index.ts @@ -0,0 +1,2 @@ +export { Box } from './box.tsx' +export type { BoxProps } from './box.tsx' diff --git a/components/box/tsconfig.json b/components/box/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/box/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/button/d2.config.js b/components/button/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/button/d2.config.js +++ b/components/button/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/button/package.json b/components/button/package.json index dcc8b8e697..f7447ccba0 100644 --- a/components/button/package.json +++ b/components/button/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/button/src/button-strip/button-strip.e2e.stories.js b/components/button/src/button-strip/button-strip.e2e.stories.js index d6d5116d91..3fa75442ee 100644 --- a/components/button/src/button-strip/button-strip.e2e.stories.js +++ b/components/button/src/button-strip/button-strip.e2e.stories.js @@ -1,6 +1,6 @@ import React from 'react' -import { Button } from '../index.js' -import { ButtonStrip } from './index.js' +import { Button } from '../index.ts' +import { ButtonStrip } from './index.ts' export default { title: 'ButtonStrip' } diff --git a/components/button/src/button-strip/button-strip.js b/components/button/src/button-strip/button-strip.js deleted file mode 100644 index 3857a0b194..0000000000 --- a/components/button/src/button-strip/button-strip.js +++ /dev/null @@ -1,58 +0,0 @@ -import { mutuallyExclusive } from '@dhis2/prop-types' -import { spacers } from '@dhis2/ui-constants' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { Children } from 'react' - -const ButtonStrip = ({ - className, - children, - middle, - end, - dataTest = 'dhis2-uicore-buttonstrip', -}) => ( -
- {Children.map(children, (child) => ( -
{child}
- ))} - - -
-) - -const alignmentPropType = mutuallyExclusive(['middle', 'end'], PropTypes.bool) - -ButtonStrip.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - dataTest: PropTypes.string, - /** Horizontal alignment for buttons. Mutually exclusive with `middle` prop */ - end: alignmentPropType, - /** Horizontal alignment. Mutually exclusive with `end` prop */ - middle: alignmentPropType, -} - -export { ButtonStrip } diff --git a/components/button/src/button-strip/button-strip.prod.stories.js b/components/button/src/button-strip/button-strip.prod.stories.js index 92f562f8b0..f0b2bb12a4 100644 --- a/components/button/src/button-strip/button-strip.prod.stories.js +++ b/components/button/src/button-strip/button-strip.prod.stories.js @@ -1,6 +1,6 @@ import React from 'react' -import { Button, SplitButton } from '../index.js' -import { ButtonStrip } from './index.js' +import { Button, SplitButton } from '../index.ts' +import { ButtonStrip } from './index.ts' const description = ` A wrapper for buttons to add spacing and alignment. diff --git a/components/button/src/button-strip/button-strip.tsx b/components/button/src/button-strip/button-strip.tsx new file mode 100644 index 0000000000..3c3523ecc7 --- /dev/null +++ b/components/button/src/button-strip/button-strip.tsx @@ -0,0 +1,55 @@ +import { spacers } from '@dhis2/ui-constants' +import cx from 'classnames' +import React, { Children } from 'react' + +export interface ButtonStripProps { + /** Content to render inside the button strip */ + children?: React.ReactNode + className?: string + dataTest?: string + /** Horizontal alignment for buttons. Mutually exclusive with `middle` prop */ + end?: boolean + /** Horizontal alignment. Mutually exclusive with `end` prop */ + middle?: boolean +} + +const ButtonStrip = ({ + className, + children, + middle, + end, + dataTest = 'dhis2-uicore-buttonstrip', +}: ButtonStripProps) => ( +
+ {Children.map(children, (child) => ( +
{child}
+ ))} + + +
+) + +export { ButtonStrip } diff --git a/components/button/src/button-strip/index.js b/components/button/src/button-strip/index.js deleted file mode 100644 index 2d081a2c8c..0000000000 --- a/components/button/src/button-strip/index.js +++ /dev/null @@ -1 +0,0 @@ -export { ButtonStrip } from './button-strip.js' diff --git a/components/button/src/button-strip/index.ts b/components/button/src/button-strip/index.ts new file mode 100644 index 0000000000..78c8d48ade --- /dev/null +++ b/components/button/src/button-strip/index.ts @@ -0,0 +1,2 @@ +export { ButtonStrip } from './button-strip.tsx' +export type { ButtonStripProps } from './button-strip.tsx' diff --git a/components/button/src/button/__tests__/Button.test.js b/components/button/src/button/__tests__/Button.test.js index b36146c492..3778c64d4f 100644 --- a/components/button/src/button/__tests__/Button.test.js +++ b/components/button/src/button/__tests__/Button.test.js @@ -1,7 +1,7 @@ import { render, fireEvent, screen } from '@testing-library/react' import { mount } from 'enzyme' import React from 'react' -import { Button } from '../button.js' +import { Button } from '../button.tsx' describe(' - ) -} - -Button.propTypes = { - /** Component to render inside the button */ - children: PropTypes.node, - /** A className that will be passed to the ` + ) +} diff --git a/components/button/src/button/index.js b/components/button/src/button/index.js deleted file mode 100644 index 07bcffc353..0000000000 --- a/components/button/src/button/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Button } from './button.js' diff --git a/components/button/src/button/index.ts b/components/button/src/button/index.ts new file mode 100644 index 0000000000..84b2a3a1af --- /dev/null +++ b/components/button/src/button/index.ts @@ -0,0 +1,2 @@ +export { Button } from './button.tsx' +export type { ButtonProps } from './button.tsx' diff --git a/components/button/src/dropdown-button/__tests__/dropdown-button.test.js b/components/button/src/dropdown-button/__tests__/dropdown-button.test.js index a38e8e167a..4d8b6869c0 100644 --- a/components/button/src/dropdown-button/__tests__/dropdown-button.test.js +++ b/components/button/src/dropdown-button/__tests__/dropdown-button.test.js @@ -4,9 +4,9 @@ import { render, fireEvent, waitFor } from '@testing-library/react' import { mount } from 'enzyme' import React from 'react' import { act } from 'react-dom/test-utils' -import { Modal } from '../../../../modal/src/modal/modal.js' -import { Button } from '../../index.js' -import { DropdownButton } from '../dropdown-button.js' +import { Modal } from '../../../../modal/src/modal/modal.tsx' +import { Button } from '../../index.ts' +import { DropdownButton } from '../dropdown-button.tsx' describe('', () => { describe('controlled mode', () => { diff --git a/components/button/src/dropdown-button/dropdown-button.e2e.stories.js b/components/button/src/dropdown-button/dropdown-button.e2e.stories.js index 7a27a146dc..e7b17c2f50 100644 --- a/components/button/src/dropdown-button/dropdown-button.e2e.stories.js +++ b/components/button/src/dropdown-button/dropdown-button.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { DropdownButton } from './index.js' +import { DropdownButton } from './index.ts' window.onClick = window.Cypress && window.Cypress.cy.stub() diff --git a/components/button/src/dropdown-button/dropdown-button.js b/components/button/src/dropdown-button/dropdown-button.js deleted file mode 100644 index 7f782d6a58..0000000000 --- a/components/button/src/dropdown-button/dropdown-button.js +++ /dev/null @@ -1,239 +0,0 @@ -import { requiredIf } from '@dhis2/prop-types' -import { spacers, sharedPropTypes } from '@dhis2/ui-constants' -import { Layer } from '@dhis2-ui/layer' -import { Popper } from '@dhis2-ui/popper' -import PropTypes from 'prop-types' -import React, { Component } from 'react' -import { Button } from '../button/index.js' - -function ArrowDown({ className }) { - return ( - - - - - ) -} -ArrowDown.propTypes = { - className: PropTypes.string, -} - -function ArrowUp({ className }) { - return ( - - - - - ) -} -ArrowUp.propTypes = { - className: PropTypes.string, -} - -class DropdownButton extends Component { - state = { - open: false, - } - - static defaultProps = { - dataTest: 'dhis2-uicore-dropdownbutton', - } - - anchorRef = React.createRef() - - componentDidMount() { - document.addEventListener('keydown', this.handleKeyDown) - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeyDown) - } - - handleKeyDown = (event) => { - if (event.key === 'Escape' && this.state.open) { - event.preventDefault() - event.stopPropagation() - this.setState({ open: false }) - } - } - - onClickHandler = ({ name, value }, event) => { - const handleClick = (open) => { - if (this.props.onClick) { - this.props.onClick( - { - name, - value, - open, - }, - event - ) - } - } - if (typeof this.props.open === 'boolean') { - handleClick(!this.props.open) - } else { - this.setState({ open: !this.state.open }, () => { - handleClick(this.state.open) - }) - } - } - - render() { - const { - component, - children, - className, - destructive, - disabled, - icon, - large, - primary, - secondary, - small, - name, - value, - tabIndex, - type, - initialFocus, - dataTest = 'dhis2-uicore-dropdownbutton', - } = this.props - const open = - typeof this.props.open === 'boolean' - ? this.props.open - : this.state.open - const ArrowIconComponent = open ? ArrowUp : ArrowDown - - return ( -
- - - {open && ( - - - {component} - - - )} - - -
- ) - } -} - -DropdownButton.propTypes = { - /** Children to render inside the buton */ - children: PropTypes.node, - className: PropTypes.string, - /** Component to show/hide when button is clicked */ - component: PropTypes.element, - dataTest: PropTypes.string, - /** - * Applies 'destructive' button appearance, implying a dangerous action. - */ - destructive: PropTypes.bool, - /** Make the button non-interactive */ - disabled: PropTypes.bool, - icon: PropTypes.element, - /** Grants button initial focus on the page */ - initialFocus: PropTypes.bool, - /** Button size. Mutually exclusive with `small` prop */ - large: sharedPropTypes.sizePropType, - name: PropTypes.string, - /** Controls popper visibility. When implementing this prop the component becomes a controlled component */ - open: PropTypes.bool, - /** - * Applies 'primary' button appearance, implying the most important action. - */ - primary: PropTypes.bool, - /** - * Applies 'secondary' button appearance. - */ - secondary: PropTypes.bool, - /** Button size. Mutually exclusive with `large` prop */ - small: sharedPropTypes.sizePropType, - tabIndex: PropTypes.string, - /** Type of button. Can take advantage of different default behavior */ - type: PropTypes.oneOf(['submit', 'reset', 'button']), - value: PropTypes.string, - /** - * Callback triggered on click. - * Called with signature `({ name: string, value: string, open: bool }, event)` - * Is required when using the `open` prop to override the internal - * state. - */ - onClick: requiredIf( - (props) => typeof props.open === 'boolean', - PropTypes.func - ), -} - -export { DropdownButton } diff --git a/components/button/src/dropdown-button/dropdown-button.prod.stories.js b/components/button/src/dropdown-button/dropdown-button.prod.stories.js index a830acc7d6..751cd10866 100644 --- a/components/button/src/dropdown-button/dropdown-button.prod.stories.js +++ b/components/button/src/dropdown-button/dropdown-button.prod.stories.js @@ -1,7 +1,7 @@ import { sharedPropTypes } from '@dhis2/ui-constants' import { FlyoutMenu, MenuItem } from '@dhis2-ui/menu' import React, { useState } from 'react' -import { DropdownButton } from './index.js' +import { DropdownButton } from './index.ts' const description = ` Presents several actions to a user in a small space. Can replace single, individual buttons. Should only be used for actions that are related to one another. Ensure the button has a useful level that communicates that actions are contained within. Dropdown buttons do not have an explicit action, only expanding the list of contained actions. diff --git a/components/button/src/dropdown-button/dropdown-button.tsx b/components/button/src/dropdown-button/dropdown-button.tsx new file mode 100644 index 0000000000..95d1846f48 --- /dev/null +++ b/components/button/src/dropdown-button/dropdown-button.tsx @@ -0,0 +1,261 @@ +import { spacers } from '@dhis2/ui-constants' +import { Layer } from '@dhis2-ui/layer' +import { Popper } from '@dhis2-ui/popper' +import React, { Component } from 'react' +import { Button } from '../button/index.ts' + +interface ArrowDownProps { + className?: string +} + +function ArrowDown({ className }: ArrowDownProps) { + return ( + + + + + ) +} + +interface ArrowUpProps { + className?: string +} + +function ArrowUp({ className }: ArrowUpProps) { + return ( + + + + + ) +} + +interface DropdownButtonCallbackPayload { + name?: string + value?: string + open: boolean +} + +export interface DropdownButtonProps { + /** Children to render inside the button */ + children?: React.ReactNode + className?: string + /** Component to show/hide when button is clicked */ + component?: React.ReactElement + dataTest?: string + /** + * Applies 'destructive' button appearance, implying a dangerous action. + */ + destructive?: boolean + /** Make the button non-interactive */ + disabled?: boolean + icon?: React.ReactElement + /** Grants button initial focus on the page */ + initialFocus?: boolean + /** Button size. Mutually exclusive with `small` prop */ + large?: boolean + name?: string + /** Controls popper visibility. When implementing this prop the component becomes a controlled component */ + open?: boolean + /** + * Applies 'primary' button appearance, implying the most important action. + */ + primary?: boolean + /** + * Applies 'secondary' button appearance. + */ + secondary?: boolean + /** Button size. Mutually exclusive with `large` prop */ + small?: boolean + tabIndex?: string + /** Type of button. Can take advantage of different default behavior */ + type?: 'submit' | 'reset' | 'button' + value?: string + /** + * Callback triggered on click. + * Called with signature `({ name: string, value: string, open: bool }, event)` + * Is required when using the `open` prop to override the internal state. + */ + onClick?: ( + payload: DropdownButtonCallbackPayload, + event: React.MouseEvent | React.SyntheticEvent + ) => void +} + +interface DropdownButtonState { + open: boolean +} + +class DropdownButton extends Component< + DropdownButtonProps, + DropdownButtonState +> { + state: DropdownButtonState = { + open: false, + } + + static defaultProps = { + dataTest: 'dhis2-uicore-dropdownbutton', + } + + anchorRef = React.createRef() + + componentDidMount() { + document.addEventListener('keydown', this.handleKeyDown) + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleKeyDown) + } + + handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && this.state.open) { + event.preventDefault() + event.stopPropagation() + this.setState({ open: false }) + } + } + + onClickHandler = ( + { name, value }: { name?: string; value?: string }, + event: React.MouseEvent | React.SyntheticEvent + ) => { + const handleClick = (open: boolean) => { + if (this.props.onClick) { + this.props.onClick( + { + name, + value, + open, + }, + event + ) + } + } + if (typeof this.props.open === 'boolean') { + handleClick(!this.props.open) + } else { + this.setState({ open: !this.state.open }, () => { + handleClick(this.state.open) + }) + } + } + + render() { + const { + component, + children, + className, + destructive, + disabled, + icon, + large, + primary, + secondary, + small, + name, + value, + tabIndex, + type, + initialFocus, + dataTest = 'dhis2-uicore-dropdownbutton', + } = this.props + const open = + typeof this.props.open === 'boolean' + ? this.props.open + : this.state.open + const ArrowIconComponent = open ? ArrowUp : ArrowDown + + return ( +
+ + + {open && ( + + this.onClickHandler( + {}, + event as unknown as React.SyntheticEvent + ) + } + > + + {component} + + + )} + + +
+ ) + } +} + +export { DropdownButton } diff --git a/components/button/src/dropdown-button/index.js b/components/button/src/dropdown-button/index.js deleted file mode 100644 index d7449e7963..0000000000 --- a/components/button/src/dropdown-button/index.js +++ /dev/null @@ -1 +0,0 @@ -export { DropdownButton } from './dropdown-button.js' diff --git a/components/button/src/dropdown-button/index.ts b/components/button/src/dropdown-button/index.ts new file mode 100644 index 0000000000..84bc248026 --- /dev/null +++ b/components/button/src/dropdown-button/index.ts @@ -0,0 +1,2 @@ +export { DropdownButton } from './dropdown-button.tsx' +export type { DropdownButtonProps } from './dropdown-button.tsx' diff --git a/components/button/src/index.js b/components/button/src/index.js deleted file mode 100644 index 33d075ab39..0000000000 --- a/components/button/src/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { Button } from './button/index.js' -export { ButtonStrip } from './button-strip/index.js' -export { SplitButton } from './split-button/index.js' -export { DropdownButton } from './dropdown-button/index.js' diff --git a/components/button/src/index.ts b/components/button/src/index.ts new file mode 100644 index 0000000000..535302da09 --- /dev/null +++ b/components/button/src/index.ts @@ -0,0 +1,8 @@ +export { Button } from './button/index.ts' +export type { ButtonProps } from './button/index.ts' +export { ButtonStrip } from './button-strip/index.ts' +export type { ButtonStripProps } from './button-strip/index.ts' +export { SplitButton } from './split-button/index.ts' +export type { SplitButtonProps } from './split-button/index.ts' +export { DropdownButton } from './dropdown-button/index.ts' +export type { DropdownButtonProps } from './dropdown-button/index.ts' diff --git a/components/button/src/split-button/index.js b/components/button/src/split-button/index.js deleted file mode 100644 index 296b4b0a69..0000000000 --- a/components/button/src/split-button/index.js +++ /dev/null @@ -1 +0,0 @@ -export { SplitButton } from './split-button.js' diff --git a/components/button/src/split-button/index.ts b/components/button/src/split-button/index.ts new file mode 100644 index 0000000000..154dc7fc92 --- /dev/null +++ b/components/button/src/split-button/index.ts @@ -0,0 +1,2 @@ +export { SplitButton } from './split-button.tsx' +export type { SplitButtonProps } from './split-button.tsx' diff --git a/components/button/src/split-button/split-button.e2e.stories.js b/components/button/src/split-button/split-button.e2e.stories.js index c7d779547b..b47f02f92d 100644 --- a/components/button/src/split-button/split-button.e2e.stories.js +++ b/components/button/src/split-button/split-button.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { SplitButton } from './split-button.js' +import { SplitButton } from './split-button.tsx' window.onClick = window.Cypress && window.Cypress.cy.stub() diff --git a/components/button/src/split-button/split-button.js b/components/button/src/split-button/split-button.js deleted file mode 100644 index 28a4f43f9a..0000000000 --- a/components/button/src/split-button/split-button.js +++ /dev/null @@ -1,273 +0,0 @@ -import { requiredIf } from '@dhis2/prop-types' -import { spacers, sharedPropTypes } from '@dhis2/ui-constants' -import { IconChevronUp16, IconChevronDown16 } from '@dhis2/ui-icons' -import { Layer } from '@dhis2-ui/layer' -import { Popper } from '@dhis2-ui/popper' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { Component } from 'react' -import css from 'styled-jsx/css' -import { Button } from '../button/index.js' -import i18n from '../locales/index.js' - -const rightButton = css.resolve` - button { - padding: 0 ${spacers.dp12}; - } -` - -class SplitButton extends Component { - state = { - open: false, - } - - static defaultProps = { - dataTest: 'dhis2-uicore-splitbutton', - } - - anchorRef = React.createRef() - - isControlled = () => typeof this.props.open === 'boolean' - - componentDidMount() { - document.addEventListener('keydown', this.handleKeyDown) - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeyDown) - } - - handleKeyDown = (event) => { - const open = this.isControlled() ? this.props.open : this.state.open - if (event.key === 'Escape' && open) { - event.preventDefault() - event.stopPropagation() - if (this.isControlled()) { - if (this.props.onToggle) { - this.props.onToggle( - { - name: this.props.name, - value: this.props.value, - open: false, - }, - event - ) - } - } else { - this.setState({ open: false }) - } - this.anchorRef.current && this.anchorRef.current.focus() - } - } - - handlePrimaryAction = (payload, event) => { - if (this.props.onClick) { - this.props.onClick( - { - name: payload.name, - value: payload.value, - open: this.isControlled() - ? this.props.open - : this.state.open, - }, - event - ) - } - } - - handleToggle = (payload, event) => { - if (this.isControlled()) { - if (this.props.onToggle) { - this.props.onToggle( - { - name: payload.name, - value: payload.value, - open: !this.props.open, - }, - event - ) - } - } else { - this.setState((prevState) => ({ open: !prevState.open })) - } - } - - handleBackdropClick = (event) => { - if (this.isControlled()) { - if (this.props.onToggle) { - this.props.onToggle( - { - name: this.props.name, - value: this.props.value, - open: false, - }, - event - ) - } - } else { - this.setState({ open: false }) - } - } - - render() { - const open = this.isControlled() ? this.props.open : this.state.open - const { - component, - children, - className, - name, - value, - icon, - small, - large, - primary, - secondary, - destructive, - disabled, - type, - tabIndex, - dataTest = 'dhis2-uicore-splitbutton', - initialFocus, - } = this.props - - const arrow = open ? : - - return ( -
- - - - - {open && ( - - - {component} - - - )} - - {rightButton.styles} - -
- ) - } -} - -SplitButton.propTypes = { - children: PropTypes.string, - className: PropTypes.string, - /** Component to render when the dropdown is opened */ - component: PropTypes.element, - dataTest: PropTypes.string, - /** - * Applies 'destructive' button appearance, implying a dangerous action. - */ - destructive: PropTypes.bool, - /** Disables the button and makes it uninteractive */ - disabled: PropTypes.bool, - /** An icon to add inside the button */ - icon: PropTypes.element, - /** Grants the button the initial focus */ - initialFocus: PropTypes.bool, - /** Changes button size. Mutually exclusive with `small` prop */ - large: sharedPropTypes.sizePropType, - name: PropTypes.string, - /** - * Controls popper visibility. When implementing this prop the component becomes a controlled component - */ - open: PropTypes.bool, - /** - * Applies 'primary' button appearance, implying the most important action. - */ - primary: PropTypes.bool, - /** - * Applies 'secondary' button appearance. - */ - secondary: PropTypes.bool, - /** Changes button size. Mutually exclusive with `large` prop */ - small: sharedPropTypes.sizePropType, - tabIndex: PropTypes.string, - /** Type of button. Applied to html `button` element */ - type: PropTypes.oneOf(['submit', 'reset', 'button']), - /** Value associated with the button. Passed in object to onClick handler */ - value: PropTypes.string, - /** - * Callback triggered when the main button is clicked. - * Called with signature `({ name: string, value: string, open: bool }, event)` - */ - onClick: PropTypes.func, - /** - * Callback triggered when the dropdown is toggled (by clicking the chevron, pressing Escape, or clicking the backdrop). - * Called with signature `({ name: string, value: string, open: bool }, event)`. - * Required if `open` prop is used (controlled component). - */ - onToggle: requiredIf( - (props) => typeof props.open === 'boolean', - PropTypes.func - ), -} - -export { SplitButton } diff --git a/components/button/src/split-button/split-button.prod.stories.js b/components/button/src/split-button/split-button.prod.stories.js index d4ec669128..26374313fb 100644 --- a/components/button/src/split-button/split-button.prod.stories.js +++ b/components/button/src/split-button/split-button.prod.stories.js @@ -2,9 +2,9 @@ import { sharedPropTypes } from '@dhis2/ui-constants' import { FlyoutMenu, MenuItem } from '@dhis2-ui/menu' import { Modal, ModalContent, ModalTitle, ModalActions } from '@dhis2-ui/modal' import React from 'react' -import { Button } from '../button/button.js' -import { ButtonStrip } from '../button-strip/button-strip.js' -import { SplitButton } from './split-button.js' +import { Button } from '../button/button.tsx' +import { ButtonStrip } from '../button-strip/button-strip.tsx' +import { SplitButton } from './split-button.tsx' const description = ` Similar to the dropdown button, but can be triggered independently of opening the contained action list. The main action may be 'Save' and the contained actions may be "Save and add another" and "Save and open". diff --git a/components/button/src/split-button/split-button.test.js b/components/button/src/split-button/split-button.test.js index e7a7425ca6..0786610621 100644 --- a/components/button/src/split-button/split-button.test.js +++ b/components/button/src/split-button/split-button.test.js @@ -1,6 +1,6 @@ import { render, fireEvent, cleanup, waitFor } from '@testing-library/react' import React from 'react' -import { SplitButton } from './split-button.js' +import { SplitButton } from './split-button.tsx' describe('SplitButton', () => { afterEach(cleanup) diff --git a/components/button/src/split-button/split-button.tsx b/components/button/src/split-button/split-button.tsx new file mode 100644 index 0000000000..bb4ddf628d --- /dev/null +++ b/components/button/src/split-button/split-button.tsx @@ -0,0 +1,293 @@ +import { spacers } from '@dhis2/ui-constants' +import { IconChevronUp16, IconChevronDown16 } from '@dhis2/ui-icons' +import { Layer } from '@dhis2-ui/layer' +import { Popper } from '@dhis2-ui/popper' +import cx from 'classnames' +import React, { Component } from 'react' +import css from 'styled-jsx/css' +import { Button } from '../button/index.ts' +import i18n from '../locales/index.js' + +const rightButton = css.resolve` + button { + padding: 0 ${spacers.dp12}; + } +` + +interface SplitButtonCallbackPayload { + name?: string + value?: string + open: boolean +} + +export interface SplitButtonProps { + children?: string + className?: string + /** Component to render when the dropdown is opened */ + component?: React.ReactElement + dataTest?: string + /** + * Applies 'destructive' button appearance, implying a dangerous action. + */ + destructive?: boolean + /** Disables the button and makes it uninteractive */ + disabled?: boolean + /** An icon to add inside the button */ + icon?: React.ReactElement + /** Grants the button the initial focus */ + initialFocus?: boolean + /** Changes button size. Mutually exclusive with `small` prop */ + large?: boolean + name?: string + /** + * Controls popper visibility. When implementing this prop the component becomes a controlled component + */ + open?: boolean + /** + * Applies 'primary' button appearance, implying the most important action. + */ + primary?: boolean + /** + * Applies 'secondary' button appearance. + */ + secondary?: boolean + /** Changes button size. Mutually exclusive with `large` prop */ + small?: boolean + tabIndex?: string + /** Type of button. Applied to html `button` element */ + type?: 'submit' | 'reset' | 'button' + /** Value associated with the button. Passed in object to onClick handler */ + value?: string + /** + * Callback triggered when the main button is clicked. + * Called with signature `({ name: string, value: string, open: bool }, event)` + */ + onClick?: ( + payload: SplitButtonCallbackPayload, + event: React.MouseEvent | React.SyntheticEvent + ) => void + /** + * Callback triggered when the dropdown is toggled (by clicking the chevron, pressing Escape, or clicking the backdrop). + * Called with signature `({ name: string, value: string, open: bool }, event)`. + * Required if `open` prop is used (controlled component). + */ + onToggle?: ( + payload: SplitButtonCallbackPayload, + event: + | React.MouseEvent + | React.SyntheticEvent + | KeyboardEvent + ) => void +} + +interface SplitButtonState { + open: boolean +} + +class SplitButton extends Component { + state: SplitButtonState = { + open: false, + } + + static defaultProps = { + dataTest: 'dhis2-uicore-splitbutton', + } + + anchorRef = React.createRef() + + isControlled = () => typeof this.props.open === 'boolean' + + componentDidMount() { + document.addEventListener('keydown', this.handleKeyDown) + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleKeyDown) + } + + handleKeyDown = (event: KeyboardEvent) => { + const open = this.isControlled() ? this.props.open : this.state.open + if (event.key === 'Escape' && open) { + event.preventDefault() + event.stopPropagation() + if (this.isControlled()) { + if (this.props.onToggle) { + this.props.onToggle( + { + name: this.props.name, + value: this.props.value, + open: false, + }, + event + ) + } + } else { + this.setState({ open: false }) + } + this.anchorRef.current && this.anchorRef.current.focus() + } + } + + handlePrimaryAction = ( + payload: { name?: string; value?: string }, + event: React.MouseEvent | React.SyntheticEvent + ) => { + if (this.props.onClick) { + this.props.onClick( + { + name: payload.name, + value: payload.value, + open: this.isControlled() + ? this.props.open! + : this.state.open, + }, + event + ) + } + } + + handleToggle = ( + payload: { name?: string; value?: string }, + event: React.MouseEvent | React.SyntheticEvent + ) => { + if (this.isControlled()) { + if (this.props.onToggle) { + this.props.onToggle( + { + name: payload.name, + value: payload.value, + open: !this.props.open, + }, + event + ) + } + } else { + this.setState((prevState) => ({ open: !prevState.open })) + } + } + + handleBackdropClick = ( + _payload: Record, + event: React.MouseEvent + ) => { + if (this.isControlled()) { + if (this.props.onToggle) { + this.props.onToggle( + { + name: this.props.name, + value: this.props.value, + open: false, + }, + event + ) + } + } else { + this.setState({ open: false }) + } + } + + render() { + const open = this.isControlled() ? this.props.open : this.state.open + const { + component, + children, + className, + name, + value, + icon, + small, + large, + primary, + secondary, + destructive, + disabled, + type, + tabIndex, + dataTest = 'dhis2-uicore-splitbutton', + initialFocus, + } = this.props + + const arrow = open ? : + + return ( +
+ + + + + {open && ( + + + {component} + + + )} + + {rightButton.styles} + +
+ ) + } +} + +export { SplitButton } diff --git a/components/button/tsconfig.json b/components/button/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/button/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/calendar/d2.config.js b/components/calendar/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/calendar/d2.config.js +++ b/components/calendar/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/calendar/package.json b/components/calendar/package.json index d46c7fe164..63001b2afc 100644 --- a/components/calendar/package.json +++ b/components/calendar/package.json @@ -23,8 +23,9 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", - "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js", + "typecheck": "tsc --noEmit" }, "peerDependencies": { "@dhis2/d2-i18n": "^1", diff --git a/components/calendar/src/__e2e__/calendar-input.e2e.stories.js b/components/calendar/src/__e2e__/calendar-input.e2e.stories.js index ce0a882598..f750b6be42 100644 --- a/components/calendar/src/__e2e__/calendar-input.e2e.stories.js +++ b/components/calendar/src/__e2e__/calendar-input.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { CalendarInput as component } from '../index.js' +import { CalendarInput as component } from '../index.ts' import { CalendarWithClearButton } from '../stories/calendar-input.prod.stories.js' export default { title: 'CalendarInputTesting', component } diff --git a/components/calendar/src/calendar-input/__tests__/calendar-input.test.js b/components/calendar/src/calendar-input/__tests__/calendar-input.test.js index d4d06469bf..a2fc0e4c14 100644 --- a/components/calendar/src/calendar-input/__tests__/calendar-input.test.js +++ b/components/calendar/src/calendar-input/__tests__/calendar-input.test.js @@ -2,7 +2,7 @@ import { Button } from '@dhis2-ui/button' import { fireEvent, render, waitFor, within } from '@testing-library/react' import React, { useState } from 'react' import { Field, Form } from 'react-final-form' -import { CalendarInput } from '../calendar-input.js' +import { CalendarInput } from '../calendar-input.tsx' describe('Calendar Input', () => { it('allow selection of a date through the calendar widget', async () => { diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js deleted file mode 100644 index fd722cf43b..0000000000 --- a/components/calendar/src/calendar-input/calendar-input.js +++ /dev/null @@ -1,262 +0,0 @@ -import { - useDatePicker, - useResolvedDirection, - validateDateString, -} from '@dhis2/multi-calendar-dates' -import { Button } from '@dhis2-ui/button' -import { InputField } from '@dhis2-ui/input' -import { Layer } from '@dhis2-ui/layer' -import { Popper } from '@dhis2-ui/popper' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { useRef, useState, useMemo, useEffect } from 'react' -import { CalendarContainer } from '../calendar/calendar-container.js' -import i18n from '../locales/index.js' - -const offsetModifier = { - name: 'offset', - options: { - offset: [0, 2], - }, -} - -export const CalendarInput = ({ - onDateSelect: parentOnDateSelect, - calendar, - date, - dir, - locale, - numberingSystem, - weekDayFormat = 'narrow', - width = '300px', - cellSize = '32px', - clearable, - minDate, - maxDate, - format, - strictValidation, - inputWidth, - dataTest = 'dhis2-uiwidgets-calendar-inputfield', - pastOnly, - ...rest -} = {}) => { - const ref = useRef() - const calendarRef = useRef() - const popperRef = useRef() - const [open, setOpen] = useState(false) - const [partialDate, setPartialDate] = useState(date) - - useEffect(() => setPartialDate(date), [date]) - - const useDatePickerOptions = useMemo( - () => ({ - calendar, - locale, - numberingSystem, - weekDayFormat, - pastOnly, - }), - [calendar, locale, numberingSystem, weekDayFormat, pastOnly] - ) - - const onChooseDate = (date, validationOptions) => { - if (!date) { - parentOnDateSelect?.({ - calendarDateString: null, - validation: { valid: true }, - }) - return - } - - const validation = validateDateString(date, validationOptions) - parentOnDateSelect?.({ - calendarDateString: date, - validation, - }) - } - - const validationOptions = useMemo( - () => ({ - calendar, - format, - minDateString: minDate, - maxDateString: maxDate, - strictValidation, - }), - [calendar, format, maxDate, minDate, strictValidation] - ) - - const pickerResults = useDatePicker({ - onDateSelect: (result) => { - onChooseDate(result.calendarDateString, validationOptions) - setOpen(false) - }, - date, - ...validationOptions, - options: useDatePickerOptions, - }) - - const handleChange = (e) => { - setOpen(false) - setPartialDate(e.value) - } - - const handleBlur = (_, e) => { - if ( - e.relatedTarget && - (calendarRef.current?.contains(e.relatedTarget) || - popperRef.current === e.relatedTarget) - ) { - return - } - - onChooseDate(partialDate, validationOptions) - setOpen(false) - } - - const onFocus = () => { - setOpen(true) - rest?.onFocus?.() - } - - const languageDirection = useResolvedDirection(dir, locale) - - const calendarProps = useMemo( - () => ({ - date, - width, - cellSize, - isValid: pickerResults.isValid, - calendarWeekDays: pickerResults.calendarWeekDays, - weekDayLabels: pickerResults.weekDayLabels, - currMonth: pickerResults.currMonth, - currYear: pickerResults.currYear, - nextMonth: pickerResults.nextMonth, - nextYear: pickerResults.nextYear, - prevMonth: pickerResults.prevMonth, - prevYear: pickerResults.prevYear, - navigateToYear: pickerResults.navigateToYear, - navigateToMonth: pickerResults.navigateToMonth, - months: pickerResults.months, - years: pickerResults.years, - languageDirection, - }), - [cellSize, date, pickerResults, width, languageDirection] - ) - - return ( - <> -
- - {clearable && ( -
- -
- )} -
- {open && ( - setOpen(false)}> - { - popperRef.current = component?.elements?.popper - }} - > - - - - )} - - - - ) -} - -CalendarInput.propTypes = { - /** the calendar to use such gregory, ethiopic, nepali - full supported list here: https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/calendars.ts */ - calendar: PropTypes.any.isRequired, - /** Called with signature `(null)` \|\| `({ dateCalendarString: string, validation: { error: boolean, warning: boolean, validationText: string} })` with `dateCalendarString` being the stringified date in the specified calendar in the format `yyyy-MM-dd` */ - onDateSelect: PropTypes.func.isRequired, - /** the size of a single cell in the table forming the calendar */ - cellSize: PropTypes.string, - /** Whether the clear button is displayed */ - clearable: PropTypes.bool, - /** 'data-test' attribute of `InputField` component */ - dataTest: PropTypes.string, - /** the currently selected date using an iso-like format YYYY-MM-DD, in the calendar system provided (not iso8601) */ - date: PropTypes.string, - /** the direction of the library - internally the library will use rtl for rtl-languages but this can be overridden here for more control */ - dir: PropTypes.oneOf(['ltr', 'rtl']), - /** The date format to use either `YYYY-MM-DD` or `DD-MM-YYYY` */ - format: PropTypes.oneOf(['YYYY-MM-DD', 'DD-MM-YYYY']), - /** the width of input field */ - inputWidth: PropTypes.string, - /** any valid locale - if none provided, the internal library will fallback to the user locale (more info here: https://github.com/dhis2/multi-calendar-dates/blob/main/src/hooks/internal/useResolvedLocaleOptions.ts#L15) */ - locale: PropTypes.string, - /** The maximum selectable date */ - maxDate: PropTypes.string, - /** The minimum selectable date */ - minDate: PropTypes.string, - /** numbering system to use - full list here https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/numberingSystems.ts */ - numberingSystem: PropTypes.string, - /** When true, only shows years in the past (current year and earlier) */ - pastOnly: PropTypes.bool, - /** Whether to use strict validation by showing errors for out-of-range dates when enabled (default), and warnings when disabled */ - strictValidation: PropTypes.bool, - /** the format to display for the week day, i.e. Monday (long), Mon (short), M (narrow) */ - weekDayFormat: PropTypes.oneOf(['narrow', 'short', 'long']), - /** the width of the calendar component */ - width: PropTypes.string, -} diff --git a/components/calendar/src/calendar-input/calendar-input.tsx b/components/calendar/src/calendar-input/calendar-input.tsx new file mode 100644 index 0000000000..e8d20830fb --- /dev/null +++ b/components/calendar/src/calendar-input/calendar-input.tsx @@ -0,0 +1,292 @@ +import { + useDatePicker, + useResolvedDirection, + validateDateString, +} from '@dhis2/multi-calendar-dates' +import type { SupportedCalendar } from '@dhis2/multi-calendar-dates/build/types/types' +import { Button } from '@dhis2-ui/button' +import { InputField } from '@dhis2-ui/input' +import { Layer } from '@dhis2-ui/layer' +import { Popper } from '@dhis2-ui/popper' +import cx from 'classnames' +import React, { useRef, useState, useMemo, useEffect } from 'react' +import { CalendarContainer } from '../calendar/calendar-container.tsx' +import i18n from '../locales/index.js' + +const offsetModifier = { + name: 'offset' as const, + options: { + offset: [0, 2] as [number, number], + }, +} + +interface DateValidation { + valid?: boolean + error?: boolean + warning?: boolean + validationText?: string + validationCode?: string +} + +interface DateSelectPayload { + calendarDateString: string | null + validation: DateValidation +} + +export interface CalendarInputProps { + /** the calendar to use such gregory, ethiopic, nepali - full supported list here: https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/calendars.ts */ + calendar: string + /** Called with signature `(null)` \|\| `({ dateCalendarString: string, validation: { error: boolean, warning: boolean, validationText: string} })` with `dateCalendarString` being the stringified date in the specified calendar in the format `yyyy-MM-dd` */ + onDateSelect: (payload: DateSelectPayload) => void + /** the size of a single cell in the table forming the calendar */ + cellSize?: string + /** Whether the clear button is displayed */ + clearable?: boolean + /** 'data-test' attribute of `InputField` component */ + dataTest?: string + /** the currently selected date using an iso-like format YYYY-MM-DD, in the calendar system provided (not iso8601) */ + date?: string + /** the direction of the library - internally the library will use rtl for rtl-languages but this can be overridden here for more control */ + dir?: 'ltr' | 'rtl' + /** The date format to use either `YYYY-MM-DD` or `DD-MM-YYYY` */ + format?: 'YYYY-MM-DD' | 'DD-MM-YYYY' + /** the width of input field */ + inputWidth?: string + /** any valid locale - if none provided, the internal library will fallback to the user locale */ + locale?: string + /** The maximum selectable date */ + maxDate?: string + /** The minimum selectable date */ + minDate?: string + /** numbering system to use - full list here https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/numberingSystems.ts */ + numberingSystem?: string + /** When true, only shows years in the past (current year and earlier) */ + pastOnly?: boolean + /** Whether to use strict validation by showing errors for out-of-range dates when enabled (default), and warnings when disabled */ + strictValidation?: boolean + /** the format to display for the week day, i.e. Monday (long), Mon (short), M (narrow) */ + weekDayFormat?: 'narrow' | 'short' | 'long' + /** the width of the calendar component */ + width?: string + [key: string]: unknown +} + +export const CalendarInput = ({ + onDateSelect: parentOnDateSelect, + calendar, + date, + dir, + locale, + numberingSystem, + weekDayFormat = 'narrow', + width = '300px', + cellSize = '32px', + clearable, + minDate, + maxDate, + format, + strictValidation, + inputWidth, + dataTest = 'dhis2-uiwidgets-calendar-inputfield', + pastOnly, + ...rest +}: CalendarInputProps) => { + const ref = useRef(null) + const calendarRef = useRef(null) + const popperRef = useRef(null) + const [open, setOpen] = useState(false) + const [partialDate, setPartialDate] = useState(date) + + useEffect(() => setPartialDate(date), [date]) + + const useDatePickerOptions = useMemo( + () => ({ + calendar: calendar as SupportedCalendar, + locale, + numberingSystem, + weekDayFormat, + pastOnly, + }), + [calendar, locale, numberingSystem, weekDayFormat, pastOnly] + ) + + const onChooseDate = ( + date: string | null | undefined, + validationOptions?: { + calendar?: SupportedCalendar + format?: 'YYYY-MM-DD' | 'DD-MM-YYYY' + minDateString?: string + maxDateString?: string + strictValidation?: boolean + } + ) => { + if (!date) { + parentOnDateSelect?.({ + calendarDateString: null, + validation: { valid: true }, + }) + return + } + + const validation = validateDateString(date, validationOptions) + parentOnDateSelect?.({ + calendarDateString: date, + validation, + }) + } + + const validationOptions = useMemo( + () => ({ + calendar: calendar as SupportedCalendar, + format, + minDateString: minDate, + maxDateString: maxDate, + strictValidation, + }), + [calendar, format, maxDate, minDate, strictValidation] + ) + + const pickerResults = useDatePicker({ + onDateSelect: (result) => { + const { calendarDateString } = result as { + calendarDateString: string + } + onChooseDate(calendarDateString, validationOptions) + setOpen(false) + }, + date: date as string, + ...validationOptions, + options: useDatePickerOptions, + }) as ReturnType & { isValid: boolean } + + const handleChange = (e: { value: string | undefined }) => { + setOpen(false) + setPartialDate(e.value) + } + + const handleBlur = (_: unknown, e: React.FocusEvent) => { + if ( + e.relatedTarget && + (calendarRef.current?.contains(e.relatedTarget as Node) || + popperRef.current === e.relatedTarget) + ) { + return + } + + onChooseDate(partialDate, validationOptions) + setOpen(false) + } + + const onFocus = () => { + setOpen(true) + const restOnFocus = rest.onFocus as (() => void) | undefined + restOnFocus?.() + } + + const languageDirection = useResolvedDirection(dir, locale) + + const calendarProps = useMemo( + () => ({ + date, + width, + cellSize, + isValid: pickerResults.isValid, + calendarWeekDays: pickerResults.calendarWeekDays, + weekDayLabels: pickerResults.weekDayLabels, + currMonth: pickerResults.currMonth, + currYear: pickerResults.currYear, + nextMonth: pickerResults.nextMonth, + nextYear: pickerResults.nextYear, + prevMonth: pickerResults.prevMonth, + prevYear: pickerResults.prevYear, + navigateToYear: pickerResults.navigateToYear, + navigateToMonth: pickerResults.navigateToMonth, + months: pickerResults.months, + years: pickerResults.years, + languageDirection, + }), + [cellSize, date, pickerResults, width, languageDirection] + ) + + return ( + <> +
+ + {clearable && ( +
+ +
+ )} +
+ {open && ( + setOpen(false)}> + { + popperRef.current = + component?.elements?.popper ?? null + }} + > + + + + )} + + + + ) +} diff --git a/components/calendar/src/calendar/calendar-container.js b/components/calendar/src/calendar/calendar-container.js deleted file mode 100644 index 2197e065fb..0000000000 --- a/components/calendar/src/calendar/calendar-container.js +++ /dev/null @@ -1,99 +0,0 @@ -import { colors, elevations } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React, { useMemo } from 'react' -import { CalendarTable, CalendarTableProps } from './calendar-table.js' -import { - NavigationContainer, - NavigationContainerProps, -} from './navigation-container.js' - -const backgroundColor = colors.white - -export const CalendarContainer = React.memo(function CalendarContainer({ - date, - width = '240px', - cellSize = '32px', - calendarWeekDays, - weekDayLabels, - currMonth, - currYear, - nextMonth, - nextYear, - prevMonth, - prevYear, - navigateToYear, - navigateToMonth, - months, - years, - languageDirection, - calendarRef, -}) { - const navigationProps = useMemo(() => { - return { - currMonth, - currYear, - nextMonth, - nextYear, - prevMonth, - prevYear, - languageDirection, - navigateToYear, - navigateToMonth, - months, - years, - } - }, [ - currMonth, - currYear, - languageDirection, - nextMonth, - nextYear, - prevMonth, - prevYear, - navigateToYear, - navigateToMonth, - months, - years, - ]) - return ( -
-
-
- - -
-
- -
- ) -}) - -CalendarContainer.propTypes = { - /** the currently selected date using an iso-like format YYYY-MM-DD, in the calendar system provided (not iso8601) */ - date: PropTypes.string, - ...CalendarTableProps, - ...NavigationContainerProps, -} diff --git a/components/calendar/src/calendar/calendar-container.tsx b/components/calendar/src/calendar/calendar-container.tsx new file mode 100644 index 0000000000..fdd7a580fa --- /dev/null +++ b/components/calendar/src/calendar/calendar-container.tsx @@ -0,0 +1,98 @@ +import { colors, elevations } from '@dhis2/ui-constants' +import React, { useMemo } from 'react' +import { CalendarTable } from './calendar-table.tsx' +import type { CalendarTableProps } from './calendar-table.tsx' +import { NavigationContainer } from './navigation-container.tsx' +import type { NavigationContainerProps } from './navigation-container.tsx' + +const backgroundColor = colors.white + +export interface CalendarContainerProps + extends CalendarTableProps, + NavigationContainerProps { + date?: string + width?: string + calendarRef?: React.Ref +} + +export const CalendarContainer = React.memo(function CalendarContainer({ + date, + width = '240px', + cellSize = '32px', + calendarWeekDays, + weekDayLabels, + currMonth, + currYear, + nextMonth, + nextYear, + prevMonth, + prevYear, + navigateToYear, + navigateToMonth, + months, + years, + languageDirection, + calendarRef, +}: CalendarContainerProps) { + const navigationProps = useMemo(() => { + return { + currMonth, + currYear, + nextMonth, + nextYear, + prevMonth, + prevYear, + languageDirection, + navigateToYear, + navigateToMonth, + months, + years, + } + }, [ + currMonth, + currYear, + languageDirection, + nextMonth, + nextYear, + prevMonth, + prevYear, + navigateToYear, + navigateToMonth, + months, + years, + ]) + return ( +
+
+
+ + +
+
+ +
+ ) +}) diff --git a/components/calendar/src/calendar/calendar-table-cell.js b/components/calendar/src/calendar/calendar-table-cell.js deleted file mode 100644 index 82c3d28555..0000000000 --- a/components/calendar/src/calendar/calendar-table-cell.js +++ /dev/null @@ -1,97 +0,0 @@ -import { colors } from '@dhis2/ui-constants' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React from 'react' - -export const CalendarTableCell = ({ day, cellSize, selectedDate }) => { - const dayHoverBackgroundColor = colors.grey200 - const selectedDayBackgroundColor = colors.teal700 - - return ( - - - - - ) -} - -CalendarTableCell.propTypes = { - cellSize: PropTypes.string, - day: PropTypes.shape({ - dateValue: PropTypes.string, - isInCurrentMonth: PropTypes.bool, - isSelected: PropTypes.bool, - isToday: PropTypes.bool, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - onClick: PropTypes.func, - }), - selectedDate: PropTypes.string, -} diff --git a/components/calendar/src/calendar/calendar-table-cell.tsx b/components/calendar/src/calendar/calendar-table-cell.tsx new file mode 100644 index 0000000000..25facc3598 --- /dev/null +++ b/components/calendar/src/calendar/calendar-table-cell.tsx @@ -0,0 +1,102 @@ +import { colors } from '@dhis2/ui-constants' +import cx from 'classnames' +import React from 'react' + +export interface CalendarDay { + dateValue?: string + label?: string | number + onClick?: () => void + isSelected?: boolean + isToday?: boolean + isInCurrentMonth?: boolean +} + +export interface CalendarTableCellProps { + day: CalendarDay + cellSize?: string + selectedDate?: string +} + +export const CalendarTableCell = ({ + day, + cellSize, + selectedDate, +}: CalendarTableCellProps) => { + const dayHoverBackgroundColor = colors.grey200 + const selectedDayBackgroundColor = colors.teal700 + + return ( + + + + + ) +} diff --git a/components/calendar/src/calendar/calendar-table-days-header.js b/components/calendar/src/calendar/calendar-table-days-header.js deleted file mode 100644 index fbec4e4f58..0000000000 --- a/components/calendar/src/calendar/calendar-table-days-header.js +++ /dev/null @@ -1,40 +0,0 @@ -import { colors } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React from 'react' - -export const CalendarTableDaysHeader = ({ weekDayLabels }) => { - const dayNamesColor = colors.grey700 - - return ( - <> - - - {weekDayLabels.map((label, i) => ( - - {label} - - ))} - - - - - ) -} - -CalendarTableDaysHeader.propTypes = { - weekDayLabels: PropTypes.arrayOf(PropTypes.string), -} diff --git a/components/calendar/src/calendar/calendar-table-days-header.tsx b/components/calendar/src/calendar/calendar-table-days-header.tsx new file mode 100644 index 0000000000..58bf6ff1e2 --- /dev/null +++ b/components/calendar/src/calendar/calendar-table-days-header.tsx @@ -0,0 +1,41 @@ +import { colors } from '@dhis2/ui-constants' +import React from 'react' + +export interface CalendarTableDaysHeaderProps { + weekDayLabels?: string[] +} + +export const CalendarTableDaysHeader = ({ + weekDayLabels, +}: CalendarTableDaysHeaderProps) => { + const dayNamesColor = colors.grey700 + + return ( + <> + + + {weekDayLabels?.map((label, i) => ( + + {label} + + ))} + + + + + ) +} diff --git a/components/calendar/src/calendar/calendar-table.js b/components/calendar/src/calendar/calendar-table.js deleted file mode 100644 index c7c51e7ec1..0000000000 --- a/components/calendar/src/calendar/calendar-table.js +++ /dev/null @@ -1,74 +0,0 @@ -import { spacers } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React from 'react' -import { CalendarTableCell } from './calendar-table-cell.js' -import { CalendarTableDaysHeader } from './calendar-table-days-header.js' - -export const CalendarTable = ({ - weekDayLabels, - calendarWeekDays, - width, - cellSize, - selectedDate, -}) => ( -
- - - - {calendarWeekDays.map((week, weekIndex) => ( - - {week.map((day) => ( - - ))} - - ))} - -
- -
-) - -export const CalendarTableProps = { - calendarWeekDays: PropTypes.arrayOf( - PropTypes.arrayOf( - PropTypes.shape({ - calendarDate: PropTypes.string, - isInCurrentMonth: PropTypes.bool, - isSelected: PropTypes.bool, - isToday: PropTypes.bool, - label: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - zdt: PropTypes.object, - onClick: PropTypes.func, - }).isRequired - ).isRequired - ).isRequired, - cellSize: PropTypes.string, - selectedDate: PropTypes.string, - weekDayLabels: PropTypes.arrayOf(PropTypes.string), - width: PropTypes.string, -} - -CalendarTable.propTypes = CalendarTableProps diff --git a/components/calendar/src/calendar/calendar-table.tsx b/components/calendar/src/calendar/calendar-table.tsx new file mode 100644 index 0000000000..98b64f2af0 --- /dev/null +++ b/components/calendar/src/calendar/calendar-table.tsx @@ -0,0 +1,54 @@ +import { spacers } from '@dhis2/ui-constants' +import React from 'react' +import type { CalendarDay } from './calendar-table-cell.tsx' +import { CalendarTableCell } from './calendar-table-cell.tsx' +import { CalendarTableDaysHeader } from './calendar-table-days-header.tsx' + +export interface CalendarTableProps { + calendarWeekDays: CalendarDay[][] + cellSize?: string + selectedDate?: string + weekDayLabels?: string[] +} + +export const CalendarTable = ({ + weekDayLabels, + calendarWeekDays, + cellSize, + selectedDate, +}: CalendarTableProps) => ( +
+ + + + {calendarWeekDays.map((week, weekIndex) => ( + + {week.map((day) => ( + + ))} + + ))} + +
+ +
+) diff --git a/components/calendar/src/calendar/calendar.js b/components/calendar/src/calendar/calendar.js deleted file mode 100644 index 68d8196c73..0000000000 --- a/components/calendar/src/calendar/calendar.js +++ /dev/null @@ -1,104 +0,0 @@ -import { - useDatePicker, - useResolvedDirection, -} from '@dhis2/multi-calendar-dates' -import PropTypes from 'prop-types' -import React, { useMemo, useState } from 'react' -import { CalendarContainer } from './calendar-container.js' - -export const Calendar = ({ - onDateSelect, - calendar, - date, - dir, - locale, - numberingSystem, - weekDayFormat = 'narrow', - timeZone, - width = '240px', - cellSize = '32px', - pastOnly, -}) => { - const [selectedDateString, setSelectedDateString] = useState(date) - const languageDirection = useResolvedDirection(dir, locale) - - const options = { - locale, - calendar, - timeZone, - numberingSystem, - weekDayFormat, - pastOnly, - } - - const pickerResults = useDatePicker({ - onDateSelect: (result) => { - const { calendarDateString } = result - setSelectedDateString(calendarDateString) - onDateSelect(result) - }, - date: selectedDateString, - options, - }) - - const calendarProps = useMemo(() => { - return { - date, - dir, - locale, - width, - cellSize, - // minDate, - // maxDate, - // validation, // todo: clarify how we use validation props (and format) in Calendar (not CalendarInput) - // format, - isValid: pickerResults.isValid, - calendarWeekDays: pickerResults.calendarWeekDays, - weekDayLabels: pickerResults.weekDayLabels, - currMonth: pickerResults.currMonth, - currYear: pickerResults.currYear, - nextMonth: pickerResults.nextMonth, - nextYear: pickerResults.nextYear, - prevMonth: pickerResults.prevMonth, - prevYear: pickerResults.prevYear, - navigateToYear: pickerResults.navigateToYear, - navigateToMonth: pickerResults.navigateToMonth, - months: pickerResults.months, - years: pickerResults.years, - languageDirection, - } - }, [cellSize, date, dir, locale, pickerResults, width, languageDirection]) - - return ( -
- -
- ) -} - -export const CalendarProps = { - /** the calendar to use such gregory, ethiopic, nepali - full supported list here: https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/calendars.ts */ - calendar: PropTypes.any.isRequired, - /** Called with signature `(null)` \|\| `({ dateCalendarString: string, validation: { error: boolean, warning: boolean, validationText: string} })` with `dateCalendarString` being the stringified date in the specified calendar in the format `yyyy-MM-dd` */ - onDateSelect: PropTypes.func.isRequired, - /** the size of a single cell in the table forming the calendar */ - cellSize: PropTypes.string, - /** the currently selected date using an iso-like format YYYY-MM-DD, in the calendar system provided (not iso8601) */ - date: PropTypes.string, - /** the direction of the library - internally the library will use rtl for rtl-languages but this can be overridden here for more control */ - dir: PropTypes.oneOf(['ltr', 'rtl']), - /** any valid locale - if none provided, the internal library will fallback to the user locale (more info here: https://github.com/dhis2/multi-calendar-dates/blob/main/src/hooks/internal/useResolvedLocaleOptions.ts#L15) */ - locale: PropTypes.string, - /** numbering system to use - full list here https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/numberingSystems.ts */ - numberingSystem: PropTypes.string, - /** When true, only shows years in the past (current year and earlier) */ - pastOnly: PropTypes.bool, - /** the timeZone to use */ - timeZone: PropTypes.string, - /** the format to display for the week day, i.e. Monday (long), Mon (short), M (narrow) */ - weekDayFormat: PropTypes.oneOf(['narrow', 'short', 'long']), - /** the width of the calendar component */ - width: PropTypes.string, -} - -Calendar.propTypes = CalendarProps diff --git a/components/calendar/src/calendar/calendar.tsx b/components/calendar/src/calendar/calendar.tsx new file mode 100644 index 0000000000..a89f24d43d --- /dev/null +++ b/components/calendar/src/calendar/calendar.tsx @@ -0,0 +1,100 @@ +import { + useDatePicker, + useResolvedDirection, +} from '@dhis2/multi-calendar-dates' +import type { SupportedCalendar } from '@dhis2/multi-calendar-dates/build/types/types' +import React, { useMemo, useState } from 'react' +import { CalendarContainer } from './calendar-container.tsx' + +export interface CalendarProps { + /** the calendar to use such gregory, ethiopic, nepali - full supported list here: https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/calendars.ts */ + calendar: string + /** Called with signature `(null)` \|\| `({ dateCalendarString: string, validation: { error: boolean, warning: boolean, validationText: string} })` with `dateCalendarString` being the stringified date in the specified calendar in the format `yyyy-MM-dd` */ + onDateSelect: (result: { calendarDateString: string }) => void + /** the size of a single cell in the table forming the calendar */ + cellSize?: string + /** the currently selected date using an iso-like format YYYY-MM-DD, in the calendar system provided (not iso8601) */ + date?: string + /** the direction of the library - internally the library will use rtl for rtl-languages but this can be overridden here for more control */ + dir?: 'ltr' | 'rtl' + /** any valid locale - if none provided, the internal library will fallback to the user locale */ + locale?: string + /** numbering system to use - full list here https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/numberingSystems.ts */ + numberingSystem?: string + /** When true, only shows years in the past (current year and earlier) */ + pastOnly?: boolean + /** the timeZone to use */ + timeZone?: string + /** the format to display for the week day, i.e. Monday (long), Mon (short), M (narrow) */ + weekDayFormat?: 'narrow' | 'short' | 'long' + /** the width of the calendar component */ + width?: string +} + +export const Calendar = ({ + onDateSelect, + calendar, + date, + dir, + locale, + numberingSystem, + weekDayFormat = 'narrow', + timeZone, + width = '240px', + cellSize = '32px', + pastOnly, +}: CalendarProps) => { + const [selectedDateString, setSelectedDateString] = useState(date) + const languageDirection = useResolvedDirection(dir, locale) + + const options = { + locale, + calendar: calendar as SupportedCalendar, + timeZone, + numberingSystem, + weekDayFormat, + pastOnly, + } + + const pickerResults = useDatePicker({ + onDateSelect: (result) => { + const { calendarDateString } = result as { + calendarDateString: string + } + setSelectedDateString(calendarDateString) + onDateSelect(result as { calendarDateString: string }) + }, + date: selectedDateString as string, + options, + }) as ReturnType & { isValid: boolean } + + const calendarProps = useMemo(() => { + return { + date, + dir, + locale, + width, + cellSize, + isValid: pickerResults.isValid, + calendarWeekDays: pickerResults.calendarWeekDays, + weekDayLabels: pickerResults.weekDayLabels, + currMonth: pickerResults.currMonth, + currYear: pickerResults.currYear, + nextMonth: pickerResults.nextMonth, + nextYear: pickerResults.nextYear, + prevMonth: pickerResults.prevMonth, + prevYear: pickerResults.prevYear, + navigateToYear: pickerResults.navigateToYear, + navigateToMonth: pickerResults.navigateToMonth, + months: pickerResults.months, + years: pickerResults.years, + languageDirection, + } + }, [cellSize, date, dir, locale, pickerResults, width, languageDirection]) + + return ( +
+ +
+ ) +} diff --git a/components/calendar/src/calendar/navigation-container.js b/components/calendar/src/calendar/navigation-container.js deleted file mode 100644 index 2c9251e5e9..0000000000 --- a/components/calendar/src/calendar/navigation-container.js +++ /dev/null @@ -1,295 +0,0 @@ -import { colors, spacers } from '@dhis2/ui-constants' -import { IconChevronLeft16, IconChevronRight16 } from '@dhis2/ui-icons' -import PropTypes from 'prop-types' -import React from 'react' -import i18n from '../locales/index.js' - -const wrapperBorderColor = colors.grey300 -const headerBackground = colors.grey100 - -export const NavigationContainer = ({ - languageDirection, - currMonth, - currYear, - nextMonth, - nextYear, - prevMonth, - prevYear, - navigateToYear, - navigateToMonth, - months, - years, -}) => { - const PreviousIcon = - languageDirection === 'ltr' ? IconChevronLeft16 : IconChevronRight16 - const NextIcon = - languageDirection === 'ltr' ? IconChevronRight16 : IconChevronLeft16 - - const handleYearChange = (e) => { - const targetYear = parseInt(e.target.value) - navigateToYear(targetYear) - } - - const handleMonthChange = (e) => { - const selectedMonth = months.find( - (month) => month.label === e.target.value - ) - - if (selectedMonth) { - navigateToMonth(selectedMonth.value) - } - } - - return ( - <> -
-
-
- -
-
- - - - -
-
- -
-
-
-
- -
-
- - - - -
-
- -
-
-
- - - ) -} - -export const NavigationContainerProps = { - currMonth: PropTypes.shape({ - label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }), - currYear: PropTypes.shape({ - label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - value: PropTypes.number, - }), - languageDirection: PropTypes.oneOf(['ltr', 'rtl']), - months: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string.isRequired, - value: PropTypes.number.isRequired, - }) - ), - navigateToMonth: PropTypes.func, - navigateToYear: PropTypes.func, - nextMonth: PropTypes.shape({ - label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - navigateTo: PropTypes.func, - }), - nextYear: PropTypes.shape({ - label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - navigateTo: PropTypes.func, - }), - prevMonth: PropTypes.shape({ - label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - navigateTo: PropTypes.func, - }), - prevYear: PropTypes.shape({ - label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - navigateTo: PropTypes.func, - }), - years: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string.isRequired, - value: PropTypes.number.isRequired, - }) - ), -} - -NavigationContainer.propTypes = NavigationContainerProps diff --git a/components/calendar/src/calendar/navigation-container.tsx b/components/calendar/src/calendar/navigation-container.tsx new file mode 100644 index 0000000000..f0f19d2eec --- /dev/null +++ b/components/calendar/src/calendar/navigation-container.tsx @@ -0,0 +1,292 @@ +import { colors, spacers } from '@dhis2/ui-constants' +import { IconChevronLeft16, IconChevronRight16 } from '@dhis2/ui-icons' +import React from 'react' +import i18n from '../locales/index.js' + +const wrapperBorderColor = colors.grey300 +const headerBackground = colors.grey100 + +export interface NavigationContainerProps { + languageDirection?: 'ltr' | 'rtl' + currMonth?: { + label?: string | number + } + currYear?: { + label?: string | number + value?: string | number + } + nextMonth?: { + label?: string | number + navigateTo?: () => void + } + nextYear?: { + label?: string | number + navigateTo?: () => void + } + prevMonth?: { + label?: string | number + navigateTo?: () => void + } + prevYear?: { + label?: string | number + navigateTo?: () => void + } + navigateToYear?: (year: number) => void + navigateToMonth?: (month: number) => void + months?: Array<{ + label: string + value: number + }> + years?: Array<{ + label: string + value: number + }> +} + +export const NavigationContainer = ({ + languageDirection, + currMonth, + currYear, + nextMonth, + nextYear, + prevMonth, + prevYear, + navigateToYear, + navigateToMonth, + months, + years, +}: NavigationContainerProps) => { + const PreviousIcon = + languageDirection === 'ltr' ? IconChevronLeft16 : IconChevronRight16 + const NextIcon = + languageDirection === 'ltr' ? IconChevronRight16 : IconChevronLeft16 + + const handleYearChange = (e: React.ChangeEvent) => { + const targetYear = parseInt(e.target.value) + navigateToYear?.(targetYear) + } + + const handleMonthChange = (e: React.ChangeEvent) => { + const selectedMonth = months?.find( + (month) => month.label === e.target.value + ) + + if (selectedMonth) { + navigateToMonth?.(selectedMonth.value) + } + } + + return ( + <> +
+
+
+ +
+
+ + + + +
+
+ +
+
+
+
+ +
+
+ + + + +
+
+ +
+
+
+ + + ) +} diff --git a/components/calendar/src/i18n.d.ts b/components/calendar/src/i18n.d.ts new file mode 100644 index 0000000000..a2af175b40 --- /dev/null +++ b/components/calendar/src/i18n.d.ts @@ -0,0 +1,6 @@ +declare module '*/locales/index.js' { + const i18n: { + t: (value: string) => string + } + export default i18n +} diff --git a/components/calendar/src/index.js b/components/calendar/src/index.js deleted file mode 100644 index 252dc77ae4..0000000000 --- a/components/calendar/src/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { Calendar } from './calendar/calendar.js' -export { CalendarInput } from './calendar-input/calendar-input.js' diff --git a/components/calendar/src/index.ts b/components/calendar/src/index.ts new file mode 100644 index 0000000000..ccdeafde0b --- /dev/null +++ b/components/calendar/src/index.ts @@ -0,0 +1,4 @@ +export { Calendar } from './calendar/calendar.tsx' +export type { CalendarProps } from './calendar/calendar.tsx' +export { CalendarInput } from './calendar-input/calendar-input.tsx' +export type { CalendarInputProps } from './calendar-input/calendar-input.tsx' diff --git a/components/calendar/src/stories/calendar-input.prod.stories.js b/components/calendar/src/stories/calendar-input.prod.stories.js index 5239d0401f..ec70705eb3 100644 --- a/components/calendar/src/stories/calendar-input.prod.stories.js +++ b/components/calendar/src/stories/calendar-input.prod.stories.js @@ -1,7 +1,7 @@ import { Button } from '@dhis2-ui/button' import React, { useState } from 'react' import { Field, Form } from 'react-final-form' -import { CalendarInput } from '../calendar-input/calendar-input.js' +import { CalendarInput } from '../calendar-input/calendar-input.tsx' import { CalendarStoryWrapper } from './calendar-story-wrapper.js' const subtitle = `[Experimental] Calendar Input is a wrapper around Calendar displaying an input that triggers the calendar` diff --git a/components/calendar/src/stories/calendar-story-wrapper.js b/components/calendar/src/stories/calendar-story-wrapper.js index 940ab5323d..10dea75749 100644 --- a/components/calendar/src/stories/calendar-story-wrapper.js +++ b/components/calendar/src/stories/calendar-story-wrapper.js @@ -1,7 +1,7 @@ import { constants } from '@dhis2/multi-calendar-dates' import PropTypes from 'prop-types' import React, { useState } from 'react' -import { Calendar } from '../calendar/calendar.js' +import { Calendar } from '../calendar/calendar.tsx' const { calendars, numberingSystems } = constants export const CalendarStoryWrapper = (props) => { diff --git a/components/calendar/src/stories/calendar.prod.stories.js b/components/calendar/src/stories/calendar.prod.stories.js index 5d0a67b955..00ab9e6be1 100644 --- a/components/calendar/src/stories/calendar.prod.stories.js +++ b/components/calendar/src/stories/calendar.prod.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Calendar } from '../calendar/calendar.js' +import { Calendar } from '../calendar/calendar.tsx' import { CalendarStoryWrapper } from './calendar-story-wrapper.js' const subtitle = `[Experimental] Calendar component is useful for creating a variety of calendars including Ethiopic, Islamic etc..` diff --git a/components/calendar/tsconfig.json b/components/calendar/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/calendar/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/card/d2.config.js b/components/card/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/card/d2.config.js +++ b/components/card/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/card/package.json b/components/card/package.json index dca61cba4f..3331efa15f 100644 --- a/components/card/package.json +++ b/components/card/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/card/src/card.e2e.stories.js b/components/card/src/card.e2e.stories.js index 2da68f0aa9..b0badc3609 100644 --- a/components/card/src/card.e2e.stories.js +++ b/components/card/src/card.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Card } from './card.js' +import { Card } from './card.tsx' export default { title: 'Card' } export const WithChildren = () => ( diff --git a/components/card/src/card.js b/components/card/src/card.js deleted file mode 100644 index 8de324e841..0000000000 --- a/components/card/src/card.js +++ /dev/null @@ -1,32 +0,0 @@ -import { colors, elevations } from '@dhis2/ui-constants' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React from 'react' - -const Card = ({ className, children, dataTest = 'dhis2-uicore-card' }) => ( -
- {children} - - -
-) - -Card.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - dataTest: PropTypes.string, -} - -export { Card } diff --git a/components/card/src/card.prod.stories.js b/components/card/src/card.prod.stories.js index 7421eb4c57..0208538090 100644 --- a/components/card/src/card.prod.stories.js +++ b/components/card/src/card.prod.stories.js @@ -1,6 +1,6 @@ import { Box } from '@dhis2-ui/box' import React from 'react' -import { Card } from './card.js' +import { Card } from './card.tsx' const subtitle = ` A card is a container element for grouping together diff --git a/components/card/src/card.tsx b/components/card/src/card.tsx new file mode 100644 index 0000000000..b0b3c698df --- /dev/null +++ b/components/card/src/card.tsx @@ -0,0 +1,35 @@ +import { colors, elevations } from '@dhis2/ui-constants' +import cx from 'classnames' +import React from 'react' + +export interface CardProps { + children?: React.ReactNode + className?: string + dataTest?: string +} + +const Card = ({ + className, + children, + dataTest = 'dhis2-uicore-card', +}: CardProps) => ( +
+ {children} + + +
+) + +export { Card } diff --git a/components/card/src/index.js b/components/card/src/index.js deleted file mode 100644 index cec521f960..0000000000 --- a/components/card/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Card } from './card.js' diff --git a/components/card/src/index.ts b/components/card/src/index.ts new file mode 100644 index 0000000000..cf559a4979 --- /dev/null +++ b/components/card/src/index.ts @@ -0,0 +1,2 @@ +export { Card } from './card.tsx' +export type { CardProps } from './card.tsx' diff --git a/components/card/tsconfig.json b/components/card/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/card/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/center/d2.config.js b/components/center/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/center/d2.config.js +++ b/components/center/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/center/package.json b/components/center/package.json index 358cc15eab..09c3e37e1c 100644 --- a/components/center/package.json +++ b/components/center/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/center/src/center.js b/components/center/src/center.js deleted file mode 100644 index 12275421a6..0000000000 --- a/components/center/src/center.js +++ /dev/null @@ -1,54 +0,0 @@ -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { forwardRef } from 'react' - -export const Center = forwardRef( - ( - { - className, - dataTest = 'dhis2-uicore-centeredcontent', - children, - position = 'middle', - }, - ref - ) => ( -
-
{children}
- - -
- ) -) - -Center.displayName = 'Center' - -Center.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - dataTest: PropTypes.string, - /** Vertical alignment */ - position: PropTypes.oneOf(['top', 'middle', 'bottom']), -} diff --git a/components/center/src/center.prod.stories.js b/components/center/src/center.prod.stories.js index 583b844f44..44f56b42e9 100644 --- a/components/center/src/center.prod.stories.js +++ b/components/center/src/center.prod.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Center } from './center.js' +import { Center } from './center.tsx' const description = ` Centers children horizontally, and by default, vertically. diff --git a/components/center/src/center.tsx b/components/center/src/center.tsx new file mode 100644 index 0000000000..1310754ff9 --- /dev/null +++ b/components/center/src/center.tsx @@ -0,0 +1,53 @@ +import cx from 'classnames' +import React, { forwardRef } from 'react' + +export interface CenterProps { + children?: React.ReactNode + className?: string + dataTest?: string + /** Vertical alignment */ + position?: 'top' | 'middle' | 'bottom' +} + +export const Center = forwardRef( + ( + { + className, + dataTest = 'dhis2-uicore-centeredcontent', + children, + position = 'middle', + }, + ref + ) => ( +
+
{children}
+ + +
+ ) +) + +Center.displayName = 'Center' diff --git a/components/center/src/index.js b/components/center/src/index.js deleted file mode 100644 index 761aa08e19..0000000000 --- a/components/center/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Center } from './center.js' diff --git a/components/center/src/index.ts b/components/center/src/index.ts new file mode 100644 index 0000000000..a4164f5bab --- /dev/null +++ b/components/center/src/index.ts @@ -0,0 +1,2 @@ +export { Center } from './center.tsx' +export type { CenterProps } from './center.tsx' diff --git a/components/center/tsconfig.json b/components/center/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/center/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/checkbox/d2.config.js b/components/checkbox/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/checkbox/d2.config.js +++ b/components/checkbox/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/checkbox/package.json b/components/checkbox/package.json index 274923845b..84eec5340d 100644 --- a/components/checkbox/package.json +++ b/components/checkbox/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/checkbox/src/checkbox-field/__tests__/checkbox-field.test.js b/components/checkbox/src/checkbox-field/__tests__/checkbox-field.test.js index 9fbfea5387..99c5e035e3 100644 --- a/components/checkbox/src/checkbox-field/__tests__/checkbox-field.test.js +++ b/components/checkbox/src/checkbox-field/__tests__/checkbox-field.test.js @@ -1,6 +1,6 @@ import { render, fireEvent, screen } from '@testing-library/react' import React from 'react' -import { CheckboxField } from '../checkbox-field.js' +import { CheckboxField } from '../checkbox-field.tsx' describe('', () => { it('should call the onKeyDown callback when provided', () => { diff --git a/components/checkbox/src/checkbox-field/checkbox-field.e2e.stories.js b/components/checkbox/src/checkbox-field/checkbox-field.e2e.stories.js index 5d41e9ecfc..dd94554824 100644 --- a/components/checkbox/src/checkbox-field/checkbox-field.e2e.stories.js +++ b/components/checkbox/src/checkbox-field/checkbox-field.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { CheckboxField } from './index.js' +import { CheckboxField } from './index.ts' export default { title: 'CheckboxField' } export const WithLabelAndRequired = () => ( diff --git a/components/checkbox/src/checkbox-field/checkbox-field.js b/components/checkbox/src/checkbox-field/checkbox-field.js deleted file mode 100644 index 591f7c47e6..0000000000 --- a/components/checkbox/src/checkbox-field/checkbox-field.js +++ /dev/null @@ -1,116 +0,0 @@ -import { sharedPropTypes } from '@dhis2/ui-constants' -import { Field } from '@dhis2-ui/field' -import { Required } from '@dhis2-ui/required' -import PropTypes from 'prop-types' -import React from 'react' -import { Checkbox } from '../checkbox/index.js' - -const AddRequired = ({ label, required, dataTest }) => ( - - {label} - {required && } - -) -AddRequired.propTypes = { - dataTest: PropTypes.string, - label: PropTypes.node, - required: PropTypes.bool, -} - -const CheckboxField = ({ - value, - label, - name, - className, - tabIndex, - onChange, - onFocus, - onKeyDown, - onBlur, - checked, - disabled, - valid, - warning, - error, - dense, - initialFocus, - required, - helpText, - validationText, - dataTest = 'dhis2-uiwidgets-checkboxfield', -}) => ( - - - } - name={name} - tabIndex={tabIndex} - onChange={onChange} - onFocus={onFocus} - onKeyDown={onKeyDown} - onBlur={onBlur} - checked={checked} - disabled={disabled} - valid={valid} - warning={warning} - error={error} - dense={dense} - initialFocus={initialFocus} - /> - -) - -CheckboxField.propTypes = { - checked: PropTypes.bool, - className: PropTypes.string, - dataTest: PropTypes.string, - /** Smaller dimensions for information-dense layouts */ - dense: PropTypes.bool, - /** Disables the checkbox */ - disabled: PropTypes.bool, - /** Applies 'error' styling to checkbox and validation text for feedback. Mutually exclusive with `warning` and `valid` props */ - error: sharedPropTypes.statusPropType, - /** Useful instructions for the user */ - helpText: PropTypes.string, - initialFocus: PropTypes.bool, - /** Labels the checkbox */ - label: PropTypes.node, - /** Name associate with the checkbox. Passed in object as argument to event handlers */ - name: PropTypes.string, - /** Adds an asterisk to indicate this field is required */ - required: PropTypes.bool, - tabIndex: PropTypes.string, - /** Applies 'valid' styling to checkbox and validation text for feedback. Mutually exclusive with `warning` and `error` props */ - valid: sharedPropTypes.statusPropType, - /** Adds text below the checkbox to provide validation feedback. Acquires styles from `valid`, `warning` and `error` statuses */ - validationText: PropTypes.string, - /** Value associated with the checkbox. Passed in object as argument to event handlers */ - value: PropTypes.string, - /** Applies 'warning' styling to checkbox and validation text for feedback. Mutually exclusive with `valid` and `error` props */ - warning: sharedPropTypes.statusPropType, - /** Called with signature `({ name: string, value: string, checked: bool }, event)` */ - onBlur: PropTypes.func, - /** Called with signature `({ name: string, value: string, checked: bool }, event)` */ - onChange: PropTypes.func, - /** Called with signature `({ name: string, value: string, checked: bool }, event)` */ - onFocus: PropTypes.func, - /** Called with signature `({ name: string, value: string, checked: bool }, event)` */ - onKeyDown: PropTypes.func, -} - -export { CheckboxField } diff --git a/components/checkbox/src/checkbox-field/checkbox-field.prod.stories.js b/components/checkbox/src/checkbox-field/checkbox-field.prod.stories.js index 1d5ca7f738..f14710bfcc 100644 --- a/components/checkbox/src/checkbox-field/checkbox-field.prod.stories.js +++ b/components/checkbox/src/checkbox-field/checkbox-field.prod.stories.js @@ -1,6 +1,6 @@ import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' -import { CheckboxField } from './index.js' +import { CheckboxField } from './index.ts' const description = ` A \`CheckboxField\` is a Checkbox component wrapped with extra form utilities, including the ability to add a label, help text, and validation text. Validation styles like 'error' apply to all of these subcomponents. diff --git a/components/checkbox/src/checkbox-field/checkbox-field.tsx b/components/checkbox/src/checkbox-field/checkbox-field.tsx new file mode 100644 index 0000000000..08cf2295e7 --- /dev/null +++ b/components/checkbox/src/checkbox-field/checkbox-field.tsx @@ -0,0 +1,112 @@ +import { Field } from '@dhis2-ui/field' +import { Required } from '@dhis2-ui/required' +import React from 'react' +import { Checkbox } from '../checkbox/index.ts' + +interface AddRequiredProps { + label?: React.ReactNode + required?: boolean + dataTest?: string +} + +const AddRequired = ({ label, required, dataTest }: AddRequiredProps) => ( + + {label} + {required && } + +) + +export interface CheckboxFieldProps { + value?: string + label?: React.ReactNode + name?: string + className?: string + tabIndex?: string + onChange?: ( + payload: { name?: string; value?: string; checked: boolean }, + event: React.ChangeEvent + ) => void + onFocus?: ( + payload: { name?: string; value?: string; checked: boolean }, + event: React.FocusEvent + ) => void + onKeyDown?: ( + payload: { name?: string; value?: string; checked: boolean }, + event: React.KeyboardEvent + ) => void + onBlur?: ( + payload: { name?: string; value?: string; checked: boolean }, + event: React.FocusEvent + ) => void + checked?: boolean + disabled?: boolean + valid?: boolean + warning?: boolean + error?: boolean + dense?: boolean + initialFocus?: boolean + required?: boolean + helpText?: string + validationText?: string + dataTest?: string +} + +const CheckboxField = ({ + value, + label, + name, + className, + tabIndex, + onChange, + onFocus, + onKeyDown, + onBlur, + checked, + disabled, + valid, + warning, + error, + dense, + initialFocus, + required, + helpText, + validationText, + dataTest = 'dhis2-uiwidgets-checkboxfield', +}: CheckboxFieldProps) => ( + + + } + name={name} + tabIndex={tabIndex} + onChange={onChange} + onFocus={onFocus} + onKeyDown={onKeyDown} + onBlur={onBlur} + checked={checked} + disabled={disabled} + valid={valid} + warning={warning} + error={error} + dense={dense} + initialFocus={initialFocus} + /> + +) + +export { CheckboxField } diff --git a/components/checkbox/src/checkbox-field/index.js b/components/checkbox/src/checkbox-field/index.js deleted file mode 100644 index c3ac92c4bd..0000000000 --- a/components/checkbox/src/checkbox-field/index.js +++ /dev/null @@ -1 +0,0 @@ -export { CheckboxField } from './checkbox-field.js' diff --git a/components/checkbox/src/checkbox-field/index.ts b/components/checkbox/src/checkbox-field/index.ts new file mode 100644 index 0000000000..a5702818af --- /dev/null +++ b/components/checkbox/src/checkbox-field/index.ts @@ -0,0 +1,2 @@ +export { CheckboxField } from './checkbox-field.tsx' +export type { CheckboxFieldProps } from './checkbox-field.tsx' diff --git a/components/checkbox/src/checkbox/__tests__/checkbox.test.js b/components/checkbox/src/checkbox/__tests__/checkbox.test.js index a762567911..c71858c49a 100644 --- a/components/checkbox/src/checkbox/__tests__/checkbox.test.js +++ b/components/checkbox/src/checkbox/__tests__/checkbox.test.js @@ -1,6 +1,6 @@ import { render, fireEvent, screen } from '@testing-library/react' import React from 'react' -import { Checkbox } from '../checkbox.js' +import { Checkbox } from '../checkbox.tsx' describe('', () => { it('should call the onKeyDown callback when provided', () => { diff --git a/components/checkbox/src/checkbox/checkbox-icon.js b/components/checkbox/src/checkbox/checkbox-icon.js deleted file mode 100644 index 5a60cfd1cc..0000000000 --- a/components/checkbox/src/checkbox/checkbox-icon.js +++ /dev/null @@ -1,201 +0,0 @@ -import { colors } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React from 'react' -import css from 'styled-jsx/css' - -const commonStyles = css` - svg { - display: block; - pointer-events: none; - } - svg .border { - fill: ${colors.grey800}; - } - svg .background, - svg .indeterminate, - svg .checkmark { - fill: ${colors.white}; - } - - svg.checked .background, - svg.indeterminate .background { - fill: ${colors.teal500}; - } - svg.valid .background { - fill: ${colors.blue600}; - } - svg.warning .background { - fill: ${colors.yellow700}; - } - svg.error .background { - fill: ${colors.red500}; - } - - svg.checked .border, - svg.indeterminate .border { - fill: ${colors.teal900}; - } - - svg:not(.checked) .checkmark, - svg:not(.indeterminate) .indeterminate { - fill: none; - } - svg:not(.checked):not(.indeterminate) .background { - fill: ${colors.white}; - } -` - -export function CheckboxRegular({ className }) { - return ( - - - - - - - - - ) -} -CheckboxRegular.propTypes = { - className: PropTypes.string, -} - -export function CheckboxDense({ className }) { - return ( - - - - - - - - - ) -} -CheckboxDense.propTypes = { - className: PropTypes.string, -} diff --git a/components/checkbox/src/checkbox/checkbox-icon.tsx b/components/checkbox/src/checkbox/checkbox-icon.tsx new file mode 100644 index 0000000000..29c687c5f2 --- /dev/null +++ b/components/checkbox/src/checkbox/checkbox-icon.tsx @@ -0,0 +1,198 @@ +import { colors } from '@dhis2/ui-constants' +import React from 'react' +import css from 'styled-jsx/css' + +const commonStyles = css` + svg { + display: block; + pointer-events: none; + } + svg .border { + fill: ${colors.grey800}; + } + svg .background, + svg .indeterminate, + svg .checkmark { + fill: ${colors.white}; + } + + svg.checked .background, + svg.indeterminate .background { + fill: ${colors.teal500}; + } + svg.valid .background { + fill: ${colors.blue600}; + } + svg.warning .background { + fill: ${colors.yellow700}; + } + svg.error .background { + fill: ${colors.red500}; + } + + svg.checked .border, + svg.indeterminate .border { + fill: ${colors.teal900}; + } + + svg:not(.checked) .checkmark, + svg:not(.indeterminate) .indeterminate { + fill: none; + } + svg:not(.checked):not(.indeterminate) .background { + fill: ${colors.white}; + } +` + +interface CheckboxIconProps { + className?: string +} + +export function CheckboxRegular({ className }: CheckboxIconProps) { + return ( + + + + + + + + + ) +} + +export function CheckboxDense({ className }: CheckboxIconProps) { + return ( + + + + + + + + + ) +} diff --git a/components/checkbox/src/checkbox/checkbox.e2e.stories.js b/components/checkbox/src/checkbox/checkbox.e2e.stories.js index f47bee5748..941ecc81ac 100644 --- a/components/checkbox/src/checkbox/checkbox.e2e.stories.js +++ b/components/checkbox/src/checkbox/checkbox.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Checkbox } from './index.js' +import { Checkbox } from './index.ts' window.onClick = window.Cypress && window.Cypress.cy.stub() window.onChange = window.Cypress && window.Cypress.cy.stub() diff --git a/components/checkbox/src/checkbox/checkbox.js b/components/checkbox/src/checkbox/checkbox.js deleted file mode 100644 index 735a1fa6c1..0000000000 --- a/components/checkbox/src/checkbox/checkbox.js +++ /dev/null @@ -1,214 +0,0 @@ -import { mutuallyExclusive } from '@dhis2/prop-types' -import { colors, spacers, theme, sharedPropTypes } from '@dhis2/ui-constants' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { Component, createRef } from 'react' -import { CheckboxRegular, CheckboxDense } from './checkbox-icon.js' - -class Checkbox extends Component { - ref = createRef() - - componentDidMount() { - if (this.props.initialFocus) { - this.ref.current.focus() - } - - this.setIndeterminate(this.props.indeterminate) - } - - componentDidUpdate(prevProps) { - if (prevProps.indeterminate !== this.props.indeterminate) { - this.setIndeterminate(this.props.indeterminate) - } - } - - setIndeterminate(indeterminate) { - this.ref.current.indeterminate = indeterminate - } - - handleChange = (e) => { - if (this.props.onChange) { - this.props.onChange(this.createHandlerPayload(), e) - } - } - - handleBlur = (e) => { - if (this.props.onBlur) { - this.props.onBlur(this.createHandlerPayload(), e) - } - } - - handleFocus = (e) => { - if (this.props.onFocus) { - this.props.onFocus(this.createHandlerPayload(), e) - } - } - - handleKeyDown = (e) => { - if (this.props.onKeyDown) { - this.props.onKeyDown(this.createHandlerPayload(), e) - } - } - - createHandlerPayload() { - return { - value: this.props.value, - name: this.props.name, - checked: !this.props.checked, - } - } - - static defaultProps = { - checked: false, - indeterminate: false, - dataTest: 'dhis2-uicore-checkbox', - } - - render() { - const { - checked = false, - indeterminate = false, - className, - disabled, - error, - label, - name, - tabIndex, - valid, - value, - warning, - dense, - dataTest = 'dhis2-uicore-checkbox', - } = this.props - - const classes = cx({ - checked, - indeterminate, - disabled, - valid, - error, - warning, - }) - - return ( - - ) - } -} - -const uniqueOnStatePropType = mutuallyExclusive( - ['checked', 'indeterminate'], - PropTypes.bool -) - -Checkbox.propTypes = { - checked: uniqueOnStatePropType, - className: PropTypes.string, - dataTest: PropTypes.string, - dense: PropTypes.bool, - disabled: PropTypes.bool, - error: sharedPropTypes.statusPropType, - indeterminate: uniqueOnStatePropType, - initialFocus: PropTypes.bool, - label: PropTypes.node, - name: PropTypes.string, - tabIndex: PropTypes.string, - valid: sharedPropTypes.statusPropType, - value: PropTypes.string, - warning: sharedPropTypes.statusPropType, - onBlur: PropTypes.func, - /** Called with signature `(object, event)` */ - onChange: PropTypes.func, - onFocus: PropTypes.func, - onKeyDown: PropTypes.func, -} - -export { Checkbox } diff --git a/components/checkbox/src/checkbox/checkbox.prod.stories.js b/components/checkbox/src/checkbox/checkbox.prod.stories.js index 712d793beb..5c68672262 100644 --- a/components/checkbox/src/checkbox/checkbox.prod.stories.js +++ b/components/checkbox/src/checkbox/checkbox.prod.stories.js @@ -1,6 +1,6 @@ import { sharedPropTypes } from '@dhis2/ui-constants' import React from 'react' -import { Checkbox } from './index.js' +import { Checkbox } from './index.ts' const subtitle = 'A checkbox is a control that allows a user to toggle an option.' diff --git a/components/checkbox/src/checkbox/checkbox.tsx b/components/checkbox/src/checkbox/checkbox.tsx new file mode 100644 index 0000000000..fbb1307a20 --- /dev/null +++ b/components/checkbox/src/checkbox/checkbox.tsx @@ -0,0 +1,230 @@ +import { colors, spacers, theme } from '@dhis2/ui-constants' +import cx from 'classnames' +import React, { Component, createRef } from 'react' +import { CheckboxRegular, CheckboxDense } from './checkbox-icon.tsx' + +interface CheckboxHandlerPayload { + value?: string + name?: string + checked: boolean +} + +export interface CheckboxProps { + checked?: boolean + indeterminate?: boolean + className?: string + dataTest?: string + dense?: boolean + disabled?: boolean + error?: boolean + initialFocus?: boolean + label?: React.ReactNode + name?: string + tabIndex?: string + valid?: boolean + value?: string + warning?: boolean + onBlur?: ( + payload: CheckboxHandlerPayload, + event: React.FocusEvent + ) => void + onChange?: ( + payload: CheckboxHandlerPayload, + event: React.ChangeEvent + ) => void + onFocus?: ( + payload: CheckboxHandlerPayload, + event: React.FocusEvent + ) => void + onKeyDown?: ( + payload: CheckboxHandlerPayload, + event: React.KeyboardEvent + ) => void +} + +interface CheckboxState {} + +class Checkbox extends Component { + ref = createRef() + + static defaultProps = { + checked: false, + indeterminate: false, + dataTest: 'dhis2-uicore-checkbox', + } + + componentDidMount() { + if (this.props.initialFocus) { + this.ref.current?.focus() + } + + this.setIndeterminate(this.props.indeterminate) + } + + componentDidUpdate(prevProps: CheckboxProps) { + if (prevProps.indeterminate !== this.props.indeterminate) { + this.setIndeterminate(this.props.indeterminate) + } + } + + setIndeterminate(indeterminate?: boolean) { + if (this.ref.current) { + this.ref.current.indeterminate = !!indeterminate + } + } + + handleChange = (e: React.ChangeEvent) => { + if (this.props.onChange) { + this.props.onChange(this.createHandlerPayload(), e) + } + } + + handleBlur = (e: React.FocusEvent) => { + if (this.props.onBlur) { + this.props.onBlur(this.createHandlerPayload(), e) + } + } + + handleFocus = (e: React.FocusEvent) => { + if (this.props.onFocus) { + this.props.onFocus(this.createHandlerPayload(), e) + } + } + + handleKeyDown = (e: React.KeyboardEvent) => { + if (this.props.onKeyDown) { + this.props.onKeyDown(this.createHandlerPayload(), e) + } + } + + createHandlerPayload(): CheckboxHandlerPayload { + return { + value: this.props.value, + name: this.props.name, + checked: !this.props.checked, + } + } + + render() { + const { + checked = false, + indeterminate = false, + className, + disabled, + error, + label, + name, + tabIndex, + valid, + value, + warning, + dense, + dataTest = 'dhis2-uicore-checkbox', + } = this.props + + const classes = cx({ + checked, + indeterminate, + disabled, + valid, + error, + warning, + }) + + return ( + + ) + } +} + +export { Checkbox } diff --git a/components/checkbox/src/checkbox/index.js b/components/checkbox/src/checkbox/index.js deleted file mode 100644 index d139a7ef73..0000000000 --- a/components/checkbox/src/checkbox/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Checkbox } from './checkbox.js' diff --git a/components/checkbox/src/checkbox/index.ts b/components/checkbox/src/checkbox/index.ts new file mode 100644 index 0000000000..8eb5380848 --- /dev/null +++ b/components/checkbox/src/checkbox/index.ts @@ -0,0 +1,2 @@ +export { Checkbox } from './checkbox.tsx' +export type { CheckboxProps } from './checkbox.tsx' diff --git a/components/checkbox/src/index.js b/components/checkbox/src/index.js deleted file mode 100644 index 7fb939f725..0000000000 --- a/components/checkbox/src/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { Checkbox } from './checkbox/index.js' -export { CheckboxField } from './checkbox-field/index.js' diff --git a/components/checkbox/src/index.ts b/components/checkbox/src/index.ts new file mode 100644 index 0000000000..9a4a9b364a --- /dev/null +++ b/components/checkbox/src/index.ts @@ -0,0 +1,4 @@ +export { Checkbox } from './checkbox/index.ts' +export type { CheckboxProps } from './checkbox/index.ts' +export { CheckboxField } from './checkbox-field/index.ts' +export type { CheckboxFieldProps } from './checkbox-field/index.ts' diff --git a/components/checkbox/tsconfig.json b/components/checkbox/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/checkbox/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/chip/d2.config.js b/components/chip/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/chip/d2.config.js +++ b/components/chip/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/chip/package.json b/components/chip/package.json index bb0cd3bfc4..32a320a2ff 100644 --- a/components/chip/package.json +++ b/components/chip/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/chip/src/chip.e2e.stories.js b/components/chip/src/chip.e2e.stories.js index 02b6d33fce..09110fb03c 100644 --- a/components/chip/src/chip.e2e.stories.js +++ b/components/chip/src/chip.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Chip } from './chip.js' +import { Chip } from './chip.tsx' window.onClick = window.Cypress && window.Cypress.cy.stub() window.onRemove = window.Cypress && window.Cypress.cy.stub() diff --git a/components/chip/src/chip.js b/components/chip/src/chip.js deleted file mode 100644 index c06364b3b3..0000000000 --- a/components/chip/src/chip.js +++ /dev/null @@ -1,143 +0,0 @@ -import { colors, theme } from '@dhis2/ui-constants' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React from 'react' -import { Content } from './content.js' -import { Icon } from './icon.js' -import { Remove } from './remove.js' - -const DEFAULT_INLINE_MARGIN = '4' - -const Chip = ({ - selected, - dense, - disabled, - dragging, - overflow, - className, - children, - onRemove, - onClick, - icon, - dataTest = 'dhis2-uicore-chip', - marginBottom = 4, - marginLeft, - marginRight, - marginTop = 4, - marginInlineStart, - marginInlineEnd, -}) => ( - { - if (!disabled && onClick) { - onClick({}, e) - } - }} - className={cx(className, { - selected, - dense, - disabled, - dragging, - })} - data-test={dataTest} - > - - {children} - - - - - -) - -Chip.propTypes = { - children: PropTypes.any, - className: PropTypes.string, - dataTest: PropTypes.string, - dense: PropTypes.bool, - disabled: PropTypes.bool, - dragging: PropTypes.bool, - icon: PropTypes.element, - /** `margin-bottom` value, applied in `px` */ - marginBottom: PropTypes.number, - /** `margin-inline-end` value, applied in `px` */ - marginInlineEnd: PropTypes.number, - /** `margin-inline-start` value, applied in `px` */ - marginInlineStart: PropTypes.number, - /** `margin-inline-start` value, applied in `px` */ - marginLeft: PropTypes.number, - /** `margin-inline-end` value, applied in `px` */ - marginRight: PropTypes.number, - /** `margin-top` value, applied in `px` */ - marginTop: PropTypes.number, - overflow: PropTypes.bool, - selected: PropTypes.bool, - onClick: PropTypes.func, - onRemove: PropTypes.func, -} - -export { Chip } diff --git a/components/chip/src/chip.prod.stories.js b/components/chip/src/chip.prod.stories.js index 015fbfbc5b..02b7db7e2e 100644 --- a/components/chip/src/chip.prod.stories.js +++ b/components/chip/src/chip.prod.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Chip } from './chip.js' +import { Chip } from './chip.tsx' const subtitle = `Chips are useful for displaying a selection of defined choices and filters to the user.` diff --git a/components/chip/src/chip.tsx b/components/chip/src/chip.tsx new file mode 100644 index 0000000000..a76c531403 --- /dev/null +++ b/components/chip/src/chip.tsx @@ -0,0 +1,148 @@ +import { colors, theme } from '@dhis2/ui-constants' +import cx from 'classnames' +import React from 'react' +import { Content } from './content.tsx' +import { Icon } from './icon.tsx' +import { Remove } from './remove.tsx' + +const DEFAULT_INLINE_MARGIN = '4' + +export interface ChipProps { + children?: React.ReactNode + className?: string + dataTest?: string + dense?: boolean + disabled?: boolean + dragging?: boolean + icon?: React.ReactElement + /** `margin-bottom` value, applied in `px` */ + marginBottom?: number + /** `margin-inline-end` value, applied in `px` */ + marginInlineEnd?: number + /** `margin-inline-start` value, applied in `px` */ + marginInlineStart?: number + /** `margin-inline-start` value, applied in `px` */ + marginLeft?: number + /** `margin-inline-end` value, applied in `px` */ + marginRight?: number + /** `margin-top` value, applied in `px` */ + marginTop?: number + overflow?: boolean + selected?: boolean + onClick?: ( + payload: Record, + event: React.MouseEvent + ) => void + onRemove?: ( + payload: Record, + event: React.MouseEvent + ) => void +} + +const Chip = ({ + selected, + dense, + disabled, + dragging, + overflow, + className, + children, + onRemove, + onClick, + icon, + dataTest = 'dhis2-uicore-chip', + marginBottom = 4, + marginLeft, + marginRight, + marginTop = 4, + marginInlineStart, + marginInlineEnd, +}: ChipProps) => ( + { + if (!disabled && onClick) { + onClick({}, e) + } + }} + className={cx(className, { + selected, + dense, + disabled, + dragging, + })} + data-test={dataTest} + > + + {children} + + + + + +) + +export { Chip } diff --git a/components/chip/src/content.js b/components/chip/src/content.js deleted file mode 100644 index 71ae0f6241..0000000000 --- a/components/chip/src/content.js +++ /dev/null @@ -1,29 +0,0 @@ -import { spacers } from '@dhis2/ui-constants' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React from 'react' - -export const Content = ({ children, overflow }) => ( - - {children} - - - -) - -Content.propTypes = { - children: PropTypes.any, - overflow: PropTypes.bool, -} diff --git a/components/chip/src/content.tsx b/components/chip/src/content.tsx new file mode 100644 index 0000000000..1ea9b682fa --- /dev/null +++ b/components/chip/src/content.tsx @@ -0,0 +1,28 @@ +import { spacers } from '@dhis2/ui-constants' +import cx from 'classnames' +import React from 'react' + +export interface ContentProps { + children?: React.ReactNode + overflow?: boolean +} + +export const Content = ({ children, overflow }: ContentProps) => ( + + {children} + + + +) diff --git a/components/chip/src/icon.js b/components/chip/src/icon.js deleted file mode 100644 index 844cb2bc77..0000000000 --- a/components/chip/src/icon.js +++ /dev/null @@ -1,37 +0,0 @@ -import { spacers } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React from 'react' - -export const Icon = ({ icon, dataTest }) => { - if (!icon) { - return null - } - - return ( - - {icon} - - - - ) -} - -Icon.propTypes = { - dataTest: PropTypes.string.isRequired, - /** the slot for an icon is 24x24px, bigger elements will be clipped */ - icon: PropTypes.element, -} diff --git a/components/chip/src/icon.tsx b/components/chip/src/icon.tsx new file mode 100644 index 0000000000..4ac050062f --- /dev/null +++ b/components/chip/src/icon.tsx @@ -0,0 +1,36 @@ +import { spacers } from '@dhis2/ui-constants' +import React from 'react' + +export interface IconProps { + dataTest: string + /** the slot for an icon is 24x24px, bigger elements will be clipped */ + icon?: React.ReactElement +} + +export const Icon = ({ icon, dataTest }: IconProps) => { + if (!icon) { + return null + } + + return ( + + {icon} + + + + ) +} diff --git a/components/chip/src/index.js b/components/chip/src/index.js deleted file mode 100644 index 8214f999ee..0000000000 --- a/components/chip/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Chip } from './chip.js' diff --git a/components/chip/src/index.ts b/components/chip/src/index.ts new file mode 100644 index 0000000000..b559e40106 --- /dev/null +++ b/components/chip/src/index.ts @@ -0,0 +1,2 @@ +export { Chip } from './chip.tsx' +export type { ChipProps } from './chip.tsx' diff --git a/components/chip/src/remove.js b/components/chip/src/remove.js deleted file mode 100644 index 87c2f3f7df..0000000000 --- a/components/chip/src/remove.js +++ /dev/null @@ -1,82 +0,0 @@ -import { colors } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React from 'react' -import { css, resolve } from 'styled-jsx/css' - -function CancelOutline({ className }) { - return ( - - - - - - ) -} -CancelOutline.propTypes = { - className: PropTypes.string, -} - -const containerStyle = css` - span { - display: flex; - justify-content: center; - align-items: center; - height: 20px; - width: 20px; - margin-inline-end: 4px; - border-radius: 12px; - margin-inline-start: -8px; - } - span:hover { - background: ${colors.grey400}; - } -` - -const removeIcon = resolve` - svg { - fill: ${colors.grey600}; - height: 16px; - width: 16px; - cursor: pointer; - opacity: 1; - pointer-events: all; - } -` - -export const Remove = ({ onRemove, dataTest }) => { - if (!onRemove) { - return null - } - - return ( - { - e.stopPropagation() // stop onRemove from triggering onClick on container - onRemove({}, e) - }} - data-test={dataTest} - > - - {removeIcon.styles} - - - - ) -} - -Remove.propTypes = { - dataTest: PropTypes.string.isRequired, - onRemove: PropTypes.func, -} diff --git a/components/chip/src/remove.tsx b/components/chip/src/remove.tsx new file mode 100644 index 0000000000..1f11d3b95e --- /dev/null +++ b/components/chip/src/remove.tsx @@ -0,0 +1,85 @@ +import { colors } from '@dhis2/ui-constants' +import React from 'react' +import css from 'styled-jsx/css' + +interface CancelOutlineProps { + className?: string +} + +function CancelOutline({ className }: CancelOutlineProps) { + return ( + + + + + + ) +} + +const containerStyle = css` + span { + display: flex; + justify-content: center; + align-items: center; + height: 20px; + width: 20px; + margin-inline-end: 4px; + border-radius: 12px; + margin-inline-start: -8px; + } + span:hover { + background: ${colors.grey400}; + } +` + +const removeIcon = css.resolve` + svg { + fill: ${colors.grey600}; + height: 16px; + width: 16px; + cursor: pointer; + opacity: 1; + pointer-events: all; + } +` + +export interface RemoveProps { + dataTest: string + onRemove?: ( + payload: Record, + event: React.MouseEvent + ) => void +} + +export const Remove = ({ onRemove, dataTest }: RemoveProps) => { + if (!onRemove) { + return null + } + + return ( + { + e.stopPropagation() // stop onRemove from triggering onClick on container + onRemove({}, e) + }} + data-test={dataTest} + > + + {removeIcon.styles} + + + + ) +} diff --git a/components/chip/tsconfig.json b/components/chip/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/chip/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/cover/d2.config.js b/components/cover/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/cover/d2.config.js +++ b/components/cover/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/cover/package.json b/components/cover/package.json index 0b90696d64..9ea7dbcc01 100644 --- a/components/cover/package.json +++ b/components/cover/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/cover/src/cover.e2e.stories.js b/components/cover/src/cover.e2e.stories.js index 0a2b53bc1b..27e4e74e0f 100644 --- a/components/cover/src/cover.e2e.stories.js +++ b/components/cover/src/cover.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Cover } from './cover.js' +import { Cover } from './cover.tsx' window.onButtonClick = window.Cypress && window.Cypress.cy.stub() window.onCover = window.Cypress && window.Cypress.cy.stub() diff --git a/components/cover/src/cover.js b/components/cover/src/cover.js deleted file mode 100644 index 2ddb89ff52..0000000000 --- a/components/cover/src/cover.js +++ /dev/null @@ -1,51 +0,0 @@ -import { layers } from '@dhis2/ui-constants' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React from 'react' - -const createClickHandler = (onClick) => (event) => { - // don't respond to clicks that originated in the children - if (onClick && event.target === event.currentTarget) { - onClick({}, event) - } -} - -const Cover = ({ - children, - className, - dataTest = 'dhis2-uicore-componentcover', - onClick, - translucent, -}) => ( -
- {children} - -
-) - -Cover.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - dataTest: PropTypes.string, - /** Adds a semi-transparent background to the cover */ - translucent: PropTypes.bool, - onClick: PropTypes.func, -} - -export { Cover } diff --git a/components/cover/src/cover.prod.stories.js b/components/cover/src/cover.prod.stories.js index 49c5771db6..4e10e184bd 100644 --- a/components/cover/src/cover.prod.stories.js +++ b/components/cover/src/cover.prod.stories.js @@ -1,7 +1,7 @@ import { Center } from '@dhis2-ui/center' import { CircularLoader } from '@dhis2-ui/loader' import React from 'react' -import { Cover } from './cover.js' +import { Cover } from './cover.tsx' const description = ` Covers sibling components. Useful for covering a component while it is loading. diff --git a/components/cover/src/cover.tsx b/components/cover/src/cover.tsx new file mode 100644 index 0000000000..63e9f43b6a --- /dev/null +++ b/components/cover/src/cover.tsx @@ -0,0 +1,57 @@ +import { layers } from '@dhis2/ui-constants' +import cx from 'classnames' +import React from 'react' + +export interface CoverProps { + children?: React.ReactNode + className?: string + dataTest?: string + /** Adds a semi-transparent background to the cover */ + translucent?: boolean + onClick?: ( + payload: Record, + event: React.MouseEvent + ) => void +} + +const createClickHandler = + ( + onClick?: CoverProps['onClick'] + ): React.MouseEventHandler => + (event) => { + // don't respond to clicks that originated in the children + if (onClick && event.target === event.currentTarget) { + onClick({}, event) + } + } + +const Cover = ({ + children, + className, + dataTest = 'dhis2-uicore-componentcover', + onClick, + translucent, +}: CoverProps) => ( +
+ {children} + +
+) + +export { Cover } diff --git a/components/cover/src/index.js b/components/cover/src/index.js deleted file mode 100644 index 49e49ee2d0..0000000000 --- a/components/cover/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Cover } from './cover.js' diff --git a/components/cover/src/index.ts b/components/cover/src/index.ts new file mode 100644 index 0000000000..144238a7b8 --- /dev/null +++ b/components/cover/src/index.ts @@ -0,0 +1,2 @@ +export { Cover } from './cover.tsx' +export type { CoverProps } from './cover.tsx' diff --git a/components/cover/tsconfig.json b/components/cover/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/cover/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/css/d2.config.js b/components/css/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/css/d2.config.js +++ b/components/css/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/css/package.json b/components/css/package.json index acd7bd6e47..f7d0d1178f 100644 --- a/components/css/package.json +++ b/components/css/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/css/src/css-reset/css-reset.js b/components/css/src/css-reset/css-reset.js deleted file mode 100644 index 57a4798798..0000000000 --- a/components/css/src/css-reset/css-reset.js +++ /dev/null @@ -1,199 +0,0 @@ -import { theme } from '@dhis2/ui-constants' -import React from 'react' - -const CssReset = () => ( - -) - -export { CssReset } diff --git a/components/css/src/css-reset/css-reset.prod.stories.js b/components/css/src/css-reset/css-reset.prod.stories.js index c52e293128..a3088cf18d 100644 --- a/components/css/src/css-reset/css-reset.prod.stories.js +++ b/components/css/src/css-reset/css-reset.prod.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { CssReset } from './index.js' +import { CssReset } from './index.ts' const description = ` A tool for adding a global normalization stylesheet into the DOM that applies DHIS2 styles. diff --git a/components/css/src/css-reset/css-reset.tsx b/components/css/src/css-reset/css-reset.tsx new file mode 100644 index 0000000000..89a2af1e01 --- /dev/null +++ b/components/css/src/css-reset/css-reset.tsx @@ -0,0 +1,199 @@ +import { theme } from '@dhis2/ui-constants' +import React from 'react' + +const CssReset = (): React.ReactElement => ( + +) + +export { CssReset } diff --git a/components/css/src/css-reset/index.js b/components/css/src/css-reset/index.js deleted file mode 100644 index 6200dd804d..0000000000 --- a/components/css/src/css-reset/index.js +++ /dev/null @@ -1 +0,0 @@ -export { CssReset } from './css-reset.js' diff --git a/components/css/src/css-reset/index.ts b/components/css/src/css-reset/index.ts new file mode 100644 index 0000000000..d6eecca718 --- /dev/null +++ b/components/css/src/css-reset/index.ts @@ -0,0 +1 @@ +export { CssReset } from './css-reset.tsx' diff --git a/components/css/src/css-variables/css-variables.e2e.stories.js b/components/css/src/css-variables/css-variables.e2e.stories.js index 3d15080bd5..fd6193d7fa 100644 --- a/components/css/src/css-variables/css-variables.e2e.stories.js +++ b/components/css/src/css-variables/css-variables.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { CssVariables } from './index.js' +import { CssVariables } from './index.ts' export default { title: 'CssVariables' } export const WithColors = () => ( diff --git a/components/css/src/css-variables/css-variables.js b/components/css/src/css-variables/css-variables.js deleted file mode 100644 index 959956efc7..0000000000 --- a/components/css/src/css-variables/css-variables.js +++ /dev/null @@ -1,53 +0,0 @@ -import * as theme from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React from 'react' - -const toPrefixedThemeSection = (themeSectionKey) => - // eslint-disable-next-line import/namespace - Object.entries(theme[themeSectionKey]).reduce((prefixed, [key, value]) => { - prefixed[`${themeSectionKey}-${key}`] = value - - return prefixed - }, {}) - -const toCustomPropertyString = (themeSection) => - Object.entries(themeSection) - .map(([key, value]) => `--${key}: ${value};`) - .join('\n') - -const CssVariables = ({ - colors = false, - theme = false, - layers = false, - spacers = false, - elevations = false, -}) => { - const allowedProps = { colors, theme, layers, spacers, elevations } - const variables = Object.keys(allowedProps) - // Filter all props that are false - .filter((prop) => allowedProps[prop]) - // Map props to corresponding theme section and prefixes keys with section name - .map(toPrefixedThemeSection) - // Map each section to a single string of css custom property declarations - .map(toCustomPropertyString) - // Join all the sections to a single string - .join('\n') - - return ( - - ) -} - -CssVariables.propTypes = { - colors: PropTypes.bool, - elevations: PropTypes.bool, - layers: PropTypes.bool, - spacers: PropTypes.bool, - theme: PropTypes.bool, -} - -export { CssVariables } diff --git a/components/css/src/css-variables/css-variables.prod.stories.js b/components/css/src/css-variables/css-variables.prod.stories.js index cbd8285e5b..d68b953559 100644 --- a/components/css/src/css-variables/css-variables.prod.stories.js +++ b/components/css/src/css-variables/css-variables.prod.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { CssVariables } from './index.js' +import { CssVariables } from './index.ts' const description = ` A utility for adding DHIS2 theme variables to global CSS variables. diff --git a/components/css/src/css-variables/css-variables.tsx b/components/css/src/css-variables/css-variables.tsx new file mode 100644 index 0000000000..97ac9b693e --- /dev/null +++ b/components/css/src/css-variables/css-variables.tsx @@ -0,0 +1,63 @@ +import * as themeModule from '@dhis2/ui-constants' +import React from 'react' + +export interface CssVariablesProps { + colors?: boolean + theme?: boolean + layers?: boolean + spacers?: boolean + elevations?: boolean +} + +const toPrefixedThemeSection = ( + themeSectionKey: string +): Record => + Object.entries( + (themeModule as unknown as Record>)[ + themeSectionKey + ] + ).reduce>((prefixed, [key, value]) => { + prefixed[`${themeSectionKey}-${key}`] = value + + return prefixed + }, {}) + +const toCustomPropertyString = (themeSection: Record): string => + Object.entries(themeSection) + .map(([key, value]) => `--${key}: ${value};`) + .join('\n') + +const CssVariables = ({ + colors = false, + theme = false, + layers = false, + spacers = false, + elevations = false, +}: CssVariablesProps): React.ReactElement => { + const allowedProps: Record = { + colors, + theme, + layers, + spacers, + elevations, + } + const variables = Object.keys(allowedProps) + // Filter all props that are false + .filter((prop) => allowedProps[prop]) + // Map props to corresponding theme section and prefixes keys with section name + .map(toPrefixedThemeSection) + // Map each section to a single string of css custom property declarations + .map(toCustomPropertyString) + // Join all the sections to a single string + .join('\n') + + return ( + + ) +} + +export { CssVariables } diff --git a/components/css/src/css-variables/index.js b/components/css/src/css-variables/index.js deleted file mode 100644 index f468edfb34..0000000000 --- a/components/css/src/css-variables/index.js +++ /dev/null @@ -1 +0,0 @@ -export { CssVariables } from './css-variables.js' diff --git a/components/css/src/css-variables/index.ts b/components/css/src/css-variables/index.ts new file mode 100644 index 0000000000..9979758ede --- /dev/null +++ b/components/css/src/css-variables/index.ts @@ -0,0 +1,2 @@ +export { CssVariables } from './css-variables.tsx' +export type { CssVariablesProps } from './css-variables.tsx' diff --git a/components/css/src/index.js b/components/css/src/index.js deleted file mode 100644 index 609289b24a..0000000000 --- a/components/css/src/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { CssReset } from './css-reset/index.js' -export { CssVariables } from './css-variables/index.js' diff --git a/components/css/src/index.ts b/components/css/src/index.ts new file mode 100644 index 0000000000..3ebcb8e035 --- /dev/null +++ b/components/css/src/index.ts @@ -0,0 +1,3 @@ +export { CssReset } from './css-reset/index.ts' +export { CssVariables } from './css-variables/index.ts' +export type { CssVariablesProps } from './css-variables/index.ts' diff --git a/components/css/tsconfig.json b/components/css/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/css/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/divider/d2.config.js b/components/divider/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/divider/d2.config.js +++ b/components/divider/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/divider/package.json b/components/divider/package.json index b9a5671ced..9d84cc0e2f 100644 --- a/components/divider/package.json +++ b/components/divider/package.json @@ -23,7 +23,8 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "typecheck": "tsc --noEmit", "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" }, "peerDependencies": { diff --git a/components/divider/src/divider.js b/components/divider/src/divider.js deleted file mode 100644 index 10f75165db..0000000000 --- a/components/divider/src/divider.js +++ /dev/null @@ -1,53 +0,0 @@ -import { colors, spacers } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React from 'react' - -const flipMargin = (margin) => { - const splitMargin = margin.split(/\s+/) - if (splitMargin?.length === 4) { - return [ - splitMargin[0], - splitMargin[3], - splitMargin[2], - splitMargin[1], - ].join(' ') - } - return margin -} - -const Divider = ({ - className, - dataTest = 'dhis2-uicore-divider', - dense, - margin = `${spacers.dp8} 0`, -}) => { - return ( -
- - -
- ) -} - -Divider.propTypes = { - className: PropTypes.string, - dataTest: PropTypes.string, - dense: PropTypes.bool, - margin: PropTypes.string, -} - -export { Divider } diff --git a/components/divider/src/divider.prod.stories.js b/components/divider/src/divider.prod.stories.js index 797c41ed00..a157b910db 100644 --- a/components/divider/src/divider.prod.stories.js +++ b/components/divider/src/divider.prod.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Divider } from './divider.js' +import { Divider } from './divider.tsx' const description = ` A light divider to separate content. diff --git a/components/divider/src/divider.tsx b/components/divider/src/divider.tsx new file mode 100644 index 0000000000..452805b85b --- /dev/null +++ b/components/divider/src/divider.tsx @@ -0,0 +1,52 @@ +import { colors, spacers } from '@dhis2/ui-constants' +import React from 'react' + +const flipMargin = (margin: string): string => { + const splitMargin = margin.split(/\s+/) + if (splitMargin?.length === 4) { + return [ + splitMargin[0], + splitMargin[3], + splitMargin[2], + splitMargin[1], + ].join(' ') + } + return margin +} + +export interface DividerProps { + className?: string + dataTest?: string + dense?: boolean + margin?: string +} + +const Divider = ({ + className, + dataTest = 'dhis2-uicore-divider', + dense, + margin = `${spacers.dp8} 0`, +}: DividerProps) => { + return ( +
+ + +
+ ) +} + +export { Divider } diff --git a/components/divider/src/index.js b/components/divider/src/index.js deleted file mode 100644 index c08f66d2ad..0000000000 --- a/components/divider/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Divider } from './divider.js' diff --git a/components/divider/src/index.ts b/components/divider/src/index.ts new file mode 100644 index 0000000000..7edcb164b0 --- /dev/null +++ b/components/divider/src/index.ts @@ -0,0 +1,2 @@ +export { Divider } from './divider.tsx' +export type { DividerProps } from './divider.tsx' diff --git a/components/divider/tsconfig.json b/components/divider/tsconfig.json new file mode 100644 index 0000000000..dad762aaef --- /dev/null +++ b/components/divider/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "node_modules", + "build", + "**/*.stories.*", + "**/*.test.*", + "**/*.e2e.*" + ] +} diff --git a/components/field/d2.config.js b/components/field/d2.config.js index 9973893afc..026fb9f036 100644 --- a/components/field/d2.config.js +++ b/components/field/d2.config.js @@ -1,6 +1,6 @@ module.exports = { type: 'lib', entryPoints: { - lib: 'src/index.js', + lib: 'src/index.ts', }, } diff --git a/components/field/package.json b/components/field/package.json index 5d95044c7a..23e02307ab 100644 --- a/components/field/package.json +++ b/components/field/package.json @@ -23,8 +23,9 @@ }, "scripts": { "start": "storybook dev -c ../../storybook/config --port 5000", - "build": "d2-app-scripts build", - "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js" + "build": "d2-app-scripts build && node ../../scripts/post-build-rename.js", + "test": "d2-app-scripts test --jestConfig ../../jest.config.shared.js", + "typecheck": "tsc --noEmit" }, "peerDependencies": { "react": "^16.13 || ^18", diff --git a/components/field/src/field-group/field-group.e2e.stories.js b/components/field/src/field-group/field-group.e2e.stories.js index d591841f46..41fda9e666 100644 --- a/components/field/src/field-group/field-group.e2e.stories.js +++ b/components/field/src/field-group/field-group.e2e.stories.js @@ -1,6 +1,6 @@ import { Checkbox } from '@dhis2-ui/checkbox' import React from 'react' -import { FieldGroup } from './field-group.js' +import { FieldGroup } from './field-group.tsx' export default { title: 'FieldGroup' } export const WithLabelAndRequired = () => ( diff --git a/components/field/src/field-group/field-group.js b/components/field/src/field-group/field-group.js deleted file mode 100644 index bccbd38916..0000000000 --- a/components/field/src/field-group/field-group.js +++ /dev/null @@ -1,61 +0,0 @@ -import { sharedPropTypes } from '@dhis2/ui-constants' -import PropTypes from 'prop-types' -import React from 'react' -import { Field } from '../field/index.js' -import { FieldSet } from '../field-set/index.js' -const FieldGroup = ({ - children, - className, - disabled, - helpText, - validationText, - label, - name, - required, - dataTest = 'dhis2-uiwidgets-fieldsetfield', - valid, - error, - warning, -}) => ( -
- - {children} - -
-) - -FieldGroup.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - dataTest: PropTypes.string, - /** Disables the form controls within */ - disabled: PropTypes.bool, - /** Applies 'error' styling to validation text for feedback. Mutually exclusive with `warning` and `valid` props */ - error: sharedPropTypes.statusPropType, - /** Useful instructions for the user */ - helpText: PropTypes.string, - /** Labels the Field Group */ - label: PropTypes.string, - /** Name associate with the Field Group. Passed in object as argument to event handlers */ - name: PropTypes.string, - /** Adds an asterisk to indicate this field is required */ - required: PropTypes.bool, - /** Applies 'valid' styling to validation text for feedback. Mutually exclusive with `warning` and `error` props */ - valid: sharedPropTypes.statusPropType, - /** Adds text at the bottom of the field to provide validation feedback. Acquires styles from `valid`, `warning` and `error` statuses */ - validationText: PropTypes.string, - /** Applies 'warning' styling to validation text for feedback. Mutually exclusive with `valid` and `error` props */ - warning: sharedPropTypes.statusPropType, -} - -export { FieldGroup } diff --git a/components/field/src/field-group/field-group.prod.stories.js b/components/field/src/field-group/field-group.prod.stories.js index aa447fcab8..709ca06df0 100644 --- a/components/field/src/field-group/field-group.prod.stories.js +++ b/components/field/src/field-group/field-group.prod.stories.js @@ -3,7 +3,7 @@ import { Checkbox } from '@dhis2-ui/checkbox' import { Radio } from '@dhis2-ui/radio' import { Switch } from '@dhis2-ui/switch' import React from 'react' -import { FieldGroup } from './field-group.js' +import { FieldGroup } from './field-group.tsx' const description = ` Wraps a group of form components like Radios, Checkboxes, or Switches. The FieldGroup wraps the form controls in a FieldSet and a Field component to group them and add a label, help text, and/or validation text. diff --git a/components/field/src/field-group/field-group.tsx b/components/field/src/field-group/field-group.tsx new file mode 100644 index 0000000000..769dc63507 --- /dev/null +++ b/components/field/src/field-group/field-group.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { Field } from '../field/index.ts' +import { FieldSet } from '../field-set/index.ts' + +export interface FieldGroupProps { + children?: React.ReactNode + className?: string + dataTest?: string + /** Disables the form controls within */ + disabled?: boolean + /** Applies 'error' styling to validation text for feedback. Mutually exclusive with `warning` and `valid` props */ + error?: boolean + /** Useful instructions for the user */ + helpText?: string + /** Labels the Field Group */ + label?: string + /** Name associate with the Field Group. Passed in object as argument to event handlers */ + name?: string + /** Adds an asterisk to indicate this field is required */ + required?: boolean + /** Applies 'valid' styling to validation text for feedback. Mutually exclusive with `warning` and `error` props */ + valid?: boolean + /** Adds text at the bottom of the field to provide validation feedback. Acquires styles from `valid`, `warning` and `error` statuses */ + validationText?: string + /** Applies 'warning' styling to validation text for feedback. Mutually exclusive with `valid` and `error` props */ + warning?: boolean +} + +const FieldGroup = ({ + children, + className, + disabled, + helpText, + validationText, + label, + name, + required, + dataTest = 'dhis2-uiwidgets-fieldsetfield', + valid, + error, + warning, +}: FieldGroupProps) => ( +
)} + dataTest={dataTest} + > + + {children} + +
+) + +export { FieldGroup } diff --git a/components/field/src/field-group/index.js b/components/field/src/field-group/index.js deleted file mode 100644 index 72eb253d01..0000000000 --- a/components/field/src/field-group/index.js +++ /dev/null @@ -1 +0,0 @@ -export { FieldGroup } from './field-group.js' diff --git a/components/field/src/field-group/index.ts b/components/field/src/field-group/index.ts new file mode 100644 index 0000000000..8ff90eb1c5 --- /dev/null +++ b/components/field/src/field-group/index.ts @@ -0,0 +1,2 @@ +export { FieldGroup } from './field-group.tsx' +export type { FieldGroupProps } from './field-group.tsx' diff --git a/components/field/src/field-set/field-set.e2e.stories.js b/components/field/src/field-set/field-set.e2e.stories.js index fd282f0833..c6094ecd15 100644 --- a/components/field/src/field-set/field-set.e2e.stories.js +++ b/components/field/src/field-set/field-set.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { FieldSet } from './field-set.js' +import { FieldSet } from './field-set.tsx' export default { title: 'FieldSet' } export const WithChildren = () =>
I am a child
diff --git a/components/field/src/field-set/field-set.js b/components/field/src/field-set/field-set.js deleted file mode 100644 index 1f14a80a4e..0000000000 --- a/components/field/src/field-set/field-set.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' - -const FieldSet = ({ - className, - children, - dataTest = 'dhis2-uicore-fieldset', -}) => ( -
- {children} - -
-) - -FieldSet.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - dataTest: PropTypes.string, -} - -export { FieldSet } diff --git a/components/field/src/field-set/field-set.prod.stories.js b/components/field/src/field-set/field-set.prod.stories.js index 934cd9fb57..486a2ca9f3 100644 --- a/components/field/src/field-set/field-set.prod.stories.js +++ b/components/field/src/field-set/field-set.prod.stories.js @@ -2,8 +2,8 @@ import { Help } from '@dhis2-ui/help' import { Legend } from '@dhis2-ui/legend' import { Radio } from '@dhis2-ui/radio' import React from 'react' -import { Field } from '../index.js' -import { FieldSet } from './field-set.js' +import { Field } from '../index.ts' +import { FieldSet } from './field-set.tsx' const description = ` A container for grouping several Field components together. Use a \`\` component to add helpful guiding text. diff --git a/components/field/src/field-set/field-set.tsx b/components/field/src/field-set/field-set.tsx new file mode 100644 index 0000000000..6e536e4798 --- /dev/null +++ b/components/field/src/field-set/field-set.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +export interface FieldSetProps { + children?: React.ReactNode + className?: string + dataTest?: string +} + +const FieldSet = ({ + className, + children, + dataTest = 'dhis2-uicore-fieldset', +}: FieldSetProps) => ( +
+ {children} + +
+) + +export { FieldSet } diff --git a/components/field/src/field-set/index.js b/components/field/src/field-set/index.js deleted file mode 100644 index 35436087b2..0000000000 --- a/components/field/src/field-set/index.js +++ /dev/null @@ -1 +0,0 @@ -export { FieldSet } from './field-set.js' diff --git a/components/field/src/field-set/index.ts b/components/field/src/field-set/index.ts new file mode 100644 index 0000000000..e4ea159ea2 --- /dev/null +++ b/components/field/src/field-set/index.ts @@ -0,0 +1,2 @@ +export { FieldSet } from './field-set.tsx' +export type { FieldSetProps } from './field-set.tsx' diff --git a/components/field/src/field/field.e2e.stories.js b/components/field/src/field/field.e2e.stories.js index 5b7a34dd31..73c600270f 100644 --- a/components/field/src/field/field.e2e.stories.js +++ b/components/field/src/field/field.e2e.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { Field } from './field.js' +import { Field } from './field.tsx' export default { title: 'Field' } export const WithChildren = () => I am a child diff --git a/components/field/src/field/field.js b/components/field/src/field/field.js deleted file mode 100644 index 5d0d2bff32..0000000000 --- a/components/field/src/field/field.js +++ /dev/null @@ -1,93 +0,0 @@ -import { sharedPropTypes } from '@dhis2/ui-constants' -import { Box } from '@dhis2-ui/box' -import { Help } from '@dhis2-ui/help' -import { Label } from '@dhis2-ui/label' -import PropTypes from 'prop-types' -import React from 'react' - -const Field = ({ - children, - className, - disabled, - helpText, - label, - labelId, - name, - validationText, - required, - dataTest = 'dhis2-uicore-field', - valid, - error, - warning, -}) => ( - - {label && ( - - )} - - - {children} - - - {helpText && {helpText}} - - {validationText && ( - - {validationText} - - )} - -) - -Field.propTypes = { - children: PropTypes.node, - - className: PropTypes.string, - - dataTest: PropTypes.string, - - /** Disabled status, shown when mouse is over label */ - disabled: PropTypes.bool, - - /** Field status. Mutually exclusive with `valid` and `warning` props */ - error: sharedPropTypes.statusPropType, - - /** Useful text within the field */ - helpText: PropTypes.string, - - /** Label at the top of the field */ - label: PropTypes.string, - - /** id passed to the label element */ - labelId: PropTypes.string, - - /** `name` will become the target of the `for`/`htmlFor` attribute on the `