Skip to content

Commit

Permalink
feat: Add horizontal start and end label position for form field (#2881)
Browse files Browse the repository at this point in the history
Fixes: #2809

[category:Components]

Release Note:
The orientation prop on the FormField component will be updated to accept the following values: `vertical`, `horizontalStart`, and `horizontalEnd`. `horizontal` will still be accepted as a value in v12, but it will be deprecated and slated for removal in a future major release. These changes are intended to provide more flexibility with label alignments on FormField elements.

Instances where the orientation prop of the FormField component is set to `horizontal` will automatically default to `horizontalStart` via a codemod. A console warning message will also appear with a message to change the prop value to either horizontalStart or horizontalEnd.

Co-authored-by: manuel.carrera <manuel.carrera@workday.com>
Co-authored-by: @josh-bagwell <44883293+josh-bagwell@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 12, 2024
1 parent 3f613f7 commit bd882e1
Show file tree
Hide file tree
Showing 43 changed files with 681 additions and 76 deletions.
3 changes: 3 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export default defineConfig({
retries: {
runMode: 2,
},
env: {
NODE_ENV: 'development', // or 'development', 'production', etc.
},

blockHosts: ['cdn.fontawesome.com'],

Expand Down
5 changes: 3 additions & 2 deletions cypress/component/Modal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,6 @@ context(`given the [Components/Popups/Modal, Without close icon] story is render
context(`given the [Components/Popups/Modal, Custom focus] story is rendered`, () => {
beforeEach(() => {
cy.mount(<CustomFocus />);
cy.wait(150);
});

context('when button is focused', () => {
Expand All @@ -467,7 +466,9 @@ context(`given the [Components/Popups/Modal, Custom focus] story is rendered`, (

context('when the target button is clicked', () => {
beforeEach(() => {
cy.findByRole('button', {name: 'Acknowledge License'}).click();
cy.findByRole('button', {name: 'Acknowledge License'}).should('exist');
cy.findByRole('button', {name: 'Acknowledge License'}).focus();
cy.focused().click();
});

it('should open the modal', () => {
Expand Down
8 changes: 8 additions & 0 deletions cypress/component/Select.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,14 @@ describe('Select', () => {

context('when the menu is opened', () => {
beforeEach(() => {
cy.window().then(win => {
// @ts-ignore mocking window process
win.process = {
env: {
NODE_ENV: 'development',
},
};
});
cy.findByRole('combobox').focus().realType('{downarrow}');
});

Expand Down
8 changes: 8 additions & 0 deletions cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ declare global {
Cypress.Commands.add('mount', mount);

before(() => {
cy.window().then(win => {
// @ts-ignore mocking window for each test
win.process = {
env: {
NODE_ENV: 'development',
},
};
});
cy.configureAxe({
rules: [
{id: 'landmark-one-main', enabled: false}, // stories don't require navigation rules
Expand Down
14 changes: 14 additions & 0 deletions modules/codemod/lib/v12/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Transform} from 'jscodeshift';

import renameFormFieldHorizontal from './renameFormFieldHorizontal';

const transform: Transform = (file, api, options) => {
// These will run in order. If your transform depends on others, place yours after dependent transforms
const fixes = [
// add codemods here
renameFormFieldHorizontal,
];
return fixes.reduce((source, fix) => fix({...file, source}, api, options) as string, file.source);
};

export default transform;
50 changes: 50 additions & 0 deletions modules/codemod/lib/v12/renameFormFieldHorizontal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {API, FileInfo, Options, JSXOpeningElement, JSXIdentifier} from 'jscodeshift';
import {hasImportSpecifiers} from '../v6/utils';
import {getImportRenameMap} from './utils/getImportRenameMap';

const formFieldPackage = '@workday/canvas-kit-preview-react/form-field';
const packages = [formFieldPackage];
const packageImports = ['FormField'];

export default function transformer(file: FileInfo, api: API, options: Options) {
const j = api.jscodeshift;

const root = j(file.source);

// exit if the named imports aren't found
if (!hasImportSpecifiers(api, root, packages, packageImports)) {
return file.source;
}

// getImportRenameMap utility will tell us if the file containsCanvasImports
// and give us an importMap to track what identifiers we need to update
const {importMap, styledMap} = getImportRenameMap(j, root, '@workday/canvas-kit-preview-react');

root
.find(j.JSXOpeningElement, (value: JSXOpeningElement) => {
const isCorrectImport = Object.entries(importMap).some(
([original, imported]) =>
imported === (value.name as JSXIdentifier).name && packageImports.includes(original)
);

const isCorrectStyled = Object.entries(styledMap).some(
([original, styled]) =>
styled === (value.name as JSXIdentifier).name && packageImports.includes(original)
);

return isCorrectImport || isCorrectStyled;
})
.forEach(nodePath => {
nodePath.node.attributes?.forEach(attr => {
if (attr.type === 'JSXAttribute') {
if (attr.name.name === 'orientation') {
if (attr.value && attr.value.type === 'StringLiteral') {
attr.value = j.stringLiteral('horizontalStart');
}
}
}
});
});

return root.toSource();
}
6 changes: 6 additions & 0 deletions modules/codemod/lib/v12/spec/expectTransformFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {runInlineTest} from 'jscodeshift/dist/testUtils';

export const expectTransformFactory =
(fn: Function) => (input: string, expected: string, options?: Record<string, any>) => {
return runInlineTest(fn, options, {source: input}, expected, {parser: 'tsx'});
};
75 changes: 75 additions & 0 deletions modules/codemod/lib/v12/spec/renameFormFieldHorizontal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {expectTransformFactory} from './expectTransformFactory';
import transform from '../renameFormFieldHorizontal';
import {stripIndent} from 'common-tags';

const expectTransform = expectTransformFactory(transform);

describe('rename horizontal', () => {
it('should not change non-canvas imports', () => {
const input = stripIndent`
import {FormField} from 'any-other-package'
<>
<FormField hasError={true} />
</>
`;

const expected = stripIndent`
import {FormField} from 'any-other-package'
<>
<FormField hasError={true} />
</>
`;
expectTransform(input, expected);
});

it('should rename orientation horizontal to horizontalStart', () => {
const input = stripIndent`
import {FormField} from '@workday/canvas-kit-preview-react/form-field'
<>
<FormField orientation="horizontal" />
</>
`;

const expected = stripIndent`
import {FormField} from '@workday/canvas-kit-preview-react/form-field'
<>
<FormField orientation="horizontalStart" />
</>
`;
expectTransform(input, expected);
});

it('should change renamed FormField', () => {
const input = stripIndent`
import {FormField as CanvasForm} from '@workday/canvas-kit-preview-react/form-field'
<>
<CanvasForm orientation="horizontal" />
</>
`;

const expected = stripIndent`
import {FormField as CanvasForm} from '@workday/canvas-kit-preview-react/form-field'
<>
<CanvasForm orientation="horizontalStart" />
</>
`;
expectTransform(input, expected);
});

it('should change styled FormField', () => {
const input = stripIndent`
import {FormField} from '@workday/canvas-kit-preview-react/form-field'
const StyledForm = styled(FormField)({color: "#000"});
<StyledForm orientation="horizontal" />
`;

const expected = stripIndent`
import {FormField} from '@workday/canvas-kit-preview-react/form-field'
const StyledForm = styled(FormField)({color: "#000"});
<StyledForm orientation="horizontalStart" />
`;
expectTransform(input, expected);
});
});
63 changes: 63 additions & 0 deletions modules/codemod/lib/v12/utils/getImportRenameMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {Collection, JSCodeshift, CallExpression} from 'jscodeshift';

export function getImportRenameMap(
j: JSCodeshift,
root: Collection<any>,
mainPackage = '@workday/canvas-kit-react',
packageName = ''
) {
let containsCanvasImports = false;

// build import name remapping - in case someone renamed imports...
// i.e. `import { IconButton as StyledIconButton } ...`
const importMap: Record<string, string> = {};
const styledMap: Record<string, string> = {};
root.find(j.ImportDeclaration, node => {
// imports our module
const value = node.source.value;
if (
typeof value === 'string' &&
(value === mainPackage || value.startsWith(mainPackage) || value === packageName)
) {
containsCanvasImports = true;
(node.specifiers || []).forEach(specifier => {
if (specifier.type === 'ImportSpecifier') {
if (!specifier.local || specifier.local.name === specifier.imported.name) {
importMap[specifier.imported.name] = specifier.imported.name;
} else {
importMap[specifier.imported.name] = specifier.local.name;
}
}
});
}
return false;
});

root
.find(j.CallExpression, (node: CallExpression) => {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'styled' &&
node.arguments[0].type === 'Identifier'
) {
return true;
}
return false;
})
.forEach(nodePath => {
if (
(nodePath.parent.value.type === 'CallExpression' ||
nodePath.parent.value.type === 'TaggedTemplateExpression') &&
nodePath.parent.parent.value.type === 'VariableDeclarator' &&
nodePath.parent.parent.value.id.type === 'Identifier'
) {
const styledName = nodePath.parent.parent.value.id.name;

if (nodePath.value.arguments[0].type === 'Identifier') {
styledMap[nodePath.value.arguments[0].name] = styledName;
}
}
});

return {containsCanvasImports, importMap, styledMap};
}
40 changes: 33 additions & 7 deletions modules/docs/mdx/12.0-UPGRADE-GUIDE.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ A note to the reader:
- [Component Updates](#component-updates)
- [Styling API and CSS Tokens](#styling-api-and-css-tokens)
- [Avatar](#avatar)
- [Form Field](#form-field)
- [Text Area](#text-area)
- [Troubleshooting](#troubleshooting)
- [Glossary](#glossary)
Expand Down Expand Up @@ -102,7 +103,8 @@ from Main instead.

**PRs:** [#2825](https://github.com/Workday/canvas-kit/pull/2825),
[#2795](https://github.com/Workday/canvas-kit/pull/2795),
[#2793](https://github.com/Workday/canvas-kit/pull/2793)
[#2793](https://github.com/Workday/canvas-kit/pull/2793),
[#2881](https://github.com/Workday/canvas-kit/pull/2881)

Several components have been refactored to use our new
[Canvas Tokens](https://workday.github.io/canvas-tokens/?path=/docs/docs-getting-started--docs) and
Expand All @@ -113,6 +115,7 @@ The following components are affected:

- `Avatar`
- `Dialog`
- `FormField`
- `Modal`
- `Popup`
- `TextArea`
Expand All @@ -125,13 +128,36 @@ The following components are affected:
### Avatar

- Avatar no longer uses `SystemIconCircle` for sizing.
- `Avatar.Size` is no longer supported. The `size` prop type has change to accept the following:
- `Avatar.Size` has been deprecated. The `size` prop still accepts `Avatar.Size` in addition to the
following values:
`"extraExtraLarge" | "extraLarge" | "large" | "medium" | "small" | "extraSmall" | (string{})`
- `Avatar.Variant` is no longer supported. Enum `AvatarVariant` has been removed. The `variant` type
prop now accepts `"dark" | "light"`
- `Avatar.Variant` has been deprecated. The `variant` prop still accepts `Avatar.Variant` in
addition to the following values: `"dark" | "light"`
- The `as` prop now accepts any element, not just `div`.

> **Note:** Both `Avatar.Size` and `Avatar.Variant` have been deprecated in favor of the new string
> union types. You will see a console warn message while in development if you're still using this.
> Please update your types and usage before v13.
### Form Field

**PR** [#2881](https://github.com/Workday/canvas-kit/pull/2881)

The orientation prop on the `FormField` component will be updated to accept the following values:
`vertical`, `horizontalStart`, and `horizontalEnd`. `horizontal` will still be accepted as a value
in v12, but it will be deprecated and slated for removal in a future major release. These changes
are intended to provide more flexibility with label alignments on `FormField` elements.

> **Note**: The horizontal alignment of start and end are relative to its container, meaning start
> and end match the flex property of `flex-start` and `flex-end`. This is especially applicable for
> moving between RTL (right-to-left) and LTR (left-to-right) languages.
> **Note:** Orientation "horizontal" has been deprecated. You will see a console warn message
> suggesting to update your types and usage to `horizontalStart`, `horizontalEnd` or `vertical`.
🤖 The codemod will handle the change of `orientation="horizontal"` to
`orientation="horizontalStart"` if you're using the string literal values.

### Text Area

**PR:** [#2825](https://github.com/Workday/canvas-kit/pull/2825)
Expand Down Expand Up @@ -209,5 +235,5 @@ experimental code and is analagous to code in alpha.

Breaking changes can be deployed to Labs at any time without triggering a major version update and
may not be subject to the same rigor in communcation and migration strategies reserved for breaking
changes in [Preview](#preview) and [Main](#main). `import { opacity } from
"@workday/canvas-tokens-web/dist/es6/system"`
changes in [Preview](#preview) and [Main](#main).
`import { opacity } from "@workday/canvas-tokens-web/dist/es6/system"`
17 changes: 13 additions & 4 deletions modules/preview-react/form-field/lib/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import {FormFieldHint} from './FormFieldHint';
import {FormFieldContainer} from './FormFieldContainer';
import {formFieldStencil} from './formFieldStencil';

//TODO: Remove `horizontal` option in v13 and the console warn message.
export interface FormFieldProps extends FlexProps, GrowthBehavior {
/**
* The direction the child elements should stack
* The direction the child elements should stack. In v13, `horizontal` will be removed. Please use `horizontalStart` or `horizontalEnd` for horizontal alignment.
* @default vertical
*/
orientation?: 'vertical' | 'horizontal';
orientation?: 'vertical' | 'horizontalStart' | 'horizontalEnd' | 'horizontal';
children: React.ReactNode;
}

Expand Down Expand Up @@ -82,7 +83,7 @@ export const FormField = createContainer('div')({
* `FormField.Container` allows you to properly center `FormField.Label` when the `orientation` is set to `horizontal` and there is hint text..
*
* ```tsx
* <FormField orientation="horizontal">
* <FormField orientation="horizontalStart">
* <FormField.Label>First Name</FormField.Label>
* <FormField.Container>
* <FormField.Input as={TextInput} value={value} onChange={(e) => console.log(e)} />
Expand All @@ -96,13 +97,21 @@ export const FormField = createContainer('div')({
Container: FormFieldContainer,
},
})<FormFieldProps>(({children, grow, orientation, ...elemProps}, Element, model) => {
// TODO: Remove this warning in v13 once we remove horizontal support in favor of horizontalStart and horizontalEnd.
if (process && process.env.NODE_ENV === 'development') {
if (orientation === 'horizontal') {
console.warn(
'FormField: Orientation option of "horizontal" is deprecated and will be removed in v13. Please update your types and value to use the string literal of "horizontalStart". The following values will be accepted in v13: "horizontalStart" | "horizontalEnd" | "vertical".'
);
}
}
return (
<Element
{...mergeStyles(
elemProps,
formFieldStencil({
grow,
orientation,
orientation: orientation === 'horizontal' ? 'horizontalStart' : orientation,
error: model.state.error,
required: model.state.isRequired,
})
Expand Down
Loading

0 comments on commit bd882e1

Please sign in to comment.