Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HoverText UI Component #597

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion assets/dist/build-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"components.js": "ee-components.70edb8ff0de64cc4b3a2.dist.js",
"components.css": "ee-components.e901a343f37109e97193.dist.css",
"components.js": "ee-components.31274ce12670a8583471.dist.js",
"data-stores.js": "ee-data-stores.f73e3005a0c775a3644e.dist.js",
"eejs.js": "ee-eejs.92c4b00318987b4164ec.dist.js",
"helpers.js": "ee-helpers.dc0e5af71606d964984f.dist.js",
Expand Down

Large diffs are not rendered by default.

111 changes: 111 additions & 0 deletions assets/dist/ee-components.e901a343f37109e97193.dist.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions assets/src/components/ui/hover-text/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Internal dependencies
*/
import './style.css';

export const HOVER_TEXT_POSITION_TOP_LEFT = 'top-left';
export const HOVER_TEXT_POSITION_TOP_RIGHT = 'top-right';
export const HOVER_TEXT_POSITION_BOTTOM_LEFT = 'bottom-left';
export const HOVER_TEXT_POSITION_BOTTOM_RIGHT = 'bottom-right';

/**
* HoverText
* Adds a text element that is only displayed when the wrapped component (children) is hovered over
*
* @function
* @param {string} hoverText The actual text to be displayed when the child entity is hovered over
* @param {string} htmlId Used to generate an HTML "id" attribute.
* The description and "-hover-text" are appended.
* @param {string} htmlClass Used to generate an HTML "class" attribute.
* The description and "-hover-text" are appended.
* @param {string} description Two or three words (hyphenated) that describe the component
* that the hover text is being added to
* @param {string} position Where the hover text appears relative to the wrapped component
* @param {string} children The child entity that gets wrapped with the hover elements
* @return {string} The child entity wrapped with the hover elements
*/
export const HoverText = ( { hoverText, htmlId, htmlClass, description, position, children } ) => {
position = position ?
position :
HOVER_TEXT_POSITION_TOP_RIGHT;
const pointerCharCodes = {};
pointerCharCodes[ HOVER_TEXT_POSITION_TOP_LEFT ] = 9701;
pointerCharCodes[ HOVER_TEXT_POSITION_TOP_RIGHT ] = 9700;
pointerCharCodes[ HOVER_TEXT_POSITION_BOTTOM_LEFT ] = 9698;
pointerCharCodes[ HOVER_TEXT_POSITION_BOTTOM_RIGHT ] = 9699;
return hoverText && (
<div
id={ `${ htmlId }-${ description }-hover-text` }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's potential for non unique ids to be implemented if there are multiple instances of this component in use. Granted it would mean that the developer would have to provide the same htmlId and description for this to happen but its still possible. I think it'd be good to wrap this with the withInstanceId HOC so the id is guaranteed to be unique.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do that, but it's not actually critical that this has a totally unique id as we are not using that for any kind of targeting. It's really only needed if a dev wants to tweak the styling for a specific hover text in a component, in which case, the dev can simply ensure that it has a unique id, by passing one.

The withInstanceId HOC was necessary in Gutenberg because of the way blocks and the GB admin works which absolutely requires unique ids in order to function properly, otherwise performing an action like deleting a block could affect more than one instance. For our purposes we are not using ids for control like that, and would only ever need it for styling purposes (remember this is just hover text) so having an id that potentially changes due to the number of calls to withInstanceId would actually be counter to our purposes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So its okay to have potentially invalid html rendered?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an html id attribute is not needed then I would say don't bother adding it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would actually be counter to our purposes.

What purposes would it be counter to?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the case, and you decide to keep this component, I'd prefer it if you either:

  • remove the id and developers can tweak styling by the htmlClass prop. I don't see how having an id is necessary if they can provide a className. If an id is necessary for styling then that is argument for it being unique.
    OR
  • wrap the component with withInstanceId and implement so the html id attribute is unique.

However, my preference is that you just use PopOver component from @wordpress/components because from what I've seen so far, I don't see the need for re-inventing the wheel here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So its okay to have potentially invalid html rendered?

No you are making a strawman argument as that was not my point. A form input for example can still work correctly even if a developer fails to give it a unique id. That does not mean that invalid html is ok, it only means that nothing breaks as a result of that invalid html. With Gutenberg blocks, it is critical that some components have a unique id else the editor will not be able to target them properly and things will not work.

I don't see how having an id is necessary if they can provide a className.

class names are not intended to be unique so if you have multiple elements that share the same class, then you need to start relying on more complex css to target a specific element, which may not always be possible.

If an id is necessary for styling then that is argument for it being unique.

There is nothing preventing the htmlId prop here from being unique. It CAN be unique simply by setting a unique value when adding the component to your app. This is no different than our PHP form system that generates ids for form elements in that it is up to the developer to ensure that the data they are inputting results in valid html. If i had two inputs in a row in a form and used "my-input" for the id for both inputs, then that would result in invalid html but it would NOT prevent the form from operating correctly in any other way, as long as I did not require those input ids for any kind of targeting (JS or CSS). Having a duplicate id for some hover text is not good, but it will not break anything.

With Gutenberg blocks it is a totally different scenario because the editor can create multiple instances of a block dynamically, so I could add one, two, three... ten... one hundred... (one miiiillllliiiion) copies of the same block to a post, and in order for GB to be able to target each one of those instances separately, they all need to have an id that is at least partially dynamically generated. This can also apply to components being used within the block, especially if you are adding form elements to the Gutenberg sidebar, because again, those may need to have a unique id for targeting purposes.

Here is the first sentence from the documentation for the withInstanceId HOC:

Some components need to generate a unique id for each instance.

There is nothing in the documentation stating that every single component that implements an id must absolutely use the withInstanceId HOC in order to guarantee the uniqueness of that id. But SOME components do need to generate a unique id for each instance. To suggest that a non-critical component like some hover text needs to utilize the withInstanceId HOC in order to guarantee a totally unique id attribute is totally overboard. If it is that much of an issue, then the more sane approach would be to use the withInstanceId HOC on a more appropriate parent component higher up in the hierarchy of your app, and then pass that id down to any child elements and have them incorporate that into any ids that they set. In other words, there is no reason why the html id for some hover text could not incorporate a unique id that was passed down from a withInstanceId HOC that was used higher up in the app.

Copy link
Contributor

@nerrad nerrad Jul 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand how withInstanceId works and its purpose. I'm not making a straw man argument. My point is that IF someone was to generate a bunch of these components without passing in a unique htmlId prop (and other props used for the id are the same), then the result would be invalid html rendered. I'm not saying it won't work, I'm saying it would be invalid. So my question was simply asking "So its okay to have potentially invalid html rendered?" (note I emphasized potentially here). Your answer can simply be that you believe it is.

I still stand by my outlined preferences here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is nothing stopping this html id prop from being unique, but the dev implementing it in their component would be responsible for actually generating a unique id in the first place, and that is no different than a bunch of our other systems that we already have in place (like our form system).

Again, the sane thing to do would be to use withInstanceId on a more appropriate parent component in any app that can be implemented more than once on the same page, and then pass that unique id down through the component hierarchy. In other words, only use the withInstanceId HOC for things that can have multiple instances generated dynamically. It's total overkill to employ it every single time you plan to implement an html id attribute.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that. However, that's why one of the options I gave was:

remove the id and developers can tweak styling by the htmlClass prop. I don't see how having an id is necessary if they can provide a className. If an id is necessary for styling then that is argument for it being unique.

Developers can pass along a unique className for the css class if they want. In other words don't set an id if it's not necessary.

Now you didn't mention it, but just in case its one possible reason you are explicitly adding an id here (beyond for styling purposes), if the id is something you're adding so the element can be targeted by js, it'd be better if you wrap the component with React.forwardRef and set a ref on the element you want targeted as that's the recommended way to integrate custom binding to rendered elements.

className={ `${ htmlClass }-${ description }-hover-text ee-hover-text-position-${ position } ee-${ description } ee-hover-text` }
>
<div className="ee-hover-text-content">
{ children }
</div>
<div className={ 'ee-hover-text-notice-wrapper' }>
<div className="ee-hover-text-notice ee-small-shadow">
<span className={ 'ee-hover-text-text' }>
{ hoverText }
</span>
<span className={ 'ee-hover-text-pointer ee-small-text-shadow' }>
{ String.fromCharCode( pointerCharCodes[ position ] ) }
</span>
</div>
</div>
</div>
);
};
108 changes: 108 additions & 0 deletions assets/src/components/ui/hover-text/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* -------------------------------------------------------------------------
* Event Espresso HoverText Stylesheet
* (c) 2018 Event Espresso
* -------------------------------------------------------------------------*/

.ee-hover-text {
cursor: pointer;
display: inline-block;
position: relative;
}

.ee-hover-text-notice-wrapper {
left: 50%;
overflow: visible;
position: absolute;
top: 0;
z-index: 999999;
}

.ee-hover-text .ee-hover-text-notice {
background-color: #58575c;
color: #fff;
display: inline-block;
font-size: 12px !important;
opacity: 0;
padding: .5rem 1rem;
position: absolute;
visibility: hidden;
white-space: nowrap;
transition: opacity 250ms 125ms, visibility 250ms 125ms;
}

.ee-hover-text-pointer {
color: #58575c;
text-shadow: 0 2px 4px rgba(0, 0, 0, .5);
display: inline-block;
position: absolute;
}

.ee-hover-text:hover .ee-hover-text-notice {
opacity: 1;
visibility: visible;
transition: opacity 250ms 500ms, visibility 250ms 500ms;
}

/* HOVER_TEXT_POSITION_TOP_LEFT */
.ee-hover-text-position-top-left .ee-hover-text-notice {
right: -.7rem;
top: -3.2rem;
}

.ee-hover-text-position-top-left .ee-hover-text-pointer {
right: 1rem;
bottom: -.93rem;
}

.ee-toggle-modal-close.ee-hover-text-position-top-left .ee-hover-text-notice {
right: .9rem;
top: -2.4rem;
}

/* HOVER_TEXT_POSITION_TOP_RIGHT */
.ee-hover-text-position-top-right .ee-hover-text-notice {
left: -.6rem;
top: -3.2rem;
}

.ee-hover-text-position-top-right .ee-hover-text-pointer {
left: 1rem;
bottom: -.93rem;
}

.ee-toggle-modal-close.ee-hover-text-position-top-right .ee-hover-text-notice {
left: -2.2rem;
top: -2.4rem;
}

/* HOVER_TEXT_POSITION_BOTTOM_LEFT */
.ee-hover-text-position-bottom-left .ee-hover-text-notice {
right: -.6rem;
top: 2.5rem;
}

.ee-hover-text-position-bottom-left .ee-hover-text-pointer {
right: 1rem;
top: -.93rem;
}

.ee-toggle-modal-close.ee-hover-text-position-bottom-left .ee-hover-text-notice {
right: 1rem;
top: 3.2rem;
}

/* HOVER_TEXT_POSITION_BOTTOM_RIGHT */
.ee-hover-text-position-bottom-right .ee-hover-text-notice {
left: -.6rem;
top: 2.5rem;
}

.ee-hover-text-position-bottom-right .ee-hover-text-pointer {
left: 1rem;
top: -.93rem;
}

.ee-toggle-modal-close.ee-hover-text-position-bottom-right .ee-hover-text-notice {
left: -2.2rem;
top: 3.2rem;
}
48 changes: 48 additions & 0 deletions assets/src/components/ui/hover-text/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { shallow } from 'enzyme';
import { HoverText } from "../";


describe( 'HoverText', () => {
it( 'renders nothing if hoverText not supplied', () => {
const wrapper = shallow( <HoverText /> );
expect( wrapper.find( '.ee-hover-text' ) ).toHaveLength( 0 );
} );
it( 'renders default when hoverText supplied', () => {
const wrapper = shallow( <HoverText hoverText={ 'click-this' } /> );
expect( wrapper ).toHaveLength( 1 );
const hover = wrapper.find( '.ee-hover-text' );
expect( hover ).toHaveLength( 1 );
expect( hover.props().id ).toEqual( 'undefined-undefined-hover-text' );
expect( hover.props().className ).toEqual(
'undefined-undefined-hover-text ee-hover-text-position-top-right ee-undefined ee-hover-text'
);
} );
it( 'renders correctly when all props supplied', () => {
const wrapper = shallow(
<HoverText
hoverText={ 'click-this' }
htmlId={ 'test-id' }
htmlClass={ 'test-class' }
description={ 'what-to-do' }
dashicon={ 'yes' }
>
<div>CLICK ME</div>
</HoverText>
);
expect( wrapper ).toHaveLength( 1 );
const hover = wrapper.find( '.ee-hover-text' );
expect( hover ).toHaveLength( 1 );
expect( hover.props().id ).toEqual( 'test-id-what-to-do-hover-text' );
expect( hover.props().className ).toEqual(
'test-class-what-to-do-hover-text ee-hover-text-position-top-right ee-what-to-do ee-hover-text'
);
const text = wrapper.find( '.ee-hover-text-text' );
expect( text ).toHaveLength( 1 );
expect( text.text() ).toEqual( 'click-this' );
const content = wrapper.find( '.ee-hover-text-content' );
expect( content ).toHaveLength( 1 );
expect( content.text() ).toEqual( 'CLICK ME' );
} );
} );

// assets/src/components/ui/hover-text/test/index.js
1 change: 1 addition & 0 deletions assets/src/components/ui/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { HoverText } from './hover-text';
export * from './image';
9 changes: 9 additions & 0 deletions core/domain/services/assets/CoreAssetManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class CoreAssetManager extends AssetManager

const CSS_HANDLE_EE_CUSTOM = 'espresso_custom_css';

const CSS_HANDLE_EE_COMPONENTS = 'eventespresso-components';

/**
* @var EE_Currency_Config $currency_config
*/
Expand Down Expand Up @@ -294,6 +296,13 @@ private function loadCoreCss()
);
}
}
$this->addStylesheet(
CoreAssetManager::CSS_HANDLE_EE_COMPONENTS,
$this->registry->getCssUrl(
$this->domain->assetNamespace(),
'components'
)
);
}


Expand Down
3 changes: 2 additions & 1 deletion docs/AA--Javascript-in-EE/components/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ These are components that are more generic in nature.

| Component Type | Description |
| -------------- | ----------- |
[image](image/README.md) | These are various ui components related to the `<img>` tag
| [image](image/README.md) | These are various ui components related to the `<img>` tag |
| [HoverText](hover-text/README.md) | A component that is only displayed when the wrapped child component is hovered over. |
Loading